IN 查询扫了 200 万行却只取 10 条——用「日期夹逼」零 DDL 提速 3750 倍
后台管理系统的单据明细页,点开任意一张单据,转圈超过 20 秒,偶尔直接超时。线上 MySQL RDS,表里 400 万行,逻辑上只查 10 条,怎么可能这么慢?
这是一类典型的「冷列 IN 查询」陷阱。EXPLAIN 一看,rows: 2001923——优化器老老实实扫了 200 万行,用时 26 秒。
现象
系统采用常见的两步查询模式:
- 第一步:分页聚合,按日期范围 + 单据类型筛选,返回 10 个单据编号;
- 第二步:用这 10 个编号做 IN 查询,拉取明细行。
第二步的 SQL 大致如下:
SELECT * FROM sales_records
WHERE del_flag = 0
AND doc_no IN ('D001', 'D002', ..., 'D010');EXPLAIN 显示:
type: ref key: idx_del_docdate_doctype rows: 2001923
Extra: Using index condition; Using where
rows 超过两百万,查了约半张表。
根因
这张表上只有一个复合索引 (del_flag, doc_date, doc_type),没有 doc_no 的单列索引。
MySQL 的 B-tree 复合索引有一条铁律:范围谓词之后的列无法继续走索引。这里 del_flag = 0 是等值匹配,本可以精确定位,但 del_flag=0 命中的行占全表 95%——相当于把索引的第一层几乎全部选中。之后的 doc_no IN (...) 完全没有索引支撑,只能在这 200 万行里逐行过滤。
WHERE del_flag=0 是低基数等值条件,del_flag=0 覆盖 95% 的行时,type=ref 实际上是扫描了整个软删分区。后续 AND doc_no IN (...) 列没有索引,MySQL 只能在这个巨大分区里逐行匹配,几百万行扫下去是必然的。
正解
关键洞察:第一步分页查询已经有 doc_date BETWEEN :start AND :end 约束,返回的 10 条单据的 doc_date 全部落在这个区间里。把这个区间的上下界也传给第二步,优化器就能把 del_flag=0 + doc_date BETWEEN 的组合走进索引的 range 扫描,把 200 万行窗口压缩到几百行。
实现步骤
第一步分页:顺手带出日期边界
SELECT doc_no, MAX(doc_date) AS doc_date
FROM sales_records
WHERE del_flag = 0
AND doc_date BETWEEN :startDate AND :endDate
AND doc_type IN ('NS', 'SR')
GROUP BY doc_no
ORDER BY MAX(doc_date) DESC
LIMIT 10 OFFSET 0;
-- 返回:[(D001, '2026-04-28'), (D002, '2026-04-27'), ...]应用层计算边界
$minDate = min(array_column($page1Rows, 'doc_date')); // '2026-04-20'
$maxDate = max(array_column($page1Rows, 'doc_date')); // '2026-04-28'第二步 IN 查询:加上日期夹逼
SELECT * FROM sales_records
WHERE del_flag = 0
AND doc_date BETWEEN :minDate AND :maxDate -- 夹逼关键行
AND doc_no IN (:d1, :d2, ..., :d10);修复后 EXPLAIN:
type: range key: idx_del_docdate_doctype rows: 912
Extra: Using index condition
扫描行数从 2,001,923 降到 912,实测耗时从 26.26 秒 降至 7 毫秒,提速约 3750 倍。DDL 成本:零。代码改动:6 行 PHP。
夹逼列必须满足三点:① 在复合索引里是连续前缀(等值键之后即可);② 随业务实体单调递增(用创建日期,避免 updated_at);③ 可以从第一步查询结果里直接 MIN/MAX 计算,无需额外 DB 往返。
排查清单
遇到「IN 查询只取少量行却很慢」时,依次确认:
EXPLAIN看rows是否远大于预期返回行数;key是否命中了一个低基数列(del_flag、status、tenant_id)打头的复合索引;SHOW INDEX FROM table确认 IN 列是否无单列索引;- 业务上是否有自然的时间锚点,且第一步分页已经按该时间过滤——若有,直接套本模式。
一句话外卖
两步查询里,第一步已经「知道」结果集的时间范围,第二步却假装不知道——把这份信息传递下去,就是零成本的索引替代。