本系列为 CMU 15-445 Fall 2022 Database Systems 数据库系统 [卡内基梅隆] 课程重点知识点摘录,附加个人拙见,同样借助CMU 15-445课程内容来完成MIT 6.830 lab内容。
在关系型数据库中,我们常常通过规范化 (Normalization) 设计避免信息冗余;因此查询时,就需要通过 Join 将不同 table 中的数据合并来重建数据。
以课程开始时的 table 为例,通过将 Artist 与 Album 之间的多对多关系拆成 Artist, ArtistAlbum 以及 Album 三个 tables 来规范化数据,使得数据存储的冗余减少:
查询时我们就需要通过 Join 来重建 Artist 与 Album 的完整关系数据。
本课主要讨论 join 两个 tables 的过程。首先需要讨论的是:
逻辑上 Join 的操作的结果是:对任意一个 tuple r ∈ R 和任意一个在 Join Attributes 上对应的 tuple s ∈ S,将 r 和 s 串联成一个新的 tuple:
Join 操作的结果 tuple 中除了 Join Attributes 之外的信息与多个因素相关:
我们可以在 Join 的时候将所有非 Join Attributes 都放入新的 tuple 中,这样 Join 之后的操作都不需要从 tables 中重新获取数据:
也可以在 Join 的时候只复制 Join Attributes 以及 record id,后续操作自行根据 record id 去 tables 中获取相关数据。对于列存储数据库,这是比较理想的处理方式,被称为 Late Materialization。
由于数据库中的数据量通常较大,无法一次性载入内存,因此 Join Algorithm 的设计目的,在于减少磁盘 I/O,因此我们衡量 Join Algorithm 好坏的标准,就是 I/O 的数量。此外我们不需要考虑 Join 结果的大小,因为不论使用怎样的 Join Algorithm,结果集的大小都一样。
以下的讨论都建立在这样的情景上:
本节要介绍的 Join Algorithms 罗列如下:
不同的 Join Algorithms 有各自的适用场景,需要具体问题具体分析。
对 R 中的每个 tuple,都全表扫描一次 S,是一种暴力解法,它的成本为:
举例:
假设:
成本:
假设 0.1 ms/IO,则总时长约为 1.3 小时。
如果我们使用小表 S 作为 Outer Table,那么:
则总时长约为 1.1 小时。
总结: 这个算法很差,因为对于R关系中每个元组,都需要把S关系遍历一遍。
每次取 R 中一个 block 的所有 tuples 出来,让它们同时与 S 中的所有 tuples Join 一次,它的成本为:
举例:
假设:
成本:
使用大表 M 作为 Outer Table,成本为:
总共用时约 50 秒。
使用小表 S 作为 Outer Table,成本为:
以上的计算都假设 DBMS 只为 Nested Loop Join Algorithm 分配 3 块 buffers,其中 2 块用于读入,1 块用于写出;若 DBMS 能为算法分配 B 块 buffers,则可以使用 B-2 块来读入 Outer Table,1 块用于读入 Inner Table,1 块用于写出,此时,成本为
如果 Outer Table 能够直接放入内存中,则成本为 M + N。
总结: 这种算法会有较少的磁盘IO产生,因为对于关系R中每个block,会把关系S扫描一遍,同时在这种算法下,小表应该作为outer table,因为小表有更少的pages。
之前的两种 Nested Loop Join 速度慢的原因在于,需要对 Inner Table 作多次全表扫描,若 Inner Table 在 Join Attributes 上有索引或者临时建一个索引 (只需全表扫描一次):
此时 Join 的成本为:
其中 C 为 index probe 的成本。
从上面的讨论中,我们可以导出以下几个结论:
Sort-Merge Join 顾名思义,分为两个阶段:
算法如下:
Sort Merge的成本分析如下:
举例:
假设:
成本:
Sort-Merge Join 的最坏情况就是当两个 tables 中的所有 Join Keys 都只有一个值,这时候 Join 的成本变为:
Sort-Merge Join 适用于:
核心思想:
本算法分为两个阶段:
这里明确 T 的定义:
但 Basic Hash Join Algorithm 有一个弱点,就是有可能 T 无法被放进内存中,由于 hash table 的查询一般都是随机查询,因此在 Probe 阶段,T 可能在 memory 与 disk 中来回移动。
当两个 table 都无法放入 memory 时,我们可以:
如果每个 partition 仍然无法放入内存中,则可以递归地使用不同的 hash function 进行 partition,即 recursive partitioning:
probe: 探测
成本分析:
假设我们有足够的 buffers 能够存下中间结果:
举例:
假设:
计算:
如果 DBMS 已经知道 tables 大小,则可以使用 static hash table,否则需要使用 dynamic hash table
Algorithm | IO Cost | Example |
---|---|---|
Simple Nested Loop Join | M+(m×N) | 1.3 hours |
Block Nested Loop Join | M+(M×N) | 50 secs |
Index Nested Loop Join | M+(m×C) | 20 secs |
Sort-Merge Join | M+N+(sort cost) | 0.59 secs |
Hash Join | 3(M+N) | 0.45 secs |
Hash Join 在绝大多数场景下是最优选择,但当查询包含 ORDER BY 或者数据极其不均匀的情况下,Sort-Merge Join 会是更好的选择,DBMSs 在执行查询时,可能使用其中的一种到两种方法。