sql执行语句流程解析
整个处理流程在exec_simple_query函数中完成,代码架构如下:
/*
* exec_simple_query
*
* Execute a "simple Query" protocol message.
*/
static void
exec_simple_query(const char *query_string)
{
...
//原始语法树获取
/*
* Do basic parsing of the query or queries (this should be safe even if
* we are in aborted transaction state!)
*/
parsetree_list = pg_parse_query(query_string);
...
//循环处理sql语句
/*
* Run through the raw parsetree(s) and process each one.
*/
foreach(parsetree_item, parsetree_list)
{
...
//对原始语法树进行分析和重写,生成查询语法树
querytree_list = pg_analyze_and_rewrite(parsetree, query_string,
NULL, 0, NULL);
//对查询语法树进行优化,生成执行计划
plantree_list = pg_plan_queries(querytree_list,
CURSOR_OPT_PARALLEL_OK, NULL);
...
//执行语句
/*
* Run the portal to completion, and then drop it (and the receiver).
*/
(void) PortalRun(portal,
FETCH_ALL,
true, /* always top level */
true,
receiver,
receiver,
completionTag);
...
}
...
}
查询物理优化
什么是物理优化:在有效的查询路径中选择一条最优的路径
物理优化原理:减少查询访问路径中对元组的物理访问代价;因为物理I/O是当前数据库架构的主要瓶颈,所以物理优化的主要目的就是减少物理I/O操作。
物理优化代码实现:逻辑优化和物理优化的代码在query_planner函数中实现,先进行逻辑优化(详见05-sql语句执行流程解析2-查询逻辑优化),最后将逻辑优化的结果作为参数传递给make_one_rel函数做物理优化。一下主要对make_one_rel函数进行分析。
物理优化代码事项步骤:
- 1、查询系统物理参数
- 2、搜索所有可能的查询路径
- 3、选择最优路径
make_one_rel函数树状图如下:
后续程序比较复杂,为了方便描述,使用树结构进行解析
make_one_rel
|-- ...
|-- set_base_rel_sizes //设置查询语句中每个基表的物理参数
|-- loop循环:结构体root->simple_rel_array中获取基表信息
|-- set_rel_size //根据基表类型对基本结构体进行填充,对基表类型进行大小估计
|-- check_index_predicates //部分索引检查
|-- set_baserel_size_estimates //物理参数估计:涉及到“选择率”
|-- set_base_rel_pathlists //设置每个基表的可行扫描方案
|-- loop循环:结构体root->simple_rel_array中获取基表信息
|-- set_rel_pathlist //对基表类型构建访问路径
|-- set_plain_rel_pathlist //普通基表的处理
|-- create_plain_partial_paths //顺序访问
|-- create_index_paths //索引访问
|-- create_tidscan_paths //TID 访问
|-- ... //子查询,公共表达式等参考普通基表
|-- make_rel_from_joinlist //创建所有可行方案的查询访问路径
|-- draft.md
|-- page.md
|-- post.md
|-- draft.md
set_base_rel_sizes 设置查询语句中每个基表的物理参数
check_index_predicates 部分索引检查
通过索引获取地址是比较常用的技术,在实际物理参数获取时,因为不需要生成全部索引(因为索引查询也需要访问I/O),所以引入部分索引---在源数据表中的部分数据上建立索引,由一组约束条件来约束所建立的索引数据的范围。
部分索引语法部分使用where指定约束条件。
set_baserel_size_estimates 判断约束条件的查询时间
这里涉及到数据查询约束条件导致的查询代价,所以需要知道满足约束条件的数据元组个数。
个数可以通过两个方式获取:
- 全表扫描 ------ 会花费巨大的I/O代价,所以不是最小代价
- 满足约束条件的元组比例 ------ 需要通过某个算法获取到元组比例,这里使用的“选择率”
选择率:描述一张表里某个数值得区分度。(因为存储的数据在变化,所以该值也在变化)
[选择率]:索引在全表中可能得出的区分度(随着存储数据变数,区分度也是变化的,这就是查询计划漂移,有人在注研究这个,这里不讨论)。因为全表扫描会大量访问I/O,所以创建索引时使用where添加判断语句,创建部分索引。
数值类型计算公式:
[Num(No.(x - 1)) + (N - X.min)/(X.max-X.min)] / NumOfBucket
Num(No.(x - 1)) :约束条件下满足的桶之前所有的桶数量
N:约束条件
X.max、X.min:约束条件下满足桶的上下边界
NumOfBucket:桶的总数
cpu_cost_function io_cost_function查询执行代价
最后会通过CPU和I/O的代价计算出最优或者次优的查询计划
set_base_rel_pathlists 设置每个基表的可行扫描方案
普通基表是子查询、公共表达式(CTE)等的基础,所以这里重点分析普通基表的访问路径构建方法。
基表的三种查询路径:顺序访问(Sequential scan),索引访问(Index scan),直接TID访问(TID scan)
由此可知:对基表的访问方式也是三种,set_base_rel_pathlists 函数中会对每种方式进行构建,以找到最优的访问方式。