子查询结果集过大导致OOM或卡顿,根本原因是数据库默认物化整个子结果到内存;应改用JOIN+覆盖索引、流式读取、游标分页等方案优化。

子查询结果集过大时,数据库不是卡住就是OOM,根本原因不是SQL写错了,而是默认执行策略把整个子结果物化进内存——必须绕过它。
MySQL里IN (SELECT ...)超10万行就崩,改用JOIN + 覆盖索引
MySQL 5.7/8.0对IN (SELECT ...)的物化行为极不友好:子查询哪怕只返回user_id,也会建临时表、排序、去重,内存直接飙高。更糟的是,如果主表也大,join_buffer_size不够就会落盘,查着查着被KILL。
- 把
WHERE id IN (SELECT id FROM orders WHERE status = 'paid')改成JOIN orders o ON t.id = o.user_id AND o.status = 'paid' -
orders表上必须有(status, user_id)联合索引,否则JOIN照样全表扫 - 别在
JOIN条件里用函数,比如DATE(o.created_at) = '2024-01-01'会让索引失效,换成o.created_at >= '2024-01-01' AND o.created_at
PostgreSQL中EXISTS比IN省内存,但得带关联条件+索引
EXISTS确实不物化子结果,但它不是自动优化的——如果子查询没写对,照样退化成嵌套循环扫描。关键看能不能“尽早退出”。
- 写成
EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.created_at > '2024-01-01'),u.id必须是users主键,否则得单独给u.code建索引 - 禁止写
EXISTS (SELECT 1 FROM orders)这种无关联子查询,它等价于SELECT 1恒真,数据库会当半连接处理,反而物化 - 子查询返回超10万行时,PG 12+虽默认哈希物化,但一旦超出
work_mem限制,仍回退为嵌套循环——此时加LIMIT 50000或拆成created_at BETWEEN分段更稳
JDBC流式读取必须显式设fetchSize,且ResultSet不能漏关
Java里executeQuery()默认把整张结果集缓存在JVM堆里,哪怕你只调一次next()。这不是慢,是必然OOM。
- URL加参数:
jdbc:postgresql://host/db?defaultFetchSize=1000(PG)或jdbc:mysql://host/db?useCursorFetch=true&defaultFetchSize=1000(MySQL) - 代码里必须
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { ... },漏关任意一层都会导致连接和结果集长期驻留 - 别在循环里反复
executeQuery()——每次新建Statement却不关旧的,泄漏比数据还快
真要分块处理,别信OFFSET,用游标分页+ID范围拉取
LIMIT 1000 OFFSET 999000这种写法,数据库得先扫完前999000行再丢弃,I/O和CPU呈指数增长。后台导出百万行报表时,第999页常从200ms涨到8s+。
- 应用层先
SELECT id FROM large_table WHERE created_at > ? ORDER BY id LIMIT 1000,拿到一批ID后,再WHERE id IN (…)或JOIN查详情 - 复合条件分页更可靠:
WHERE created_at > ? OR (created_at = ? AND id > ?),避免漏/重数据 - 注意时序一致性:若数据实时写入,两次分块之间可能有新记录插入,需加时间戳锁或业务幂等校验
最易被忽略的一点:子查询里出现SELECT *或TEXT/JSON字段,不只是拖慢网络,还会让数据库执行计划误判成本,主动放弃索引走全表扫描——查100万行和查10行,代价可能差几百倍。

















