优先用主键分批查+精准in替代单次大join,因数据库join默认全量载入右表内存匹配,几千万行必溢出,调work_mem或join_buffer_size仅延缓崩溃且易拖垮并发。

直接结论:别调 work_mem 或 join_buffer_size,优先用主键分批查 + 精准 IN 替代单次大 JOIN。因为数据库执行 JOIN 时默认把右表全量载入内存匹配,几千万行就必然溢出,调参数只是延缓崩溃,还容易拖垮并发。
为什么调内存参数基本没用
数据库不是“内存小才慢”,而是算法决定它必须加载多少数据。比如 PostgreSQL 默认选 HASH JOIN,就得把整个右表建哈希表;MySQL 在无索引时启用 Block Nested-Loop Join,靠 join_buffer_size 缓冲,但该缓冲只对无索引场景生效。如果 EXPLAIN 里没出现 Using join buffer 或 Hash 行带 disk,说明问题不在缓冲大小,而在索引缺失或驱动表选错。
-
work_mem设太高会直接拖垮并发:100 个连接 × 256MB = 25GB,机器可能开始swap - MySQL 8.0.22+ 默认启用
hash_join,此时join_buffer_size完全不参与,调了也没用 - SQL Server 的
HASH JOIN内存估算依赖统计信息,过期统计会导致优化器误判,调内存也救不了
怎么安全地分批替代 JOIN
核心是把“一次全量匹配”拆成“多次小范围拉取”,但必须满足前提、控制边界、避免新瓶颈。
- 左表要有单调主键(如
id BIGINT PRIMARY KEY AUTO_INCREMENT)或时间字段(如created_at),否则无法安全分段 - 每批
IN列表长度建议 ≤ 1000 —— MySQL 默认max_allowed_packet=4MB,2000 个INT就超限;PostgreSQL 对VALUES构造也有解析开销 - 禁止应用层字符串拼接长
IN:Python 应用用executemany(),Java 用PreparedStatement批量绑定 - PostgreSQL 推荐用
VALUES子句代替裸IN:WHERE o.user_id IN (SELECT id FROM (VALUES (1),(2),...,(1000)) AS v(id)) - MySQL 8.0+ 可建临时表:
CREATE TEMPORARY TABLE tmp_ids(id BIGINT); INSERT INTO tmp_ids VALUES (); SELECT ... JOIN tmp_ids ON ...
分批后必须验证的三件事
能跑通不代表性能好。真正卡住的往往是隐性扫描——你以为走了索引,其实没生效。
- 检查
EXPLAIN中type是否为ref或eq_ref(MySQL)、Index Scan(PostgreSQL),而不是ALL或Seq Scan -
JOIN字段类型必须严格一致:INT对BIGINT、VARCHAR(50)对VARCHAR(100)都会触发隐式转换,索引失效 - 复合索引顺序要匹配
JOIN条件:若写JOIN t1 ON t1.a = t2.a AND t1.b = t2.b,t1上索引必须是(a,b),不是(b,a)
最常被忽略的一点:分批逻辑本身依赖左表主键/时间字段的单调性,一旦有删除、跳号、分布式 ID 冲突,BETWEEN 或游标式分页就会漏数据。上线前必须用真实数据压测覆盖边界场景。










