文章目录
Chapter 12: Query Processing
概述(Overview)
查询处理步骤
关系数据库管理系统查询处理阶段:
- 查询分析
- 查询检查
- 查询优化
- 查询执行
查询分析
任务:对查询语句进行扫描、词法分析和语法分析
- 词法分析:从查询语句中识别出正确的语言符号
- 语法分析:进行语法检查
查询检查
任务:合法权检查、视图转换、安全性检查、完整性初步检查
-
合法权检查:根据数据字典中的用户权限和完整性约束定义对用户的存取权限进行检查
-
视图转换:对视图的操作,需要用视图的消解方法把对视图的操作转换成对基本表的操作
-
安全性检查:根据数据字典中有关的模式定义,检查语句中的数据库对象,如关系名、属性名是否存在且有效
检查通过后把 SQL 查询语句转换成内部表示,即等价的 关系代数表达式 ;一般使用 **查询树(语法分析树)**来表示扩展的关系代数表达式
查询优化
任务:选择一个高效执行的查询处理策略
分类:
- 代数 / 逻辑优化:关系代数表达式的优化
- 物理优化:存取路径和底层操作算法的选择
选择依据:基于 规则、代价 和 语义
查询执行
任务:依据优化器得到的执行策略生成查询执行计划,由代码生成器生成执行查询计划的代码;由查询执行引擎接受查询执行计划、执行该计划并返回结果给查询
Def: 查询执行计划 :用于执行一个查询的原语操作序列
注意:执行一个查询,不仅要提供关系代数表达式,而且还要对表达式加上注释来说明如何执行每个操作。加了“如何执行”注释的关系代数运算成为计算原语
分类:
- 自顶向下
- 自底向上
查询代价的度量(Measures of Query Cost)
Def:查询代价:执行一个查询所耗费的时间
注意:磁盘上存取数据的代价通常是最主要的代价,因为磁盘存取比内存操作速度慢,CPU速度的提升比磁盘的速度提升快得多;因此我们忽略CPU的时间消耗
度量方法
方法:使用 传送磁盘的块数、搜索磁盘的次数 来度量查询计划的代价
- t T t_T tT —— 磁盘子系统传输一个块平均消耗时间
- t S t_S tS —— 磁盘块平均访问时间(磁盘搜索时间 + 旋转延迟)
则一次传输
b
b
b 个块以及执行
S
S
S 次磁盘搜索的操作的查询代价为:
C
o
s
t
=
b
×
t
T
+
S
×
t
S
Cost=b \times t_T + S \times t_S
Cost=b×tT+S×tS
选择运算(Selection Operation)
实现方法
- 全表扫描方法
- 索引扫描方法
例:考虑 <Predicate> 的几种情况:
SELECT * FROM Student WHERE <Predicate>;
# C1: 无条件
# C2: Sno = '201215121'
# C3: Sage > 20
# C4: Sdept = 'CS' AND Sage > 20
全表扫描方法
方法:对查询的基本表顺序扫描,逐一检查每个元组是否满足选择条件,把满足条件的元组作为结果输出
实现:假设可以使用的内存为 M 块:
① 按照物理次序读 Student 的 M 块到内存
② 检查内存的每个元组 t,如果满足条件,则输出 t;
③ 如果 student 还有其他块未被处理,重复 ① ②
特点:适合小表,不适合大表
索引扫描方法
方法:通过索引先找到满足条件的元组主码或元组指针,再通过元组指针直接再查询的基本表中找到元组
特点:适合于选择条件中的属性上有索引( B+树索引 或 Hash索引 )
C2:(假设 Sno 上有索引,或 Sno 是散列码)算法:
① 使用索引(或散列)得到 Sno 为 201215121 元组的指针
② 通过元组指针在 Student 表中检索到该学生
C3:(假设 Sage 上有 B+ 树索引)算法:
① 使用 B+ 树索引找到 Sage = 20 的索引项,以此为入口点在 B+ 树的顺序集上得到 Sage > 20 的所有元组指针
② 通过这些元组指针到 Student 表中检索到所有年龄大于 20 的学生
C4:(假设 Sdept 和 Sage 上都有索引)
算法一:
① 分别用索引扫描找到 Sdept = ‘CS’ 的一组元组指针和 Sage > 20 的另一组元组指针
② 求得这两组指针的交集
③ 通过元组指针在 Student 表中得到计算机系年龄大于 20 的学生
算法二:
① 找到 Sdept = ‘CS’ 的一组元组指针
② 通过这些元组指针到 Student 表中检索,并对得到的元组检查另一些选择条件(Sage > 20)
③ 把满足条件的元组作为结果输出
查询代价
全表扫描方法
一次初始搜索加上
b
r
b_r
br 个块传输,即:
C
o
s
t
=
t
S
+
b
r
×
t
T
Cost=t_S + b_r{\times}t_T
Cost=tS+br×tT
索引扫描方法
C2:Sno = ‘201215121’;设 Sno 为 B+ 树主索引,索引高度为
h
i
h_i
hi ;则索引查找需要穿越树的高度,再加上一次 I/O 来取记录(每个这样的 I/O 需要一次搜索和一次块传输),即:
C
o
s
t
=
(
h
i
+
1
)
×
(
t
T
+
t
S
)
Cost=(h_i+1){\times}(t_T+t_S)
Cost=(hi+1)×(tT+tS)
C3:Sage > 20;设在 Sage 上有 B+ 树索引(二级索引),索引高度为
h
i
h_i
hi ,b 是包含具有指定搜索码记录的块数,(假定这些块是顺序存储的叶子块,且不需要额外搜索)则:
C
o
s
t
=
h
i
×
(
t
T
+
t
S
)
+
t
S
+
b
×
t
T
Cost=h_i{\times}(t_T+t_S)+t_S+b{\times}t_T
Cost=hi×(tT+tS)+tS+b×tT
C4:Sdept = ‘CS’ AND Sage > 20;设在 Sdept 上有 B+ 树索引(二级索引),索引高度为
h
i
h_i
hi ;按照算法二,一共有 n 个满足条件的元组(不一定顺序存放,即需要额外搜索),则:
C
o
s
t
=
(
h
i
+
n
)
×
(
t
T
+
t
S
)
Cost=(h_i+n){\times}(t_T+t_S)
Cost=(hi+n)×(tT+tS)
连接运算(Join Operation)
(连接运算是查询处理最耗时的操作之一,我们这里只讨论等值连接/自然连接最常用的实现算法)
常用算法
- 嵌套循环算法(nested loop join)
- 排序 - 合并算法(sort-merge join)
- 索引连接算法(index join)
- Hash Join 算法
例:
SELECT * FROM Student, SC WHERE Student.Sno = SC.Sno
嵌套循环算法
① 对外层循环(Student 表)的每一个元组 s,检索内层循环 (SC 表)中的每一个元组 sc
② 检查这两个元组在连接属性 Sno 上是否相等
③ 如果满足连接条件,则串接后作为结果输出,直到外层循环表中的元组处理完为止
排序 - 合并算法
① 未排序的情况下,先对 Student 表和 SC 表按连接属性 Sno 排序(假设升序)
② 取 Student 表中的第一个 Sno,依次扫描 SC 表中具有相同 Sno 的元组
③ 当扫描到 Sno 不相同(SC.Sno > Student.Sno)的第一个 SC 元组时,返回 Student 表,扫描它的下一个元组,再重复 ②,直至 Student 表扫描完
(此算法 Student 表和 SC 表都只要扫描一遍;即使是大表,加上排序和扫描的时间,总的时间一般仍会减少)
索引连接算法
① 在 SC 表上已经建立 Sno 属性的索引
② 对 Student 中的每一个元组,由 Sno 值通过 SC 的索引查找相应的 SC 元组
③ 把这些 SC 元组和 Student 元组连接起来
④ 循环执行 ② 和 ③ ,直到 Student 表中的元组处理完为止
Hash Join 算法
① 把连接属性 Sno 作为 hash 码,用同一个 hash 函数把 Student 表和 SC 表中的元组散列到 hash 表中
② 划分阶段(building phase / partitioning phase)
- 对包含较少元组的表(如 Student)进行一遍处理
- 把它的元组按 hash 函数分散到 hash 表的桶中
③ 试探阶段(probing phase,也称为 连接阶段 join phase )
- 对另一个表 (SC 表)进行一遍处理
- 把 SC 表的元组也按同一个 hash 函数 (hash 码是连接属性)进行散列
- 把 SC 元组与桶中来自 Student 表并与之相匹配的元组连接起来
注意:上面 hash join 算法前提是,假设两个表中较小的表在第一阶段后可以完全放入内存的 hash 桶中
查询代价
假设 | Student | SC |
---|---|---|
记录数 | 5,000 | 10,000 |
块数 | 100 | 400 |
嵌套循环算法
① 当 Student 表作为外层循环
block transfers:5000 × {\times} × 400 + 100 = 2,000,100
(首先需要将 Student 的每个块都转移到内存一次,即 100 次;接着,对每条记录,都需要对 SC 的所有块都遍历一遍,所以需要转移 5000 × {\times} × 400 次 )
seeks:5000 + 100 = 5100
(Student 的每个块转移都需要查询一次,因此转移 100 块需要 Seek 100次;对于 Student 每个元组,都需要 Seek SC 表的起始位置一次,因此需要 Seek 5000 次)
因此查询代价为:
C
o
s
t
=
2000100
t
T
+
5100
t
S
Cost=2000100t_T+5100t_S
Cost=2000100tT+5100tS
② 当 SC 表作为外层循环
block transfer:10000 × \times × 100 + 400 = 1000400
seeks:10000 + 400 = 10400
因此查询代价为:
C
o
s
t
=
1000400
t
T
+
10400
t
S
Cost=1000400t_T+10400t_S
Cost=1000400tT+10400tS
块嵌套循环算法
算法:(嵌套循环的变式,相当于四层 for 循环)
① 遍历表 r 中的每个块 B r B_r Br
② 对于表 r 中的每个块 B r B_r Br ,遍历表 s 中的每个块 B s B_s Bs
③ 对于当前 B r B_r Br 和 B s B_s Bs ,遍历二者的每个元组 t r t_r tr 和 t s t_s ts ,若满足结合条件,则将 t r ⋅ t s t_r\cdot t_s tr⋅ts 加入结果集
( b r b_r br 、 b s b_s bs 分别代表内外层循环的表的块数)
最坏情况:(Seek 是这样计算的:表 r 每个块都需要查询一次起始位置,即
b
r
b_r
br 次;对于表 r 中每个块,都需要查询一次表 s 的起始位置,即
b
r
b_r
br 次)
C
o
s
t
=
(
b
r
×
b
s
+
b
r
)
t
T
+
2
b
r
t
S
Cost=(b_r{\times}b_s+b_r)t_T+2b_rt_S
Cost=(br×bs+br)tT+2brtS
最好情况:(Seek 是这样计算的:表 r 和表 s 只需要查询一次起始位置,并保存下来,重复使用)
C
o
s
t
=
(
b
r
+
b
s
)
t
T
+
2
t
S
Cost=(b_r+b_s)t_T+2t_S
Cost=(br+bs)tT+2tS