• CMU15-445 数据库系统播客:数据库系统 Join 算法与原理

CMU15-445 数据库系统播客:数据库系统 Join 算法与原理

2025-08-16 12:32:09 栏目:宝塔面板 43 阅读

在关系型数据库系统中, 连接(Join)操作是核心且至关重要的一环 。数据库通过 规范化(Normalization) 将数据拆分到不同表中以减少信息冗余。然而,当我们需要获取跨多个表的综合信息时,就必须使用 Join 操作,将被拆分的表重新组合,以 重建数据间的逻辑关系,确保信息完整 。 

连接操作的核心概念

在深入探讨具体算法之前,我们首先需要理解评估和执行连接操作的一些基本原则。

连接的评估基准:I/O 成本

一个查询在执行时会被数据库的查询优化器转换成一个“查询计划树”。这个树的叶子节点代表对数据表的访问,而中间节点则是各种操作符,Join 就是其中最重要的一种。

评估 Join 算法优劣的主要标准是其磁盘 I/O 成本 。因为磁盘的读写速度远慢于内存,I/O 操作往往是数据库性能的最大瓶颈。为了量化分析,我们通常设定如下变量:

  • 表 R:占用 M 个磁盘页(Page),包含 m 条元组(Tuple)。
  • 表 S:占用 N 个磁盘页,包含 n 条元组。

在后续的成本分析中,我们主要关注 Join 操作本身的 I/O 开销,而不计算最终结果输出的开销,因为后者对于所有算法来说都是相同的。

外层表与内层表的选择:为何“小表驱动大表”?

在执行 Join 时,查询计划会指定一个 外层表(Outer Table,或称驱动表) 和一个 内层表(Inner Table,或称被驱动表) 。在查询计划树中,左侧输入通常是外层表,右侧是内层表。

一个核心的优化原则是: 尽可能选择小表作为外层表 。这里的“小”通常指占用磁盘页(Blocks/Pages)较少,或者在某些算法中指元组数量(Rows)较少。

为什么这个选择如此重要?我们可以通过一些基础的 Join 算法 I/O 公式来直观理解:

对于块嵌套循环连接(Block Nested Loop Join) ,其 I/O 成本公式为:外层表 I/O + (外层表块数 × 内层表 I/O)

  • 大表 R (1000页) 作外层,小表 S (500页) 作内层 :成本为 M + (M × N) = 1000 + (1000 × 500) = 501,000 次 I/O。
  • 小表 S (500页) 作外层,大表 R (1000页) 作内层 :成本为 N + (N × M) = 500 + (500 × 1000) = 500,500 次 I/O。 在此算法中,总 I/O 差别不大,但外层表越小,外层循环次数越少。

对于索引嵌套循环连接(Index Nested Loop Join) ,其成本与外层表的 元组数量 密切相关。其 I/O 成本公式为:外层表 I/O + (外层表元组数 × 内层表索引查找成本)

  • 大表 R (m=100,000) 作外层 :成本为 M + (m × C) = 1000 + (100,000 × C)
  • 小表 S (n=40,000) 作外层 :成本为 N + (n × C) = 500 + (40,000 × C)

其中 C 是单次索引查找的成本。显然,外层表的元组数越少,对内层表的索引“探测”次数就越少,总 I/O 成本也显著降低。

结论 :外层表扮演着“驱动”的角色,它的每一行(或每一块)都会触发一次对内层表的操作。选择小表作为外层表,可以有效减少驱动次数,从而大幅降低对内层表的扫描或查找总成本,这是 Join 优化的一个基础出发点。

连接的输出方式:物化 vs. 记录 ID

连接操作符找到匹配的元组后,如何将结果传递给上层操作符?主要有两种方式:

数据物化 (Materialization) :将匹配元组中所有需要的属性值复制出来,形成一个全新的输出元组。

  • 优点 :后续操作符无需再访问原始表,因为所有信息都已备齐。
  • 缺点 :如果涉及的属性很多,生成的元组会很“宽”,占用大量内存且复制成本高。

延迟物化 (Late Materialization) :只输出连接键和匹配元组的记录 ID(Tuple ID)。

  • 优点 :输出的中间结果非常小,节省内存。这在 列式存储 数据库中尤其高效,因为它避免了提前读取查询最终不需要的列。
  • 缺点 :当上层操作符需要其他属性时,必须根据记录 ID 回溯到原始数据页去获取,这可能引入额外的随机 I/O。如果连接的选择性很低(即匹配的元组很多),回溯成本可能会非常高昂。

三大主流连接算法

数据库系统主要实现了三类连接算法:嵌套循环连接、排序合并连接和哈希连接。

嵌套循环连接 (Nested Loop Join)

这是最基础、最直观的连接算法,其思想类似于编程中的嵌套 for 循环。

简单嵌套循环 (Simple/Stupid Nested Loop Join)

对于外层表的 每一条元组 ,遍历内层表的 所有元组 进行比较。这种方式性能极差,因为会导致对内层表的重复全盘扫描。

块嵌套循环 (Block Nested Loop Join)

这是对简单版本的巨大改进。算法按 块(Page/Block) 进行循环,而不是按元组。对于外层表的 每一块 ,遍历内层表的 所有块 。

索引嵌套循环 (Index Nested Loop Join)

如果内层表的连接键上存在 索引 ,系统就可以利用该索引来避免全表扫描。对于外层表的每一条元组,使用其连接键值去 探测 (Probe) 内层表的索引,快速定位匹配的元组。

  • 适用场景 : 在 OLTP (联机事务处理) 场景中极为常见,因为外键列上通常建有索引。
  • 动态索引 : 即使预先没有索引,如果优化器判断创建临时索引的成本低于全表扫描的成本,它甚至可以在查询执行期间 动态创建索引 ,查询结束后再丢弃。

排序合并连接 (Sort-Merge Join)

此算法通过预先排序来优化连接过程,分为两个阶段:

  1. 排序阶段 (Sort Phase) :如果数据尚未排序,使用外部归并排序(当数据大于内存时)或快速排序(当数据可载入内存时)分别对两个表按连接键进行排序。
  2. 合并阶段 (Merge Phase) :使用两个指针(游标)同步地扫描两个已排序的表。
  • 比较指针处的键值,移动键值较小的那个指针。
  • 如果键值相等,则找到一个匹配,输出结果。此时,需要特别处理 重复键 。例如,当内层表有多个与外层表当前元组匹配的键时,需要固定外层表指针,移动内层表指针直到不匹配。如果外层表下一条元组的键值仍然相同,内层表的指针需要 回溯 到重复键的起始位置,以确保所有组合都被找到。

优势场景 :

  • 数据已序 :当一个或两个表已经按连接键排序(例如,连接键上存在聚簇索引),可以免去排序成本。
  • 结果需排序 :当查询包含 ORDER BY 子句,且排序键恰好是连接键时,此算法可以直接产出有序结果,避免了额外的排序操作,实现“一举两得”。
  • 最坏情况 :如果所有元组的连接键都相同,合并阶段会退化成类似嵌套循环的行为,性能急剧下降。但这种情况在现实中极为罕见。

哈希连接 (Hash Join)

哈希连接是现代数据库系统中最快、最主流的连接算法,它利用哈希函数“分而治之”。如果两个元组的连接键相等,那么它们的哈希值也必然相等。因此,我们只需在哈希到同一位置的元组“桶”内进行比较即可。

基本哈希连接算法

  1. 构建阶段 (Build Phase) :选择 外层表(通常是小表) ,扫描它并使用哈希函数 h1 在内存中构建一个 哈希表 。哈希表的键是连接键的哈希值,值通常是元组本身或其引用。
  2. 探测阶段 (Probe Phase) :扫描 内层表 ,对每一条元组的连接键计算相同的哈希值,然后去哈希表中查找(探测)。如果找到,再比较原始连接键值以确认精确匹配(处理哈希冲突)。

优化:布隆过滤器 (Bloom Filter)

在构建哈希表的同时,可以额外构建一个 布隆过滤器 。这是一种高效的概率型数据结构(本质是一个位图),用于快速判断一个元素 是否可能 在一个集合中。

  • 特性 :它 绝不会漏报 (False Negative,如果它说“无”,就一定没有),但 可能误报 (False Positive,如果它说“有”,可能实际没有)。它体积小、速度快,可以完全放入 CPU 缓存。
  • 工作方式 :在探测内层表时,先用布隆过滤器检查。如果过滤器返回“无”,则该元组一定不匹配,可直接跳过,避免了访问内存或磁盘中的哈希表。这被称为 旁路信息传递 (Sideways Information Passing) 。

Grace 哈希连接 (处理大数据)

当外层表太大,无法在内存中为其完整构建一个哈希表时,基本哈希连接就会因频繁的磁盘交换而失效。 Grace 哈希连接 (或称分区哈希连接)解决了这个问题。

  1. 分区阶段 (Partition Phase) :使用一个哈希函数 h1,将 两个表 都散列成多个 分区(Partitions) ,并写入磁盘。h1 的选择要保证同一连接键的元组一定进入相同的分区对(例如,R表的分区 i 和 S表的分区 i)。
  2. 连接阶段 (Join Phase) :依次将每一对分区(如 R 的分区1 和 S 的分区1)加载到内存中。因为每个分区都足够小,可以为其构建内存哈希表,然后在该分区内部执行基本的哈希连接。
  • 递归分区 :如果分区后某个分区依然过大,无法载入内存,系统会对该分区 再次使用一个新的哈希函数 h2 进行递归分区 ,直到所有子分区都足够小。此方法巧妙地将随机 I/O 转换为了高效的顺序 I/O。

算法选择与总结

现代数据库的查询优化器内置了 成本估算模型 ,它会综合考虑表大小、数据分布特征、内存可用性、索引情况和查询需求(如是否需要排序)等因素,为每个查询动态选择最优的 Join 算法。

下表总结了我们示例中不同算法的性能对比:

算法类型

理论 I/O 成本公式

示例 I/O 次数

示例耗时 (0.1ms/IO)

简单嵌套循环

~50,000,000

~1.4 小时

块嵌套循环

501,000

~50 秒

索引嵌套循环

201,000

~20 秒

排序合并连接

7,500

0.75 秒

Grace 哈希连接

4,500

0.45 秒

(注:示例基于 R 表 M=1000页, m=100k元组; S 表 N=500页, n=40k元组;索引查找成本 C=2 IO)

核心结论

  • 在大多数情况下, 哈希连接是性能最佳的选择 ,尤其对于大规模数据的等值连接。
  • 排序合并连接 在特定场景下有奇效:当数据 已排序 或查询结果 需要按连接键排序 时,它可能是最优选。此外,对于 不等值连接 (如 > 或 <)和范围连接,排序合并连接通常优于哈希连接。
  • 索引嵌套循环连接 是 OLTP 轻量级查询的首选,特别是当外层表结果集很小且内层表有可用索引时。

一个优秀的数据库系统会智能地运用这些算法。这正是 SQL 这种声明式语言的强大之处:用户只需描述“需要什么”,而无需关心“如何获取”,底层的数据库系统会负责选择最高效的路径来执行查询。这些经典的算法思想不仅在单机数据库中至关重要,在分布式数据库中也同样适用,只是将磁盘 I/O 成本替换为了开销更高的网络 I/O 成本。

本文地址:https://www.yitenyun.com/313.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL Web 应用 异步数据库 数据同步 ACK 双主架构 循环复制 TIME_WAIT 运维 负载均衡 JumpServer SSL 堡垒机 跳板机 HTTPS JumpServer安装 堡垒机安装 Linux安装JumpServer Deepseek 宝塔面板 Linux宝塔 Docker 生命周期 esxi esxi6 root密码不对 无法登录 web无法登录 HexHub 序列 核心机制 Windows Windows server net3.5 .NET 安装出错 服务器 管理口 HTTPS加密 宝塔面板打不开 宝塔面板无法访问 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 Windows宝塔 Mysql重置密码 Oracle 处理机制 InnoDB 数据库锁 无法访问宝塔面板 开源 PostgreSQL 存储引擎 监控 连接控制 机制 服务器性能 Serverless 无服务器 语言 ES 协同 技术 Spring Redis 异步化 缓存方案 缓存架构 缓存穿透 分页查询 索引 group by 高可用 Undo Log GreatSQL 连接数 SQL 动态查询 机器学习 日志文件 MIXED 3 响应模型 优化 万能公式 SVM Embedding R2DBC R edis 线程 数据 主库 工具 自定义序列化 云原生 RocketMQ 长轮询 配置 Postgres OTel Iceberg Netstat Linux 服务器 端口 Linux 安全 ​Redis 推荐模型 SQLark scp Linux的scp怎么用 scp上传 scp下载 scp命令 AI 助手 共享锁 PG DBA openHalo SQLite-Web SQLite 数据库管理工具 向量数据库 大模型 存储 Recursive Hash 字段 电商 系统 OB 单机版 查询 Ftp 架构 • 索引 • 数据库 锁机制 流量 Rsync 修改DNS Centos7如何修改DNS 向量库 Milvus 人工智能 推荐系统 redo log 重做日志 数据分类 加密 磁盘架构 MySQL 9.3 聚簇 非聚簇 sftp 服务器 参数 线上 库存 预扣 场景 同城 双活 信息化 智能运维 防火墙 黑客 业务 Python 高效统计 今天这篇文章就跟大家 Doris SeaTunnel MVCC 数据备份 传统数据库 向量化 不宕机 分库 分表 Redisson 锁芯 mini-redis INCR指令 INSERT COMPACT 缓存 窗口 函数 RDB AOF 网络架构 网络配置 prometheus Alert PostGIS 启动故障 事务 Java 开发 Canal Web 崖山 新版本 filelock MongoDB 数据结构 IT运维 B+Tree ID 字段 分布式 集中式 ZODB 核心架构 订阅机制 引擎 性能 数据脱敏 加密算法 Go 数据库迁移 容器 容器化 数据类型 虚拟服务器 虚拟机 内存 读写 网络故障 DBMS 管理系统 频繁 Codis 模型 JOIN Redis 8.0 OAuth2 Token 国产数据库 聚簇索引 非聚簇索引 QPS 高并发 微软 SQL Server AI功能 Pottery 原子性 工具链 发件箱模式 自动重启 部署 Entity SpringAI 分页方案 排版 Testcloud 云端自动化 速度 服务器中毒 事务隔离 Web 接口 排行榜 排序 SSH Caffeine CP 行业 趋势 数据页 数据集成工具 MCP 开放协议 悲观锁 乐观锁 StarRocks 数据仓库 Redka sqlmock LRU 大表 业务场景 分布式架构 分布式锁​ dbt 数据转换工具 1 分页 优化器 AIOPS 池化技术 连接池 单点故障 仪表盘 网络 Order EasyExcel MySQL8 意向锁 记录锁 InfluxDB 事务同步 日志 RAG HelixDB IT 字典 播客 对象 双引擎 订单 单线程 主从复制 代理 Crash 代码 编程 Ansible UUIDv7 主键 UUID ID 恢复数据 LLM 语句 Valkey Valkey8.0 ReadView Pump 产业链 兼容性 数据字典 线程安全 Weaviate MGR 分布式集群 失效 List 类型 解锁 调优 表空间 Next-Key 分布式锁 Zookeeper 慢SQL优化 关系数据库 GitHub Git 查询规划 矢量存储 数据库类型 AI代理 RR 互联网 国产 用户 算法 千万级 快照读 当前读 视图 神经系统 count(*) count(主键) 行数 CAS 技巧 拦截器 动态代理 多线程 并发控制 恢复机制 闪回