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トム・カイトにSTRAGGSTRAGG(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が実現するのはそうではありません(こちらを参照)。それとも、ORDER BYが機能するためには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;

カスタム集計ソリューションよりも悪いソリューションは何でしたか?


これは機能しますが、2回テーブル全体をスキャンする必要があります。
リーリッフェル

集約する必要がある小さなテーブル(<100000行)がある場合、単純な検索ではパフォーマンスが許容範囲を超えています。これは、考えられる各方法をほぼ1時間テストした後、私の選択したソリューションです。
マチューデュムーリン14

これは、重複によって中間値が4000文字を超える場合にも機能します。これにより、regexpソリューションよりも安全になります。
ゴードンリノフ

8

これを行うカスタム集計関数を作成します。

Oracleデータベースは、一連のレコードに対して操作を実行するためのMAX、MIN、SUMなどの事前定義されたいくつかの集計関数を提供します。これらの事前定義された集計関数は、スカラーデータでのみ使用できます。ただし、これらの関数の独自のカスタム実装を作成するか、完全に新しい集計関数を定義して、複雑なデータ(たとえば、オブジェクト型、不透明型、およびLOBを使用して格納されたマルチメディアデータ)で使用できます。

ユーザー定義の集計関数は、Oracleデータベースの組み込み集計と同様に、SQL DMLステートメントで使用されます。そのような関数がサーバーに登録されると、データベースは、ネイティブ関数ではなく、指定した集約ルーチンを呼び出すだけです。

ユーザー定義の集計は、スカラーデータでも使用できます。たとえば、金融または科学アプリケーションに関連する複雑な統計データを操作するための特別な集計関数を実装する価値がある場合があります。

ユーザー定義集計は、拡張性フレームワークの機能です。ODCIAggregateインターフェースルーチンを使用して実装します。


8

これは受け入れられた答えのある古い投稿ですが、この場合LAG()分析関数はうまく機能し、注目に値すると思います。

  • LAG()は、最小限の費用で列num2の重複値を削除します
  • 結果をフィルタリングするために重要な正規表現は不要
  • 1回の全表スキャン(単純な例の表ではcost = 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;

以下の結果は、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

私の意見では、既存のカスタム集計関数を使用するほど良くないという問題に対する私の解決策がここにありました。

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 bycollect節が、組み合わせた場合distinct11.2.0.2のように動作しません:(

回避策はサブセレクトかもしれません:

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

カスタムpl / sql関数がカスタム集計関数よりも優れていることを確認できません。後者の場合、結果のSQLは確かに単純です。この問題は11.2.0.2にあったので、副選択により、回避しようとしていた追加のスキャンが追加されます。
リーリッフェル

コレクションを文字列に変換するONCEというPL / SQL関数は、数千回呼び出される集計関数よりも優れていると思います。これにより、コンテキストスイッチが大幅に削減されると思います。
ニコ

あなたの理論は良さそうで、私がカスタム集計関数を避けようとしていた理由の1つであり、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;

申し訳ありませんが、場合によっては(非常に大きなセットの場合)、このエラーが返される可能性があります。

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.

しかし、私はこれが出発点の良いポイントだと思います;)


OPには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. 1つの入力に対して1つの結果行が必要なため、オリジナルに結合します

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回の全表スキャンが必要ですが、さらに重要なことには、正しい結果が返されません。
リーリッフェル

申し訳ありませんが、古いバージョンと誤ったバージョンを貼り付けました。
ステファン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(+);

これは、指定されたデータの正しい結果を返しますが、誤った仮定を持っています。Num1とNum2は無関係です。Num1は、値a、e、i、o、u、yを含むChar1でもあります。無防備なこのソリューションでは、集計関数を使用する目的全体に反するテーブルの2回の完全スキャンが必要です。ソリューションで2つのテーブルスキャンが許可されている場合、これが優先されます(サンプルデータを使用すると、他よりもコストが低くなります)。SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
リーリッフェル

-2

DISTINCTと正規表現は非常に遅いため、最も効果的なソリューションはGROUP BYを使用した内部SELECTです。

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)を取得し、次にnum1でグループ化されたすべてのnum2の文字列を取得します。


このクエリは、要求された結果を返しません。と同じ結果を返しますSELECT * FROM ListAggTest;
リーリフェル

彼の防御では、彼はおそらく、このソリューション修正する重複とマークされた別のstackoverflow問題からこのソリューションを指摘されました。 それが私が望んでいた解決策です。私は自分のテイクを投稿するために別の仮定をしなければならないようですので、このコメントでこの質問を放っておきます。
ジェラルド・オニール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.