查询计划
SELECT R.id, S.cdate
FROM R JOIN S
ON R.id = S.id
WHERE S.value > 100
- 所有运算符被安排在一棵树上
- 数据从树叶流向树根
- 根节点的输出是查询的结果
处理模型
数据库管理系统的处理模型定义了该系统如何执行一个查询计划。
- 不同的工作负载有不同的权衡
方法一:迭代模型(Iterator Model)
方法二:物化模型(Materialization Model)
方法三:矢量/批处理模型(Vectorized/Batch Model)
迭代器模型
每个查询计划运算符都实现了Next函数。
- 在每次调用时,运算符返回一个元组,如果没有更多的元组,则返回空标记(null marker)
- 运算符实现一个循环,该循环调用其子节点的next函数来检索元组并处理它们
该模型也称为Volcano或流水线模型。
[例]
迭代模型的实现伪代码如下
几乎所有允许元组流水线的数据库管理系统都使用该模型。
有些运算符必须阻塞,直到它们的子节点发出了所有元组。
- joins, Subqueries, Order By
输出控制很容易用这种方法工作。例如 limit子句,在父节点处需要10个tuple,那么它就只会调用10次子节点的next函数。
物化模型
每个运算符一次性处理所有输入,然后一次性发出所有输出。
- 操作符将其输出“物化”为单个结果
- DBMS可以将提示下推,以避免扫描过多的元组(比如limit子句,如果不告诉子节点需要多少tuple,就会收到非常多tuple)
- 可以发送物化的行或单个列
输出可以是整个元组(NSM)或列的子集(DSM)
物化模型对于OLTP型的工作负载更好,因为查询一次只访问少量的元组。
- 降低执行/协调开销
- 更少的函数调用
对于中间结果较大的OLAP查询不太好。
矢量模型
就像迭代模型,每个运算符实现这个模型中的Next函数。
每个运算符发出一批元组,而不是一个元组。
- 运算符内部循环一次处理多个tuple
- 批处理的大小可能因硬件或查询属性而异
向量模型非常适合OLAP查询,因为它大大减少了每个运算符的调用次数。
它允许运算符使用矢量化(SIMD)指令来分批处理元组。
计划处理的方向
方法一:自顶向下
- 从根开始,从它的子节点中“拉取”数据
- 元组总是与函数调用一起传递
方法二:自底向上
- 从叶节点开始,将数据推送到它们的父节点
- 允许对流水线中的缓存/寄存器进行更严格的控制
访问方法
访问方法是DBMS访问表中存储的数据的方法。
- 在关系代数中没有定义
三种基本方法:
- 顺序扫描
- 索引扫描
- 多索引 / “位图“索引
顺序扫描
for page in table.pages:
for t in page.tuples:
if evalPred(t):
//Do something!
对于表中的每一页:
- 从缓冲池中检索它
- 迭代每个元组并检查该元组是否符合查询的条件
DBMS维护一个游标,它跟踪它检查的最后一页/slot
优化
这几乎总是DBMS执行查询所能做的最糟糕的事情。
顺序扫描优化:
- 预提取(Prefetching)
- Buffer Pool Bypass
- 并行(Parallelization)
- Zone Maps
- 延迟物化
- 堆聚簇(Heap Clustering)
ZONE MAPS
ZONE MAPS为数据库表预先计算其属性的聚合值。DBMS首先检查ZONES MAPS,以决定是否要访问该表。
[例]有如下的一个表,该表只有一列
假设有一个查询:
SELECT * FROM table
WHERE val > 600
DBMS在进行循环扫描前,会先去查看Zone Map,然后发现这个表的MAX值为400,所以这个表里肯定没有满足条件的tuple,便不会去遍历该表。
Zone Map存储在哪里呢?可以和表一起存储在page里,也可以用专门的page把Zone Map集中存储。Zone Map也许会写到磁盘上,但大部分时候存放在内存中。
Zone Map的一个问题在于维护。每当更新过数据库表,就需要对Zone Map重新计算,这个成本并不小。所以Zone Map适合于OLAP型的数据库,而不适合OLTP型的数据库。
延迟物化
列式存储的DBMS可以延迟缝合元组(元组的属性值分布在不同的page当中,最终的结果需要读取多个page,然后拼合成完整的tuple)。
[例]
下面的foo表的三个属性a,b,c分别存储在不同的page中
执行select操作时,先读取a所在的page,进行筛选,系统可以识别出父节点需要的属性,在这里,它的父节点join不需要用到属性a,因此不返回具体的a值,而仅仅返回这个tuple在表中的位置(Offset)。可以看到,这样做的好处在于可以避免传送多余的数据。
同理,在执行join操作时,读取b所在的page,它同样知道父节点不需要b的值,所以返回元组的位置
最后根节点在给出结果时,才去page里读取出具体的数据拼合成tuple。
堆聚簇(Heap Clustering)
如图,这堆page里的tuple已经按照聚簇索引排好序了,这些page本身也是有序的。
如果查询使用聚簇索引属性访问元组,那么DBMS可以直接跳转到它需要的page。
索引扫描
DBMS挑选一个索引来查找查询所需的元组。
使用哪个索引取决于:
- 索引包含哪些属性
- 查询引用哪些属性
- 属性值域
- 谓词条件(大于,小于或等于)
- 索引是否具有唯一键或非唯一键
[例]
假设我们有一个包含100个元组和两个索引的表:
- 索引1:age
- 索引2:dept
SELECT * FROM students
WHERE age < 30
AND dept = 'CS'
AND country = 'CN'
使用哪个索引取决于表中的数据是啥样的。
情景1
30岁以下的有99人,但CS学院的只有2人。
这里很明显应该选择dept作为索引,否则要用循环扫描来在99人中找2个CS学院的人。
情景二
CS学院的有99人,但30岁以下的只有2人。
选择age作为索引。
DBMS要试图避免读取不需要的数据,其中也包括了探测索引的成本。DBMS应该意识到,如果选择了一个选择性不高的属性作为索引,就会为遍历索引或查找索引付出代价。
多索引扫描
如果有多个可以供DBMS用来查询的索引:
- 使用每个索引分别计算出tuple的ID集合
- 基于查询的谓词组合这些集合(并或交)
- 检索tuple并应用其它剩余的谓词条件