目录
第十二章 查询处理
12.1 概述
查询处理(query processing)是指从数据库中提取数据时涉及的一系列活动。这些活动包括:
- 将用高层数据库语言表示的查询语句翻译成能在文件系统的物理层上使用的表达式
- 为优化查询而进行各种转换
- 查询的实际执行。
查询处理步骤如图12-1所示,基本步骤包括:
- 语法分析和翻译
- 优化
- 执行
12.2 查询代价的度量
查询处理的代价包括:磁盘存取,执行一个查询所需要的CPU时间,在并行/分布式数据库系统中的通信代价。
然而,在大型数据库中,在磁盘上存取数据的代价通常是最主要的代价。
用传送磁盘块数以及搜索磁盘次数来度量查询计划的代价:
- 假设瓷盘子系统传输一个块的数据平均消耗t_T秒,磁盘平均访问时间(磁盘搜索时间+旋转延迟)为t_S秒,则一次传输b个块,以及执行S次磁盘搜索的操作将消耗(b*t_T+S*t_S)秒。
- 其中t_T和t_S必须针对所使用的磁盘系统来计算。
- 当今高端磁盘的典型数值通常是t_S = 4毫秒,t_T=0.1毫秒(假定磁盘块的大小是4KB,传输率为40MB/s)
通果把读磁盘块和写磁盘块区分开,可以进一步细化磁盘存取代价的估算。
- 因为写磁盘块的代价通常是读磁盘块的两倍(这是由于磁盘系统在写完扇区后还会重新读取该扇区以验证写操作是否成功) ==> 本文忽略这个细节
本书给出的代价没有包括将操作最终写回磁盘的代价,当需要时需要单独考虑。
- 本书所考虑的算法代价依赖于主存中缓冲区的大小
- 最好的情形是所有的数据都可以读入到缓冲区中,不必再访问磁盘
- 最坏的情形是假定缓冲区只能容纳数目不多的块——大约每隔关系一块
- 在代价估算时,通常假定最坏的情形。
本书假定开始时数据必须从磁盘中读取出来,但是很可能这个磁盘块已经在内存缓冲区中了。
- 本书忽略这个细节
- 因此,一个查询计划中的实际磁盘存取代价可能会比估算代价小
假设计算机没有其他活动在进行,那么一个查询计划的响应时间(response time)就是所有的这些开销,并可以作为计划的代价度量。遗憾的是很难估计响应时间,因为:
- 当计划开始执行,响应时间依赖于缓存区的内容;在对查询进行优化时,该信息无法获取,而且即便可以获取也很难应用于计算。
- 在具有很多张磁盘的系统中,响应时间依赖于访问如何分布在各磁盘上,没有对分布在磁盘中的数据的详细了解这是很难估计的。
需要注意的是,可以以额外的资源消耗为代价,获取计划更好的响应时间。例如:
- 假如一个系统有多张磁盘,一个计划A需要额外的磁盘读取,但它并行的跨多张磁盘执行读,它可能比另一个计划B完成更快,尽管计划B有较少的磁盘读取,但是它从一张磁盘读。
- 但是,如果一个查询的多个实例同时使用计划A来运行,整体的响应时间可能比相同的实例使用B计划来执行要长,这是因为计划A有更多的磁盘负载。
综上:优化器通常努力去尽可能降低查询计划总的资源消耗(resource consumption),而不是尽可能降低响应时间。
估计总的磁盘访问时间(包括寻道和数据传输)的模型,就是一个基于资源消耗的查询代价的模型的实例。
12.3 选择运算
在查询处理中,文件扫描(file scan)是存取数据最低级的操作。
文件扫描是用于定位、检索满足选择条件的记录的搜索算法。
在关系系统中,若关系保存在单个专用的文件中,采用文件扫描就可以读取整个关系。
12.3.1 使用文件扫描和索引的选择
12.3.2 涉及比较的选择
A1-A6
索引结构称为存取路径(access path),他们提供了定位和存取数据的一条路径。
使用索引的搜索算法称为 索引扫描(index scan)。
考虑所有元组都存储在一个文件中,对其进行选择运算,可以有如下方式:
如11.4.2节所述,辅助索引通常不存储指向记录的指针,而是存储主索引的属性值。
这种方式下,通过辅助索引存取一条记录的代价更大:首先必须搜索辅助索引以找到主索引的搜索码值;然后查找主索引来找到记录。如果考虑到这样的实现,需要对表中的估计进行调整。
如果检索到的记录数很大时,使用辅助索引的代建甚至比线性搜索还打。因此辅助索引应该仅在选择得到的记录很少时使用。
12.3.3 复杂选择的实现
A7-A10
A7 利用一个索引的合取选择 | 首先判断是否存在某个简单条件的某个属性上存在一条存取路径。若存在,则可以用算法A2-A6中的一个来检索满足条件的记录。然后再内存缓冲区中,测试检索到的记录是否满足其他条件。
代价为:θ_i和A1-A6组合的最小代价 |
A8 使用组合索引的合取选择 | 可能存在合适的组合索引(composite index) 如果选择指定的是两个或多个属性上的等值条件,并且这些属性字段的组合又存在组合索引,则可以直接搜索索引。索引的类型可以从A2-A4中选择。 |
A9通过标识符的交实现合取选择 | 利用记录指针或记录标识符的方式实现合取选择。 该算法要求各个条件所涉及的字段上带有记录指针的索引。该算法对每个索引进行扫描,获取那些指向满足单个条件的记录的指针。所有检索到的指针的交集就是那些满足合取条件的指针的集合。然后算法利用该指针集合获取实际的记录。如果并非所有条件上都存在索引,则该算法还需要用剩余条件对所检索到的记录进行测试。
代价:扫描各个索引的代价的总和+获取检索到的指针列表的交集中的记录的待机
优化:对指针列表进行排序并按照排序顺序检索记录能够减少该算法的代价。因此: (1)应把指向一个磁盘块中所有记录的指针归并到一起,这样只需一次I/O操作就可以获取到该磁盘块中选择的所有记录 并且(2)磁盘块的读取也可以按照存储次序执行,这样磁盘臂的移动最少。 |
A10 通过标识符的并实现析取选择 | 如果在析取选择中,所有条件上均有相应的存取路径存在,则逐个扫描索引获取满足单个条件的元组指针。检索到的所有指针的并集就是指向满足析取条件的所有元组的指针集。然后利用这些指针检索实际的记录。
然而,即使只有一个条件不存在存取路径,就不得不对整个关系进行一次线性扫描找出满足该条件的元组。因此,只要析取式中有一个这样的条件,最有效的存取方式就是线性扫描。 |
12.4 排序
数据排序在数据库系统中有很重要的作用,主要原因有两个:
- SQL查询会指明对结果进行排序
- 当输入的关系已排序时,关系运算中的一些运算(如连接运算)能够得到高效实现
在逻辑上排序和在物理上排序:
逻辑排序:通过在排序码上建立索引,然后使用该索引按序读取关系,可以完成对关系的逻辑排序。
- 缺点:因为该种方式没有实现物理排序,因此顺序读取元组可能导致每次读取有一个元组就要访问一次磁盘(磁盘搜索+磁盘块传输),这样做的代价很大。
出于这个原因,有时需要在物理上进行排序。
12.4.1 外部排序归并算法
外排序(external sorting):对不能完全放在内存中的关系的排序称为外排序。
外排序中最常用的技术是:外部归并排序(external sort-merge)算法。
外部归并排序算法介绍:设M表示内存缓冲区中可以用于排序的块数,即内存缓冲区可以容纳的磁盘块数。
- 第一阶段:建立多个排序好的归并段(run)。每个归并段内部是排序好的,但仅包含关系中的部分记录。
12.4.2 外部排序归并的代价分析
磁盘块传输代价:
12.5 连接运算
12.5.1 嵌套循环连接
12.5.2 块嵌套循环连接
嵌套循环和块嵌套循环的进一步改进:
- 如果自然连接或等值连接的连接属性是内层关系的码,则对每个外层关系元组/块,内层循环一旦找到首条匹配的元组就可以终止
- 块嵌套循环连接算法中,外层关系可以以内存中最多能容纳的磁盘块的大小 减去 预留给内层关系及输出结果的缓冲空间 作为单位进行循环。即:使用内存能够容纳的最大块数M-2为单位读取外层关系做循环。
- 磁盘块传输次数:b_r*[b_r/(M-2)] + b_r
- 磁盘搜索:2*[b_r/(M-2)]
- 对内层循环轮流做向前、向后的扫描。该扫描方法对磁盘块读写请求进行排序,使得上一次扫描时留在缓冲区的数据可以重用,从而减少磁盘存取次数。
- 若内层循环的连接属性上有索引,可以用更有效的索引查找法替代文件扫描法,见下节
12.5.3 索引嵌套循环连接
在嵌套循环连接中,如果在内层循环的连接属性上有索引,则可以用索引查找替代文件扫描。这称为索引嵌套循环连接(indexed nested-loop join)。
12.5.4 归并连接
12.5.4.1 归并算法
12.5.4.2 代价分析
12.5.4.3 混合归并连接
当两个关系的连接属性上存在辅助索引时,可以对未排序的元组执行归并连接越算的变种:
- 通过索引扫描记录,从而按顺序检索记录
- 缺点:记录可能分散存储在文件的多个块中,从而每个元组的读取都需要一次磁盘访问,代价很大。
改进:混合归并-连接技术(hybrid merge-join algorithm),把索引和归并连接结合起来
- 假设两个关系,有一个排序了,一个没有排序,但是未排序的关系在连接属性上存在B+树辅助索引
- 混合归并-连接算法:
- 把已经排序的关系 与 B+树辅助索引叶结点进行归并,所得结果包含了已排序关系的元组 及 未排序关系的元组地址
- 将该文件按未排序关系元组的地址进行排序,从而能够对相关元组按照物理存储顺序进行有效的检索,最终完成连接运算。
12.5.5 散列连接
也可用于自然连接和等值连接
12.5.5.1 基本思想
12.5.5.2 递归划分
递归划分(recursive partitioning):
如果n_h的值大于或者等于内存块数M,因为没有足够的缓冲块,所以划分不能一趟完成。
这时,完成关系的划分要重复多趟。
每一趟中,输入的最大划分不超过用于输出的缓冲块数目。
每一趟产生的存储桶在下一趟中分别被读入并再次划分,产生更小的划分。
每次划分采用与上一次不同的散列函数。
系统不断重复输入的分裂过程直到构造用输入关系的每个划分都能被内存容纳为止。
12.5.5.3 溢出处理
12.5.5.4 散列连接的代价
例如:takes和students连接。内存有20块,student划分成5个划分,每个划分占20块,则划分只需要一趟。
takes类似也划分成5个划分,每个划分80块。忽略部分满的代价,共需3*(100+400)=1500次块传输;
在划分过程中有足够的内存分配3个输入缓冲区和输出缓冲区,共计2([100/3]+[400/3])次磁盘搜索。
若主存较大,散列连接性能可以提高。当主存中容纳整个构造用输入关系时,n_h可以设置成0
此时,不管探查用输入的大小如何,不必将关系划分为临时文件,因而散列连接算法可以快速执行。
其估计代价降为:b_r+b_s次磁盘块传输和两次磁盘搜索。
12.5.5.5 混合散列连接
12.5.5.6 复杂连接
嵌套循环连接与块嵌套循环连接可以在任何连接条件下使用,其他的连接技术比它们的效率更高,但是只能处理简单的连接条件,如自然连接或等值连接。
复杂连接:
合取:可以用前面的连接技术 计算按照单个连接条件的连接结果 作为中间结果==> 再从中选取满足其它条件的元组
析取:计算每个连接的结果,再取并集。
12.6 其他运算
去处重复、投影、集合运算、外连接、聚集
12.6.1 去除重复
- 可以用排序的方法去除重复,如:外排
- 创建归并段时,如果发现重复,可以在将归并段写回磁盘时去处重复
- 在归并时,去除归并段与归并段之间的重复元组
- 最坏情形:与排序的代价估计是一样的
- 可以用散列来实现去除重复
- 基于整个元组上的一个散列函数对整个关系进行划分
- 每个划分被读入内存,并建立内存索引。在这个过程中,只插入不在索引中存在的元组。
- 划分中的所有元组处理完后,散列索引中的元组被写到结果中
- 代价估算:与散列连接中的构造用输入关系的处理(划分,以及读入每个划分)的代价一样。
12.6.2 投影
在每个元组上进行投影,再去除结果集中的重复
12.6.3 集合运算
并、交、差
- 方法一:先排序,再对每个排序的关系扫描一次,得到所需结果。
- 两个输入关系都要扫描一次,如果已经按相同顺序排序,其代价为b_r+b_s此块传输
- 最坏的情况下,只有一个缓冲块,则需要b_r+b_s此块传输和磁盘搜索。
- 分配额外的缓冲块可以减少磁盘搜索的次数
- 若一开始没排序,则还需考虑排序的代价
- 方法二:散列的方式。使用相同的散列函数对每个关系进行划分,由此得到对应的划分。对每对划分执行如下操作:
12.6.4 外连接
左外连接、全外连接、右外连接
外连接的计算策略:
- 方法1:计算响应的自然连接/等值连接,然后将未连接的结果加入到结果集中作为最终的连接结果。
- 方法2:扩展连接算法
- 扩展嵌套循环连接、块嵌套循环连接==> 较简单的实现左外连接和右外连接,但是全外连接很难
- 扩展归并连接==> 可以计算全外连接,左外连接,和右外连接
- 计算全外连接:当两个关系的归并完成后,将两个关系中那些与另外一个关系的任何元组都不匹配的元组填充空值后写到结果中。
- 扩展散列连接算法 ==> 可计算全外连接、左外连接和右外连接
12.6.5 聚集
avg, min, max, sum, count
使用排序或散列,将元组进行分组;再在每个分组上进行聚集运算得到结果。
12.7 表达式计算
如何计算包含多个运算的表达式:
- 物化materialized:以适当的顺序每次执行一次操作;每次计算的结果被物化(materialized)到一个临时关系以备后用
- 缺点:需要构造临时关系,这些临时关系必须写到磁盘上
- 流水线pipeline法:同时计算多个运算,运算的结果传递给下一个,而不必保存临时关系
12.7.1 物化
12.7.2 流水线
12.7.2.1 流水线的实现
流水线的两种实现方式:
- 需求驱动的流水线(demand-driven pipeline):消极的lazily
- 思想:
- 系统不停地向位于流水线顶端的操作发出需要元组的请求
- 每当一个操作收到需要元组的请求,它就计算下一个(若干个)元组并返回
- 如果该操作的输入不是来自流水线,则返回的下一个(若干个)元组可以由输入关系计算得到,同时系统记载目前为止已经返回了哪些元组。
- 如果该操作的某些输入来自流水线,则该操作也发出请求以获得来自流水线输入的元组,并使用该元组计算输出元组,返回给父层。
- 具体实现:
- 流水线中的每个操作可以用迭代算子(iterator)来实现,迭代算子提供:open(), next(), close()函数。
- 调用open()后,对next()的每次调用返回该操作的下一个输出元组
- close()告诉迭代算子不在需要元组了。
- 迭代算子维护两次调用之间的状态,使得下一个next()调用记录可以获取下面的结果元组。
- 流水线中的每个操作可以用迭代算子(iterator)来实现,迭代算子提供:open(), next(), close()函数。
- 思想:
- 生产者驱动的流水线(producer-driven pipeline):积极的eagerly
- 思想:
- 各操作不等待元组请求,而是积极的eagerly产生元组。
- 生产者驱动的流水线中的每一个操作作为系统中一个单独的进程或线程建模,以处理流水线输入的元组流,并产生相应的输出元组流。
- 具体实现:
- 系统为每一对相邻的操作创建一个缓冲区,来保存从上一个操作传递到下一个操作的元组
- 对应不同操作的进程或者线程会并发执行。
- 流水线底部的每个操作会不断产生输出元组,并将其存放至缓冲区,直到缓冲区已满。
- 当缓冲区满时,该操作会等待,直到其父操作取出缓冲区元组进行消费,为其提供空间。
- 思想:
12.7.2.2 流水线的执行算法
阻塞操作blocking operation:直到所有的输入元组都被检查完之前,不能输出任何结果,这类操作称为阻塞操作。
例如:排序。
其他操作(例如连接)本身不阻塞,但具体的计算算法可能会阻塞。
例如:
- 散列连接算法是一个阻塞操作,又因为在输出任何元组之前,它要求两个输入都被完全取回并划分。
- 索引嵌套连接算法随着得到外部关系的元组就可以输出结果元组。因此,它成为在外部(左边)关系上的流水线化(pipelined),尽管在索引嵌套连接算法执行之前,必须充分构建索引所导致在其索引(右边)输入的阻塞。
- 混合散列连接可以看做是在被探查关系上部分流水线的。
- 因为,当它从探查关系中接收元组时,它可以输出来自第一个划分中的元组
- 然而,不位于第一个划分的元组只有在接收整个流水线输入关系后才能输出。
- 如果构造用输入可以全部存储在内存中,则混合散列连接可以再探查用输入上提供流水线计算
- 如果构造用输入可以大部分存放在内存中,则混合散列连接可以再探查用输入上近似流水线。
- 归并连接:
- 如果两个输入在连接属性上均是有序的,并且是等值连接,则可以使用归并连接,并且两个输入都可流水线化。
- 如果两个输入在连接属性上没有排序,==》 采用双流水线连接(double-pipelined join)技术
总结
- 对于一个查询,系统首先要做的事就是将之翻译成系统内部表示形式。对于关系系统而言,内部形式通常是基于关系代数的。在产生查询的内部形式过程中,语法分析器检查用户查询语句的语法,验证出现在查询语句中的关系名是否是数据库中的关系名等。如果查询语句是用视图表达的,语法分析器就把所有对视图名的引用替换成计算该视图的关系代数表达式
- 给定一个查询,通常由很多计算它的方法。将用户输入的查询语句转换成等价的,执行效率更高的查询语句,是优化器的责任。
- 对于包含简单选择的查询语句,可以通过线性扫描或利用索引来处理。通过计算简单选择结果的并和交,可以处理复杂选择操作。
- 可以利用外排对大于内存的关系进行排序
- 涉及自然连接的查询语句可以有多种处理方法,如何处理取决于是否有索引可用以及关系的物理存储形式。
- 若连接的结果大小几乎与两个连接的笛卡尔积相当,则采用块嵌套循环连接策略最好。
- 若存在索引,则可以用索引嵌套循环连接。
- 若关系已经排序,则采用归并连接比较可取。为了能够使用归并连接,在连接计算前对关系排序是可取的。
- 散列连接算法吧关系划分成多个部分,使每个部分都能被内存容纳。划分过程是通过连接属性上的散列函数进行的,这样相应的划分可以独立的进行连接
- 去除重复、投影、集合操作(并、交、差)、聚集操作都可以用排序和散列实现
- 外连接操作可以通过对连接算法的简单扩展来实现
- 散列与排序在某种意义下是对偶的。因为任何能用散列实现的操作(如:去除重复、投影、聚集、连接、外连接操作)都可以用排序来实现,反之亦然。
- 可以采用物化的方式进行表达式的计算。系统计算每个子表达式的结果并将其存储到磁盘上,然后用它进行父表达式的计算。
- 流水线方法在子表达式产生输出的同时就在父表达式的计算中使用其输出结果,帮助我们避免了许多子查询的结果写到磁盘的操作。