浅析PostgreSQL并行查询代价估算

举报
onon 发表于 2020/03/11 16:51:20 2020/03/11
【摘要】 并行查询特性最早是在PostgreSQL 9.6中引入。自那时起,社区一直在扩展该功能。在PostgreSQL 11和PostgreSQL 12中,社区开发人员向数据库引擎添加了更多的相关功能。本文就PostgreSQL查询优化器对并行执行代价估算的工作原理进行阐述,不足之处希望大家多多指正。串行顺序扫描执行代价估算 为了方便解释,用一个简单的例子来说明问题。首先通过代码片段1创...

并行查询特性最早是在PostgreSQL 9.6中引入。自那时起,社区一直在扩展该功能。在PostgreSQL 11PostgreSQL 12中,社区开发人员向数据库引擎添加了更多的相关功能。本文就PostgreSQL查询优化器对并行执行代价估算的工作原理进行阐述,不足之处希望大家多多指正。

串行顺序扫描执行代价估算

       为了方便解释,用一个简单的例子来说明问题。首先通过代码片段1创建一个表包含两列。

   1.  //代码片段1
   2.  mydb=# CREATE TABLE people (id int PRIMARY KEY NOT NULL, age int NOT NULL); 
   3.  CREATE TABLE

为了演示并行查询带来的益处,利用代码片段2往表people插入一千万条数据,其中age字段是0-100之间的随机数。

      1.  //代码片段2
      2.  mydb=# INSERT INTO people   
      3.         SELECT id, (random()*100)::integer AS age   
      4.         FROM generate_series(1,10000000) AS id;  
      5.  INSERT 0 10000000

默认情况下PostgreSQL是打开并行执行的。我们首先关闭并行执行开关,检查串行查询执行的耗时。通过将配置参数max_parallel_workers_per_gather设置为0可以关闭并行执行特性。从查询执行计划可以看到,顺序扫描的执行代价为144.248msPg的执行计划显示的时间由两部分组成,‘..’前面的称之为start_up_cost,后面部分叫做run_cost。总的查询执行代价为169.248msPg如何计算得到这些数字的?

     1.  //代码片段3
     2.  mydb=# SET max_parallel_workers_per_gather TO 0;  
     3.  SET  
     4.  mydb=# explain analyze select count(*) from people;  
     5.                                 QUERY PLAN  
     6.  --------------------------------------------------------------------------------
     7.   Aggregate  (cost=169248.60..169248.61 rows=1 width=8) (actual time=1375.237..1375.237 rows=1 loops=1)  
     8.     ->  Seq Scan on people  (cost=0.00..144248.48 rows=10000048 width=0) (actual time=0.036..837.964 rows=10000000 loops=1)  
     9.   (4 rows)

Pg查询执行时间total_cost = start_up_cost + run_cost,具体来说pg的运行时的代价由以下的式子计算得到:

                      total_cost = cpu_run_cost + disk_run_cost

                                          = (cpu_tuple_cost + cpu_operator_cost) × Ntuple + seq_page_cost×Npage (1)

其中cpu_tuple_cost是CPU传输该记录的代价;cpu_operator_cost指函数运算或其他操作符的开销。有了上述计算规则以后,可以通过Pg的元数据表pg_class查询表people相关统计信息,见代码片段4。可以得到Npage=44248Ntuple = 10000048.Pg源代码的cost.h头文件定义了公式(1)相关的计算代价,见代码片段5

     1.  //代码片段4
     2.  mydb=# SELECT relpages, reltuples FROM pg_class WHERE relname = 'people';  
     3.   relpages |   reltuples  
     4.  ----------+---------------  
     5.      44248 | 1.0000048e+07  
     6.  (1 row)

计算代价如下:

     1.  //代码片段5
     2.  /* defaults for costsize.c's Cost parameters */  
     3.  /* NB: cost-estimation code should use the variables, not these constants! */  
     4.  /* If you change these, update backend/utils/misc/postgresql.sample.conf */  
     5.  #define DEFAULT_SEQ_PAGE_COST  1.0  
     6.  #define DEFAULT_RANDOM_PAGE_COST  4.0  
     7.  #define DEFAULT_CPU_TUPLE_COST  0.01  
     8.  #define DEFAULT_CPU_INDEX_TUPLE_COST 0.005  
     9.  #define DEFAULT_CPU_OPERATOR_COST  0.0025  
    10.  #define DEFAULT_PARALLEL_TUPLE_COST 0.1  
    11.  #define DEFAULT_PARALLEL_SETUP_COST  1000.0

代码片段5的计算代价,放入公式(1),我们可以得到顺序扫描的代价:

                               total_cost = (cpu_tuple_cost)×Ntuple + seq_page_cost×Npage

                                          = 0.01×1.0000048e+07 + 1×44248 

                                          = 144248.48

这就是代码片段3第8行的顺序执行代价。而总的执行代价,需要对每条元组进行count函数操作,因此需要加上cpu_operator_cost,因此聚集函数count的查询执行start_up_cost为:

                               Start_up_cost = cpu_run_cost + disk_run_cost

                                             = (cpu_tuple_cost + cpu_operator_cost)×Ntuple + seq_page_cost×Npage (1)

                                             = (0.01 + 0.0025) ×1.0000048e+07 + 1×44248

                                             = 169248.6 (代码片段3第7行所示)

最后,系统只需要对这条元组传输即可,因此总的查询执行代价为:

                              total_cost = (cpu_tuple_cost)×Ntuple + start_up_cost

                                         =  0.01 + 169248.6

                                         =  169248.61代码片段37行所示)


并行顺序扫描执行代价估算

现在我们将并行执行的特性打开令max_parallel_workers_per_gather=2,相应的执行计划如下:

     1.  //代码片段6
     2.  mydb=# set max_parallel_workers_per_gather = 2;  
     3.  SET  
     4.  mydb=# explain analyze select count(*) from people;  
     5.                           QUERY PLAN  
     6.  ---------------------------------------------------------------------------------------------------------------
     7.   Finalize Aggregate  (cost=97331.80..97331.81 rows=1 width=8) (actual time=524.896..524.896 rows=1 loops=1)  
     8.     ->  Gather  (cost=97331.58..97331.79 rows=2 width=8) (actual time=524.828..526.162 rows=3 loops=1)  
     9.           Workers Planned: 2  
     10.           Workers Launched: 2  
     11.           ->  Partial Aggregate  (cost=96331.58..96331.59 rows=1 width=8) (actual time=522.605..522.606 rows=1 loops=3)  
     12.                ->  Parallel Seq Scan on people  (cost=0.00..85914.87 rows=4166687 width=0) (actual time=0.033..317.765 rows=3333333 loops=3)  
     13.  Planning Time: 0.307 ms  
     14.  Execution Time: 526.209 ms  
     15. (8 rows)

为了解释并行查询执行代价,我们首先看看不同进程(为什么用进程是历史原因)之间工作量的分配。当并发进程只有两个的时候,主进程需要将大部分时间花费在执行工作,当并行进程的增加,主进程逐渐将工作转向聚合部分结果。Pg的进程工作量分配正是建立在这个基础之上。具体的分配算法见代码片段7第22行-27行。并行执行的框架如图1所示。

    1.  //代码片段7
    2.  static double  
    3.  get_parallel_divisor(Path *path)  
    4.  {  
    5.      double      parallel_divisor = path->parallel_workers;  
    6.    
    7.       /* 
    8.        * Early experience with parallel query suggests that when there is only
    9.        * one worker, the leader often makes a very substantial contribution to
    10.       * executing the parallel portion of the plan, but as more workers are 
    11.       * added, it does less and less, because it's busy reading tuples from the 
    12.       * workers and doing whatever non-parallel post-processing is needed.  By 
    13.       * the time we reach 4 workers, the leader no longer makes a meaningful 
    14.       * contribution.  Thus, for now, estimate that the leader spends 30% of 
    15.       * its time servicing each worker, and the remainder executing the 
    16.       * parallel plan. 
    17.       */  
    18.     if (parallel_leader_participation)  
    19.      {  
    20.         double      leader_contribution;  
    21.   
    22.         leader_contribution = 1.0 - (0.3 * path->parallel_workers);  
    23.         if (leader_contribution > 0)  
    24.             parallel_divisor += leader_contribution;  
    25.     }  
    26.   
    27.    return parallel_divisor; 
    28.


                                                                                                            图1.并行查询框架


因此当并行执行的worker为2的时候,每个工作进程所分配得到的工作量为:10000048.0 / (2 + (1 – 0.3 * 2)) = 4166686.66 rows.这就是代码片段6第13行得到每个worker处理的元组(rows)数目。这里需要注意的是虽然是并行扫描但是IO代价仍然是按照全表扫描的代价计算。这主要是因为每个并发进程在同一份数据上按照block-by-block的方式顺序推进全局next指针,这等价于全表扫描。

总结

PostgreSQL的口号是开源数据库中最强大的关系型数据库系统。其实业界也一直将其称为开源数据库的oracle。此言不虚,因为很多企业客户在去“O”问题上选择用PostgreSQL替代Oracle。PostgreSQL不仅在语法上高度兼容Oracle而且在性能上也向Oracle靠拢,例如并行查询特性就是PostgreSQL引以为豪的特性之一。PostgreSQL实现了并行scan,merge-join,hash-join,以及partition-join等。这篇文章对PostgreSQL的并行查询进行了初步的分析,探讨了并行工作进程之间的工作量分配,查询执行代价的计算,以及并行查询的总体框架。后续我们计划对PostgreSQL的并行join算法进行深入探讨以更好地了解PostgreSQL的并行查询功能.





【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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