查询改写是什么
查询改写是在语义完全等价的前提下换一种写法,让优化器能利用索引或减少扫描量。常被用到的几类:把 OR 条件改写成 UNION ALL 以便各自命中索引、把超长 IN 列表改成 JOIN 临时表、把深分页改成基于游标的翻页、把精确 COUNT 换成估算值。它的纪律是改写前后都用 EXPLAIN 与结果集对照,凭感觉重写是事故来源。
基本信息
| 目标 | 语义等价前提下减少扫描行数或提升索引利用率 |
|---|---|
| 常见手法 | OR 改写、IN 列表转 JOIN、深分页优化、COUNT 估算、子查询转连接 |
| 验证方式 | 改写前后 EXPLAIN 对照,并比对结果集与行数 |
| 风险点 | UNION 与 UNION ALL 去重语义不同,NULL 与排序语义需逐项确认 |
| 适用前提 | 列上有可用索引,改写才有意义 |
典型使用场景
- 宽范围 OR 查询:两个不同列上的 OR 无法同时用索引,拆成两条再合并
- 超长 IN 列表:几千个 ID 拼进 IN 会让解析与优化都很慢,改成 JOIN 临时表
- 深分页:LIMIT 1000000, 20 需要扫描并丢弃大量行,改成基于上次最大主键的游标翻页
- 大表计数:精确 COUNT 代价高且常常没人看,监控口径改用估算值
- 子查询转连接:相关子查询在部分版本上执行效率差,改写成 JOIN 更可控
OR 改写与深分页
OR 改写与深分页
-- 两个不同列上的 OR 往往退化成全表扫描
SELECT id FROM appdb.orders WHERE user_id = 42 OR sku = 'SKU-1';
-- 改写为两条各自走索引的查询再合并去重
SELECT id FROM appdb.orders WHERE user_id = 42
UNION
SELECT id FROM appdb.orders WHERE sku = 'SKU-1';
-- 深分页:偏移量大时扫描并丢弃大量行
SELECT id, created_at FROM appdb.orders ORDER BY id LIMIT 1000000, 20;
-- 改写为游标翻页,只扫需要的那一段
SELECT id, created_at FROM appdb.orders WHERE id > 1234567 ORDER BY id LIMIT 20;
-- 大表计数先用估算值,避免长时间扫描
SELECT table_rows FROM information_schema.tables
WHERE table_schema = 'appdb' AND table_name = 'orders';改写手法与注意
| 手法 | 收益 | 必须核对 |
|---|---|---|
| OR 拆成 UNION | 各分支都能命中索引 | UNION 去重与 UNION ALL 语义差别 |
| 超长 IN 改 JOIN 临时表 | 减少解析与优化开销 | 临时表需要索引,否则连接本身变慢 |
| 深分页改游标 | 避免丢弃大量行 | 业务上能否接受恒定排序键与不支持跳页 |
| 精确 COUNT 改估算 | 避免大表长时间扫描 | 对账与决策场景不能用估算值 |
| 子查询改连接 | 执行计划更稳定 | 结果集行数是否因一对多而放大 |
改写的心跳检查:把改写前后的结果集分别导出,做行数与关键字段比对(包括 NULL 处理与去重),确认完全一致后再上生产。
深分页还有一种常见做法是延迟关联:先用覆盖索引取出主键,再按主键回表取完整行,适合必须跳页但仍想减少回表次数的场景。