查询处理
查询处理:将用户的查询和数据修改命令转变为数据库上的操作序列并且执行这些操作。
查询编译预览
后台进程处理连接的客户端发出的所有查询。后端由五个子系统组成:解析器,分析器,重写,计划生成,执行。
步骤如下:(用户输入SQL语句)
- 后台接收,解析器将SQL语句生成解析树; (词法分析,语法分析)
- 传给分析器,分析器对解析树执行语义分析生成查询树; (语义分析,安全性检查,完整性初步检查,)
- 进入重写系统,如果存在规则系统中存储的规则,重写器使用规则转换查询树,此时的查询树通常是查询的代数表达式; (实现规则系统的系统)
注:视图是使用规则系统实现的
- 为了执行时间更短更有效,将初始查询计划被转化为一个预期所需执行时间较小的等价的计划。抽象的查询计划需要被转化为物理查询计划,分析结果和逻辑计划一样,物理计划用表达式树来表示。 (查询优化)
- 执行程序通过按计划树创建的顺序访问表和索引来执行查询。 (查询执行)
简单来说就是 对查询语句进行扫描,词法分析,和语法分析。再从查询语句中识别出语言符号,进行语法检查和语义分析,如果有错误则报告语法错误,如果没有语法错误就转入下步处理,再将查询树进行查询优化包括代数优化和物理优化,将查询执行计划(计划树),传入下一步查询执行,代码生成,输出查询计划的执行代码。
查询树
以select * from tbl where id<8为例:
查询树的根是在 parsenodes.h 中定义的查询结构;
得到parsetree_list指针地址
然后查看parsetree_list内容:是一个select子句
再查看select子句中的targetlist
计划树
计划树是由计划节点的元素组成,它连接到PlannedStmt结构的平面树列表,这些元素在plannodes.h中定义
计划树是排序节点和顺序扫描节点的列表;执行程序通过顺序扫描来扫描 table:tbl,再扫描tb2,将结果进行哈希比较id是否相等,再将两表进行连接,然后对获得的结果进行排序
actual time和cost是不同的,一个是实际花销,一个是评估的消耗。
查询优化
查询优化是基于成本的,成本是无量纲值,这些不是绝对的指标,而是比较运营相对绩效的指标。所以成本用定义的函数估算,例如:顺序扫描和索引扫描的成本分别由 cost_seqscan() 和 cost_index() 估计。
总成本=启动成本(获取第一个元组之前花费的成本)+运行成本(获取所有元组的成本)的总和。
单表查询中的成本估算
'运行成本'=CPU运算成本+磁盘运算成本
=(cpu_tuple_cost+cpu_operator_cost)*元组数+磁盘运行成本*页面数
创建一个具有10000个元组的表tbl,并给这个表建立索引
查询这个表的所有元组和所有页的编号得:
共45页,10000个元组
顺序扫描
∵文件默认设置,seq_page_cost,cpu_tuple_cost和cpu_operator_cost在postgresql.conf文件中设置,默认值分别为1.0,0.01和0.0025;
样例一:
∴顺序扫描 运行成本=
(0.01+0.0025)x10000+1.0x45=170
样例二:
发现运行成本=145
why?
∵少了where无需单独对某个属性值取出进行判断,所以少了cpu_operator_cost时间。
∴它的结果是 0.01*10000+1.0*45=145
样例三:
发现运行成本=样例一的结果=170
why?
∵虽然我们插入的元组是按顺序1到10000的但是系统并不知元组已排序,所以顺序查询的时候还是会从第一条读到最后一条。
小结:
从运行成本估算中可以看出,PostgreSQL假设所有页面都将从存储中读取;也就是说,PostgreSQL不考虑扫描的页面是否在共享缓冲区中。
索引扫描
因为如果是全表顺序扫描,当数据量大的时候效率极低所以索引扫描应运而生。
索引扫描不用将表中所有数据都扫描一遍,而是根据页号去找到该数据
索引方法有:BTree,hash等
1.全表扫描成本=磁盘IO成本+CPU成本=(页数*页存取+微调数)+(表的行数据*元组存取+微调数)
2.计算走索引的查询成本
元组数*元组存取+微调数+1*页存取+微调数
列的索引相关性,id和data相关性是1的意思是在同一元组中他们俩的值是一样的
排序
order by,排序成本是使用 cost_sort() 函数估算的。要排序的所有元组如果可以都存储在内存中,就用快排,如果是需要创建新临时文件则用合并排序算法,如果排序中包含limit语句,则使用堆排序优化排序过程。
排序算法启动成本为对目标元组进行排序的成本
排序算法运行成本是读取已排序元组的成本。
B+树索引代价估计:
单字节查询利用** b+ 树主索引: h+s/p **(向上取整)
Select …
from T
where T.a >= 21 and T.a <= 33
单字节查询利用** b+ 树辅助索引的:( h-1 ) +(k/f) 向上取整 +s**
Select …
from T
where T.a >= 21 and T.a <= 33
多字节查询是** h1-1+[k1/f1]+h2-1+[k2/f2]+s ( s **是结果的个数)
Select …
from T
where T.a >= 21 and T.a <= 30 and T.b >= 21 and T.b <= 30.
如果单字段没有建立索引,那么就直接顺序扫描。代价br
如果多字段中:
①. 字段a和字段b都没有建立索引。那么它的代价就是b_a+b_b
②. 如果只是字段a没有索引或者字段b没有索引,那么就先找到有索引的哪个的记录,然后去除掉不满足没有索引的字段的记录。
代价:与有索引的哪一个一样 如果用主索引就是** h+s/p **** 如果是辅助索引就是****h-1+(k/f)****向上取整 **+s
如果字段a和字段b都有索引,那么就两个树都取到叶子节点的位置,然后如果都是and and那就是S1∩S2 如果是or or 那就是S1∪S2
创建单表查询计划树
- 执行预处理
- 简化目标列表、限制子句
- 规范化布尔表达式
- 合并and/or表达式
按照代码表达的意思对代码进行简单修改,不修改代码原意
- 通过估算所有可能访问路径的成本,获得最便宜的访问路径
主要计算创建路径,顺序扫描访问,索引访问,如果可以执行位图扫描就创建位图扫描路径
因为上面三种扫描访问方法都会将路径添加到列表中,最终在reloptinfo结构中获取最便宜的访问路径。
如果是一个没有索引的简单单表,那么它的查询是用顺序扫描成本,因为目标表没有索引
如果是一个具有两个索引的单表查询,那么最便宜路径就是用索引
- 从最便宜的路径创建计划树
创建计划树计划树的根是在 plannodes.h 中定义的 PlannedStmt 结构。
根路径是排序路径,子路径是扫描路径,sortnode指向seqscannode 限定符包括where子句的id<300
连接操作
数据库中有嵌套循环连接,归并连接,哈希连接
嵌套循环连接
理论知识,在数据库中少用,当两个表需要做连接时,需要考虑哪个关系小哪个关系大,因为内存块固定,设两表分别为R,S,每一次迭代时,我们读取S的M-1个块和R的B®个块,磁盘I/O的数量就是Bs (M-1+Br )/(M-1),要考虑内存块大小,且要决定哪个做内部表,哪个做外部表。
索引嵌套循环连接
如果内部表有索引,那么索引可以查找满足匹配外部表的每个元组的连接条件的元组,则计划程序将考虑使用索引直接搜索内部元组,而不是顺序扫描。
成本=(cpu操作耗时+cpu存取元组耗时)*外部表元组数+参数化内部索引总成本
归并连接
先对数据集进行排序,由于每个输入都已排序,因此 Merge Join 运算符将从每个输入获取一行并将其进行比较。例如,对于内联接操作,如果行相等则返回。如果行不相等,则废弃值较小的行并从该输入获得另一行。这一过程将重复进行,直到处理完所有的行为止。
优点:因为经过初始化的排序后,合并过程是已经经过优化的,再生成输出行的时候会块。当哈希连接过程中哈希表无法一次构建是,排序合并的成本性能高于哈希
归并的代价一定是br+bs,但是其它的代价就看排序算法就可以自己看
哈希连接
哈希连接需要构建一个哈希表,因为排序的成本高,所以哈希的性能是比排序合并好的