背景 :
在不允许修改 索引结构 和 字段类型 的前提下,优化一个千万级数据表的查询性能。
一、问题背景
某业务表按 城市 + 小区 + 月份 维度存储统计数据,字段 data_time 用 varchar 存储月份(如 2025-01)。
查询目标
查询 2025 年某小区的月度数据。
二、精简后的表结构(仅保留相关字段)
sql
CREATE TABLE t_zhuge_project (
id INT PRIMARY KEY AUTO_INCREMENT,
data_time VARCHAR(12), -- 月份,如 2025-01
city_name VARCHAR(100),
district_name VARCHAR(64),
project_name VARCHAR(64),
date_level VARCHAR(64), -- month / year
KEY index_sort (data_time, city_name),
KEY index_mom_near (data_time, city_name, district_name, project_name, date_level),
KEY index_data_time (data_time(7))
);
三、最初的 SQL:范围查询(未走索引)
sql
SELECT *
FROM t_zhuge_project
WHERE data_time >= '2025-01'
AND data_time <= '2025-12'
AND city_name = '上海'
AND project_name = '露香苑'
AND date_level = 'month';
执行计划关键结果
type: ALL
rows: 12224888
Extra: Using where
问题现象
- 全表扫描
- 扫描行数 1200 万+
- 查询性能极差
四、为什么 varchar 的 BETWEEN 会失效?
核心原因
data_time是 varchar 类型 ,BETWEEN变成了字符串范围比较
经过编码之后,MySQL 无法保证:'2025-01' ~ '2025-12' 在字符串排序规则下等价于"连续时间区间"。
优化器判断
👉 使用索引收益不确定,全表扫描更稳定
五、优化手段:用 IN 替代 BETWEEN
在 不能改字段类型、不改索引 的前提下,将范围查询改为 等值枚举:
sql
SELECT *
FROM t_zhuge_project
WHERE data_time IN (
'2025-01','2025-02','2025-03','2025-04','2025-05','2025-06',
'2025-07','2025-08','2025-09','2025-10','2025-11','2025-12'
)
AND city_name = '上海'
AND project_name = '露香苑'
AND date_level = 'month';
六、优化后的执行计划
type: range
key: index_sort
rows: 342
Extra: Using index condition; Using where
优化效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 扫描行数 | 12,224,888 | 342 |
| 执行方式 | 全表扫描 | 索引扫描 |
| 性能 | 极慢 | 毫秒级 |
七、为什么走了索引,却没走 index_mom_near?
这是最容易被误解、但最关键的一点。
1️⃣ 联合索引的最左前缀原则被「中断」
index_mom_near 结构:
text
(data_time, city_name, district_name, project_name, date_level)
WHERE 条件中 缺少 district_name,即不满足最左匹配原则:
text
data_time ✅
city_name ✅
district_name ❌
project_name ❌(失效)
date_level ❌(失效)
👉 一旦中断,后续列全部无法使用
2️⃣ 实际可用列对比
| 索引 | 实际能用的列 |
|---|---|
| index_sort | data_time + city_name |
| index_mom_near | data_time + city_name |
3️⃣ MySQL 为什么选 index_sort?
原因很现实:
index_sort索引更短- B+Tree 层级更浅
- IO 成本更低
👉 在"可用列一样"的情况下,MySQL 一定选更瘦的索引
4️⃣ ICP(Index Condition Pushdown)加成
执行计划中出现:
Using index condition
说明:
- MySQL 在索引层就做了条件过滤
- 进一步降低了回表成本
index_sort的性价比更高
八、在不能改索引的情况下,可选方案总结
✅ 方案 1:IN + 索引收敛(已采用)
- 强烈推荐
- 可控、可解释、无副作用
🧠 方案 2:子查询先用索引收敛,再精确过滤
sql
SELECT *
FROM t_zhuge_project t
JOIN (
SELECT id
FROM t_zhuge_project
WHERE data_time IN (...)
AND city_name = '上海'
) s ON t.id = s.id
WHERE t.project_name = '露香苑'
AND t.date_level = 'month';
九、总结(可以直接背下来)
MySQL 选索引,不看"业务合理性",只看"IO 成本"
- varchar 时间字段:
IN>BETWEEN- 联合索引:中间断列 = 后面全废
- 前导列一致时:索引越短越容易被选中