Устранить дубликаты в ListAgg (Oracle)


44

До Oracle 11.2 я использовал пользовательскую агрегатную функцию для объединения столбца в строку. 11.2 Добавлена LISTAGGфункция, поэтому я пытаюсь использовать ее вместо этого. Моя проблема в том, что мне нужно устранить дубликаты в результатах и, похоже, я не могу этого сделать.

Вот пример.

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

То, что я хочу увидеть, это:

      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

Вот listaggверсия, которая близка, но не устраняет дубликаты.

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

У меня есть решение, но это хуже, чем продолжать использовать пользовательскую функцию агрегирования.


Должно order by nullбыть order by Num2или я запутался?
Джек Дуглас

@ Джек - Это не имеет значения для устранения дубликатов. В зависимости от вашего использования, это может быть желательно.
Ли Риффель

вздох LISTAGG продолжает отставать от Тома КайтаSTRAGG , с которым это так же легко, какSTRAGG(DISTINCT ...)
Baodad

Наконец это возможно: LISTAGG DISTINCT
lad2025

Ответы:


32

Вы можете использовать регулярные выражения и regexp_replaceудалить дубликаты после объединения listagg:

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

Это может быть проще, если Oracle поддерживает регулярные выражения или группы без захвата, но это не так .

Однако это решение позволяет избежать сканирования источника более одного раза.

DBFiddle здесь


Обратите внимание, что для того, чтобы этот метод REGEX_REPLACE работал для удаления дубликатов, все дублированные значения должны быть рядом друг с другом в агрегированной строке.
Баодад

2
Вот чего ORDER BY Num2добивается, не правда ли (см. Здесь ). Или вы просто пытаетесь указать, что вам нужен ORDER BY, чтобы он работал?
Джек Дуглас

13

Насколько я вижу, с доступной на данный момент спецификацией языка это самый короткий путь для достижения того, чего вы хотите, если это необходимо сделать 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;

Каким было ваше решение, которое было хуже, чем нестандартное совокупное решение ?


Это работает, но нужно сделать два полных сканирования таблицы.
Ли Риффель

Если у вас небольшая таблица, которую нужно агрегировать (<100000 строк), производительность будет более чем приемлемой для простого извлечения. Это было мое решение выбора после почти часа тестирования всех возможных способов!
Матье Думулин

Это также работает, когда дубликаты помещают промежуточное значение более 4000 символов. Это делает его более безопасным, чем regexpрешение.
Гордон Линофф

8

Создайте пользовательскую агрегатную функцию для этого.

База данных Oracle предоставляет ряд предопределенных агрегатных функций, таких как MAX, MIN, SUM для выполнения операций с набором записей. Эти предопределенные агрегатные функции могут использоваться только со скалярными данными. Однако вы можете создавать свои собственные пользовательские реализации этих функций или определять совершенно новые агрегатные функции для использования со сложными данными, например, с мультимедийными данными, хранящимися с использованием типов объектов, непрозрачных типов и больших объектов.

Пользовательские агрегатные функции используются в инструкциях SQL DML так же, как встроенные агрегаты базы данных Oracle. Как только такие функции зарегистрированы на сервере, база данных просто вызывает предоставленные вами процедуры агрегации вместо собственных.

Определяемые пользователем агрегаты также могут использоваться со скалярными данными. Например, может быть целесообразно реализовать специальные агрегатные функции для работы со сложными статистическими данными, связанными с финансовыми или научными приложениями.

Определяемые пользователем агрегаты являются функцией расширяемой платформы. Вы реализуете их, используя процедуры интерфейса ODCIAggregate.


8

Хотя это старый пост с принятым ответом, я думаю, что аналитическая функция LAG () хорошо работает в этом случае и заслуживает внимания:

  • LAG () удаляет повторяющиеся значения в столбце num2 с минимальными затратами
  • Нет необходимости в нетривиальном регулярном выражении для фильтрации результатов
  • Всего одно полное сканирование таблицы (стоимость = 4 для простого примера таблицы)

Вот предложенный код:

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;

Представленные ниже результаты соответствуют желанию ФП:

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

Здесь было мое решение проблемы, которая, на мой взгляд, не так хороша, как использование нашей пользовательской агрегатной функции, которая уже существует.

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

Вместо этого используйте WMSYS.WM_Concat.

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

Примечание. Эта функция недокументирована и не поддерживается. См. Https://forums.oracle.com/forums/message.jspa?messageID=4372641#4372641 .


6
Если вы звоните в службу поддержки Oracle и используете wm_concat(даже если вы утверждаете, что wm_concatпроблема сама по себе не является причиной), у них будут основания для отказа в помощи, поскольку она не документирована и не поддерживается - не в случае, если вы используете пользовательский агрегат или какой-либо другой поддерживаемая функция.
Джек Дуглас

5

Вы также можете использовать оператор collect и затем написать собственную функцию pl / sql, которая преобразует коллекцию в строку.

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

Вы можете использовать distinctи order byв collectпредложении, но если объединить, то distinctне будет работать с 11.2.0.2 :(

Обходной путь может быть подвыбор:

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

Я не вижу, как пользовательская функция pl / sql будет лучше, чем пользовательская агрегатная функция. Результирующий SQL, конечно, проще для последнего. Так как эта проблема была на 11.2.0.2, выборка добавила бы дополнительное сканирование, которое я пытался избежать.
Ли Риффель

Я бы сказал, что функция PL / SQL под названием ONCE для преобразования коллекции в строку может быть лучше, чем агрегатная функция, вызываемая тысячи раз. Я думаю, что это сильно уменьшит переключение контекста.
Нико

Ваша теория звучит хорошо и была одной из причин, по которой я пытался избежать пользовательской функции агрегирования и предпочитал встроенную функцию агрегирования, такую ​​как LISTAGG. Если вы хотите провести сравнение сроков, меня будут интересовать результаты.
Ли Риффель

2

Я создал это решение до того, как столкнулся с ListAgg, но все еще есть случаи, такие как проблема с дублирующимся значением, тогда этот инструмент полезен. Версия ниже имеет 4 аргумента, чтобы дать вам контроль над результатами.

Пояснение CLOBlist принимает в качестве параметра конструктор CLOBlistParam. CLOBlistParam имеет 4 аргумента

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

Пример использования

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

Ссылка на Gist ниже.

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

Я знаю, что через некоторое время после первоначальной публикации, но это было первое место, которое я нашел после того, как Гуглинг нашел ответ на ту же проблему, и подумал, что кто-то еще, кто приземлился здесь, может быть рад найти краткий ответ, не основанный на слишком сложных запросах. или регулярные выражения

Это даст вам желаемый результат:

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

Моя идея состоит в том, чтобы реализовать хранимую функцию следующим образом:

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;

Извините, но в некоторых случаях (для очень большого набора) Oracle может вернуть эту ошибку:

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.

но я думаю, что это хорошее начало;)


Обратите внимание, что у ОП уже есть свои собственные LISTAGGфункции; они явно пытались выяснить, смогут ли они найти эффективный способ сделать это, используя встроенную LISTAGGфункцию, доступную с версии 11.2.
RDFozz

0

Попробуй это:

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

Проблема с другими возможными решениями заключается в том, что нет корреляции между результатами для столбца 1 и столбца 2. Чтобы обойти это, внутренний запрос создает эту корреляцию, а затем удаляет дубликаты из этого набора результатов. Когда вы делаете listagg, набор результатов уже чист. проблема была больше связана с получением данных в удобном для использования формате.


1
Вы можете добавить некоторые объяснения того, как это работает.
jkavalik

Спасибо за ответ и добро пожаловать на сайт. Было бы еще полезнее, если бы вы могли описать, почему это работает и как это поможет.
Том V - Команда Моника

Я пытался обновить ответ, но он продолжает выдавать ошибку. --- Проблема с другими возможными решениями заключается в том, что нет корреляции между результатами для столбца 1 и столбца 2. Чтобы обойти это, внутренний запрос создает эту корреляцию, а затем удаляет дубликаты из этого набора результатов. Когда вы делаете listagg, набор результатов уже чист. проблема была больше связана с получением данных в удобном для использования формате.
Кевин

-2

SQL был разработан как простой язык, очень близкий к английскому. Так почему бы тебе не написать это по-английски?

  1. устранить дубликаты в num2 и использовать listagg в качестве агрегатной функции - не аналитической, чтобы вычислить concat для строки
  2. присоединиться к оригиналу, так как вы хотите одну строку результатов для одного ввода

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


Благодарю за ваш ответ. Это решение требует двух полных сканирований таблицы, но, что более важно, не возвращает правильные результаты.
Ли Риффель

Извините за это, я вставил более старую и неправильную версию.
Штефан Оравец

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

Это возвращает правильные результаты для данных, но имеет неверное предположение. Num1 и Num2 не связаны. Num1 также может быть Char1, содержащим значения a, e, i, o, u, y. Как бы то ни было, это решение требует двух полных сканирований таблицы, что лишает цели использования агрегатной функции. Если бы решение разрешало два сканирования таблицы, то это было бы предпочтительным (с данными выборки оно дешевле, чем что-либо еще). SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Ли Риффель

-2

Наиболее эффективным решением является внутренний SELECT с GROUP BY, потому что DISTINCT и регулярные выражения работают медленно.

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

Это решение довольно простое - сначала вы получаете все уникальные комбинации num1 и num2 (внутренний SELECT), а затем вы получаете строку всех num2, сгруппированных по num1.


Этот запрос не возвращает запрошенные результаты. Возвращает те же результаты, что и SELECT * FROM ListAggTest;.
Ли Риффель

В свою защиту, он, вероятно , указывает на решение этой проблемы с другой StackOverflow отметился дублем , что это решение делает исправление. это решение, которое я хотел. Кажется, я должен сделать различные предположения, чтобы опубликовать свой собственный дубль, и поэтому я оставлю этот вопрос наедине с этим комментарием.
Джерард ONeill
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.