LISTAGG no Oracle para retornar valores distintos

94

Estou tentando usar a LISTAGGfunção no Oracle. Eu gostaria de obter apenas os valores distintos para essa coluna. Existe uma maneira de obter apenas os valores distintos sem criar uma função ou procedimento?

  col1 col2 Created_by
   1 2 Smith 
   1 2 John 
   1 3 Ajay 
   1 4 Ram 
   1 5 Jack 

Preciso selecionar col1 e LISTAGGcol2 (a coluna 3 não é considerada). Quando faço isso, obtenho algo assim como resultado de LISTAGG: [2,2,3,4,5]

Preciso remover o duplicado '2' aqui; Preciso apenas dos valores distintos de col2 contra col1.

Priyanth
fonte
5
Esta pergunta foi feita em Administradores de banco de dados : Eliminar duplicatas em ListAgg (Oracle)
Andriy M
Você pode mostrar a saída esperada (linhas) da amostra? O que você deseja ver se houver mais de um valor para col1?
a_horse_with_no_name
A produção esperada do LISTAGG é [2,3,4,5]. O segundo '2' deve ser removido. E minha tabela tem mais de 1000 linhas.
Priyanth
O que você deseja ver se houver mais de um valor para col1?
a_horse_with_no_name
O código é o seguinte: - SELECT col1, LISTAGG (col2, ',') dentro do grupo (ordem por col2) FROM table T WHERE .... Portanto, ele deve mostrar todos os valores distintos de col2 correspondentes a col1, separados por vírgula.
Priyanth

Respostas:

77

19c e mais tarde:

select listagg(distinct the_column, ',') within group (order by the_column)
from the_table

18c e anteriores:

select listagg(the_column, ',') within group (order by the_column)
from (
   select distinct the_column 
   from the_table
) t

Se precisar de mais colunas, algo como isto pode ser o que você está procurando:

select col1, listagg(col2, ',') within group (order by col2)
from (
  select col1, 
         col2,
         row_number() over (partition by col1, col2 order by col1) as rn
  from foo
  order by col1,col2
)
where rn = 1
group by col1;
um cavalo sem nome
fonte
2
Semelhante ao que eu tinha em mente também. Se listaggfor a única função de agregação na consulta, isso deve servir. Combiná-lo com outras funções de agregação, no entanto, é mais complicado.
Andriy M
Sim. Minha consulta é semelhante a esta.
Priyanth
1
@a_horse_with_no_name: A instrução select acima fornece valores duplicados para mim. Eu quero remover as duplicatas. col1 col2 Criado por 1 2 Smith 1 2 John 1 3 Ajay 1 4 Ram 1 5 Jack Eu preciso selecionar col1 e o LISTAGG de col2 (a coluna 3 não é considerada). Enquanto faço isso, obterei algo assim como o resultado de LISTAGG: -> [2,2,3,4,5] Eu preciso remover a duplicata'2 'aqui. Preciso apenas dos valores distintos de col2 contra col1 .
Priyanth
@a_horse_with_no_name: Eu tentei o código- e recebi a mensagem de erro conforme abaixo ORA-01489: o resultado da concatenação de string é muito longo 01489. 00000 - "o resultado da concatenação de string é muito longo" * Causa: O resultado da concatenação de string é maior que o máximo Tamanho.
Priyanth
@Priyanth: então você está sem sorte. O comprimento total excede 4000 bytes e o Oracle não pode lidar com isso. Você precisará fazer a agregação no código do seu aplicativo.
a_horse_with_no_name
47

Veja como resolver seu problema.

select  
      regexp_replace(
    '2,2,2.1,3,3,3,3,4,4' 
     ,'([^,]+)(,\1)*(,|$)', '\1\3')

from dual

retorna

2,2.1,3,4

Do oracle 19C é construído veja aqui

A partir de 18C e anteriores, tente dentro do grupo, consulte aqui

Caso contrário, use expressões regulares

RESPOSTA abaixo:

select col1, 

regexp_replace(
    listagg(
     col2 , ',') within group (order by col2)  -- sorted
    ,'([^,]+)(,\1)*(,|$)', '\1\3') )
   from tableX
where rn = 1
group by col1; 

Nota: O procedimento acima funcionará na maioria dos casos - a lista deve ser classificada, você pode ter que cortar todos os espaços à direita e à esquerda, dependendo de seus dados.

Se você tiver muitos itens em um grupo> 20 ou tamanhos de string grandes, você poderá encontrar o limite de tamanho de string do oráculo 'o resultado da concatenação de string é muito longo'.

No oracle 12cR2 você pode suprimir este erro, veja aqui . Como alternativa, coloque um número máximo de membros em cada grupo. Isso só funcionará se não houver problema em listar apenas os primeiros membros. Se você tiver strings de variáveis ​​muito longas, isso pode não funcionar. você terá que experimentar.

select col1,

case 
    when count(col2) < 100 then 
       regexp_replace(
        listagg(col2, ',') within group (order by col2)
        ,'([^,]+)(,\1)*(,|$)', '\1\3')

    else
    'Too many entries to list...'
end

from sometable
where rn = 1
group by col1;

Outra solução (não tão simples) para evitar o limite de tamanho da string do oráculo - o tamanho da string é limitado a 4000. Graças a esta postagem aqui pelo usuário 3465996

select col1  ,
    dbms_xmlgen.convert(  -- HTML decode
    dbms_lob.substr( -- limit size to 4000 chars
    ltrim( -- remove leading commas
    REGEXP_REPLACE(REPLACE(
         REPLACE(
           XMLAGG(
             XMLELEMENT("A",col2 )
               ORDER BY col2).getClobVal(),
             '<A>',','),
             '</A>',''),'([^,]+)(,\1)*(,|$)', '\1\3'),
                  ','), -- remove leading XML commas ltrim
                      4000,1) -- limit to 4000 string size
                      , 1)  -- HTML.decode
                       as col2
 from sometable
where rn = 1
group by col1;

V1 - alguns casos de teste - FYI

regexp_replace('2,2,2.1,3,3,4,4','([^,]+)(,\1)+', '\1')
-> 2.1,3,4 Fail
regexp_replace('2 ,2 ,2.1,3 ,3 ,4 ,4 ','([^,]+)(,\1)+', '\1')
-> 2 ,2.1,3,4 Success  - fixed length items

V2 - itens contidos em itens, por exemplo. 2,21

regexp_replace('2.1,1','([^,]+)(,\1)+', '\1')
-> 2.1 Fail
regexp_replace('2 ,2 ,2.1,1 ,3 ,4 ,4 ','(^|,)(.+)(,\2)+', '\1\2')
-> 2 ,2.1,1 ,3 ,4  -- success - NEW regex
 regexp_replace('a,b,b,b,b,c','(^|,)(.+)(,\2)+', '\1\2')
-> a,b,b,c fail!

v3 - regex graças a Igor! funciona todos os casos.

select  
regexp_replace('2,2,2.1,3,3,4,4','([^,]+)(,\1)*(,|$)', '\1\3') ,
---> 2,2.1,3,4 works
regexp_replace('2.1,1','([^,]+)(,\1)*(,|$)', '\1\3'),
--> 2.1,1 works
regexp_replace('a,b,b,b,b,c','([^,]+)(,\1)*(,|$)', '\1\3')
---> a,b,c works

from dual
ozmike
fonte
3
Resultado justo, mas não tão simples. Com tamanhos de dados sérios que você encontrará ORA-01489: result of string concatenation is too long.
Pero
1
Eu não diria que é uma solução simples, mas muito atraente. Eu não sabia que o número de correspondência pode ser usado na string de pesquisa, não apenas na string de substituição. Briliant.
Peter Krassoi de
1
Como uma advertência, esse método requer que os valores sejam classificados, de forma que os valores duplicados sejam consecutivos. Caso contrário, ele falha. Mas simples é bom! E estou usando esse método para meu caso particular. Obrigado!
StewS2
2
super simples não funciona por mais de 3 repetições! , por exemplo, a,b,b,b,b,cse tornaria a,b,b,c:-( (Oracle 11.2)
Andreas Dietrich
4
@AndreasDietrich - A solução a seguir parece estar sempre correta:regexp_replace(your_string, '([^,]+)(,\1)*(,|$)', '\1\3')
Egor Skriptunoff
10

você pode usar a wm_concatfunção não documentada .

select col1, wm_concat(distinct col2) col2_list 
from tab1
group by col1;

esta função retorna a coluna clob, se você quiser, pode usar dbms_lob.substrpara converter clob em varchar2.

Kemalettin Erbakırcı
fonte
15
Não, não use isso.
Koshinae
1
Isso era exatamente o que eu precisava e funcionou perfeitamente dentro da minha consulta agregada existente, em vez de envolver essa consulta em uma externa. O que há de errado em usar wm_concat(distinct x)?
Ehryk
1
porque não está documentado e não existe em 12c. mas de qualquer maneira nas versões antigas acho que é a melhor maneira.
Kemalettin Erbakırcı
1
Obrigado @ kemalettinerbakırcı! @thg você deve considerar que se algo não está documentado, você não sabe quais são os seus efeitos colaterais e qualquer outro tipo de coisa que a Documentação diz sobre as funções documentadas; você apenas a usa como uma caixa preta e você só sabe qual alavanca faz o quê baseado no folclore.
Koshinae
4
Nunca use wm_concat. Consulte Por que não usar a função WM_CONCAT no Oracle? .
Lalit Kumar B de
7

Superei esse problema agrupando os valores primeiro e, em seguida, fazendo outra agregação com o listagg. Algo assim:

select a,b,listagg(c,',') within group(order by c) c, avg(d)
from (select a,b,c,avg(d)
      from   table
      group by (a,b,c))
group by (a,b)

apenas um acesso completo à tabela, relativamente fácil de expandir para consultas mais complexas

RonaldM
fonte
6

Se a intenção é aplicar essa transformação a várias colunas, estendi a solução de a_horse_with_no_name:

SELECT * FROM
(SELECT LISTAGG(GRADE_LEVEL, ',') within group(order by GRADE_LEVEL) "Grade Levels" FROM (select distinct GRADE_LEVEL FROM Students) t)                     t1,
(SELECT LISTAGG(ENROLL_STATUS, ',') within group(order by ENROLL_STATUS) "Enrollment Status" FROM (select distinct ENROLL_STATUS FROM Students) t)          t2,
(SELECT LISTAGG(GENDER, ',') within group(order by GENDER) "Legal Gender Code" FROM (select distinct GENDER FROM Students) t)                               t3,
(SELECT LISTAGG(CITY, ',') within group(order by CITY) "City" FROM (select distinct CITY FROM Students) t)                                                  t4,
(SELECT LISTAGG(ENTRYCODE, ',') within group(order by ENTRYCODE) "Entry Code" FROM (select distinct ENTRYCODE FROM Students) t)                             t5,
(SELECT LISTAGG(EXITCODE, ',') within group(order by EXITCODE) "Exit Code" FROM (select distinct EXITCODE FROM Students) t)                                 t6,
(SELECT LISTAGG(LUNCHSTATUS, ',') within group(order by LUNCHSTATUS) "Lunch Status" FROM (select distinct LUNCHSTATUS FROM Students) t)                     t7,
(SELECT LISTAGG(ETHNICITY, ',') within group(order by ETHNICITY) "Race Code" FROM (select distinct ETHNICITY FROM Students) t)                              t8,
(SELECT LISTAGG(CLASSOF, ',') within group(order by CLASSOF) "Expected Graduation Year" FROM (select distinct CLASSOF FROM Students) t)                     t9,
(SELECT LISTAGG(TRACK, ',') within group(order by TRACK) "Track Code" FROM (select distinct TRACK FROM Students) t)                                         t10,
(SELECT LISTAGG(GRADREQSETID, ',') within group(order by GRADREQSETID) "Graduation ID" FROM (select distinct GRADREQSETID FROM Students) t)                 t11,
(SELECT LISTAGG(ENROLLMENT_SCHOOLID, ',') within group(order by ENROLLMENT_SCHOOLID) "School Key" FROM (select distinct ENROLLMENT_SCHOOLID FROM Students) t)       t12,
(SELECT LISTAGG(FEDETHNICITY, ',') within group(order by FEDETHNICITY) "Federal Race Code" FROM (select distinct FEDETHNICITY FROM Students) t)                         t13,
(SELECT LISTAGG(SUMMERSCHOOLID, ',') within group(order by SUMMERSCHOOLID) "Summer School Key" FROM (select distinct SUMMERSCHOOLID FROM Students) t)                               t14,
(SELECT LISTAGG(FEDRACEDECLINE, ',') within group(order by FEDRACEDECLINE) "Student Decl to Prov Race Code" FROM (select distinct FEDRACEDECLINE FROM Students) t)          t15

Este é o Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Produção de 64 bits.
Não consegui usar STRAGG porque não há como DISTINTO e ORDEM.

O desempenho é dimensionado linearmente, o que é bom, pois estou adicionando todas as colunas de interesse. O procedimento acima levou 3 segundos para 77 mil linhas. Por apenas um rollup, 0,172 segundos. Eu sei que havia uma maneira de distinguir várias colunas em uma tabela em uma passagem.

Jeff Humphreys
fonte
6

Se você deseja valores distintos em várias colunas, deseja controle sobre a ordem de classificação, não deseja usar uma função não documentada que pode desaparecer e não deseja mais de uma verificação completa da tabela, você pode achar esta construção útil:

with test_data as 
(
      select 'A' as col1, 'T_a1' as col2, '123' as col3 from dual
union select 'A', 'T_a1', '456' from dual
union select 'A', 'T_a1', '789' from dual
union select 'A', 'T_a2', '123' from dual
union select 'A', 'T_a2', '456' from dual
union select 'A', 'T_a2', '111' from dual
union select 'A', 'T_a3', '999' from dual
union select 'B', 'T_a1', '123' from dual
union select 'B', 'T_b1', '740' from dual
union select 'B', 'T_b1', '846' from dual
)
select col1
     , (select listagg(column_value, ',') within group (order by column_value desc) from table(collect_col2)) as col2s
     , (select listagg(column_value, ',') within group (order by column_value desc) from table(collect_col3)) as col3s
from 
(
select col1
     , collect(distinct col2) as collect_col2
     , collect(distinct col3) as collect_col3
from test_data
group by col1
);
user3853599
fonte
1
Você pode economizar mais tempo se substituir "union" s por "union all".
burkay
4

Que tal criar uma função dedicada que tornará a parte "distinta":

create or replace function listagg_distinct (t in str_t, sep IN VARCHAR2 DEFAULT ',') 
  return VARCHAR2
as 
  l_rc VARCHAR2(4096) := '';
begin
  SELECT listagg(val, sep) WITHIN GROUP (ORDER BY 1)
    INTO l_rc
    FROM (SELECT DISTINCT column_value val FROM table(t));
  RETURN l_rc;
end;
/

E então use-o para fazer a agregação:

SELECT col1, listagg_distinct(cast(collect(col_2) as str_t ), ', ')
  FROM your_table
  GROUP BY col_1;
flupke
fonte
4

Para contornar o problema do comprimento da string, você pode usar XMLAGGqual é semelhante a, listaggmas retorna um clob.

Você pode então analisar usando regexp_replacee obter os valores exclusivos e, em seguida, transformá-lo novamente em uma string usando dbms_lob.substr(). Se você tiver uma grande quantidade de valores distintos, ainda assim ficará sem espaço, mas para muitos casos o código abaixo deve funcionar.

Você também pode alterar os delimitadores que usa. No meu caso, eu queria '-' em vez de ',' mas você deve ser capaz de substituir os travessões no meu código e usar vírgulas, se quiser.

select col1,
    dbms_lob.substr(ltrim(REGEXP_REPLACE(REPLACE(
         REPLACE(
           XMLAGG(
             XMLELEMENT("A",col2)
               ORDER BY col2).getClobVal(),
             '<A>','-'),
             '</A>',''),'([^-]*)(-\1)+($|-)', 
           '\1\3'),'-'), 4000,1) as platform_mix
from table
user3465996
fonte
Essa é uma ótima ideia, precisa chamar dbms_xmlgen.convert (string, 1) para remover e & -> & amp conversões. Veja o link da
ozmike
3

Refinando ainda mais a correção de @YoYo para abordagem baseada em row_number () de @ a_horse_with_no_name usando DECODE vs CASE ( eu vi aqui ). Vejo que @Martin Vrbovsky também tem essa resposta de abordagem de caso.

select
  col1, 
  listagg(col2, ',') within group (order by col2) AS col2_list,
  listagg(col3, ',') within group (order by col3) AS col3_list,
  SUM(col4) AS col4
from (
  select
    col1, 
    decode(row_number() over (partition by col1, col2 order by null),1,col2) as col2,
    decode(row_number() over (partition by col1, col3 order by null),1,col3) as col3
  from foo
)
group by col1;
Beej
fonte
2

O próximo Oracle 19c oferecerá suporte DISTINCTcom LISTAGG.

LISTAGG com opção DISTINCT :

Este recurso está vindo com 19c:

SQL> select deptno, listagg (distinct sal,', ') within group (order by sal)  
  2  from scott.emp  
  3  group by deptno;  

EDITAR:

Oracle 19C LISTAGG DISTINCT

A função de agregação LISTAGG agora oferece suporte à eliminação de duplicatas usando a nova palavra-chave DISTINCT. A função de agregação LISTAGG ordena as linhas para cada grupo em uma consulta de acordo com a expressão ORDER BY e, a seguir, concatena os valores em uma única string. Com a nova palavra-chave DISTINCT, os valores duplicados podem ser removidos da expressão especificada antes da concatenação em uma única string. Isso elimina a necessidade de criar um processamento de consulta complexo para encontrar os valores distintos antes de usar a função LISTAGG agregada. Com a opção DISTINCT, o processamento para remover valores duplicados pode ser feito diretamente na função LISTAGG. O resultado é um SQL mais simples, rápido e eficiente.

Lukasz Szozda
fonte
0

Alguém já pensou em usar uma cláusula PARTITION BY? Funcionou para mim nesta consulta para obter uma lista de serviços de aplicativo e o acesso.

SELECT DISTINCT T.APP_SVC_ID, 
       LISTAGG(RTRIM(T.ACCESS_MODE), ',') WITHIN GROUP(ORDER BY T.ACCESS_MODE) OVER(PARTITION BY T.APP_SVC_ID) AS ACCESS_MODE 
  FROM APP_SVC_ACCESS_CNTL T 
 GROUP BY T.ACCESS_MODE, T.APP_SVC_ID

Tive de cortar minha cláusula where para o NDA, mas essa é a ideia.

James
fonte
Não entendo como essa consulta leva itens distintos para o LISTAGG. Parece que você teria apenas um T.ACCESS_MODEpor linha, já que está agrupando por ele?
jpmc26
0

Acho que isso poderia ajudar - CASE o valor das colunas para NULL se for duplicado - então não é anexado à string LISTAGG:

with test_data as 
(
      select 1 as col1, 2 as col2, 'Smith' as created_by from dual
union select 1, 2, 'John' from dual
union select 1, 3, 'Ajay' from dual
union select 1, 4, 'Ram' from dual
union select 1, 5, 'Jack' from dual
union select 2, 5, 'Smith' from dual
union select 2, 6, 'John' from dual
union select 2, 6, 'Ajay' from dual
union select 2, 6, 'Ram' from dual
union select 2, 7, 'Jack' from dual
)
SELECT col1  ,
      listagg(col2 , ',') within group (order by col2 ASC) AS orig_value,
      listagg(CASE WHEN rwn=1 THEN col2 END , ',') within group (order by col2 ASC) AS distinct_value
from 
    (
    select row_number() over (partition by col1,col2 order by 1) as rwn, 
           a.*
    from test_data a
    ) a
GROUP BY col1   

Resulta em:

COL1  ORIG         DISTINCT
1   2,2,3,4,5   2,3,4,5
2   5,6,6,6,7   5,6,7
Martin Vrbovsky
fonte
0

listagg () ignora valores NULL, então em uma primeira etapa você poderia usar a função lag () para analisar se o registro anterior tinha o mesmo valor, se sim então NULL, senão 'novo valor'.

WITH tab AS 
(           
          SELECT 1 as col1, 2 as col2, 'Smith' as created_by FROM dual
UNION ALL SELECT 1 as col1, 2 as col2, 'John'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 3 as col2, 'Ajay'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 4 as col2, 'Ram'   as created_by FROM dual
UNION ALL SELECT 1 as col1, 5 as col2, 'Jack'  as created_by FROM dual
)
SELECT col1
     , CASE 
       WHEN lag(col2) OVER (ORDER BY col2) = col2 THEN 
         NULL 
       ELSE 
         col2 
       END as col2_with_nulls
     , created_by
  FROM tab;

Resultados

      COL1 COL2_WITH_NULLS CREAT
---------- --------------- -----
         1               2 Smith
         1                 John
         1               3 Ajay
         1               4 Ram
         1               5 Jack

Observe que o segundo 2 é substituído por NULL. Agora você pode envolver um SELECT com listagg ().

WITH tab AS 
(           
          SELECT 1 as col1, 2 as col2, 'Smith' as created_by FROM dual
UNION ALL SELECT 1 as col1, 2 as col2, 'John'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 3 as col2, 'Ajay'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 4 as col2, 'Ram'   as created_by FROM dual
UNION ALL SELECT 1 as col1, 5 as col2, 'Jack'  as created_by FROM dual
)
SELECT listagg(col2_with_nulls, ',') WITHIN GROUP (ORDER BY col2_with_nulls) col2_list
  FROM ( SELECT col1
              , CASE WHEN lag(col2) OVER (ORDER BY col2) = col2 THEN NULL ELSE col2 END as col2_with_nulls
              , created_by
           FROM tab );

Resultado

COL2_LIST
---------
2,3,4,5

Você também pode fazer isso em várias colunas.

WITH tab AS 
(           
          SELECT 1 as col1, 2 as col2, 'Smith' as created_by FROM dual
UNION ALL SELECT 1 as col1, 2 as col2, 'John'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 3 as col2, 'Ajay'  as created_by FROM dual
UNION ALL SELECT 1 as col1, 4 as col2, 'Ram'   as created_by FROM dual
UNION ALL SELECT 1 as col1, 5 as col2, 'Jack'  as created_by FROM dual
)
SELECT listagg(col1_with_nulls, ',') WITHIN GROUP (ORDER BY col1_with_nulls) col1_list
     , listagg(col2_with_nulls, ',') WITHIN GROUP (ORDER BY col2_with_nulls) col2_list
     , listagg(created_by, ',')      WITHIN GROUP (ORDER BY created_by) created_by_list
  FROM ( SELECT CASE WHEN lag(col1) OVER (ORDER BY col1) = col1 THEN NULL ELSE col1 END as col1_with_nulls
              , CASE WHEN lag(col2) OVER (ORDER BY col2) = col2 THEN NULL ELSE col2 END as col2_with_nulls
              , created_by
           FROM tab );

Resultado

COL1_LIST COL2_LIST CREATED_BY_LIST
--------- --------- -------------------------
1         2,3,4,5   Ajay,Jack,John,Ram,Smith
lxxxvi
fonte
0

Você pode fazer isso por meio da substituição RegEx. Aqui está um exemplo:

-- Citations Per Year - Cited Publications main query. Includes list of unique associated core project numbers, ordered by core project number.
SELECT ptc.pmid AS pmid, ptc.pmc_id, ptc.pub_title AS pubtitle, ptc.author_list AS authorlist,
  ptc.pub_date AS pubdate,
  REGEXP_REPLACE( LISTAGG ( ppcc.admin_phs_org_code || 
    TO_CHAR(ppcc.serial_num,'FM000000'), ',') WITHIN GROUP (ORDER BY ppcc.admin_phs_org_code || 
    TO_CHAR(ppcc.serial_num,'FM000000')),
    '(^|,)(.+)(,\2)+', '\1\2')
  AS projectNum
FROM publication_total_citations ptc
  JOIN proj_paper_citation_counts ppcc
    ON ptc.pmid = ppcc.pmid
   AND ppcc.citation_year = 2013
  JOIN user_appls ua
    ON ppcc.admin_phs_org_code = ua.admin_phs_org_code
   AND ppcc.serial_num = ua.serial_num
   AND ua.login_id = 'EVANSF'
GROUP BY ptc.pmid, ptc.pmc_id, ptc.pub_title, ptc.author_list, ptc.pub_date
ORDER BY pmid;

Também postado aqui: Oracle - valores Listagg exclusivos

DKroot
fonte
0

Use a função listagg_clob criada assim:

create or replace package list_const_p
is
list_sep varchar2(10) := ',';
end list_const_p;
/
sho err

create type listagg_clob_t as object(
v_liststring varchar2(32767),
v_clob clob,
v_templob number,

static function ODCIAggregateInitialize(
sctx IN OUT listagg_clob_t
) return number,
member function ODCIAggregateIterate(
self IN OUT listagg_clob_t, value IN varchar2
) return number,
member function ODCIAggregateTerminate(
self IN OUT listagg_clob_t, returnValue OUT clob, flags IN number
) return number,
member function ODCIAggregateMerge(
self IN OUT listagg_clob_t, ctx2 IN OUT listagg_clob_t
) return number
);
/
sho err

create or replace type body listagg_clob_t is

static function ODCIAggregateInitialize(sctx IN OUT listagg_clob_t)
return number is
begin
sctx := listagg_clob_t('', '', 0);
return ODCIConst.Success;
end;

member function ODCIAggregateIterate(
self IN OUT listagg_clob_t,
value IN varchar2
) return number is
begin
if nvl(lengthb(v_liststring),0) + nvl(lengthb(value),0) <= 4000 then
self.v_liststring:=self.v_liststring || value || list_const_p.list_sep;
else
if self.v_templob = 0 then
dbms_lob.createtemporary(self.v_clob, true, dbms_lob.call);
self.v_templob := 1;
end if;
dbms_lob.writeappend(self.v_clob, length(self.v_liststring), v_liststring);
self.v_liststring := value || list_const_p.list_sep;
end if;
return ODCIConst.Success;
end;

member function ODCIAggregateTerminate(
self IN OUT listagg_clob_t,
returnValue OUT clob,
flags IN number
) return number is
begin
if self.v_templob != 0 then
dbms_lob.writeappend(self.v_clob, length(self.v_liststring), self.v_liststring);
dbms_lob.trim(self.v_clob, dbms_lob.getlength(self.v_clob) - 1);
else
self.v_clob := substr(self.v_liststring, 1, length(self.v_liststring) - 1);
end if;
returnValue := self.v_clob;
return ODCIConst.Success;
end;

member function ODCIAggregateMerge(self IN OUT listagg_clob_t, ctx2 IN OUT listagg_clob_t) return number is
begin
if ctx2.v_templob != 0 then
if self.v_templob != 0 then
dbms_lob.append(self.v_clob, ctx2.v_clob);
dbms_lob.freetemporary(ctx2.v_clob);
ctx2.v_templob := 0;
else
self.v_clob := ctx2.v_clob;
self.v_templob := 1;
ctx2.v_clob := '';
ctx2.v_templob := 0;
end if;
end if;
if nvl(lengthb(self.v_liststring),0) + nvl(lengthb(ctx2.v_liststring),0) <= 4000 then
self.v_liststring := self.v_liststring || ctx2.v_liststring;
ctx2.v_liststring := '';
else
if self.v_templob = 0 then
dbms_lob.createtemporary(self.v_clob, true, dbms_lob.call);
self.v_templob := 1;
end if;
dbms_lob.writeappend(self.v_clob, length(self.v_liststring), self.v_liststring);
dbms_lob.writeappend(self.v_clob, length(ctx2.v_liststring), ctx2.v_liststring);
self.v_liststring := '';
ctx2.v_liststring := '';
end if;
return ODCIConst.Success;
end;
end;
/
sho err

CREATE or replace FUNCTION listagg_clob (input varchar2) RETURN clob
PARALLEL_ENABLE AGGREGATE USING listagg_clob_t;
/
sho err 

Cornel
fonte
0

Eu escrevi uma função para lidar com isso usando expressões regulares. Os parâmetros in são: 1) a própria chamada listagg 2) Uma repetição do delimitador

create or replace function distinct_listagg
  (listagg_in varchar2,
   delimiter_in varchar2)

   return varchar2
   as
   hold_result varchar2(4000);
   begin

   select rtrim( regexp_replace( (listagg_in)
      , '([^'||delimiter_in||']*)('||
      delimiter_in||'\1)+($|'||delimiter_in||')', '\1\3'), ',')
      into hold_result
      from dual;

return hold_result;

end;

Agora você não precisa repetir a expressão regular toda vez que fizer isso, simplesmente diga:

select distinct_listagg(
                       listagg(myfield,', ') within group (order by 1),
                       ', '
                       )
     from mytable;
Flemack
fonte
0

Se você não precisa de uma ordem específica de valores concatenados e o separador pode ser uma vírgula, você pode fazer:

select col1, stragg(distinct col2)
  from table
 group by col1
mik
fonte
0

Eu precisei de uma versão DISTINTA disso e fiz esta funcionar.

RTRIM(REGEXP_REPLACE(
                       (value, ', ') WITHIN GROUP( ORDER BY value)), 
                            '([^ ]+)(, \1)+','\1'),', ') 
Roberto franco
fonte
0

Um aspecto irritante com LISTAGGisso é que se o comprimento total da string concatenada exceder 4.000 caracteres (limite para VARCHAR2em SQL), o erro abaixo é gerado, o que é difícil de gerenciar nas versões do Oracle até 12.1

ORA-01489: o resultado da concatenação da string é muito longo

Um novo recurso adicionado em 12cR2 é a ON OVERFLOWcláusula de LISTAGG. A consulta que inclui esta cláusula seria semelhante a:

SELECT pid, LISTAGG(Desc, ' ' on overflow truncate) WITHIN GROUP (ORDER BY seq) AS desc
FROM B GROUP BY pid;

O procedimento acima restringirá a saída a 4000 caracteres, mas não gerará o ORA-01489erro.

Estas são algumas das opções adicionais da ON OVERFLOWcláusula:

  • ON OVERFLOW TRUNCATE 'Contd..' : Isso será exibido 'Contd..'no final da string (o padrão é ...)
  • ON OVERFLOW TRUNCATE '' : Isso exibirá os 4000 caracteres sem nenhuma string de terminação.
  • ON OVERFLOW TRUNCATE WITH COUNT: Isso exibirá o número total de caracteres no final, após os caracteres de terminação. Por exemplo:- '...(5512) '
  • ON OVERFLOW ERROR: Se você espera que o LISTAGGfalhe com o ORA-01489erro (que é o padrão de qualquer maneira).
Kaushik Nayak
fonte
0

Implementei esta função armazenada:

CREATE TYPE LISTAGG_DISTINCT_PARAMS AS OBJECT (ELEMENTO VARCHAR2(2000), SEPARATORE VARCHAR2(10));

CREATE TYPE T_LISTA_ELEMENTI AS TABLE OF VARCHAR2(2000);

CREATE TYPE T_LISTAGG_DISTINCT AS OBJECT (

    LISTA_ELEMENTI T_LISTA_ELEMENTI,
        SEPARATORE VARCHAR2(10),

    STATIC FUNCTION ODCIAGGREGATEINITIALIZE(SCTX  IN OUT            T_LISTAGG_DISTINCT) 
                    RETURN NUMBER,

    MEMBER FUNCTION ODCIAGGREGATEITERATE   (SELF  IN OUT            T_LISTAGG_DISTINCT, 
                                            VALUE IN                    LISTAGG_DISTINCT_PARAMS ) 
                    RETURN NUMBER,

    MEMBER FUNCTION ODCIAGGREGATETERMINATE (SELF         IN     T_LISTAGG_DISTINCT,
                                            RETURN_VALUE OUT    VARCHAR2, 
                                            FLAGS        IN     NUMBER      )
                    RETURN NUMBER,

    MEMBER FUNCTION ODCIAGGREGATEMERGE       (SELF               IN OUT T_LISTAGG_DISTINCT,
                                                                                        CTX2                 IN         T_LISTAGG_DISTINCT    )
                    RETURN NUMBER
);

CREATE OR REPLACE TYPE BODY T_LISTAGG_DISTINCT IS 

    STATIC FUNCTION ODCIAGGREGATEINITIALIZE(SCTX IN OUT T_LISTAGG_DISTINCT) RETURN NUMBER IS 
    BEGIN
                SCTX := T_LISTAGG_DISTINCT(T_LISTA_ELEMENTI() , ',');
        RETURN ODCICONST.SUCCESS;
    END;

    MEMBER FUNCTION ODCIAGGREGATEITERATE(SELF IN OUT T_LISTAGG_DISTINCT, VALUE IN LISTAGG_DISTINCT_PARAMS) RETURN NUMBER IS
    BEGIN

                IF VALUE.ELEMENTO IS NOT NULL THEN
                        SELF.LISTA_ELEMENTI.EXTEND;
                        SELF.LISTA_ELEMENTI(SELF.LISTA_ELEMENTI.LAST) := TO_CHAR(VALUE.ELEMENTO);
                        SELF.LISTA_ELEMENTI:= SELF.LISTA_ELEMENTI MULTISET UNION DISTINCT SELF.LISTA_ELEMENTI;
                        SELF.SEPARATORE := VALUE.SEPARATORE;
                END IF;
        RETURN ODCICONST.SUCCESS;
    END;

    MEMBER FUNCTION ODCIAGGREGATETERMINATE(SELF IN T_LISTAGG_DISTINCT, RETURN_VALUE OUT VARCHAR2, FLAGS IN NUMBER) RETURN NUMBER IS
      STRINGA_OUTPUT            CLOB:='';
            LISTA_OUTPUT                T_LISTA_ELEMENTI;
            TERMINATORE                 VARCHAR2(3):='...';
            LUNGHEZZA_MAX           NUMBER:=4000;
    BEGIN

                IF SELF.LISTA_ELEMENTI.EXISTS(1) THEN -- se esiste almeno un elemento nella lista

                        -- inizializza una nuova lista di appoggio
                        LISTA_OUTPUT := T_LISTA_ELEMENTI();

                        -- riversamento dei soli elementi in DISTINCT
                        LISTA_OUTPUT := SELF.LISTA_ELEMENTI MULTISET UNION DISTINCT SELF.LISTA_ELEMENTI;

                        -- ordinamento degli elementi
                        SELECT CAST(MULTISET(SELECT * FROM TABLE(LISTA_OUTPUT) ORDER BY 1 ) AS T_LISTA_ELEMENTI ) INTO LISTA_OUTPUT FROM DUAL;

                        -- concatenazione in una stringa                        
                        FOR I IN LISTA_OUTPUT.FIRST .. LISTA_OUTPUT.LAST - 1
                        LOOP
                            STRINGA_OUTPUT := STRINGA_OUTPUT || LISTA_OUTPUT(I) || SELF.SEPARATORE;
                        END LOOP;
                        STRINGA_OUTPUT := STRINGA_OUTPUT || LISTA_OUTPUT(LISTA_OUTPUT.LAST);

                        -- se la stringa supera la dimensione massima impostata, tronca e termina con un terminatore
                        IF LENGTH(STRINGA_OUTPUT) > LUNGHEZZA_MAX THEN
                                    RETURN_VALUE := SUBSTR(STRINGA_OUTPUT, 0, LUNGHEZZA_MAX - LENGTH(TERMINATORE)) || TERMINATORE;
                        ELSE
                                    RETURN_VALUE:=STRINGA_OUTPUT;
                        END IF;

                ELSE -- se non esiste nessun elemento, restituisci NULL

                        RETURN_VALUE := NULL;

                END IF;

        RETURN ODCICONST.SUCCESS;
    END;

    MEMBER FUNCTION ODCIAGGREGATEMERGE(SELF IN OUT T_LISTAGG_DISTINCT, CTX2 IN T_LISTAGG_DISTINCT) RETURN NUMBER IS
    BEGIN
        RETURN ODCICONST.SUCCESS;
    END;

END; -- fine corpo

CREATE
FUNCTION LISTAGG_DISTINCT (INPUT LISTAGG_DISTINCT_PARAMS) RETURN VARCHAR2
    PARALLEL_ENABLE AGGREGATE USING T_LISTAGG_DISTINCT;

// Example
SELECT LISTAGG_DISTINCT(LISTAGG_DISTINCT_PARAMS(OWNER, ', ')) AS LISTA_OWNER
FROM SYS.ALL_OBJECTS;

Sinto muito, mas em alguns casos (para um conjunto muito grande), o Oracle pode retornar este erro:

Object or Collection value was too large. The size of the value
might have exceeded 30k in a SORT context, or the size might be
too big for available memory.

mas acho que este é um bom ponto de partida;)

Massimiliano Palese
fonte
0

select col1, listaggr(col2,',') within group(Order by col2) from table group by col1 significando agregar as strings (col2) na lista mantendo a ordem n, então depois lidar com as duplicatas como grupo por col1, significando mesclar duplicatas col1 em 1 grupo. talvez isto pareça limpo e simples como deveria ser e se no caso de você desejar col3 também, você precisa adicionar mais um listagg () que éselect col1, listaggr(col2,',') within group(Order by col2),listaggr(col3,',') within group(order by col3) from table group by col1

Himanshu Ahuja
fonte
0

Usando SELECT DISTINCT ... como parte de uma subconsulta antes de chamar LISTAGG é provavelmente a melhor maneira para consultas simples, conforme observado por @a_horse_with_no_name

No entanto, em consultas mais complexas, pode não ser possível ou fácil de fazer isso. Eu fiz isso surgir em um cenário que estava usando a abordagem top-n usando uma função analítica.

Então eu encontrei a COLLECTfunção agregada. Está documentado que o modificador UNIQUEou está DISTINCTdisponível. Somente em 10g , ele falha silenciosamente (ignora o modificador sem erro). Porém, para superar isso, a partir de outra resposta , cheguei a esta solução:

SELECT
  ...
  (
    SELECT LISTAGG(v.column_value,',') WITHIN GROUP (ORDER BY v.column_value)
    FROM TABLE(columns_tab) v
  ) AS columns,
  ...
FROM (
  SELECT
    ...
    SET(CAST(COLLECT(UNIQUE some_column ORDER BY some_column) AS tab_typ)) AS columns_tab,
    ...
)

Basicamente, ao usar SET, eu removo as duplicatas da minha coleção.

Você ainda precisaria definir o tab_typcomo um tipo de coleção básico e, no caso de a VARCHAR, seria, por exemplo:

CREATE OR REPLACE type tab_typ as table of varchar2(100)
/

Também como uma correção para a resposta de @a_horse_with_no_name na situação de várias colunas, onde você pode querer agregar ainda em uma terceira (ou mais) colunas:

select
  col1, 
  listagg(CASE rn2 WHEN 1 THEN col2 END, ',') within group (order by col2) AS col2_list,
  listagg(CASE rn3 WHEN 1 THEN col3 END, ',') within group (order by col3) AS col3_list,
  SUM(col4) AS col4
from (
  select
    col1, 
    col2,
    row_number() over (partition by col1, col2 order by null) as rn2,
    row_number() over (partition by col1, col3 order by null) as rn3
  from foo
)
group by col1;

Se você deixasse rn = 1como uma condição where para a consulta, agregaria outras colunas incorretamente.

YoYo
fonte
0

Muito simples - use em sua consulta uma subconsulta com uma seleção distinta:

SELECT question_id,
       LISTAGG(element_id, ',') WITHIN GROUP (ORDER BY element_id)
FROM
       (SELECT distinct question_id, element_id
       FROM YOUR_TABLE)
GROUP BY question_id;
Wim Folkerts
fonte
-1

A maneira mais simples de lidar com vários listagg's é usar 1 WITH (fator de subconsulta) por coluna contendo um listagg dessa coluna a partir de um select distinto:

    WITH tab AS 
    (           
        SELECT 1 as col1, 2 as col2, 3 as col3, 'Smith' as created_by FROM dual
        UNION ALL SELECT 1 as col1, 2 as col2, 3 as col3,'John'  as created_by FROM dual
        UNION ALL SELECT 1 as col1, 3 as col2, 4 as col3,'Ajay'  as created_by FROM dual
        UNION ALL SELECT 1 as col1, 4 as col2, 4 as col3,'Ram'   as created_by FROM dual
        UNION ALL SELECT 1 as col1, 5 as col2, 6 as col3,'Jack'  as created_by FROM dual
    )
    , getCol2 AS
    (
        SELECT  DISTINCT col1, listagg(col2,',') within group (order by col2)  over (partition by col1) AS col2List
        FROM ( SELECT DISTINCT col1,col2 FROM tab)
    )
    , getCol3 AS
    (
        SELECT  DISTINCT col1, listagg(col3,',') within group (order by col3)  over (partition by col1) AS col3List
        FROM ( SELECT DISTINCT col1,col3 FROM tab)
    )
    select col1,col2List,col3List
    FROM getCol2
    JOIN getCol3
    using (col1)

Que dá:

col1  col2List  col3List
1     2,3,4,5   3,4,6
DS.
fonte