这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
前面已经讨论了在硬盘上如何存储和管理存储(disk manager),在内存中如何进行数据缓存(buffer pool manager)以及如何访问数据(哈希表存储中间数据,索引加速数据访问,并发控制策略用于多线程访问)。
Time to talk about how to execute queries.
Query plan
解析query,生成语法树。query plan是真实的执行流程,自底向上。最终从根节点输出的结果就是query的执行结果。
External merge sort
Queries often want to retrieve tuples in a specific order.
- 消除重复(distinct)
- 前面提到过,建立索引的一个优化方案是先把数据进行排序
- 聚合操作(group by)
由于数据通常无法全部加载到内存中,所以常说的快排等等内部排序(数据记录在内存中的排序)是无法使用的。
归并排序是分治的思想。内部排序中也有归并排序,只不过外部排序中,将数据分片后各自独立的运行,然后再合并。
- sorting:只排序当前块中的数据(按照内存能放下来的大小来分片),排序后写回到文件中
- merging:将各自内部有序的文件合并成一个整体有序的大文件(类似合并k个有序数组)
2路归并排序
一些参数:
- 2路:每次合并的是两个有序页面
- 全部数据划分成N个pages
- 每个线程占用B个buffer pages来存放输入输出数据。也就是每次可以加载B个页面到内存中。每次对一个page排序完后,写回到磁盘中
- 在merge阶段,需要使用三个buffer pages,两个用来存放input(同时合并两个page),一个存放output
Double buffering optimization
内存中有更多的空闲buffer page的时候,可以在每次排序一个页面的时候,预先将下一个待排序的页面加载到内存中。
Using b+ trees for sorting
如果需要排序的某个表的某个属性上已经建立了索引,那么可以直接在这个索引上读取数据。但也要具体情况分析:
- 如果是聚簇索引,也就是具体存放数据的索引,那么可以直接从叶子节点最左边依次读取到最右边。非常高效,避免了计算开销,并且磁盘的访问也是顺序的。
- 如果是非聚簇索引则性能很差。因为通常来说,每一个叶子节点中的每一条记录都需要一次磁盘IO来读取数据。
Aggregations
聚合操作,将多个tuples合并成一个标量值。有两种方法,排序或哈希。
Hash aggregate
但是对于一些不需要数据有序的聚合操作(例如group by 和distinct),使用哈希是更好的方案。
- 对ordering没有要求
- 计算成本低很多
例如,前面提到的两个问题,哈希操作每次要检查当前记录是否在哈希表中已经存在一个entry
- distinct:直接丢弃重复的数据
- Group by:执行聚合计算(例如,通过每个entry对应一个数组来实现分组)
当然,还会面临到内存中是否能放得下的问题。
If the DBMS must spill data to disk, then we need to be smarter
External hashing aggregate
所以同样需要外部哈希。
- phase1: partition,正常进行哈希,将key映射到对应的bucket中。某个bucket放满了就落盘。
- phase2:rehash,对每个分区构建内存中的哈希表,然后做相应的计算。
看到这我有点迷了,看看后面的例子。
Partition
找到目标数据后,去掉无关列,对目标属性进行哈希
Rehash
以distinct操作为例,前一步将数据划分为了不同的partition,并写回到了磁盘上。对于每个分区数据:
- 加载到内存中,并使用一个新的哈希函数将它映射到一个内存中的新哈希表中。
- 遍历这个新哈希表的每个bucket(entry),将所有匹配目标要求的tuple取出来。
这基于每个partition都要能放入内存的假设。
如这个例子,distinct只需要在每个分区中取出一个数据(唯一的),所以使用新函数h2将每个分区的key映射到新的哈希表中就可以了。最终结果就是从新哈希表中依次取出每个元素。