PostgreSQL 递归死循环案例及解法

2 minute read

背景

PostgreSQL 提供的递归语法是很棒的,例如可用来解决树形查询的问题,解决Oracle用户 connect by的语法兼容性。

请参考

https://yq.aliyun.com/articles/54657

但是如果参与递归查询的数据集有问题,例如数据打结的问题。则会导致递归死循环,可能导致临时文件暴增,把空间占满,影响业务。

案例

假设c1,c2是上下级关系,c2是c1的上级ID。

创建测试表如下

create table test(c1 int, c2 int, info text);
create index idx_test_01 on test(c1);
create index idx_test_02 on test(c2);

插入一组测试数据,其中(1,1,’test’)是个结,如果用递归查询的话,会导致无法退出循环。

insert into test values 
(9,8,'test'), 
(8,7,'test'), 
(7,6,'test'), 
(6,5,'test'), 
(5,4,'test'), 
(4,3,'test'), 
(3,2,'test'), 
(2,1,'test'), 
(1,1,'test');

递归查询,从c1=9开始往上检索,到1之后会一直往下走,无法终结。

with recursive t(c1,c2,info) as (
  select * from test where c1=9 
  union all 
  select t2.* from test t2 join t on (t.c2 =t2.c1) 
) 
select count(*) from t;

可以在数据库的临时文件目录,看到不停增长的临时文件。

total 96M
-rw------- 1 digoal digoal 89M Jul 23 20:07 pgsql_tmp8997.5

一段时间后

...
-rw------- 1 digoal digoal 575M Jul 23 20:09 pgsql_tmp8997.5

继续产生

total 2.5G
-rw------- 1 digoal digoal 1.0G Jul 23 20:10 pgsql_tmp8997.5
-rw------- 1 digoal digoal 1.0G Jul 23 20:14 pgsql_tmp8997.6
-rw------- 1 digoal digoal 435M Jul 23 20:15 pgsql_tmp8997.7

如何解决死循环的问题

临时文件相关的数据库参数介绍

#temp_buffers = 8MB                     # min 800kB

临时空间buffer大小

#
#temp_file_limit = -1                   # limits per-session temp file space
					# in kB, or -1 for no limit

单个会话最多允许产生多少临时文件

#
log_temp_files = 102400                 # log temporary files equal or larger
                                        # than the specified size in kilobytes;
                                        # -1 disables, 0 logs all temp files

当临时文件使用超过多少时,记录日志

但是别被它误解,是QUERY结束的时候记录的,中途不记录。

可以通过改内核实现阶段性的记录。

#
#temp_tablespaces = ''                  # a list of tablespace names, '' uses
                                        # only default tablespace

可以指定临时目录的表空间,默认是default tablespace

看完以上几个参数,大家应该心里有数了。

通过设置temp_file_limit即可限制当前会话允许使用的最大临时空间。

测试

手工退出刚才的死循环QUERY

postgres=# with recursive t(c1,c2,info) as (select * from test where c1=9 union all select t2.* from test t2 join t on (t.c2 =t2.c1) ) select count(*) from t;
^CCancel request sent
ERROR:  57014: canceling statement due to user request
LOCATION:  ProcessInterrupts, postgres.c:2988

QUERY退出后才记录临时文件的日志,社区版本的问题,没有阶段性记录临时空间的使用

2016-07-23 20:17:42.227 CST,"postgres","postgres",8997,"[local]",5793598b.2325,10,"SELECT",2016-07-23 19:48:27 CST,2/2490781,0,ERROR,57014,"canceling statement due to user request",,,,,,"with recursive t(c1,c2,info) as (select * from test where c1=9 union all select t2.* from test t2 join t on (t.c2 =t2.c1) ) select count(*) from t;",,,"psql"
2016-07-23 20:17:43.521 CST,"postgres","postgres",8997,"[local]",5793598b.2325,11,"SELECT",2016-07-23 19:48:27 CST,2/0,0,LOG,00000,"temporary file: path ""base/pgsql_tmp/pgsql_tmp8997.7"", size 1073741824",,,,,,,,,"psql"
2016-07-23 20:17:43.747 CST,"postgres","postgres",8997,"[local]",5793598b.2325,12,"SELECT",2016-07-23 19:48:27 CST,2/0,0,LOG,00000,"temporary file: path ""base/pgsql_tmp/pgsql_tmp8997.6"", size 1073741824",,,,,,,,,"psql"
2016-07-23 20:17:43.991 CST,"postgres","postgres",8997,"[local]",5793598b.2325,13,"SELECT",2016-07-23 19:48:27 CST,2/0,0,LOG,00000,"temporary file: path ""base/pgsql_tmp/pgsql_tmp8997.5"", size 1073741824",,,,,,,,,"psql"

设置会话的临时文件使用为10MB,继续测试,可以看到效果很明显

死循环的问题解决了

postgres=# set temp_file_limit='10MB';
SET
postgres=# with recursive t(c1,c2,info) as (select * from test where c1=9 union all select t2.* from test t2 join t on (t.c2 =t2.c1) ) select count(*) from t;
ERROR:  53400: temporary file size exceeds temp_file_limit (10240kB)
LOCATION:  FileWrite, fd.c:1491

RDS PG内核改进建议

建议可以动态设置temp_file_limit,根据实际的剩余空间设置反馈机制,保证有足够的剩余空间,不至于TEMP文件把空间全部撑爆。

可以将会话级别的临时空间限制,改为分组限制。

例如group a 允许使用100MB,group b允许使用1GB。

又或者是用户或数据库级别的限制。

借鉴Greenplum的resource queue的管理手段,把资源控制做起来也是一种方法。

《Greenplum 资源隔离的原理与源码分析》

https://yq.aliyun.com/articles/57763

阶段性的记录临时文件的日志,而不是QUERY结束时记录

小结

用户使用递归语句时一定要注意防止死循环,通过设置会话级别的temp_file_limit可以预防,还有一种方法是使用pg_hint_plan,在语句中使用HINT,例如:

/*+ 
Set (temp_file_limit='10MB')
*/
with recursive t(c1,c2,info) as (select * from test where c1=9 union all select t2.* from test t2 join t on (t.c2 =t2.c1) ) select count(*) from t;

临时文件会在QUERY结束后自动清理。

数据库启动时,startup进程也会清理temp文件。

Flag Counter

digoal’s 大量PostgreSQL文章入口