查询编译
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() //生成最佳连接路径
//生成计划
}