Oracle 11g 里没有 LISTAGG(DISTINCT),四种替代写法

LISTAGG 是 11g R2 引入的,但 DISTINCT 关键字要到 19c 才支持。如果你在 11g 上写:

-- 11g 会直接报 ORA-30482: DISTINCT option not allowed for this function
SELECT LISTAGG(DISTINCT dept_name, ',') WITHIN GROUP (ORDER BY dept_name)
FROM   emp;

那就得换思路。下面四种写法都能work,但代价不同。

写法一:子查询先去重,再拼接

最直观,也最不容易出错。

SELECT LISTAGG(dept_name, ',') WITHIN GROUP (ORDER BY dept_name) AS depts
FROM (
    SELECT DISTINCT dept_name
    FROM   emp
    WHERE  dept_name IS NOT NULL
);

注意 IS NOT NULL 不能省。LISTAGG 会跳过 NULL,但去重子查询里 NULL 会影响你判断结果对不对。

适用场景:数据量不大,或者只拼一列。

写法二:REGEXP_REPLACE 去重(单值技巧)

先拼再删重复,SQL 短,但只适合确定不会互相包含的短字符串。

SELECT RTRIM(
         REGEXP_REPLACE(
           LISTAGG(dept_name, ',') WITHIN GROUP (ORDER BY dept_name),
           '([^,]+)(,\1)+', '\1'
         ),
         ','
       ) AS depts
FROM emp;

风险点:

  • 值里本身含逗号就会解析错
  • 长度超过 4000 字节会截断
  • 顺序不保证是字典序

所以这个方法我只在临时排查时用,不进正式代码。

写法三:XMLAGG 拼接

XMLAGG 不受 4000 字节限制,适合拼长字符串。

SELECT RTRIM(
         EXTRACT(
           XMLAGG(XMLELEMENT(e, dept_name || ',') ORDER BY dept_name).getClobVal()
         ).getClobVal(),
         ','
       ) AS depts
FROM (SELECT DISTINCT dept_name FROM emp WHERE dept_name IS NOT NULL);

代价是:可读性差,且 getClobVal() 返回 CLOB,下游如果按 VARCHAR2 处理要额外转换。

写法四:分析函数 + 只取首行

如果去重逻辑复杂(比如按多个字段判重、要保留某个时间点最新的一条),用 ROW_NUMBER 更可控。

SELECT LISTAGG(dept_name, ',') WITHIN GROUP (ORDER BY dept_name) AS depts
FROM (
    SELECT dept_name,
           ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY hire_date DESC) AS rn
    FROM   emp
    WHERE  dept_name IS NOT NULL
)
WHERE rn = 1;

这个写法的好处是:判重规则完全由你决定,而不是靠字符串层面的技巧。

怎么选

  • 只是拼接去重 → 写法一
  • 结果可能超过 4000 字节 → 写法三
  • 去重规则涉及多列或有优先级 → 写法四
  • 写法二 → 只用于临时排查

一个容易被忽略的坑

LISTAGG 的结果长度上限是 4000 字节(VARCHAR2)。字符集是 AL32UTF8 时,一个中文占 3 字节,所以大约 1300 个汉字就会报 ORA-01489。

如果业务上确实可能超长,从设计阶段就该改用 CLOB 方案(写法三),而不是等报错了再改。