我在Oracle中使用LISTAGG功能时遇到了一些非常不寻常的行为。
我知道LISTAGG如果处理超过4000个字符就会失败。
因为我知道这一点,所以我有一个CASE语句来替换计数超过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的原因,即使它永远不会达到。
答案 0 :(得分:4)
选择列表列/表达式的最终评估,包括短路的案例表达,在检索数据之后发生。此时已经完成了任何分组等。
此效果不仅发生在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次,因为窗口排序(因此分析计算)在可以评估案例表达式之前完成。
所以问题实际上不是返回表达式(在你的情况下是listagg()
)在case表达式中被评估的时候它不应该是;在对案例表达条件进行考虑之前,它正在被评估并抛出异常。