今天在排查一个线上问题时,遇到一个典型的慢查询案例,让我重新审视了数据库索引的使用细节。这个看似简单的优化过程,实际上涉及了索引的底层原理和查询优化器的行为。
问题背景
我们的用户表 users 结构如下:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255),
status TINYINT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
线上突然出现了一个执行时间超过5秒的查询:
SELECT id, name, email
FROM users
WHERE status = 1
AND created_at >= '2023-01-01'
ORDER BY created_at DESC
LIMIT 20;
排查过程
1. 查看现有索引
首先检查了表的索引情况:
SHOW INDEX FROM users;
发现只有主键索引和几个单列索引:
PRIMARY(id)idx_status(status)idx_created_at(created_at)
2. 分析执行计划
使用 EXPLAIN 查看查询执行计划:
EXPLAIN SELECT id, name, email
FROM users
WHERE status = 1
AND created_at >= '2023-01-01'
ORDER BY created_at DESC
LIMIT 20;
执行计划显示:
- 使用了
idx_created_at索引 - 扫描了约50万行数据
- 使用了
Using where过滤
问题很明显:MySQL 选择了 created_at 索引,但需要回表检查每行数据的 status 字段,导致大量不必要的I/O操作。
解决方案
方案1:创建复合索引
基于查询模式,创建了复合索引:
CREATE INDEX idx_status_created_at ON users(status, created_at);
方案2:考虑索引顺序
这里有个关键点:索引列的顺序很重要。我考虑了两种顺序:
(status, created_at)(created_at, status)
经过分析,选择 (status, created_at) 的原因:
status的基数较低(只有几个状态值)created_at用于排序和范围查询- 查询中
status = 1是等值条件,适合放在前面
优化效果
创建索引后,再次查看执行计划:
EXPLAIN SELECT id, name, email
FROM users
WHERE status = 1
AND created_at >= '2023-01-01'
ORDER BY created_at DESC
LIMIT 20;
优化结果:
- 扫描行数从50万降到200行
- 执行时间从5秒降到20毫秒
- 使用了
Using index和反向扫描
深入思考
1. 索引选择性的重要性
- 高选择性列应该放在复合索引的前面
- 等值条件比范围条件更适合作为前导列
- 排序和分组操作的列应该考虑放在索引中
2. 覆盖索引的威力
如果查询只需要索引中的列,就能避免回表操作:
-- 如果只需要 status 和 created_at,这个查询会更快
SELECT status, created_at
FROM users
WHERE status = 1
AND created_at >= '2023-01-01';
3. 索引的维护成本
创建索引不是免费的,需要考虑:
- 写操作的性能影响
- 磁盘空间占用
- 索引统计信息的维护
经验总结
- 理解查询模式:索引设计应该基于实际的查询需求
- 分析执行计划:
EXPLAIN是排查性能问题的利器 - 考虑复合索引:单列索引往往无法满足复杂查询
- 注意索引顺序:正确的列顺序能让索引发挥最大效用
- 定期审查索引:随着业务发展,索引策略需要调整
这次经历让我再次认识到,数据库优化不仅仅是加索引那么简单,更需要深入理解数据访问模式和索引的工作原理。每个索引都应该有明确的用途,避免"为了索引而索引"的盲目做法。
暂无评论