Eliminar duplicados en ListAgg (Oracle)


44

Antes de Oracle 11.2, estaba usando una función de agregado personalizada para concatenar una columna en una fila. 11.2 Se agregó la LISTAGGfunción, así que estoy tratando de usar eso en su lugar. Mi problema es que necesito eliminar duplicados en los resultados y parece que no puedo hacerlo.

Aquí hay un ejemplo.

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

Lo que quiero ver es esto:

      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

Aquí hay una listaggversión que está cerca, pero no elimina duplicados.

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

Tengo una solución, pero es peor que continuar usando la función de agregado personalizada.


¿Debería order by nullestar order by Num2o me estoy confundiendo?
Jack Douglas

@Jack: no hay diferencia en la eliminación de duplicados. Dependiendo de su uso, puede ser deseable.
Leigh Riffel

el suspiro LISTAGG sigue siendo inferior al de Tom KyteSTRAGG , con el que es tan fácil comoSTRAGG(DISTINCT ...)
Baodad el

Finalmente es posible: LISTAGG DISTINCT
lad2025

Respuestas:


32

Puede usar expresiones regulares y regexp_replaceeliminar los duplicados después de la concatenación con listagg:

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

Esto podría ser más ordenado si el sabor regex de Oracle admite grupos de búsqueda anticipada o no capturadores, pero no lo hace .

Sin embargo, esta solución evita escanear la fuente más de una vez.

DBFiddle aquí


Tenga en cuenta que para que esta técnica REGEX_REPLACE funcione para eliminar duplicados, los valores duplicados deben estar todos juntos en la cadena agregada.
Baodad

2
Eso es lo que ORDER BY Num2logra, ¿no? (Ver aquí ). ¿O simplemente estás tratando de señalar que necesitas ORDER BY para que funcione?
Jack Douglas

13

Hasta donde puedo ver, con la especificación de idioma disponible actualmente, este es el más corto para lograr lo que desea si debe hacerse 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;

¿Cuál fue su solución peor que la solución agregada personalizada ?


Esto funciona, pero tiene que hacer dos escaneos de tabla completa.
Leigh Riffel

Cuando tiene una tabla pequeña que necesita agregar (<100000 filas), el rendimiento es más que aceptable para una recuperación simple. ¡Esta ha sido mi solución preferida después de casi una hora de probar cada una de las formas posibles!
Mathieu Dumoulin

Esto también funciona cuando los duplicados pondrían el valor intermedio sobre 4000 caracteres. Eso lo hace más seguro que la regexpsolución.
Gordon Linoff

8

Cree una función de agregado personalizada para hacer esto.

La base de datos Oracle proporciona una serie de funciones agregadas predefinidas, como MAX, MIN, SUM para realizar operaciones en un conjunto de registros. Estas funciones agregadas predefinidas solo se pueden usar con datos escalares. Sin embargo, puede crear sus propias implementaciones personalizadas de estas funciones, o definir funciones agregadas completamente nuevas, para usar con datos complejos, por ejemplo, con datos multimedia almacenados usando tipos de objetos, tipos opacos y LOB.

Las funciones de agregado definidas por el usuario se usan en las sentencias DML de SQL al igual que los agregados integrados de la base de datos Oracle. Una vez que tales funciones se registran en el servidor, la base de datos simplemente invoca las rutinas de agregación que proporcionó en lugar de las nativas.

Los agregados definidos por el usuario también se pueden usar con datos escalares. Por ejemplo, puede valer la pena implementar funciones agregadas especiales para trabajar con datos estadísticos complejos asociados con aplicaciones financieras o científicas.

Los agregados definidos por el usuario son una característica del Marco de Extensibilidad. Se implementan utilizando rutinas de interfaz ODCIAggregate.


8

Aunque esta es una publicación antigua con una respuesta aceptada, creo que la función analítica LAG () funciona bien en este caso y es notable:

  • LAG () elimina valores duplicados en la columna num2 con un gasto mínimo
  • No es necesario utilizar expresiones regulares no triviales para filtrar resultados
  • Solo una exploración de tabla completa (costo = 4 en una tabla de ejemplo simple)

Aquí está el código propuesto:

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;

Los resultados a continuación parecen ser lo que desea el OP:

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

Aquí estaba mi solución al problema que, en mi opinión, no es tan bueno como usar nuestra función de agregado personalizada que ya existe.

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

Utilice WMSYS.WM_Concat en su lugar.

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

Nota: Esta función no está documentada ni es compatible. Ver https://forums.oracle.com/forums/message.jspa?messageID=4372641#4372641 .


66
Si llama al soporte de Oracle y está utilizando wm_concat(incluso si argumenta wm_concatque no está causando el problema) , tendrían motivos para negarse a ayudar porque está indocumentado y no es compatible, no es el caso si utiliza un agregado personalizado o cualquier otro Característica compatible.
Jack Douglas el

5

También puede usar una instrucción de recopilación y luego escribir una función personalizada pl / sql que convierta la colección en una cadena.

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

Puede usar distincty order byen una collectcláusula, pero si se combina distinct, no funcionará a partir del 11.2.0.2 :(

La solución podría ser una subselección:

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

No veo cómo una función pl / sql personalizada sería mejor que una función agregada personalizada. El SQL resultante es ciertamente más simple para este último. Como este problema estaba en 11.2.0.2, la subselección agregaría un escaneo adicional que estaba tratando de evitar.
Leigh Riffel

Yo diría que una función PL / SQL llamada ONCE para convertir la colección en una cadena podría ser mejor que la función agregada llamada miles de veces. Creo que esto reduciría mucho los cambios de contexto.
Nico

Su teoría suena bien y fue una de las razones por las que estaba tratando de evitar la función agregada personalizada y prefería una función agregada incorporada como LISTAGG. Si desea hacer algunas comparaciones de tiempo, me interesarían los resultados.
Leigh Riffel

2

Creé esta solución antes de encontrar ListAgg, pero todavía hay ocasiones, como este problema de valor duplicado, entonces esta herramienta es útil. La siguiente versión tiene 4 argumentos para darle control sobre los resultados.

Explicación CLOBlist toma el constructor CLOBlistParam como parámetro. CLOBlistParam tiene 4 argumentos

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

Ejemplo de uso

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

Enlace a Gist está abajo.

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

Sé que es en algún momento después de la publicación original, pero este fue el primer lugar que encontré después de buscar en Google una respuesta al mismo problema y pensé que alguien más que aterrizó aquí podría estar feliz de encontrar una respuesta sucinta que no dependa de consultas demasiado complicadas o expresiones regulares.

Esto le dará el resultado deseado:

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

Mi idea es implementar una función almacenada como esta:

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;

Lo siento, pero en algunos casos (para un conjunto muy grande), Oracle podría devolver este error:

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.

pero creo que este es un buen punto de partida;)


Tenga en cuenta que el OP ya tenía su propia LISTAGGfunción personalizada ; intentaban explícitamente ver si podían encontrar una manera eficiente de hacerlo utilizando la LISTAGGfunción integrada disponible a partir de la versión 11.2.
RDFozz

0

Prueba este:

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

El problema con otras posibles soluciones es que no existe una correlación entre los resultados para la columna 1 y la columna 2. Para evitar esto, la consulta interna crea esta correlación y luego elimina los duplicados de ese conjunto de resultados. Cuando haces la listagg, el conjunto de resultados ya está limpio. El problema tenía más que ver con obtener los datos en un formato utilizable.


1
Es posible que desee agregar alguna explicación de cómo funciona.
jkavalik

Gracias por la respuesta y bienvenido al sitio. Podría ser aún más útil si pudiera describir por qué funciona esto y cómo ayudaría.
Tom V

He estado tratando de actualizar la respuesta pero sigue apareciendo errores. --- El problema con otras posibles soluciones es que no existe una correlación entre los resultados para la columna 1 y la columna 2. Para evitar esto, la consulta interna crea esta correlación y luego elimina los duplicados de ese conjunto de resultados. Cuando haces la listagg, el conjunto de resultados ya está limpio. El problema tenía más que ver con obtener los datos en un formato utilizable.
Kevin

-2

SQL fue diseñado como un lenguaje simple, muy cercano al inglés. Entonces, ¿por qué no lo escribes como en inglés?

  1. eliminar duplicados en num2 y usar listagg como función agregada, no analítica, para calcular concat en cadena
  2. unirse al original, ya que desea una fila de resultados para una entrada

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


Gracias por su respuesta. Esta solución requiere dos escaneos completos de la tabla, pero lo más importante no devuelve los resultados correctos.
Leigh Riffel

Lo siento, he pegado alguna versión anterior e incorrecta.
Š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(+);

Esto devuelve los resultados correctos para los datos proporcionados, pero tiene una suposición incorrecta. Num1 y Num2 no están relacionados. Num1 podría ser Char1 que contenga valores a, e, i, o, u, y. Sin embargo, esta solución requiere dos escaneos completos de la tabla que anulan el propósito de usar la función de agregado. Si la solución permitiera dos escaneos de tabla, esto sería preferible (con los datos de la muestra tiene un costo menor que cualquier otra cosa). SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Leigh Riffel el

-2

La solución más efectiva es SELECT interno con GROUP BY, porque DISTINCT y las expresiones regulares son lentas como el infierno.

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

Esta solución es bastante simple: primero obtienes todas las combinaciones únicas de num1 y num2 (SELECT interno) y luego obtienes la cadena de todos los num2 agrupados por num1.


Esta consulta no devuelve los resultados solicitados. Devuelve los mismos resultados que SELECT * FROM ListAggTest;.
Leigh Riffel

En su defensa, fue probablemente señaló a esta solución de otro problema stackoverflow marcada duplicado que esta solución hace fix. que 's la solución que quería. Parece que tengo que hacer diferentes suposiciones para publicar mi propia toma, por lo que dejaré esta pregunta sola con este comentario.
Gerard ONeill
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.