Éliminer les doublons dans ListAgg (Oracle)


44

Avant Oracle 11.2, j'utilisais une fonction d'agrégation personnalisée pour concaténer une colonne dans une ligne. 11.2 Ajout de la LISTAGGfonction, j'essaie donc de l'utiliser à la place. Mon problème est que je dois éliminer les doublons dans les résultats et ne semble pas pouvoir le faire.

Voici un exemple.

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

Ce que je veux voir, c'est ceci:

      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

Voici une listaggversion qui est proche, mais n'élimine pas les doublons.

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

J'ai une solution, mais c'est pire que de continuer à utiliser la fonction d'agrégat personnalisé.


Devrait order by nullêtre order by Num2ou que je reçois confus?
Jack Douglas

@ Jack - Cela ne fait aucune différence pour l'élimination des doublons. Selon votre utilisation, cela peut être souhaitable.
Leigh Riffel

le soupir LISTAGG continue d'être en deçà de Tom KyteSTRAGG , avec lequel il est aussi facile queSTRAGG(DISTINCT ...)
Baodad

Enfin c'est possible: LISTAGG DISTINCT
lad2025

Réponses:


32

Vous pouvez utiliser des expressions régulières et regexp_replacesupprimer les doublons après la concaténation avec listagg:

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

Cela pourrait être plus ordonné si la saveur regex d'Oracle prenait en charge les groupes avec ou sans capture, mais ce n'est pas le cas .

Cependant, cette solution évite d'analyser la source plus d'une fois.

DBFiddle ici


Notez que pour que cette technique REGEX_REPLACE fonctionne pour supprimer les doublons, les valeurs en double doivent toutes être côte à côte dans la chaîne agrégée.
Baodad

2
C'est ce qui ORDER BY Num2accomplit n'est-ce pas (voir ici ). Ou essayez-vous simplement de souligner que vous avez besoin de ORDER BY pour que cela fonctionne?
Jack Douglas

13

Autant que je sache, avec la spécification de langue actuellement disponible, il s’agit de la solution la plus courte pour obtenir ce que vous voulez si elle doit être exécutée 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;

Quelle était votre solution qui était pire que la solution d'agrégat personnalisé ?


Cela fonctionne, mais doit effectuer deux analyses complètes de la table.
Leigh Riffel

Si vous avez besoin d'agréger une petite table (<100 000 lignes), les performances sont plus qu'acceptables pour une extraction simple. Cela a été ma solution de choix après presque une heure d’essais de toutes les manières possibles!
Mathieu Dumoulin

Cela fonctionne également lorsque les doublons placent la valeur intermédiaire sur 4000 caractères. Cela le rend plus sûr que la regexpsolution.
Gordon Linoff

8

Créez une fonction d'agrégat personnalisée pour le faire.

La base de données Oracle fournit un certain nombre de fonctions d'agrégat prédéfinies telles que MAX, MIN, SUM pour effectuer des opérations sur un ensemble d'enregistrements. Ces fonctions d'agrégation prédéfinies ne peuvent être utilisées qu'avec des données scalaires. Toutefois, vous pouvez créer vos propres implémentations personnalisées de ces fonctions ou définir de toutes nouvelles fonctions d'agrégat à utiliser avec des données complexes, par exemple avec des données multimédia stockées à l'aide de types d'objet, de types opaques et de LOB.

Les fonctions d'agrégat définies par l'utilisateur sont utilisées dans les instructions SQL DML, tout comme les agrégats intégrés à la base de données Oracle. Une fois que ces fonctions sont enregistrées auprès du serveur, la base de données appelle simplement les routines d'agrégation que vous avez fournies à la place des procédures natives.

Les agrégats définis par l'utilisateur peuvent également être utilisés avec des données scalaires. Par exemple, il peut être intéressant de mettre en œuvre des fonctions globales spéciales permettant de manipuler des données statistiques complexes associées à des applications financières ou scientifiques.

Les agrégats définis par l'utilisateur sont une fonctionnalité de Extensibility Framework. Vous les implémentez à l'aide des routines d'interface ODCIAggregate.


8

Bien que ce soit un ancien message avec une réponse acceptée, je pense que la fonction analytique de LAG () fonctionne bien dans ce cas et est remarquable:

  • LAG () supprime les valeurs en double dans la colonne num2 avec un coût minimal
  • Pas besoin d'expression régulière non triviale pour filtrer les résultats
  • Un seul scan complet de table (coût = 4 sur un exemple de table)

Voici le code proposé:

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;

Les résultats ci-dessous semblent correspondre à ce que souhaite le PO:

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

Voici ma solution au problème qui, à mon avis, n’est pas aussi agréable que d’utiliser notre fonction d’agrégat personnalisée qui existe déjà.

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

Utilisez WMSYS.WM_Concat à la place.

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

Remarque: cette fonction est non documentée et non prise en charge. Voir https://forums.oracle.com/forums/message.jspa?messageID=4372641#4372641 .


6
Si vous appelez le support Oracle et que vous utilisez wm_concat(même si vous affirmez que le wm_concatproblème n'est pas lui-même à l'origine du problème), ils auraient des raisons de refuser de fournir une aide, car elle est non documentée et non prise en charge. Ce n'est pas le cas si vous utilisez un agrégat personnalisé ou tout autre fonctionnalité prise en charge.
Jack Douglas

5

Vous pouvez également utiliser une instruction collect, puis écrire une fonction pl / sql personnalisée qui convertit la collection en chaîne.

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

Vous pouvez utiliser distinctet order bydans une collectclause, mais si vous le distinctcombinez, cela ne fonctionnera pas à partir du 11.2.0.2 :(

La solution de rechange pourrait être une sous-sélection:

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

Je ne vois pas comment une fonction pl / sql personnalisée serait meilleure qu'une fonction d'agrégat personnalisée. Le SQL résultant est certainement plus simple pour ce dernier. Comme ce problème était sur 11.2.0.2, la sous-sélection ajouterait une analyse supplémentaire que j'essayais d'éviter.
Leigh Riffel le

Je dirais qu'une fonction PL / SQL appelée ONCE pour convertir la collection en chaîne pourrait être meilleure que la fonction d'agrégation appelée des milliers de fois. Je pense que cela réduirait beaucoup les changements de contexte.
Nico

Votre théorie semble bonne et c’est une des raisons pour lesquelles j’essayais d’éviter la fonction d’agrégat personnalisée et je préférais une fonction d’agrégat intégrée telle que LISTAGG. Si vous souhaitez faire des comparaisons temporelles, je serais intéressé par les résultats.
Leigh Riffel

2

J'ai créé cette solution avant de rencontrer ListAgg, mais il existe encore des cas, tels que ce problème de valeur en double, alors cet outil est utile. La version ci-dessous a 4 arguments pour vous permettre de contrôler les résultats.

Explication CLOBlist prend le constructeur CLOBlistParam en tant que paramètre. CLOBlistParam a 4 arguments

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

Exemple d'utilisation

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

Lien vers Gist est ci-dessous.

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

Je sais que c'est quelque temps après la publication initiale, mais c'était la première fois que je trouvais une réponse au même problème après Google et que quelqu'un qui aurait atterri ici serait heureux de trouver une réponse succincte qui ne repose pas sur des requêtes trop compliquées. ou regexes.

Cela vous donnera le résultat souhaité:

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

Mon idée est d'implémenter une fonction stockée comme ceci:

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;

Désolé, mais dans certains cas (pour un très gros lot), Oracle pourrait renvoyer cette erreur:

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.

mais je pense que c'est un bon point de départ;)


Notez que le PO avait déjà sa propre LISTAGGfonction personnalisée ; ils essayaient explicitement de voir s'ils pouvaient trouver un moyen efficace de le faire en utilisant la LISTAGGfonction intégrée disponible à partir de la version 11.2.
RDFozz

0

Essaye celui-là:

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

Le problème avec d'autres solutions possibles est qu'il n'y a pas de corrélation entre les résultats des colonnes 1 et 2. Pour contourner ce problème, la requête interne crée cette corrélation puis supprime les doublons de cet ensemble de résultats. Lorsque vous faites le listagg, le résultat est déjà propre. le problème tenait davantage à obtenir les données dans un format utilisable.


1
Vous voudrez peut-être ajouter quelques explications sur son fonctionnement.
Jkavalik

Merci pour la réponse et bienvenue sur le site. Il serait peut-être encore plus utile de décrire pourquoi cela fonctionne et comment cela aiderait.
Tom V - Team Monica

J'ai essayé de mettre à jour la réponse, mais l'erreur continue de tomber. --- Le problème avec d'autres solutions possibles est qu'il n'y a pas de corrélation entre les résultats des colonnes 1 et 2. Pour contourner ce problème, la requête interne crée cette corrélation puis supprime les doublons de cet ensemble de résultats. Lorsque vous faites le listagg, le résultat est déjà propre. le problème tenait davantage à obtenir les données dans un format utilisable.
Kevin

-2

SQL a été conçu comme un langage simple, très proche de l'anglais. Alors, pourquoi ne l'écrivez-vous pas en anglais?

  1. élimine les doublons sur num2 et utilise listagg en tant que fonction d'agrégat - non analytique, pour calculer le concat sur chaîne
  2. joindre à l'original, comme vous voulez une ligne de résultat pour une entrée

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


Merci pour votre réponse. Cette solution nécessite deux analyses complètes de la table, mais surtout ne renvoie pas les résultats corrects.
Leigh Riffel

Désolé, j'ai collé une version plus ancienne et incorrecte.
Š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(+);

Cela renvoie les résultats corrects pour les données fournies, mais repose sur une hypothèse incorrecte. Num1 et Num2 ne sont pas liés. Num1 pourrait tout aussi bien être Char1 contenant les valeurs a, e, i, o, u, y. Reguardless, cette solution nécessite deux analyses complètes de la table, ce qui va à l'encontre de l'objectif d'utiliser la fonction d'agrégat. Si la solution permettait deux analyses de table, cette option serait préférable (avec les données de l'échantillon, le coût est inférieur à tout le reste). SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Leigh Riffel

-2

La solution la plus efficace est la sélection interne avec GROUP BY, car DISTINCT et les expressions régulières sont lents.

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

Cette solution est assez simple - vous obtenez d’abord toutes les combinaisons uniques de num1 et num2 (SELECT interne), puis la chaîne de tous les num2 regroupée par num1.


Cette requête ne renvoie pas les résultats demandés. Il retourne les mêmes résultats que SELECT * FROM ListAggTest;.
Leigh Riffel

Dans sa défense, il a probablement été signalé à cette solution d'un autre problème de stackoverflow marqué en double que cette solution ne fixe. c'est la solution que je voulais. Il semble que je doive faire des suppositions différentes pour publier ma propre prise de vue, et je vais donc laisser cette question tranquille avec ce commentaire.
Gerard ONeill
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.