PostgreSQL Merge Join 大白话详解

用生活中最直观的例子,彻底搞懂 Merge Join 是什么、为什么快、什么时候用。


一、先从生活场景开始

场景一:两摞乱序试卷找同学

期末考试,老师手里有两摞试卷:

  • A 摞 :数学试卷,500 份,乱序堆放
  • B 摞 :语文试卷,500 份,乱序堆放

现在要找出同一个同学的两份试卷配成一对。

笨办法(Nested Loop):

复制代码
拿起数学试卷第1份(张三)→ 翻遍500份语文找张三 → 找到!
拿起数学试卷第2份(李四)→ 翻遍500份语文找李四 → 找到!
...
总共翻了:500 × 500 = 25万次

聪明办法:先排序,再合并(Merge Join):

复制代码
第一步:把两摞试卷都按学号排好序(001~500)
第二步:左手拿A摞,右手拿B摞,同时从第一份开始翻:

  左手=001号, 右手=001号 → 匹配!配成一对,两手同时往后翻一张
  左手=002号, 右手=003号 → 左边小,只翻左手
  左手=003号, 右手=003号 → 匹配!两手同时往后翻一张
  左手=004号, 右手=005号 → 左边小,只翻左手
  ...
  
总共翻了:500 + 500 = 1000次

结论:排完序之后,各翻一遍就结束,两摞试卷绝不回头。


二、Merge Join 核心原理

双指针扫描

Merge Join 本质是对两个有序序列用双指针做合并,规则非常简单:

复制代码
指针A → [1, 3, 5, 7, 9]
指针B → [2, 3, 6, 7, 8]

规则:
  A == B  → 匹配,两指针同时右移
  A  < B  → A指针右移
  A  > B  → B指针右移
  任意一方到头 → 结束

逐步演示:

复制代码
步骤1:A=1, B=2  → 1<2,A右移
步骤2:A=3, B=2  → 3>2,B右移
步骤3:A=3, B=3  → 匹配!✅ 双方右移
步骤4:A=5, B=6  → 5<6,A右移
步骤5:A=7, B=6  → 7>6,B右移
步骤6:A=7, B=7  → 匹配!✅ 双方右移
步骤7:A=9, B=8  → 9>8,B右移
步骤8:A=9, B=结束 → 扫描结束

总步骤数:8次(远小于 5×5=25次)

两个序列各走一遍,绝不回头,这就是 O(N+M) 的来源。


三、时间复杂度分析

3.1 有索引时:O(N + M)

B-Tree 索引本身就是按 Key 排好序的有序结构

复制代码
索引结构(简化示意):
  key=001 → 行指针
  key=002 → 行指针
  key=003 → 行指针
  ...(天然有序!)

PostgreSQL 直接沿着两个索引做双指针扫描,无需临时排序,直接 O(N+M)。

3.2 无索引时:O(N·logN + M·logM)

没有索引,PostgreSQL 必须先把数据排序:

复制代码
第一步:对 N 行数据排序  → O(N·logN)
第二步:对 M 行数据排序  → O(M·logM)
第三步:双指针合并       → O(N+M)

总计:O(N·logN + M·logM)

举例:N = M = 500万

复制代码
有索引:500万 + 500万 = 1000万次操作
无索引:500万×23 + 500万×23 + 1000万 ≈ 2.4亿次操作

没有索引时代价远超 Hash Join,规划器通常不会选 Merge Join。


四、Merge Join vs Hash Join:选哪个?

这是实际工作中最常见的疑问,一张表说清楚:

对比项 Hash Join Merge Join
需要排序 ❌ 不需要 ✅ 必须有序(靠索引)
额外内存 需要把小表装进 HashMap 几乎为零(只有两个指针)
有索引时 不一定用索引 直接复用索引,O(N+M)
无索引时 O(N+M),照样快 需先排序,代价大
内存不够时 会写临时磁盘,变慢 不受内存限制
规划器偏好 通用首选 有合适索引时才考虑

一句话记忆

复制代码
没有索引  → Hash Join(建字典,O(N+M))
有排序索引 → Merge Join(双指针,O(N+M),且不占内存)
内存紧张  → 优先 Merge Join(不需要建 HashMap)

五、用 EXPLAIN 看规划器的选择

sql 复制代码
EXPLAIN ANALYZE
SELECT a.orde_id
FROM orders a
JOIN orders b
  ON a.orde_id = b.orde_id
 AND b.version_number = '20260522000707011'
WHERE a.version_number = '20260521172049432';

执行计划输出示例:

复制代码
-- 没有索引时,规划器选 Hash Join:
Hash Join  (cost=...)
  Hash Cond: (a.orde_id = b.orde_id)
  -> Seq Scan on orders a  (...)   ← 全表扫描
  -> Hash
       -> Seq Scan on orders b  (...)   ← 建 HashMap

-- 有 (version_number, orde_id) 联合索引时,规划器可能选 Merge Join:
Merge Join  (cost=...)
  Merge Cond: (a.orde_id = b.orde_id)
  -> Index Scan using idx_a on orders a  (...)   ← 走索引,已有序
  -> Index Scan using idx_b on orders b  (...)   ← 走索引,已有序

看到 Merge Join + Index Scan 的组合,说明规划器充分利用了索引的有序性。


六、什么情况下 Merge Join 会"失效"

6.1 Join Key 数据类型不一致

sql 复制代码
-- a.id 是 INT,b.id 是 VARCHAR
ON a.id = b.id   -- 需要隐式类型转换,破坏有序性,无法 Merge Join

6.2 索引列被函数包裹

sql 复制代码
ON LOWER(a.name) = LOWER(b.name)   -- 走不了索引,无法 Merge Join

6.3 非等值 Join

sql 复制代码
ON a.price > b.price   -- Merge Join 只支持等值连接(=)

6.4 数据分布极度不均匀

某个 Key 值有几百万重复值(低基数),双指针在这个值上会反复扫描,退化成 O(N×M)。


七、总结

Merge Join 的本质

复制代码
两个有序序列 + 双指针 = 各走一遍,O(N+M)

什么时候 Merge Join 最好用

复制代码
✅ Join Key 上双方都有 B-Tree 索引
✅ 内存资源紧张,不想建 HashMap
✅ 大范围顺序扫描场景

什么时候不要指望 Merge Join

复制代码
❌ 没有索引(需临时排序,代价大)
❌ 非等值 Join
❌ Join Key 被函数处理过
❌ 数据低基数(大量重复值)

三种策略终极对比

Nested Loop Hash Join Merge Join
比喻 两层 for 循环 建字典查字典 双指针合并有序列表
复杂度 O(N × M) O(N + M) O(N + M)
内存 极低
依赖索引 内表需要 不需要 必须
适合场景 外表极小 大表通用 大表+双方有索引

核心记忆:Merge Join = 先排好序,再双指针各走一遍。排序靠索引,索引有了就免费,索引没有就代价大。

相关推荐
auspicious航3 小时前
PostgreSQL逻辑复制全解析:从原理到跨区域实战
数据库·postgresql
暴躁小师兄数据学院4 小时前
【AI大数据工程师特训笔记】第03讲:运算符
数据库·postgresql
安当加密6 小时前
TDE透明加密性能实测:AES-NI加速能跑多快?MySQL/PostgreSQL/SQL Server三数据库对比
数据库·mysql·postgresql
倒流时光三十年7 小时前
PostgreSQL EXISTS vs IN 性能对比详解
postgresql·in·exists
king_harry7 小时前
postgresql oracle_fdw访问oracle数据
postgresql·oracle_fdw
auspicious航7 小时前
PostgreSQL性能优化实战:从查询慢如蜗牛到飞一般的体验
数据库·postgresql·性能优化
这个DBA有点耶20 小时前
DBA的AI助手:向量检索与NL2SQL入门
数据库·人工智能·postgresql·学习方法·dba
新时代农民工~1 天前
PostgreSQL 主从故障恢复自动化:实战脚本与最佳实践
数据库·postgresql·自动化
IvorySQL1 天前
PostgreSQL 18.4、17.10、16.14、15.18、14.23 版本正式发布
数据库·postgresql·区块链