SQL基础知识不扎实导致的编程错误 - l1t1/note GitHub Wiki
在测试各种数据库对递归CTE的支持程度时无意发现,如下SQL会导致无限循环或OOM报错,以为是数据库的问题,但不同数据库怎么都这么实现?查阅了SQL国际标准,其实是我的错。
with recursive t(i,lv) as(select generate_series i,1 lv from generate_series(1,6,1)
union all
select max(i),max(lv)+1 from t where lv<2)
select * from t where lv=2;
我以为上述SQL应该返回一行6, 2
, 因为一轮递归后的结果集中不含lv<2
的行,就会终止递归。
实际上,聚合函数max
有个性质,当无满足条件的行时,返回NULL
。
SQL国标征求意见稿 中写到:
如果没有行符合条件,则
COUNT
的结果为 0,任何其他聚合函数的结果均为空值。
所以,
第一轮递归select max(i),max(lv)+1 from(select 6 i, 2 lv)t where lv<2
的结果是:NULL,NULL
。
下一轮递归select max(i),max(lv)+1 from(select NULL i,NULL lv)t where lv<2
的结果还是:NULL,NULL
,于是造成无限递归。
要避免无限递归,可以这么改写:
with recursive t(i,lv) as(select generate_series i,1 lv from generate_series(1,6,1)
union all
select max(i),max(lv)+1 from t having max(lv)<2)
select * from t where lv=2;
having
是对聚合后结果的过滤,因此第2轮递归消除了为NULL
的行,结果没有产生新行,递归终止。
在受支持的数据库上可以用union
代替union all
, 如下:
with recursive t(i,lv) as(select generate_series i,1 lv from generate_series(1,6,1)
union
select max(i),max(lv)+1 from t where lv<2)
select * from t where lv=2;
union
将本轮的结果NULL,NULL
和已有结果去重复合并,结果第3轮递归没有产生新行,因此递归终止,如果最后查询select * from t
,能看到结果包含一行NULL,NULL
。