【Postgres】WITH子查询优化介绍(一)

举报
厚积薄发 发表于 2019/10/22 23:54:58 2019/10/22
【摘要】 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

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。