Elimina i duplicati in ListAgg (Oracle)


44

Prima di Oracle 11.2 utilizzavo una funzione di aggregazione personalizzata per concatenare una colonna in una riga. 11.2 Aggiunta la LISTAGGfunzione, quindi sto cercando di usarla invece. Il mio problema è che devo eliminare i duplicati nei risultati e non sembra essere in grado di farlo.

Ecco un esempio

CREATE TABLE ListAggTest AS (
  SELECT rownum Num1, DECODE(rownum,1,'2',to_char(rownum)) Num2 FROM dual 
     CONNECT BY rownum<=6
  );
SELECT * FROM ListAggTest;
      NUM1 NUM2
---------- ---------------------
         1 2
         2 2                    << Duplicate 2
         3 3
         4 4
         5 5
         6 6

Quello che voglio vedere è questo:

      NUM1 NUM2S
---------- --------------------
         1 2-3-4-5-6
         2 2-3-4-5-6
         3 2-3-4-5-6
         4 2-3-4-5-6
         5 2-3-4-5-6
         6 2-3-4-5-6

Ecco una listaggversione vicina, ma che non elimina i duplicati.

SELECT Num1, listagg(Num2,'-') WITHIN GROUP (ORDER BY NULL) OVER () Num2s 
FROM ListAggTest;

Ho una soluzione, ma è peggio che continuare a utilizzare la funzione di aggregazione personalizzata.


Dovrei order by nullessere order by Num2o mi sto confondendo?
Jack Douglas

@Jack - Non fa differenza per l'eliminazione dei duplicati. A seconda dell'uso, potrebbe essere desiderabile.
Leigh Riffel

il sospiro LISTAGG continua a non essere STRAGGSTRAGG(DISTINCT ...)
all'altezza

Finalmente è possibile: LISTAGG DISTINCT
lad2025

Risposte:


32

È possibile utilizzare espressioni regolari e regexp_replacerimuovere i duplicati dopo la concatenazione con listagg:

SELECT Num1, 
       RTRIM(
         REGEXP_REPLACE(
           (listagg(Num2,'-') WITHIN GROUP (ORDER BY Num2) OVER ()), 
           '([^-]*)(-\1)+($|-)', 
           '\1\3'),
         '-') Num2s 
FROM ListAggTest;

Questo potrebbe essere più ordinato se il sapore regex di Oracle supporta lookahead o gruppi non catturanti, ma non lo è .

Tuttavia, questa soluzione evita la scansione della fonte più di una volta.

DBFiddle qui


Si noti che affinché questa tecnica REGEX_REPLACE funzioni per la rimozione dei duplicati, i valori duplicati devono essere tutti uno accanto all'altro nella stringa aggregata.
Baodad,

2
Questo è ciò che ORDER BY Num2ottiene non è vero (vedi qui ). O stai solo cercando di sottolineare che hai bisogno di ORDER BY perché funzioni?
Jack Douglas,

13

Per quanto posso vedere, con la specifica della lingua attualmente disponibile, questo è il più breve per ottenere ciò che vuoi se deve essere fatto listagg.

select distinct
       a.Num1, 
       b.num2s
  from listaggtest a cross join (
       select listagg(num2d, '-') within group (order by num2d) num2s 
       from (
         select distinct Num2 num2d from listaggtest
       )
      ) b;

Qual è stata la tua soluzione peggiore della soluzione aggregata personalizzata ?


Funziona, ma deve fare due scansioni della tabella completa.
Leigh Riffel,

Quando si dispone di una piccola tabella che è necessario aggregare (<100000 righe), le prestazioni sono più che accettabili per un semplice recupero. Questa è stata la mia soluzione preferita dopo quasi un'ora di test in tutti i modi possibili!
Mathieu Dumoulin,

Questo funziona anche quando i duplicati metterebbero il valore intermedio su 4000 caratteri. Questo lo rende più sicuro della regexpsoluzione.
Gordon Linoff,

8

Creare una funzione di aggregazione personalizzata per fare ciò.

Il database Oracle fornisce una serie di funzioni aggregate predefinite come MAX, MIN, SUM per eseguire operazioni su un set di record. Queste funzioni aggregate predefinite possono essere utilizzate solo con dati scalari. Tuttavia, è possibile creare implementazioni personalizzate di queste funzioni o definire funzioni aggregate completamente nuove da utilizzare con dati complessi, ad esempio con dati multimediali archiviati utilizzando tipi di oggetto, tipi opachi e LOB.

Le funzioni di aggregazione definite dall'utente vengono utilizzate nelle istruzioni DML SQL proprio come gli aggregati incorporati nel database Oracle. Una volta registrate tali funzioni con il server, il database richiama semplicemente le routine di aggregazione fornite al posto di quelle native.

Gli aggregati definiti dall'utente possono essere utilizzati anche con dati scalari. Ad esempio, può essere utile implementare funzioni aggregate speciali per lavorare con dati statistici complessi associati ad applicazioni finanziarie o scientifiche.

Gli aggregati definiti dall'utente sono una caratteristica di Extensibility Framework. Li implementi usando le routine di interfaccia ODCIAggregate.


8

Anche se questo è un vecchio post con una risposta accettata, penso che la funzione analitica LAG () funzioni bene in questo caso ed è degna di nota:

  • LAG () rimuove i valori duplicati nella colonna num2 con una spesa minima
  • Non è necessaria un'espressione regolare non banale per filtrare i risultati
  • Solo una scansione della tabella completa (costo = 4 nella tabella di esempio semplice)

Ecco il codice proposto:

with nums as (
SELECT 
    num1, 
    num2, 
    decode( lag(num2) over (partition by null order by num2), --get last num2, if any
            --if last num2 is same as this num2, then make it null
            num2, null, 
            num2) newnum2
  FROM ListAggTest
) 
select 
  num1, 
  --listagg ignores NULL values, so duplicates are ignored
  listagg( newnum2,'-') WITHIN GROUP (ORDER BY Num2) OVER () num2s
  from nums;

I risultati seguenti sembrano essere ciò che l'OP desidera:

NUM1  NUM2S       
1   2-3-4-5-6
2   2-3-4-5-6
3   2-3-4-5-6
4   2-3-4-5-6
5   2-3-4-5-6
6   2-3-4-5-6 

7

Ecco la mia soluzione al problema che secondo me non è bello come usare la nostra funzione di aggregazione personalizzata che già esiste.

SELECT Num1, listagg(Num2,'-') WITHIN GROUP (ORDER BY NULL) OVER () Num2s FROM (
  SELECT Num1, DECODE(ROW_NUMBER() OVER (PARTITION BY Num2 ORDER BY NULL),
     1,Num2,NULL) Num2 FROM ListAggTest
);

5

Utilizzare invece WMSYS.WM_Concat.

SELECT Num1, Replace(Wm_Concat(DISTINCT Num2) OVER (), ',', '-')
FROM ListAggTest;

Nota: questa funzione non è documentata e non è supportata. Vedi https://forums.oracle.com/forums/message.jspa?messageID=4372641#4372641 .


6
Se chiami il supporto Oracle e stai utilizzando wm_concat(anche se ritieni wm_concatche il problema non stia causando esso stesso) , avrebbero motivi per rifiutare di aiutarti perché non è documentato e non è supportato - non è il caso se usi un aggregato personalizzato o qualsiasi altro funzione supportata.
Jack Douglas,

5

È inoltre possibile utilizzare un'istruzione di raccolta e quindi scrivere una funzione pl / sql personalizzata che converta la raccolta in una stringa.

CREATE TYPE varchar2_ntt AS TABLE OF VARCHAR2(4000);
CREATE TYPE varchar2_ntt AS TABLE OF VARCHAR2(4000);

select cast(collect(distinct num2 order by num2) as varchar2_ntt) 
from listaggtest

È possibile utilizzare distincte order byin una collectclausola ma se combinati distinctnon funzioneranno a partire dall'11.2.0.2 :(

La soluzione alternativa potrebbe essere una sottoselezione:

select collect(num2 order by num2) 
from 
( 
    select distinct num2 
    from listaggtest
)

Non riesco a vedere come una funzione pl / sql personalizzata sarebbe migliore di una funzione aggregata personalizzata. L'SQL risultante è sicuramente più semplice per quest'ultimo. Poiché questo problema era in 11.2.0.2, la sottoselezione avrebbe aggiunto un'ulteriore scansione che stavo cercando di evitare.
Leigh Riffel,

Direi che una funzione PL / SQL chiamata ONCE per convertire la raccolta in una stringa potrebbe essere migliore della funzione aggregata chiamata migliaia di volte. Penso che ciò ridurrebbe molto i cambi di contesto.
Nico,

La tua teoria suona bene ed è stata una delle ragioni per cui stavo cercando di evitare la funzione aggregata personalizzata e preferivo una funzione aggregata integrata come LISTAGG. Se desideri fare dei confronti temporali, sarei interessato ai risultati.
Leigh Riffel,

2

Ho creato questa soluzione prima di incontrare ListAgg, ma ci sono ancora occasioni, come questo problema di valore duplicato, quindi questo strumento è utile. La versione seguente ha 4 argomenti per darti il ​​controllo sui risultati.

Spiegazione CLOBlist utilizza il parametro CLOBlistParam come parametro. CLOBlistParam ha 4 argomenti

string VARCHAR2(4000) - The variable to be aggregated
delimiter VARCHAR2(100) - The delimiting string
initiator VARCHAR2(100) - An initial string added before the first value only.
no_dup VARCHAR2(1) - A flag. Duplicates are suppressed if this is Y

Esempio di utilizzo

--vertical list of comma separated values, no duplicates.
SELECT CLOBlist(CLOBlistParam(column_name,chr(10)||',','','Y')) FROM user_tab_columns
--simple csv
SELECT CLOBlist(CLOBlistParam(table_name,',','','N')) FROM user_tables

Il link a Gist è sotto.

https://gist.github.com/peter-genesys/d203bfb3d88d5a5664a86ea6ee34eeca] 1


-- Program  : CLOBlist 
-- Name     : CLOB list 
-- Author   : Peter Burgess
-- Purpose  : CLOB list aggregation function for SQL
-- RETURNS CLOB - to allow for more than 4000 chars to be returned by SQL
-- NEW type CLOBlistParam  - allows for definition of the delimiter, and initiator of sequence
------------------------------------------------------------------
--This is an aggregating function for use in SQL.
--It takes the argument and creates a comma delimited list of each instance.

WHENEVER SQLERROR CONTINUE
DROP TYPE CLOBlistImpl;
WHENEVER SQLERROR EXIT FAILURE ROLLBACK

create or replace type CLOBlistParam as object(
  string    VARCHAR2(4000)
 ,delimiter VARCHAR2(100)  
 ,initiator VARCHAR2(100)  
 ,no_dup    VARCHAR2(1)    )
/
show error

--Creating CLOBlist()
--Implement the type CLOBlistImpl to contain the ODCIAggregate routines.
create or replace type CLOBlistImpl as object
(
  g_list CLOB, -- progressive concatenation
  static function ODCIAggregateInitialize(sctx IN OUT CLOBlistImpl)
    return number,
  member function ODCIAggregateIterate(self  IN OUT CLOBlistImpl
                                     , value IN     CLOBlistParam) return number,
  member function ODCIAggregateTerminate(self        IN  CLOBlistImpl
                                       , returnValue OUT CLOB
                                       , flags       IN  number) return number,
  member function ODCIAggregateMerge(self IN OUT CLOBlistImpl
                                   , ctx2 IN     CLOBlistImpl) return number
)
/
show error


--Implement the type body for CLOBlistImpl.
create or replace type body CLOBlistImpl is
static function ODCIAggregateInitialize(sctx IN OUT CLOBlistImpl)
return number is
begin

  sctx := CLOBlistImpl(TO_CHAR(NULL));
  return ODCIConst.Success;
end;

member function ODCIAggregateIterate(self  IN OUT CLOBlistImpl
                                   , value IN     CLOBlistParam) return number is
begin

   IF self.g_list IS NULL THEN
     self.g_list := value.initiator||value.string;
   ELSIF value.no_dup = 'Y' AND
         value.delimiter||self.g_list||value.delimiter LIKE '%'||value.delimiter||value.string||value.delimiter||'%' 
         THEN
     --Do not include duplicate value    
     NULL;
  ELSE
     self.g_list := self.g_list||value.delimiter||value.string;
   END IF;

  return ODCIConst.Success;
end;

member function ODCIAggregateTerminate(self        IN  CLOBlistImpl
                                     , returnValue OUT CLOB
                                     , flags       IN  number) return number is
begin
  returnValue := self.g_list;
  return ODCIConst.Success;
end;

member function ODCIAggregateMerge(self IN OUT CLOBlistImpl
                                 , ctx2 IN     CLOBlistImpl) return number is
begin

  self.g_list := LTRIM( self.g_list||','||ctx2.g_list,',');

  return ODCIConst.Success;
end;
end;
/
show error

--Using CLOBlist() to create a vertical list of comma separated values

--  SELECT CLOBlist(CLOBlistParam(product_code,chr(10)||',','','Y'))
--  FROM   account


--DROP FUNCTION CLOBlist
--/

PROMPT Create the user-defined aggregate.
CREATE OR REPLACE FUNCTION CLOBlist (input CLOBlistParam) RETURN CLOB
PARALLEL_ENABLE AGGREGATE USING CLOBlistImpl;
/
show error

1

So che è un po 'di tempo dopo la pubblicazione originale, ma questo è stato il primo punto che ho trovato dopo aver cercato su Google una risposta allo stesso problema e ho pensato che qualcun altro che è arrivato qui potrebbe essere felice di trovare una risposta sintetica che non si basa su query eccessivamente complicate o regex.

Questo ti darà il risultato desiderato:

with nums as (
  select distinct num2 distinct_nums
  from listaggtest
  order by num2
) select num1,
         (select listagg(distinct_nums, '-') within group (order by 1) from nums) nums2list 
         from listaggtest;

1

La mia idea è quella di implementare una funzione memorizzata come questa:

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


Si noti che l'OP aveva già una propria LISTAGGfunzione personalizzata ; stavano provando esplicitamente a vedere se potevano trovare un modo efficace per farlo usando la LISTAGGfunzione integrata disponibile dalla versione 11.2.
RDFozz,

0

Prova questo:

select num1,listagg(Num2,'-') WITHIN GROUP (ORDER BY NULL) Num2s 
from (
select distinct num1
    ,b.num2
from listaggtest a
    ,(
        select num2
        from listaggtest
    ) b
    order by 1,2
    )
group by num1

Il problema con altre possibili soluzioni è che non esiste alcuna correlazione tra i risultati per la colonna 1 e la colonna 2. Per aggirare il problema, la query interna crea questa correlazione e quindi rimuove i duplicati dal set di risultati. Quando esegui il listagg il set di risultati è già pulito. il problema aveva più a che fare con il recupero dei dati in un formato utilizzabile.


1
Potresti voler aggiungere qualche spiegazione su come funziona.
jkavalik,

Grazie per la risposta e benvenuto nel sito. Potrebbe essere ancora più utile se tu potessi descrivere perché questo funziona e come potrebbe essere d'aiuto.
Tom V - Team Monica,

Ho cercato di aggiornare la risposta ma continua a sbagliare. --- Il problema con altre possibili soluzioni è che non esiste alcuna correlazione tra i risultati per la colonna 1 e la colonna 2. Per aggirare il problema, la query interna crea questa correlazione e quindi rimuove i duplicati da quel set di risultati. Quando esegui il listagg il set di risultati è già pulito. il problema aveva più a che fare con il recupero dei dati in un formato utilizzabile.
Kevin,

-2

SQL è stato progettato come un linguaggio semplice, molto vicino all'inglese. Allora perché non lo scrivi come in inglese?

  1. elimina i duplicati su num2 e usa listagg come funzione aggregata - non analitica, per calcolare il concat su stringa
  2. unisciti all'originale, come vuoi una riga di risultati per un input

select num1, num2s
  from (select num2,
               listagg(num2, '-') within group(order by num2) over() num2s
          from listaggtest
         group by num2
       )
  join listaggtest using (num2);


Grazie per la vostra risposta. Questa soluzione richiede due scansioni della tabella completa, ma soprattutto non restituisce i risultati corretti.
Leigh Riffel,

Scusatemi, ho incollato una versione precedente e errata.
Štefan Oravec,

-2
SELECT Num1, listagg(Num2,'-') WITHIN GROUP
(ORDER BY num1) OVER () Num2s FROM 
(select distinct num1 from listAggTest) a,
(select distinct num2 from ListAggTest) b
where num1=num2(+);

Ciò restituisce i risultati corretti per i dati forniti, ma ha un presupposto errato. Num1 e Num2 non sono correlati. Num1 potrebbe anche essere Char1 contenente valori a, e, i, o, u, y. Indipendentemente da ciò, questa soluzione richiede due scansioni complete della tabella che annullano l'intero scopo dell'uso della funzione di aggregazione. Se la soluzione consentisse due scansioni di tabelle, sarebbe preferibile (con i dati di esempio ha un costo inferiore rispetto a qualsiasi altra cosa). SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Leigh Riffel,

-2

La soluzione più efficace è SELEZIONA interna con GROUP BY, perché DISTINCT ed espressioni regolari sono lente come l'inferno.

SELECT num1, LISTAGG(num2, '-') WITHIN GROUP (ORDER BY num2) AS num2s
    FROM (SELECT num1, num2
              FROM ListAggTest
              GROUP BY num1, num2)
    GROUP BY num1;

Questa soluzione è piuttosto semplice: prima ottieni tutte le combinazioni uniche di num1 e num2 (SELECT interno) e poi ottieni la stringa di tutti i num2 raggruppati per num1.


Questa query non restituisce i risultati richiesti. Restituisce gli stessi risultati di SELECT * FROM ListAggTest;.
Leigh Riffel,

In sua difesa, è stato probabilmente indicò questa soluzione da un altro problema StackOverflow segnato duplicato che questa soluzione fa correzione. questa è la soluzione che volevo. Sembra che dovrò fare diverse ipotesi per pubblicare la mia opinione, e quindi lascerò questa domanda da sola con questo commento.
Gerard ONeill,
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.