3. 查询处理

目录

一、 主要流程

二、单表查询

1.计划器-扫描与排序成本估计

1.1 顺序扫描

1.2 索引扫描

1.3 排序

2.计划器—生成计划树

2.1 预处理

2.2 找出代价最小的访问路径

3.执行器

三、多表查询

1.计划器—连接成本估计

1.1 嵌套循环连接

1.2 归并接连

1.3 散列连接

2. 计划器—连接访问路径与连接节点

3. 计划器—创建多表查询计划树

3.1 预处理

3.2 获取代价最小的路径


一、 主要流程

  1. 解析器:词法、语法检查,生成语法解析树
  2. 分析器:语义分析,生成查询树
  3. 重写器:基于规则系统改写查询树
  4. 计划器:根据表的统计信息,基于代价生成一棵执行效率最高的计划树
  5. 执行器:执行计划树

以上这些“器”都是使用工作内存

视图是基于规则系统实现的。参见视图与规则系统

二、单表查询

1.计划器-扫描与排序成本估计

explain显示命令的计划树,每个节点表示执行器需要的信息,计划树从终端节点往根节点执行。

代价系数为相对性能指标。

总代价=启动代价+运行代价,expain显示的计划树cost=0.00..145.00表示启动代价为0.00,运行代价为145.00。

1.1 顺序扫描

只有运行代价:cost=系数*元组数,系数默认为1,pg假设所有的页面来自存储介质而不是缓冲区。

1.2 索引扫描

所有索引结构使用同一个代价函数:cost_index()=启动代价函数+运行代价

1.2.1 启动代价函数:

其中Hindex为索引高度,ceil函数是二分法查找指定值,hindex是走树的高度。

1.2.2 运行代价:

表和索引的CPU代价和IO代价之和,这四个值与要读取的索引元组和表元祖的数量有关。

如何知道要读取多少元组?有一个选择率(估值),将总的*选择率即可。(所以explain并不是根据历史数据估算的,而是根据选择率来估算的?)

选择率:选择率根据某一值在列中出现的频率决定,并在一开始就存储在pg_stat表中,当一列的可选值太多,就会将列的元组均分为100个直方图,并存储每个直方图的边界值,并以线性插值的方式计算出目标值的选择率。

索引相关性:在计算表元组的IO代价时,涉及一个索引相关性系数,就是在BRIN索引(提取每个范围的最大最小值,来加快顺序扫描)中也提到的那个系数,表示列值的物理顺序与逻辑顺序的统计相关性,越接近0越不相关,接近±1越相关。它会影响元组的随机访问性能。

访问谓词:一般就是where条件,表示扫描索引的起止条件。

random_page_cost:在SSD中最好将其设置为1(默认为4)(所以对于SSD来说随机存取与顺序存取速度是一样的)。

1.3 排序

如果能在工作内存中放下所有元组,则用快排,否则创建临时文件,使用归并排序。

代价函数:cost_sort()=O(N)+log2(N)+O(N)

对于一个查询并排序的语句,总的启动代价=索引代价+排序的启动代价

2.计划器—生成计划树

2.1 预处理

简单替换一些写法,将多个and/or表达式压平为多元表达式:

2.2 找出代价最小的访问路径

将可能的顺序扫描路径及成本、索引扫描路径及成本、位图扫描路径及成本都放在RelOptInfo结构的pathList变量中,其他Limit、orderby、aggregate成本等放在RelOptInfo结构的其他变量中。

where子句、排序,甚至扫描的路径与成本(可能包含多个路径与成本)都在其他脚本中执行成本估算,并将路径与代价放在RelOptInfo结构中,然后需要比较各类扫描的成本值,选择最小的,放在新的RelOptInfo结构中,并根据新的RelOptInfo生成一棵成本最小的计划树。

3.执行器

同样树也是从叶节点往根节点执行的。对应着explain的计划树。

因为计划树的每个节点还存储了目标表、以及相应操作,执行器调用每个节点的函数(这些函数都在src/beckend/executor中)

执行器在处理查询时会用到工作内存与临时缓存区,如果内存分配不出来,就会在base/pg_tmp下使用临时文件。在执行explain analyse命令时,可能会看到占用临时文件的信息。

三、多表查询

1.计划器—连接成本估计

下面提到的三类连接均可用于内连接、外连接、左连接、右连接等。

哪个是内表,哪个是外表是由计划器在遍历各种可能之后,根据连接成本定的,与写的顺序无关。

1.1 嵌套循环连接

这个连接中外表只需扫描一遍,内表要扫描很多遍,就是两层的嵌套循环以找到内外表上相同的列值。外表的行越少越好:https://severalnines.com/database-blog/overview-join-methods-postgresql

只有运行代价,与内外表的元组数乘积成正比,虽然多表查询一定会估计该成本,但很少使用它,而是使用它的变体。

(考虑连接条件的话,A表总的元组数决定了A表扫描一遍的代价,A表匹配条件的元组数(这些元组构成了外表)决定了扫描内表的次数,B表的总元组数决定了B表扫描一遍的代价,B表匹配条件的元组数(这些元组构成了内表)决定了内表扫描的代价。)

变体1—物化内表

将内表扫描一遍加载到工作内存中以加快内表的扫描速度,(即物化过程,所以物化不一定要落盘?如果内存不够,就使用临时文件),再执行循环连接,对内表的多次扫描称为重扫描。之所以加载内表,是因为内表在连接过程中会被扫描很多次。

变体2—内表连接列上有索引

内表目标列上有索引的话,可以将两层循环缩短到一层,通过索引找到内表上和外表一样的列值。

变体3—外表连接列上有索引

外部循环会产生一个数据集(也就是外表)来驱动连接条件。这个数据集可以通过使用索引范围扫描,全表扫描等方式从表里生成。如果表中的该列有索引,那么使用索引扫描来确定外表是更快的。

在连接阶段,也可以使用索引来扫描外表。(在连接阶段,因为都是要扫描所有行的,所以我没有看出来这个阶段中的索引扫描比顺序扫描更有优势,但在生成外表阶段,索引扫描有优势)

变体三可以和前面提到的基础版、变体一、变体二组合使用,形成三种情况的嵌套循环连接:

1.2 归并接连

归并连接只能用于自然连接与等值连接,系统是否使用归并连接与数据的排序成本很相关,两个表的连接列会先排序,连接的过程如下:

For each tuple r in A
    For each tuple s in B
         If (r.ID = s.ID)
              Emit output tuple (r,s)
              Break;
         If (r.ID > s.ID)
              Continue;
         Else
              Break;

归并连接的启动成本为内外表的排序成本之和。

归并连接的排序是将内外表都加载到内存中操作的,如果内存不够,则使用临时文件。

除了前面多了个排序操作,它的变体与嵌套循环连接类似,就是分为内表物化、内表索引、外表索引+基本、外表索引+内表物化、外表索引+内表索引等。

(什么是物化,如果物化是指将元组从磁盘中加载到内存或临时文件的话,那这里的排序就已经物化了,为什么内表还能再物化?)

1.3 散列连接

散列连接先对内表的连接字段创建散列表,将散列的桶与内表对应上,再对外表逐条扫描,并对连接字段进行散列,找到相应的桶,在桶里面与已有的内表连接字段进行等值比较(因为桶的编号只会取散列值的后几位,所以很可能同一个桶里的值是不一样的),如果找到了,则进行连接。其实就相当于先对内表创建索引,散列表就是一种索引。

不考虑创建散列表的冲突的话,启动成本与运行成本都是O(N(内表)+N(外表))。

内存散列连接

当内表大小不超过工作内存的1/4的话,就会使用内存散列连接。

带倾斜的混合散列连接

分批构建与探测:

当内表很大,不能完全装入散列表中时,PG将操作分为 2的幂 个批次,第一个批次在工作内存中分配,其余的在临时文件中。以4批次,每批次8个桶为例,需要取散列值的后5位,前两位决定在哪个批次,后三位决定在哪个桶。

批次是一下子全分配的,但构建散列表与探测是分批的,只能一批一批地轮流进内存进行构建与探测。

倾斜批次:

第一个批次因为一开始就在内存中,所以速度较快,PG使用倾斜批次尽量让数据在第一批次中连接。比如10%的用户买了70%的东西,pg会将10%的客户存在第一批次中,以和尽量多的已售东西连接。pg通过已售东西表中的买家出现频率(高频的被标记为MCV(most common values),(前面提到的选择率大部分就是这个),是一直存在pg_stats上的)来确定哪些买家属于10%。倾斜批次也在内存中。

第一次构建与探测:

在第一次构建阶段,pg先判断内表元组是否属于倾斜批次,如果不属于,再进行散列决定元组去哪个批次。如果是第1批次,则会构建进相应的桶,如果是临时文件中,就进临时文件,但不会进相应的桶。

在第一次探测阶段,先分配3个与批次对应的临时文件用于存储外表。先判断外表元组的连接字段是否被标记为MVC,是的话在倾斜批次上探测。否则计算其所在批次,如果为第一批次,则进行探测,如果为临时文件中的批次,则将其存在刚刚分配的对应批次的临时文件中。

第n次构建与探测:

 扫描一遍外表后,对倾斜批次与第一批次的连接已经该连的都连完了。将倾斜批次与第一批次移出,加载内表存在第二批次并构建,然后加载外表存的第二批次并探测。以此类推。

注意,处于第二批次的外表只可能与处于第二批次的内表连接,这是哈希值的作用。

2. 计划器—连接访问路径与连接节点

连接访问路径都基于joinpath实现

所有连接访问路径:

连接节点 NestedLoopNode, MergeJoinNode and HashJoinNode都基于joinnode实现

3. 计划器—创建多表查询计划树

3.1 预处理

  • 处理with查询
  • 上拉子查询:如果有一个FROM子查询,并且没有GROUP BY、HAVING、ORDER BY、LIMIT和DISTINCT子句,并且没有使用INTERSECT或EXCEPT,则planner会通过pull_up_subquerys()函数将其转换为连接形式。例如,下面可以转换为自然连接查询。不用说,这种转换是在查询树中完成的。
testdb=# SELECT * FROM tbl_a AS a, (SELECT * FROM tbl_b) as b WHERE a.id = b.id;
	 	       	     ↓
testdb=# SELECT * FROM tbl_a AS a, tbl_b as b WHERE a.id = b.id;
  • 如果可以的话,将外连接转换为内连接

3.2 获取代价最小的路径

因为各个索引与各种连接方法的组合非常多,当表小于12张时,计划器使用动态规划来获取最佳计划,否则使用遗传算法(基因查询优化器,使优化器能快速找到一个近似最优的路径)。

动态规划:

  1. 先根据单表查询章节中的找出单表的各种索引扫描的代价最小的访问路径
  2. 再根据连接成本估计章节中找出两张表间各种连接方法的最小代价访问路径
  3. 将两表的最小连接结果视为一张表,与第三张表进行连接,通过两张表间各种连接方法找出最小代价访问路径,以此类推。(需要注意的是,动态规划的局部最优不一定是全局最优,但是一个比较优的)

上图是个例子,先顺序扫描表a(内表,行11),对其进行散列(行10),在顺序扫描表b(外表,行9),将a与b进行哈希连接(行7),索引扫描表3(内表,行13),框框中a与b的散列连接结果视为外表,通过索引与表3进行嵌套循环连接(行5,内表索引循环连接的话,内外表都只需循环1次)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据库系统之查询处理与优化 查询处理的过程 第⼀步 就是语法分析器与翻译器处理,对SQL查询语句进⾏语法验证,保证查询语句的正确性,然后将SQL查询语句翻译为可使⽤的形式 第⼆步 是将SQL查询语句翻译为系统对应的内部表⽰形式,即关系代数表达式 第三步 就是对关系代数表达式进⾏优化,找出最优的效率最⾼的关系代数表达式 第四步 根据这个关系代数表达式制定好执⾏计划(执⾏计划是指执⾏⼀个查询的计算机原语,也就是说它是标注了如何执⾏⼀个或多个关系代数操 作的操作序列) 第五步 执⾏引擎按照执⾏计划进⾏执⾏,对数据库中的表,函数进⾏操作,最终得到查询结果响应给⽤户。 优化器(就是对关系代数的优化,是通过等价变换得到优化执⾏⽅案,也就是操作执⾏的顺序,包含了注释是否需要采⽤索引,具体采⽤的 操作执⾏算法等) 查询代价的度量 1.查询处理的代价可通过该查询对各种资源的使⽤情况进⾏度量 2.然⽽在⼤型数据库系统中,在磁盘存取数据的代价通常是最主要的代价 3.我们可以使⽤传送磁盘块数以及搜索磁盘次数来度量查询计划代价。假设磁盘⼦系统传送⼀个块的数据平均消耗tr秒,磁盘块平均访问时 间(磁盘搜索时间加上旋转延迟)为ts秒,那么依次传送b个块以及执⾏S次磁盘搜索的操作消耗btr+Sts秒 SQL中常见的查询 1.整个表数据全部读取,也就是没有任何条件的查询 2.有⼀个单⼀条件准确定位某⼀个数据记录 3.有⼀个范围条件要定位多个数据记录 4.单个条件和范围条件的结合 选择操作典型实现⽅法 1.简单的全表扫描⽅法 顺序扫描,输出满⾜条件的元组,适合⼩表,不适合⼤表 2.索引(或散列)扫描⽅法 通过索引先找到满⾜条件的元组主码或元组指针,再通过元组指针直接在查询的基本表中找到元组。适合选择条件中的属性上有索引(例如 B+树索引或Hash索引) 选择操作的实现 例:select * from Student,SC where Student.sno=SC.sno 1.嵌套循环⽅法 对外层循环(Student)的每⼀个元组(s),检索内层循环(SC)中的每⼀个元组(sc),检查这两个元组在连接属性(sno)上是否相等,如果满⾜连接 条件,则串接后作为结果输出,知道外层循环表中的元组处理完为⽌。 2.排序-合并⽅法(适合连接的诸表已经排好序的情况) 如果连接的表没有排好序,先对Student表和SC表按连接属性sno排序,取Student表中第⼀个sno,依次扫描SC表中具有相同sno的元 组,当扫描到sno不相同的第⼀个SC元组时,返回Student表扫描它的下⼀个元组,再扫描SC表中具有相同sno的元组,把它们连接起来 重复上述步骤直到Student表扫描完。 3.索引连接⽅法 在SC表上建⽴属性sno的索引,(如果原来没有索引),对Student表中每⼀个元组,由sno值通过SC的索引查找相应的SC元组,把这些SC 元组和Student表中的元组处理完为⽌。 4.Hash Join⽅法 把连接属性作为hash码,⽤同⼀个hash函数把R和S中的元组散列到同⼀个hash⽂件中。 查询的代价主要考虑的内容 在分布式数据库系统中,查询代价除了考虑CPU代价和I/O代价外,由于数据分布在不同的场地上,使得查询处理中还要考虑站点间传输数 据的代价;分布透明性是指⽤户不需要了解数据分⽚的位置,分⽚的分配位置以及数据复制的过程;分布式查询优化⼀般需要考虑操作的执 ⾏顺序和数据在不同场地间的传输顺序;执⾏分布式数据库查询时,导致数据传输量⼤的主要原因时数据间的连接操作和并操作。 查询优化 同⼀个SQL查询语句的不同关系代数表达式,它查询代价时不同的,可以根据关系代数表达式的等价转换,将关系代数表达式变成代价较低 的关系代价表达式,来实现查询优化。 查询树和查询执⾏计划之间的区别 查询树不是最优的,不包含实际执⾏时选择的算法,执⾏计划=优化后的查询树+选择的实际执⾏的算法(连接运算是⽤块连接还是散列连 接,是否⽤流⽔线等); 查询执⾏的时候是⽤的执⾏计划,即优化后的查询树加上相应操作的具体算法; 查询树的基本优化策略 选择下移优化策略(优先做选择,后做关系连接,将选择移到靠近关系) 投影下移优化策略(通过等价规则先进⾏投影,去除对查询⽆意义的属性,再做连接) 选择连接顺序优化策略(⼩关系的连接优先,这样做中间结果元组会很少,这个代价也会很低)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值