消除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 nullorder by Num2还是我越来越糊涂?
杰克·道格拉斯

@Jack-对重复消除没有影响。根据您的用途,可能是理想的。
Leigh Riffel 2012年

叹息 LISTAGG仍然不及汤姆·凯特STRAGGTom Kyte),这就像STRAGG(DISTINCT ...)
Baodad

终于有可能:LISTAGG DISTINCT
lad2025

Answers:


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技术可用于删除重复项,重复值在聚合字符串中必须都彼此相邻。
Baodad

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;

您的解决方案比自定义聚合解决方案还差什么?


这可行,但是必须进行两次全表扫描。
Leigh Riffel

当您有一个需要汇总(小于100000行)的小表时,对于简单的检索而言,性能是可以接受的。在对每种可能的方法进行了近一个小时的测试之后,这是我选择的解决方案!
Mathieu Dumoulin 2014年

当重复项将中间值超过4000个字符时,这也适用。这使其比regexp解决方案更安全。
Gordon Linoff

8

创建一个自定义聚合函数来执行此操作。

Oracle数据库提供了许多预定义的聚合函数,例如MAX,MIN,SUM,用于对一组记录执行操作。这些预定义的聚合函数只能与标量数据一起使用。但是,您可以创建这些函数的自定义实现,或定义全新的聚合函数,以用于复杂数据,例如,用于使用对象类型,不透明类型和LOB存储的多媒体数据。

用户定义的聚合函数在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;

以下结果似乎是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

您可以在子句中使用distinctand order bycollect但是如果结合使用distinct,从11.2.0.2开始将不起作用:(

解决方法可能是子选择:

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

我看不到自定义的pl / sql函数比自定义的聚合函数更好。生成的SQL对于后者肯定更简单。由于此问题在11.2.0.2上,因此subselect将添加我试图避免的其他扫描。
Leigh Riffel

我想说一个称为ONCE的PL / SQL函数将集合转换为字符串可能比称为数千次的聚合函数更好。我认为这将大大减少上下文切换。
Nico

您的理论听起来不错,这也是我尝试避免使用自定义聚合函数并偏爱诸如LISTAGG之类的内置聚合函数的原因之一。如果您想进行一些时序比较,我会对结果感兴趣。
Leigh Riffel

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

链接到下面。

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.

但是我认为这是一个很好的起点;)


请注意,OP已经具有自己的自定义LISTAGG功能。他们明确地试图查看他们是否可以找到一个有效的方法,以使用LISTAGG11.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

感谢您的回答,欢迎访问该网站。如果您可以描述它为什么起作用以及如何起作用,可能会更加有用。
Tom 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);


感谢您的答复。此解决方案需要两次全表扫描,但更重要的是不会返回正确的结果。
Leigh Riffel

抱歉,我粘贴了一些较旧且错误的版本。
斯特凡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。这种方法无可救药,需要对表进行两次完整扫描,从而破坏了使用聚合函数的全部目的。如果解决方案允许进行两次表扫描,则将是首选方法(对于示例数据,其成本比其他任何方法都要低)。SELECT Num1, ( SELECT LISTAGG(Num2) WITHIN GROUP (ORDER BY Num2) FROM (SELECT distinct Num2 FROM listAggTest) ) Num2 FROM ListAggTest;
Leigh Riffel 2013年

-2

最有效的解决方案是使用GROUP BY进行内部SELECT,因为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),然后获得按num1分组的所有num2的字符串。


该查询不返回请求的结果。它返回与相同的结果SELECT * FROM ListAggTest;
Leigh Riffel 2013年

在辩护中,他可能是从另一个stackoverflow问题指出此解决方案的,该问题被标记为该解决方案确实可以解决。 就是我想要的解决方案。似乎我不得不做出不同的假设来发表自己的看法,因此,我将在本评论中保留这个问题。
Gerard ONeill '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.