LISTAGG 被评估并在无法访问的 case 语句中失败
Posted
技术标签:
【中文标题】LISTAGG 被评估并在无法访问的 case 语句中失败【英文标题】:LISTAGG is evaluated and fails in an unreachable case statement 【发布时间】:2016-08-25 08:40:03 【问题描述】:我在 Oracle 中使用 LISTAGG 函数时遇到了一些非常不寻常的行为。
我知道如果 LISTAGG 处理超过 4000 个字符,它就会失败。
因为我知道这一点,所以我有一个 CASE 语句,用“Too many to count”消息替换计数超过 100 个字符的单元格。
CREATE TABLE EMP (
ID VARCHAR2(401),
DEP VARCHAR2(10)
);
INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 401), 'FOO'); -- Run exactly 9 times
INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 5), 'BAR'); -- Run 3 times
为了简单起见,让我们忽略计数 > 100 的特殊情况,只说应该排除 FOO,应该包括 BAR。
SELECT DEP,
CASE
WHEN DEP = 'BAR' THEN
LISTAGG(ID, ',')
WITHIN GROUP (ORDER BY NULL)
OVER (PARTITION BY DEP)
ELSE
'Too many to count'
END AS ID_LIST
FROM EMP;
这提供的结果应该如下所示(但具有不同的随机字符):
但是,仅添加一个额外的行,使 FOO 部门的总数达到 10...
INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 401), 'FOO'); -- Same as before
导致我们在重新运行相同的选择时遇到异常:
ORA-01489: result of string concatenation is too long
01489. 00000 - "result of string concatenation is too long"
*Cause: String concatenation result is more than the maximum size.
*Action: Make sure that the result is less than the maximum size.
奇怪的是,即使将 case 语句中的条件更改为 1=2,也会发生这种情况。
我不确定这里发生了什么。似乎 SQL 决定评估该语句,而不管它是否有任何使用它的意图,因此在遇到 4000+ 字符 LISTAGG 时失败。
我有一些解决我的问题的方法,但我真的很想了解更多关于为什么 SQL 决定(显然)运行 LISTAGG,即使它永远不会被访问。
【问题讨论】:
【参考方案1】:选择列表列/表达式(包括短路的 case 表达式)的最终评估发生在检索数据之后。到那时,任何分组等都已经完成。
这种影响不仅发生在 listagg()
中,还可以在返回表达式中的任何聚合或分析函数调用中看到 - 尽管除非有副作用,否则很难发现。
作为演示,我创建了一个简单的包,其中包含可以从查询中调用的函数:
create package p as
n number := 0;
function f return number;
end;
/
create package body p as
function f return number as
begin
n := n + 1;
return n;
end;
end;
/
这实质上是在模拟特定于会话的序列;序列也展示了这种行为,but appearently for a different reason 所以我不想为此使用一个。
在 case 表达式中调用该函数会如您所愿;仅在条件匹配时调用:
select dep,
case
when dep = 'BAR' then
p.f
else
-1
end as id_list
from emp;
DEP ID_LIST
---------- -------
FOO -1
...
BAR 1
BAR 2
BAR 3
FOO -1
select p.f from dual;
F
----------
4
该函数仅在条件匹配时才被调用。其执行计划仅显示全表扫描:
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 13 | 91 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS FULL| EMP | 13 | 91 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------
改为使用聚合调用:
select dep,
case
when dep = 'BAR' then
count(p.f)
else
-1
end as id_list
from emp
group by dep;
DEP ID_LIST
---------- -------
FOO -1
BAR 3
select p.f from dual;
F
----------
18
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 13 | 91 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 13 | 91 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMP | 13 | 91 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
...该函数被调用了 13 次而不是 3 次;该计划逐步显示哈希组,这必须在评估案例之前发生在所有检索到的行中。
解析版本也是如此:
select dep,
case
when dep = 'BAR' then
count(p.f) over (partition by dep)
else
-1
end as id_list
from emp;
DEP ID_LIST
---------- -------
BAR 3
BAR 3
BAR 3
FOO -1
...
select p.f from dual;
F
----------
32
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 13 | 91 | 4 (25)| 00:00:01 |
| 1 | WINDOW SORT | | 13 | 91 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMP | 13 | 91 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
...再次调用该函数 13 次,因为窗口排序(以及分析计算)在 case 表达式被评估之前完成。
所以问题不在于返回表达式(在您的情况下为listagg()
)在不应该的情况下在 case 表达式中进行评估;在考虑 case 表达式条件之前,它正在被评估并抛出异常。
【讨论】:
当case语句中的第一个when
条件为1=0
时,优化器并没有做得更好,这仍然很奇怪。优化器应该在整个编译之前将case when 1=0 then <whatever> else 'Too many to count' end as id_list
重写为'Too many to count' as id_list
。优化器在解析查询的其他部分非常聪明,但在这种情况下显然不是这样。
有一个 MOS 说明建议它应该对数字比较进行优化(所以 1=0),而不是字符串比较('T'='F')。但即便如此,在这个过程中一定为时已晚——它会在非聚合版本中做到这一点,但是当它不得不担心聚合等时,查询重写的规模可能太大了。也许这对他们来说太小众了看过...
谢谢亚历克斯,这是一个非常彻底的解释。我应该考虑自己做一个解释计划。以上是关于LISTAGG 被评估并在无法访问的 case 语句中失败的主要内容,如果未能解决你的问题,请参考以下文章