Усуньте дублікати в 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чи я заплутався?
Джек Дуглас

@Jack - це не має значення для усунення дублікатів. Залежно від вашого використання, це може бути бажаним.
Лі Риффель

зітхання LISTAGG продовжує відставати від Тома КейтаSTRAGG , з яким це так просто, якSTRAGG(DISTINCT ...)
Баодад

Нарешті можливо: 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досягається чи не так (див. Тут ). Або ви просто намагаєтесь вказати, що вам потрібен ЗАМОВЛЕННЯ, щоб він працював?
Джек Дуглас

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 для виконання операцій над набором записів. Ці заздалегідь визначені сукупні функції можна використовувати лише зі скалярними даними. Однак ви можете створити власні реалізовані функції цих функцій або визначити абсолютно нові сукупні функції для використання зі складними даними - наприклад, з мультимедійними даними, що зберігаються з використанням типів об'єктів, непрозорих типів та LOB.

Визначені користувачем функції агрегату використовуються в операторах SQL DML так само, як вбудовані агрегати бази даних Oracle. Після реєстрації таких функцій на сервері база даних просто викликає підпрограми агрегації, які ви надали замість власних.

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

Описані користувачем агрегати є особливістю рамки розширення. Ви реалізуєте їх за допомогою підпрограм інтерфейсу ODCIAggregate.


8

Хоча це старий пост із прийнятою відповіддю, я думаю, що аналітична функція LAG () в цьому випадку добре працює і є примітною:

  • LAG () видаляє дублікати значень у колонці №2 з мінімальними витратами
  • Немає необхідності у нетривіальному регулярному вираженні для фільтрації результатів
  • Лише одне сканування повної таблиці (вартість = 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

Ви також можете використовувати оператор збирання, а потім написати спеціальну функцію 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 як сукупну функцію - не аналітичну, для обчислення конматів у рядку
  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, оскільки РОЗМІСТ та регулярні вирази повільні як пекло.

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.