【Postgres】WITH子查询优化介绍(一)
【摘要】 PostgreSQL12 版本已经发布,在该版本中对索引、WITH子查询等方面做了许多方面的优化,极大地提升了数据库性能。接下来几篇博客中,将介绍自己学习掌握到的相关优化,和大家一起学习参考。
WITH子查询是一种常见的SQL编程技巧,利用它可以清晰地展示查询语句的“物理含义“信息,便于理解。但是,WITH子查询有一个不足的地方在于缺少统计信息,如果查询语句涉及join关联,可能会出现由于估算信息的偏差导致生成错误的执行计划。
测试数据
生成测试表和数据如下
create table test1(a int);
create table test2(a int);
insert into test1 select generate_series(1,100000);
insert into test2 select generate_series(1,201);
analyze test1;
analyze test2;
下面语句在PG11.5上获取的执行计划信息如下
postgres=# explain with t as (select * from test2) select * from test1 where a in (select * from t);
QUERY PLAN
---------------------------------------------------------------------
Hash Join (cost=12.03..2273.78 rows=50000 width=4)
Hash Cond: (test1.a = t.a)
CTE t
-> Seq Scan on test2 (cost=0.00..3.01 rows=201 width=4)
-> Seq Scan on test1 (cost=0.00..1443.00 rows=100000 width=4)
-> Hash (cost=6.52..6.52 rows=200 width=4)
-> HashAggregate (cost=4.52..6.52 rows=200 width=4)
Group Key: t.a
-> CTE Scan on t (cost=0.00..4.02 rows=201 width=4)
(9 rows)
postgres=# explain with t as (select * from test2 where a <= 200) select * from test1 where a in (select * from t);
QUERY PLAN
-------------------------------------------------------------------
Hash Semi Join (cost=10.01..1722.49 rows=200 width=4)
Hash Cond: (test1.a = t.a)
CTE t
-> Seq Scan on test2 (cost=0.00..3.51 rows=200 width=4)
Filter: (a <= 200)
-> Seq Scan on test1 (cost=0.00..1443.00 rows=100000 width=4)
-> Hash (cost=4.00..4.00 rows=200 width=4)
-> CTE Scan on t (cost=0.00..4.00 rows=200 width=4)
(8 rows)
从上面的执行计划可以看出,WITH子查询是单独计算的,并且经过聚合去重之后,得到的不重数个数都是200,即使第一个语句中子查询子句的查询结果包含有201条不重复记录,且优化器对第一条语句的估算返回结果是50000,对第二条语句的估算返回结果是200。
行数估计
先简单介绍下,WITH子查询语句下的满足条件行的估计。由于WITH子查询是单独计算的,因此不存在实际的统计信息,在PostgreSQL中针对这一类情况按照下面情况计算其唯一值个数
/src/include/utils/selfuncs.h
/* default number of distinct values in a table */
#define DEFAULT_NUM_DISTINCT 200
从上面可以看出优化器默认认为的不重复值个数是200,这个值是固定的,而不是通过分析采样获取的。
返回行数的估计中,最重要的就是选择率的计算。从执行计划上看,这里采用的是hash semi join(由于采用有in子查询),因此选择率计算函数为 eqjoinsel_semi,由于构造的元组都是数字,因此最终采用下面的函数片段计算选择率
/*
* Without MCV lists for both sides, we can only use the heuristic
* about nd1 vs nd2.
*/
double nullfrac1 = stats1 ? stats1->stanullfrac : 0.0;
// 是否采用默认值,在get_variable_numdistinct中指定,由于WITH没有任何统计信息,采用默认
// 值,同时在该函数前面也会进行检查判断是否是默认采样值(主要依据是获取的结果是否大于等于实际
// 值,这里实际值201,因此这里采用默认值,true
if (!isdefault1 && !isdefault2)
{
if (nd1 <= nd2 || nd2 < 0)
selec = 1.0 - nullfrac1;
else
selec = (nd2 / nd1) * (1.0 - nullfrac1);
}
else
// 计算最终选择率,这里nullfrac是0,因此最终结果是0.5
selec = 0.5 * (1.0 - nullfrac1);
如果将查询语句修改后,计算选择率的函数进入上半部分,在该例中采用的是均匀分布估算方式,最终估算的结果没有偏差。这里的测试用例比较简单,主要是想说明估算误差的情况,如果遇到比较特殊的语句,采用WITH后,可能性能会发生变化,这一点需要注意。
改进
在PostgreSQL12版本中,支持对子查询的内联处理,将对满足条件的查询语句直接进行下推处理,从而避免了中间一层的行数估计,避免了因为默认值导致的估算偏差,在最新版本执行计划如下:
postgres=# explain with t as (select * from test2) select * from test1 where a in (select * from t);
QUERY PLAN
-------------------------------------------------------------------
Hash Semi Join (cost=5.52..1713.26 rows=201 width=4)
Hash Cond: (test1.a = test2.a)
-> Seq Scan on test1 (cost=0.00..1443.00 rows=100000 width=4)
-> Hash (cost=3.01..3.01 rows=201 width=4)
-> Seq Scan on test2 (cost=0.00..3.01 rows=201 width=4)
(5 rows)
postgres=# explain with t as (select * from test2 where a <= 200) select * from test1 where a in (select * from t);
QUERY PLAN
-------------------------------------------------------------------
Hash Semi Join (cost=6.01..1713.74 rows=200 width=4)
Hash Cond: (test1.a = test2.a)
-> Seq Scan on test1 (cost=0.00..1443.00 rows=100000 width=4)
-> Hash (cost=3.51..3.51 rows=200 width=4)
-> Seq Scan on test2 (cost=0.00..3.51 rows=200 width=4)
Filter: (a <= 200)
(6 rows)
可以看到,执行计划中没有对WITH子查询的单独操作,而是转化为表TEST1和TEST2的之间关联操作。不是所有的WITH子查询在数据库内部都会进行改写,下面四种情况目前不支持
/*
* Consider inlining the CTE (creating RTE_SUBQUERY RTE(s)) instead of
* implementing it as a separately-planned CTE.
*
* We cannot inline if any of these conditions hold:
*
* 1. The user said not to (the CTEMaterializeAlways option).
*
* 2. The CTE is recursive.
*
* 3. The CTE has side-effects; this includes either not being a plain
* SELECT, or containing volatile functions. Inlining might change
* the side-effects, which would be bad.
*
* 4. The CTE is multiply-referenced and contains a self-reference to
* a recursive CTE outside itself. Inlining would result in multiple
* recursive self-references, which we don't support.
*/
if ((cte->ctematerialized == CTEMaterializeNever ||
(cte->ctematerialized == CTEMaterializeDefault &&
cte->cterefcount == 1)) &&
!cte->cterecursive &&
cmdType == CMD_SELECT &&
!contain_dml(cte->ctequery) &&
(cte->cterefcount <= 1 ||
!contain_outer_selfref(cte->ctequery)) &&
!contain_volatile_functions(cte->ctequery))
{
inline_cte(root, cte);
/* Make a dummy entry in cte_plan_ids */
root->cte_plan_ids = lappend_int(root->cte_plan_ids, -1);
continue;
}
目前有几种情况的WITH子查询不支持内联处理,后面博客将详细介绍该优化patch详细内容。优化patch如下
Allow user control of CTE materialization, and change the default behavior
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)