Eliminar duplicatas no ListAgg (Oracle)


44

Antes do Oracle 11.2, eu estava usando uma função agregada personalizada para concatenar uma coluna em uma linha. 11.2 Adicionada a LISTAGGfunção, então estou tentando usá-la. Meu problema é que preciso eliminar duplicatas nos resultados e não parece capaz de fazer isso.

Aqui está um exemplo.

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

O que eu quero ver é o seguinte:

      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

Aqui está uma listaggversão que está próxima, mas não elimina duplicatas.

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

Eu tenho uma solução, mas é pior do que continuar usando a função agregada personalizada.


Deveria order by nullestar order by Num2ou estou ficando confuso?
Jack Douglas

@ Jack - Não faz diferença para a eliminação duplicada. Dependendo do seu uso, pode ser desejável.
Leigh Riffel

suspiro LISTAGG continua a ficar aquém do Tom Kyte deSTRAGG , com o qual é tão fácil comoSTRAGG(DISTINCT ...)
Baodad

Finalmente é possível: LISTAGG DISTINCT
lad2025 17/01

Respostas:


32

Você pode usar expressões regulares e regexp_replaceremover as duplicatas após a concatenação com listagg:

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

Isso poderia ser mais organizado se o sabor de regex da Oracle suportasse grupos de aparência ou sem captura, mas não .

No entanto, esta solução evita verificar a fonte mais de uma vez.

DBFiddle aqui


Observe que, para que esta técnica REGEX_REPLACE funcione para remover duplicatas, os valores duplicados devem estar todos próximos um do outro na cadeia de caracteres agregada.
Baodad 12/12

2
É isso que ORDER BY Num2alcança, não é (veja aqui ). Ou você está apenas tentando ressaltar que precisa do ORDER BY para que ele funcione?
Jack Douglas

13

Tanto quanto posso ver, com a especificação de idioma atualmente disponível, este é o mais curto para alcançar o que você deseja, se for necessário 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 foi a sua solução pior do que a solução agregada personalizada ?


Isso funciona, mas precisa fazer duas varreduras de tabela completas.
Leigh Riffel

Quando você tem uma tabela pequena que precisa agregar (<100000 linhas), o desempenho é mais do que aceitável para uma recuperação simples. Esta foi a minha solução de escolha após quase uma hora de teste de todas as formas possíveis!
Mathieu Dumoulin

Isso também funciona quando as duplicatas colocam o valor intermediário acima de 4000 caracteres. Isso o torna mais seguro que a regexpsolução.
Gordon Linoff

8

Crie uma função agregada personalizada para fazer isso.

O banco de dados Oracle fornece várias funções agregadas predefinidas, como MAX, MIN, SUM, para executar operações em um conjunto de registros. Essas funções agregadas predefinidas podem ser usadas apenas com dados escalares. No entanto, você pode criar suas próprias implementações personalizadas dessas funções ou definir funções agregadas totalmente novas para usar com dados complexos - por exemplo, com dados multimídia armazenados usando tipos de objeto, tipos opacos e LOBs.

As funções agregadas definidas pelo usuário são usadas nas instruções SQL DML, assim como os agregados internos do banco de dados Oracle. Depois que essas funções são registradas no servidor, o banco de dados simplesmente chama as rotinas de agregação que você forneceu em vez das nativas.

Agregados definidos pelo usuário também podem ser usados ​​com dados escalares. Por exemplo, pode valer a pena implementar funções agregadas especiais para trabalhar com dados estatísticos complexos associados a aplicações financeiras ou científicas.

Agregados definidos pelo usuário são um recurso do Extensibility Framework. Você os implementa usando rotinas de interface ODCIAggregate.


8

Embora este seja um post antigo com uma resposta aceita, acho que a função analítica LAG () funciona bem nesse caso e é digna de nota:

  • LAG () remove valores duplicados na coluna num2 com um custo mínimo
  • Não há necessidade de expressão regular não trivial para filtrar resultados
  • Apenas uma verificação completa da tabela (custo = 4 na tabela de exemplo simples)

Aqui está o código 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;

Os resultados abaixo parecem ser o que o OP deseja:

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

Aqui estava minha solução para o problema que, na minha opinião, não é tão bom quanto usar nossa função agregada personalizada que já 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

Use WMSYS.WM_Concat em vez disso.

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

Nota: Esta função não é documentada e não é suportada. Consulte https://forums.oracle.com/forums/message.jspa?messageID=4372641#4372641 .


6
Se você ligar para o suporte da Oracle e estiver usando wm_concat(mesmo se você argumentar wm_concatque o problema não está causando o problema), eles teriam motivos para se recusar a ajudar porque não é documentado e não é suportado - não é o caso se você usar um agregado personalizado ou qualquer outro recurso suportado.
Jack Douglas

5

Você também pode usar uma instrução collect e, em seguida, escrever uma função pl / sql personalizada que converta a coleção em uma string.

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

Você pode usar distincte order byem uma collectcláusula, mas se combinado distinct, não funcionará a partir de 11.2.0.2 :(

A solução alternativa pode ser uma subseleção:

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

Não vejo como uma função pl / sql personalizada seria melhor do que uma função agregada personalizada. O SQL resultante é certamente mais simples para o último. Como esse problema estava em 11.2.0.2, a subseleção adicionaria uma verificação adicional que eu estava tentando evitar.
Leigh Riffel

Eu diria que uma função PL / SQL chamada ONCE para converter a coleção em uma string poderia ser melhor do que a função agregada chamada milhares de vezes. Eu acho que isso reduziria muito as alternâncias de contexto.
Nico

Sua teoria parece boa e foi uma das razões pelas quais eu estava tentando evitar a função agregada personalizada e preferia uma função agregada integrada como LISTAGG. Se você quiser fazer algumas comparações de tempo, eu estaria interessado nos resultados.
Leigh Riffel

2

Criei esta solução antes de encontrar o ListAgg, mas ainda existem ocasiões, como esse problema de valor duplicado, e essa ferramenta é útil. A versão abaixo possui 4 argumentos para fornecer controle sobre os resultados.

Explicação O CLOBlist usa o contrator CLOBlistParam como um parâmetro. CLOBlistParam possui 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

Exemplo 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

O link para Gist está abaixo.

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

Eu sei que é algum tempo após a postagem original, mas este foi o primeiro local que encontrei após pesquisar no Google por uma resposta para o mesmo problema e pensei que alguém que chegou aqui pode ficar feliz em encontrar uma resposta sucinta que não se baseie em consultas excessivamente complicadas. ou regexes.

Isso fornecerá o resultado desejado:

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

Minha idéia é implementar uma função armazenada 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;

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

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

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


Observe que o OP já tinha sua própria LISTAGGfunção personalizada ; eles estavam tentando explicitamente ver se conseguiam encontrar uma maneira eficiente de fazer isso usando a LISTAGGfunção interna disponível na versão 11.2.
RDFozz 27/02

0

Tente 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

O problema com outras soluções possíveis é que não há correlação entre os resultados da coluna 1 e da coluna 2. Para contornar isso, a consulta interna cria essa correlação e remove as duplicatas desse conjunto de resultados. Quando você faz a lista, o conjunto de resultados já está limpo. o problema tinha mais a ver com a obtenção dos dados em um formato utilizável.


1
Você pode querer adicionar uma explicação de como isso funciona.
Jkavalik 28/10/2015

Obrigado pela resposta e bem-vindo ao site. Pode ser ainda mais útil se você puder descrever por que isso funciona e como isso ajudaria.
Tom V

Eu tenho tentado atualizar a resposta, mas ela continua com erros. --- O problema com outras soluções possíveis é que não há correlação entre os resultados da coluna 1 e da coluna 2. Para contornar isso, a consulta interna cria essa correlação e remove as duplicatas desse conjunto de resultados. Quando você faz a lista, o conjunto de resultados já está limpo. o problema tinha mais a ver com a obtenção dos dados em um formato utilizável.
Kevin

-2

O SQL foi projetado como linguagem simples, muito próxima do inglês. Então, por que você não escreve como em inglês?

  1. elimine duplicatas no num2 e use listagg como função agregada - não analítica, para calcular a concat na string
  2. junte-se ao original, pois você deseja uma linha de resultado para uma 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);


Obrigado pela sua resposta. Essa solução requer duas varreduras completas da tabela, mas o mais importante é não retornar os resultados corretos.
Leigh Riffel

Desculpe por isso, colei uma versão antiga e incorreta.
Štefan Oravec 18/02

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

Isso retorna os resultados corretos para os dados fornecidos, mas tem uma suposição incorreta. Num1 e Num2 não são relacionados. Num1 também poderia ser Char1 contendo os valores a, e, i, o, u, y. Independentemente disso, esta solução requer duas varreduras completas da tabela, anulando todo o objetivo de usar a função agregada. Se a solução permitisse duas varreduras de tabela, seria preferível (com os dados de amostra, o custo é menor do que qualquer outra coisa). SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Leigh Riffel

-2

A solução mais eficaz é SELECT interna com GROUP BY, porque DISTINCT e expressões regulares são lentas como o 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;

Essa solução é bem simples - primeiro você obtém todas as combinações únicas de num1 e num2 (SELECT interno) e, em seguida, obtém a sequência de todos os num2 agrupados por num1.


Esta consulta não retorna os resultados solicitados. Retorna os mesmos resultados que SELECT * FROM ListAggTest;.
Leigh Riffel

Em sua defesa, ele provavelmente estava apontado para esta solução de outro problema stackoverflow marcado duplicado que esta solução faz correção. essa é a solução que eu queria. Parece que tenho que fazer suposições diferentes para postar minha própria opinião e, portanto, deixarei essa pergunta em paz com este comentário.
precisa
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.