子查询在千万级数据上可用但需正确编写:exists必须相关联、in列表应转临时表、分页子查询须索引支持,否则易导致全表扫描。

子查询在千万级数据上不是不能用,而是很容易写成“伪优化”——表面用了子查询,实际执行计划仍是全表扫描或嵌套循环。关键不在“用不用”,而在“怎么写”和“用在哪”。
EXISTS 子查询必须带相关条件,否则等于全表扫
很多人把 EXISTS 当作 IN 的替代品,却漏掉最核心的一点:子查询里必须引用外层表字段,形成相关子查询(correlated subquery)。否则数据库会把它当常量判断处理。
- 错误写法:
SELECT * FROM orders WHERE EXISTS (SELECT 1 FROM tmp_ids)→ 优化器直接判定为“只要 tmp_ids 有数据就全返回”,orders 表全扫 - 正确写法:
SELECT * FROM orders o WHERE EXISTS (SELECT 1 FROM tmp_ids t WHERE t.id = o.user_id)→ 每行 orders 都只查一次索引匹配,命中即停 - PostgreSQL/MySQL 都要求
t.id和o.user_id类型严格一致,否则隐式转换会让t.id索引失效
IN 子句超几百值就危险,别硬拼长列表
动态拼接 IN (1,2,3,...,9999) 看似简单,但 MySQL 5.7+ 受 max_allowed_packet 限制,PostgreSQL 则因成本估算失准易选错执行计划。你看到的“卡住”,大概率是优化器放弃走索引了。
- MySQL 中,IN 列表超过约 500 项后,
EXPLAIN常显示type: ALL(全表扫描) - PostgreSQL 中,
IN转成多个OR后,planner 可能误判选择性,跳过可用索引 - 替代方案不是“换写法”,而是“换载体”:把长列表落地为临时表 + 索引,再用
JOIN或EXISTS
子查询做分页时,必须只查主键且 ORDER BY 字段要有索引
用 SELECT * FROM t WHERE id IN (SELECT id FROM t ORDER BY created_at DESC LIMIT 1000000, 20) 是典型误区——子查询本身没索引支持排序,就会触发 Using filesort,比原查询还慢。
- 必须确保
ORDER BY created_at字段上有索引;若存在大量重复值,建议建复合索引如(created_at, id)避免排序不确定性 - 子查询里只能
SELECT id,不能带其他字段或WHERE过滤(除非该条件能走索引且不破坏 ID 序列连续性) - 更稳写法是延迟关联:
SELECT t.* FROM t INNER JOIN (SELECT id FROM t WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20) tmp ON t.id = tmp.id
真正卡住千万级子查询的,往往不是语法本身,而是索引缺失、类型不匹配、或子查询脱离了相关上下文。每次写完,务必用 EXPLAIN 看一眼 type 和 key 列——如果没出现 range 或 ref,或者 key 是 NULL,那这个子查询就已经失效了。










