今天在排查一个线上问题时,遇到一个典型的慢查询案例,让我重新审视了数据库索引的使用细节。这个看似简单的优化过程,实际上涉及了索引的底层原理和查询优化器的行为。

问题背景

我们的用户表 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. 索引的维护成本

创建索引不是免费的,需要考虑:

  • 写操作的性能影响
  • 磁盘空间占用
  • 索引统计信息的维护

经验总结

  1. 理解查询模式:索引设计应该基于实际的查询需求
  2. 分析执行计划EXPLAIN 是排查性能问题的利器
  3. 考虑复合索引:单列索引往往无法满足复杂查询
  4. 注意索引顺序:正确的列顺序能让索引发挥最大效用
  5. 定期审查索引:随着业务发展,索引策略需要调整

这次经历让我再次认识到,数据库优化不仅仅是加索引那么简单,更需要深入理解数据访问模式和索引的工作原理。每个索引都应该有明确的用途,避免"为了索引而索引"的盲目做法。