postgres 查询处理流程分析

Posted by CCH on July 11, 2020

查询编译

PostgreSQL 从客户端接收一用户的SQL查询之后,backend得到的是一串SQL字符串,会先调用由lex 和 yacc编写的查询编译模块,将SQL字符串解析成内部的数据结构——ParseTree构成的链表,这一步主要通过pg_parse_query函数完成,这一步主要检查的是SQL的语法,如果存在语法错误,就是在这一步报错。PG的语法定义,可以看gram.y的源文件。

语义分析

得到ParseTree之后,需要对查询进行语义分析与查询重写,由pg_analyze_and_rewrite完成。语义分析的工作,就是检查SQL里是否由不符合语义规定的部分,比方说,它会去看用户要查询的表是否存在,要查询的列是否在表中存在,SQL里使用的函数是否存在定义等等情况。查询分析的输入,是查询编译产生的ParseTree构成的链表,输出则是Query链表。 查询分析会分根据查询的类别(T_SelectStmt, T_UpdateStmt, T_InsertStmt..等等)调用不同的函数进行语义分析。对SQL的每个子句,都会调用transformXXStmt来进行语义分析,如对SQL的From子句,调用的就是transfformfromStmt函数来,对目标属性的分析处理,则调用transformTargetList来进行,我们平常写的Select * from .. 的星号,就是在这一步展开成表中所有的列名的。

查询重写

查询分析结束之后,需要进行查询重写。查询重写的任务,是根据预先定义的查询转换规则对查询进行重新挑中。转换规则可以看pg_rewrite 这张系统表。这里举一个简单的查询重写的使用例子,方便读者理解

假设我有一张表 test,我现在需要对表的每一次insert操作,都把这次insert操作的执行者和执行时间记录到另一张test_log表中去


chaohao.cch=# \d test
     Table "public.test"
 Column |  Type   | Modifiers
--------+---------+-----------
 a      | integer |
 b      | integer |

chaohao.cch=# \d test_log
              Table "public.test_log"
  Column  |            Type             | Modifiers
----------+-----------------------------+-----------
 log_who  | text                        |
 log_when | timestamp without time zone |

# 创建一个ALSO类型的重写规则
chaohao.cch=# create rule log_test as on insert to test do also insert into test_log values(current_user,current_timestamp);
CREATE RULE

#现在执行一次insert操作
chaohao.cch=# insert into test values (1,1);
INSERT 0 1

#可以看到test_log表里也记录这次操作的执行者和执行时间
chaohao.cch=# select * from test_log;
   log_who   |          log_when
-------------+----------------------------
 chaohao.cch | 2020-07-11 20:21:48.015184
(1 row)

#从查询计划也可以看到,虽然SQL是insert into test,但实际计划,还有对test_log的写入
chaohao.cch=# explain insert into test values (1,1);
                      QUERY PLAN
------------------------------------------------------
 Insert on test  (cost=0.00..0.01 rows=1 width=0)
   ->  Result  (cost=0.00..0.01 rows=1 width=0)

 Insert on test_log  (cost=0.00..0.02 rows=1 width=0)
   ->  Result  (cost=0.00..0.02 rows=1 width=0)
(5 rows)

其实在PostrgresSQl中的视图,就是由查询重写来实现的。在创建视图时,系统会按照视图的定义生成相应的重写规则,规则的动作,则是视图创建时Select语句的拷贝。 关于查询重写更详细的介绍,可以看官方文档

查询优化

一条SQL,可以采用不同的方式执行。比如

select * from test where a > 2; 

我可以直接对test表进行全表扫描。如果test表有索引,也可以考虑通过索引来扫描,获取符合条件的元祖的位置。

select * from a,b,c;

表的连接顺序,也可以有多种。可以先处理{a},{b}连接,再处理{a,b} 与{c}的连接; 也可以先处理{b},{c}连接,再对{a} {b,c}进行连接; 还可以{a} {c}进行连接,之后{a,c} 与{b}进行连接。

尽管不同的连接顺序,不同的扫描方法,最终返回给用户的结果是一样的,但是执行的效率却可能天差地别。查询优化器的目的,就是选择一种预计执行效率最高的执行方案。

在查询中,最耗时的步骤就是表的连接,因此查询优化的核心哲学就是 “先做表的选择操作, 后做表的连接”。举例说明

select * from a join b on a.t1 = b.t1 where a.t2 > 100 and b.t2 <> 3;

对于上面的查询,优化器就会先用 a.t2 > 100 这个条件,把满足这个要求的记录从a表中先过滤出来,用b.t2 <> 3 这个条件,把满足这个条件的记录从b表中过滤出来,再对过滤后的结果进行join (连接) 操作。因为过滤后,左右两表的记录数会减少,从而降低了连接操作的成本。

优化器总体处理流程

查询优化的入口是pg_plan_queries函数,它负责将Query链表转换为PlannedStmt链表。pg_plan_queries遍历链表中的每一个Query结构体,对于非Utility类型的Query,调用pg_plan_query函数对其进行查询优化。pg_plan_query则调用planner函数进入查询优化模块。

值得注意的是,planner函数中会去检查planner_hook函数指针是否为空,若为空,则去调用g官方提供的优化器standard_planner。planner_hook可供开发者加入自己实现的查询优化器。

下面用伪代码说明优化器模块各个函数调用的关系以及作用,优化器的细节会再写一篇文章详细介绍。

standard_planner(){
	subsequry_planner()
	SS_finilize_plan() //clearup
}
subsequry_planner() {
	pull_up_sublinks() //提升子链接
	pull_up_subqueries() //提升子查询
	preprocess_expression() //表达式预处理
	preprocess_qual_conditions() 

	inheritance_planer()
	grouping_planer() //生成最优连接路径
}
grouping_planer() {
	// 1. 预处理groupby子句
	// 2. 计算代表排序需求的路径关键字
	// 3. 确定排序关键字

	query_planner() //生成最佳连接路径

	//生成计划
}