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

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

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

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