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 = 先排好序,再双指针各走一遍。排序靠索引,索引有了就免费,索引没有就代价大。

相关推荐
睡不醒男孩03082317 小时前
PostgreSQL 数据库运维转型:从传统模式到 CLup 平台的 25 个核心 FAQ
运维·数据库·postgresql
JOJO数据科学18 小时前
pgAdmin4 Electron 鸿蒙 PC 适配全记录:从白屏到连接 PostgreSQL
postgresql·electron·harmonyos
日取其半万世不竭19 小时前
PostgreSQL 跑在 Docker 里怎么备份?恢复成功才算备份成功
数据库·docker·postgresql
倒流时光三十年20 小时前
PostgreSQL LEAST 表达式函数详解
数据库·postgresql
Rain50921 小时前
2.4. PostgreSQL 数据库连接与实战指南
前端·数据库·人工智能·后端·postgresql·数据分析
倒流时光三十年2 天前
PostgreSQL CASE 条件表达式详解
数据库·postgresql
倒流时光三十年2 天前
PostgreSQL COALESCE 条件表达式函数详解
数据库·postgresql
雁無痕2 天前
Postgresql启动无监听端口问题的解决
postgresql
倒流时光三十年2 天前
PostgreSQL NULLIF 条件表达式函数详解
数据库·sql·postgresql
倒流时光三十年2 天前
PostgreSQL VALUES 列表详解
数据库·postgresql