LISTAGG in Oracle per restituire valori distinti


94

Sto cercando di utilizzare la LISTAGGfunzione in Oracle. Vorrei ottenere solo i valori distinti per quella colonna. C'è un modo in cui posso ottenere solo i valori distinti senza creare una funzione o una procedura?

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

Devo selezionare col1 e LISTAGGcol2 (la colonna 3 non è considerata). Quando lo faccio, ottengo qualcosa di simile come risultato di LISTAGG: [2,2,3,4,5]

Devo rimuovere il duplicato "2" qui; Ho bisogno solo dei valori distinti di col2 contro col1.



Potete mostrare l'uscita prevista (righe) dal campione? Cosa vuoi vedere se c'è più di un valore per col1?
a_horse_with_no_name

L'output atteso del LISTAGG è [2,3,4,5]. Il secondo "2" dovrebbe essere rimosso. E la mia tabella ha più di 1000 righe.
Priyanth

Cosa vuoi vedere se c'è più di un valore per col1?
a_horse_with_no_name

Il codice è così: - SELEZIONA col1, LISTAGG (col2, ',') all'interno del gruppo (ordina per col2) DALLA tabella T DOVE .... Quindi, dovrebbe mostrare tutti i valori distinti di col2 corrispondenti a col1, separati da virgola.
Priyanth

Risposte:


77

19c e versioni successive:

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

18c e precedenti:

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

Se hai bisogno di più colonne, qualcosa del genere potrebbe essere quello che stai cercando:

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;

2
Simile a quello che avevo in mente anche io. Se listaggè l'unica funzione di aggregazione nella query, dovrebbe funzionare. Combinarlo con altre funzioni aggregate, tuttavia, è più complicato.
Andriy M

Sì. La mia domanda è simile a questa.
Priyanth

1
@a_horse_with_no_name: L'istruzione select sopra fornisce valori duplicati per me. Voglio rimuovere i duplicati. col1 col2 Creato da 1 2 Smith 1 2 John 1 3 Ajay 1 4 Ram 1 5 Jack Devo selezionare col1 e il LISTAGG di col2 (la colonna 3 non è considerata). Mentre lo faccio otterrò qualcosa del genere come risultato di LISTAGG: -> [2,2,3,4,5] Devo rimuovere il duplicato "2" qui. Ho bisogno solo dei valori distinti di col2 contro col1 .
Priyanth

@a_horse_with_no_name: ho provato il codice- e ho ricevuto il messaggio di errore come sotto ORA-01489: il risultato della concatenazione di stringhe è troppo lungo 01489. 00000 - "il risultato della concatenazione di stringhe è troppo lungo" * Causa: il risultato della concatenazione di stringhe è superiore al massimo taglia.
Priyanth

@ Priyanth: allora sei sfortunato. La lunghezza totale supera i 4000 byte e Oracle non può gestirla. Sarà necessario eseguire l'aggregazione nel codice dell'applicazione.
a_horse_with_no_name

47

Ecco come risolvere il tuo problema.

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

from dual

ritorna

2,2.1,3,4

È costruito nell'oracolo del XIX sec., Vedi qui

Dal XVIII secolo in poi, prova all'interno del gruppo vedi qui

Altrimenti usa espressioni regolari

RISPOSTA di seguito:

select col1, 

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

Nota: quanto sopra funzionerà nella maggior parte dei casi: l'elenco dovrebbe essere ordinato, potrebbe essere necessario tagliare tutto lo spazio iniziale e finale a seconda dei dati.

Se hai molti elementi in un gruppo> 20 o stringhe di grandi dimensioni, potresti incappare nel limite di dimensione della stringa Oracle "il risultato della concatenazione di stringhe è troppo lungo".

Da Oracle 12cR2 puoi sopprimere questo errore vedi qui . In alternativa, inserisci un numero massimo di membri in ogni gruppo. Funzionerà solo se è consentito elencare solo i primi membri. Se hai stringhe variabili molto lunghe, questo potrebbe non funzionare. dovrai sperimentare.

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;

Un'altra soluzione (non così semplice) per evitare, si spera, il limite di dimensione della stringa Oracle: la dimensione della stringa è limitata a 4000. Grazie a questo post qui di user3465996

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 - alcuni casi di test - 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 -items contenuti all'interno di elementi es. 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 grazie Igor! funziona in tutti i casi.

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

3
Risultato giusto, ma non così semplice. Con dimensioni di dati importanti in cui ti imbatterai ORA-01489: result of string concatenation is too long.
Pero

1
Non la definirei una soluzione semplice ma molto attraente. Non sapevo che il numero di corrispondenza potesse essere utilizzato nella stringa di ricerca non solo nella stringa di sostituzione. Brillante.
Peter Krassoi

1
Come avvertimento, questo metodo richiede che i valori siano ordinati, in modo che i valori duplicati siano consecutivi. Altrimenti fallisce. Ma semplice è buono! E sto usando questo metodo per il mio caso particolare. Grazie!
StewS2

2
super semplice non funziona per più di 3 ripetizioni! , ad esempio a,b,b,b,b,c, diventerebbe a,b,b,c:-( (Oracle 11.2)
Andreas Dietrich

4
@AndreasDietrich - La seguente soluzione sembra essere sempre corretta:regexp_replace(your_string, '([^,]+)(,\1)*(,|$)', '\1\3')
Egor Skriptunoff

10

puoi usare la wm_concatfunzione non documentata .

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

questa funzione restituisce la colonna clob, se vuoi puoi usarla dbms_lob.substrper convertire clob in varchar2.


15
No, non usarlo.
Koshinae

1
Questo era esattamente ciò di cui avevo bisogno e funzionava perfettamente all'interno della mia query aggregata esistente invece di avvolgere quella query in una più esterna. Cosa c'è di sbagliato nell'usare wm_concat(distinct x)?
Ehryk

1
perché non è documentato e non esiste su 12c. ma comunque sulle vecchie versioni penso che sia il modo migliore.
Kemalettin Erbakırcı

1
Grazie @ kemalettinerbakırcı! @thg dovresti considerare che se qualcosa non è documentato, non sai quali sono i suoi effetti collaterali e qualsiasi altro tipo di cose che la Documentazione ti dice sulle funzioni documentate; lo usi solo come una scatola nera e sai solo quale leva fa cosa in base al folklore.
Koshinae


7

Ho superato questo problema raggruppando prima i valori, quindi eseguendo un'altra aggregazione con listagg. Qualcosa come questo:

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)

solo un accesso completo alla tabella, relativamente facile da espandere a query più complesse


6

Se l'intento è applicare questa trasformazione a più colonne, ho esteso la soluzione 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

Questo è Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Produzione a 64 bit.
Non sono riuscito a usare STRAGG perché non c'è modo di DISTINCT e ORDER.

Le prestazioni vengono ridimensionate in modo lineare, il che è positivo, poiché aggiungo tutte le colonne di interesse. Quanto sopra ha richiesto 3 secondi per 77.000 righe. Per un solo rollup, 0,172 secondi. Però c'era un modo per distinguere più colonne in una tabella in un unico passaggio.


6

Se desideri valori distinti su MULTIPLE colonne, desideri il controllo sull'ordinamento, non desideri utilizzare una funzione non documentata che potrebbe scomparire e non desideri più di una scansione completa della tabella, potresti trovare utile questo costrutto:

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
);

1
Puoi risparmiare un po 'di tempo se sostituisci "union" con "union all".
burkay

4

Che ne dici di creare una funzione dedicata che renderà la 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 poi usalo per fare l'aggregazione:

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

4

Per aggirare il problema della lunghezza delle stringhe puoi usare XMLAGGche è simile alistagg ma restituisce un clob.

È quindi possibile analizzare utilizzando regexp_replacee ottenere i valori univoci e quindi trasformarli di nuovo in una stringa utilizzando dbms_lob.substr(). Se si dispone di una quantità enorme di valori distinti, lo spazio sarà comunque esaurito in questo modo, ma per molti casi il codice seguente dovrebbe funzionare.

Puoi anche modificare i delimitatori che utilizzi. Nel mio caso volevo "-" invece di "," ma dovresti essere in grado di sostituire i trattini nel mio codice e utilizzare le virgole se lo desideri.

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

Questa è un'ottima idea per chiamare dbms_xmlgen.convert (string, 1) per rimuovere e & -> & amp conversioni. Vedi il mio post link
ozmike

3

Ulteriore perfezionamento della correzione di @ YoYo all'approccio basato su row_number () di @ a_horse_with_no_name utilizzando DECODE vs CASE ( ho visto qui ). Vedo che @Martin Vrbovsky ha anche questa risposta sull'approccio al 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;

2

Il prossimo Oracle 19c supporterà DISTINCTcon LISTAGG.

LISTAGG con opzione DISTINCT :

Questa funzione è disponibile con 19c:

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

MODIFICARE:

Oracle 19C LISTAGG DISTINCT

La funzione di aggregazione LISTAGG ora supporta l'eliminazione dei duplicati utilizzando la nuova parola chiave DISTINCT. La funzione di aggregazione LISTAGG ordina le righe per ogni gruppo in una query in base all'espressione ORDER BY e quindi concatena i valori in una singola stringa. Con la nuova parola chiave DISTINCT, i valori duplicati possono essere rimossi dall'espressione specificata prima della concatenazione in una singola stringa. Ciò elimina la necessità di creare complesse elaborazioni di query per trovare i valori distinti prima di utilizzare la funzione aggregata LISTAGG. Con l'opzione DISTINCT, l'elaborazione per rimuovere i valori duplicati può essere eseguita direttamente all'interno della funzione LISTAGG. Il risultato è un SQL più semplice, veloce ed efficiente.


0

Qualcuno ha pensato di utilizzare una clausola PARTITION BY? In questa query ha funzionato per me ottenere un elenco di servizi applicativi e l'accesso.

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

Ho dovuto tagliare la mia clausola Where per l'accordo di non divulgazione, ma hai capito.


Non capisco come questa query richieda elementi distinti per LISTAGG. Sembra che ne avresti solo uno T.ACCESS_MODEper riga dato che lo stai raggruppando?
jpmc26

0

Penso che questo potrebbe aiutare - CASE il valore delle colonne su NULL se è duplicato - quindi non viene aggiunto alla stringa 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   

Risultati in:

COL1  ORIG         DISTINCT
1   2,2,3,4,5   2,3,4,5
2   5,6,6,6,7   5,6,7

0

listagg () ignora i valori NULL, quindi in un primo passaggio è possibile utilizzare la funzione lag () per analizzare se il record precedente aveva lo stesso valore, se sì, allora NULL, altrimenti 'nuovo valore'.

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;

Risultati

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

Notare che il secondo 2 è sostituito da NULL. Ora puoi racchiudere un SELECT con listagg () attorno ad esso.

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 );

Risultato

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

Puoi farlo anche su più colonne.

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 );

Risultato

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

0

Puoi farlo tramite la sostituzione RegEx. Ecco un esempio:

-- 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;

Pubblicato anche qui: Oracle - valori Listagg univoci


0

Usa la funzione listagg_clob creata in questo modo:

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 


0

Ho scritto una funzione per gestirlo utilizzando espressioni regolari. I parametri in sono: 1) la chiamata listagg stessa 2) Una ripetizione del delimitatore

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;

Ora non devi ripetere l'espressione regolare ogni volta che lo fai, dì semplicemente:

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

0

Se non hai bisogno di un ordine particolare di valori concatenati e il separatore può essere una virgola, puoi fare:

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

0

Ho bisogno di una versione DISTINCT di questo e ho risolto questo.

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

0

Un aspetto fastidioso LISTAGGè che se la lunghezza totale della stringa concatenata supera i 4000 caratteri (limite per VARCHAR2SQL), viene generato l'errore seguente, che è difficile da gestire nelle versioni Oracle fino alla 12.1

ORA-01489: il risultato della concatenazione di stringhe è troppo lungo

Una nuova funzionalità aggiunta in 12cR2 è la ON OVERFLOWclausola di LISTAGG. La query che include questa clausola sarebbe simile a:

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

Quanto sopra limiterà l'output a 4000 caratteri ma non genererà l' ORA-01489errore.

Queste sono alcune delle opzioni aggiuntive della ON OVERFLOWclausola:

  • ON OVERFLOW TRUNCATE 'Contd..' : Verrà visualizzato 'Contd..'alla fine della stringa (l'impostazione predefinita è ...)
  • ON OVERFLOW TRUNCATE '' : Questo visualizzerà i 4000 caratteri senza alcuna stringa di terminazione.
  • ON OVERFLOW TRUNCATE WITH COUNT: Questo visualizzerà il numero totale di caratteri alla fine dopo i caratteri finali. Ad esempio: - ' ...(5512)'
  • ON OVERFLOW ERROR: Se ti aspetti LISTAGGche l' ORA-01489errore non riesca (che è comunque predefinito).

0

Ho implementato questa funzione memorizzata:

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;

Mi dispiace, ma in alcuni casi (per un set molto grande), Oracle potrebbe restituire questo errore:

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.

ma penso che questo sia un buon punto di partenza;)


0

select col1, listaggr(col2,',') within group(Order by col2) from table group by col1 il che significa aggregare le stringhe (col2) nella lista mantenendo l'ordine n, quindi trattare i duplicati come gruppo per col1, il che significa unire i duplicati col1 in 1 gruppo. forse questo sembra pulito e semplice come dovrebbe essere e se nel caso vuoi anche col3 devi solo aggiungere un altro listagg () che èselect col1, listaggr(col2,',') within group(Order by col2),listaggr(col3,',') within group(order by col3) from table group by col1


0

Utilizzando SELECT DISTINCT ... come parte di una sottoquery prima di chiamare LISTAGG è probabilmente il modo migliore per query semplici, come notato da @a_horse_with_no_name

Tuttavia, in query più complesse, potrebbe non essere possibile, o facile, eseguire questa operazione. Ho avuto questo risultato in uno scenario che utilizzava l'approccio top-n utilizzando una funzione analitica.

Quindi ho trovato la COLLECTfunzione aggregata. È documentato che sia disponibile il modificatore UNIQUEo DISTINCT. Solo in 10g , fallisce silenziosamente (ignora il modificatore senza errori). Tuttavia, per ovviare a questo, da un'altra risposta , sono arrivato a questa soluzione:

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,
    ...
)

Fondamentalmente, usando SET , rimuovo i duplicati nella mia raccolta.

Dovresti comunque definire tab_typcome tipo di raccolta di base e, nel caso di a VARCHAR, questo sarebbe ad esempio:

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

Anche come correzione alla risposta di @a_horse_with_no_name sulla situazione a più colonne, dove potresti voler aggregare ancora su una terza (o più) colonne:

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 lasci la rn = 1condizione come dove alla query, aggregheresti altre colonne in modo errato.


0

Molto semplice: utilizza nella tua query una sottoquery con una selezione 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;

-1

Il modo più semplice per gestire più listagg è utilizzare 1 WITH (fattore di sottoquery) per colonna contenente un listagg di quella colonna da una selezione distinta:

    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)

Che dà:

col1  col2List  col3List
1     2,3,4,5   3,4,6
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.