Skip to content
清晨的一缕阳光
返回

索引优化实战案例

引言

理论知识的最终目的是指导实践。本章通过 5 个真实的索引优化案例,展示从问题发现、分析诊断到优化实施、效果验证的完整流程。

案例涵盖:

案例 1:电商订单列表查询优化

业务背景

某电商平台的订单列表查询接口,随着数据量增长,响应时间越来越慢。

表结构:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT NOT NULL,
    merchant_id BIGINT NOT NULL,
    status TINYINT NOT NULL DEFAULT 1,
    amount DECIMAL(10,2) NOT NULL,
    create_time DATETIME NOT NULL,
    pay_time DATETIME,
    finish_time DATETIME,
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB;

-- 数据量:500 万行

问题描述

慢查询 SQL:

-- 用户订单列表(按时间倒序,分页)
SELECT * FROM orders 
WHERE user_id = 10086 
ORDER BY create_time DESC 
LIMIT 0, 20;
-- 执行时间:2-3 秒

性能指标:

分析诊断

1. EXPLAIN 分析:

EXPLAIN SELECT * FROM orders 
WHERE user_id = 10086 
ORDER BY create_time DESC 
LIMIT 0, 20;

-- 输出:
-- +----+------+--------+------+---------------+------+---------+------+
-- | id | type | table  | type | possible_keys | key  | key_len| ref  | 
-- +----+------+--------+------+---------------+------+---------+------+
-- |  1 | SIMPLE | orders | ref  | idx_create_time | NULL | NULL   | NULL| 
-- +----+------+--------+------+---------------+------+---------+------+
-- rows: 50000
-- Extra: Using where; Using filesort

问题诊断:

2. 查看索引:

SHOW INDEX FROM orders;
-- 只有 idx_create_time 索引
-- 没有 user_id 相关的索引!

3. 查看数据分布:

-- 查看用户订单分布
SELECT user_id, COUNT(*) as order_count 
FROM orders 
GROUP BY user_id 
ORDER BY order_count DESC 
LIMIT 10;

-- 结果:
-- 最大用户订单数:523
-- 平均用户订单数:15

优化方案

方案 1:创建联合索引

-- 创建 (user_id, create_time) 联合索引
CREATE INDEX idx_user_time ON orders(user_id, create_time DESC);

-- 验证
EXPLAIN SELECT * FROM orders 
WHERE user_id = 10086 
ORDER BY create_time DESC 
LIMIT 0, 20;

-- 新输出:
-- type: ref
-- key: idx_user_time
-- rows: 523
-- Extra: NULL(无 filesort)

效果验证:

-- 执行时间对比
-- 优化前:2.1 秒
-- 优化后:0.005 秒
-- 提升:420 倍!

方案 2:覆盖索引优化(可选)

-- 如果只需要部分字段
SELECT id, order_no, amount, create_time 
FROM orders 
WHERE user_id = 10086 
ORDER BY create_time DESC 
LIMIT 0, 20;

-- 创建覆盖索引
CREATE INDEX idx_user_time_cover ON orders(user_id, create_time DESC, id, order_no, amount);

-- 无需回表,性能更优
-- 执行时间:0.003 秒

经验总结

问题根因:

优化要点:

  1. 为 WHERE 条件列创建索引
  2. 将 ORDER BY 列加入联合索引
  3. 考虑覆盖索引避免回表

索引设计原则:

-- 联合索引列顺序
WHERE 条件列 + ORDER BY+ SELECT

-- 本例:
(user_id, create_time) -- 基础索引
(user_id, create_time, id, order_no, amount) -- 覆盖索引

案例 2:用户登录性能优化

业务背景

用户登录接口响应慢,高峰期出现超时。

表结构:

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    phone VARCHAR(11),
    email VARCHAR(100),
    password_hash VARCHAR(255),
    status TINYINT DEFAULT 1,
    last_login_time DATETIME,
    INDEX idx_username (username)
) ENGINE=InnoDB;

-- 数据量:1000 万行

问题描述

慢查询 SQL:

-- 手机号登录
SELECT * FROM users 
WHERE phone = '13800138000' AND status = 1;
-- 执行时间:1.5 秒

性能指标:

分析诊断

1. EXPLAIN 分析:

EXPLAIN SELECT * FROM users 
WHERE phone = '13800138000' AND status = 1;

-- 输出:
-- type: ALL(全表扫描!)
-- key: NULL
-- rows: 10000000
-- Extra: Using where

2. 查看索引:

SHOW INDEX FROM users;
-- 只有 idx_username 索引
-- phone 列没有索引!

3. 查看数据分布:

-- 查看手机号分布
SELECT COUNT(DISTINCT phone) / COUNT(*) as uniqueness 
FROM users;
-- 结果:0.99(几乎唯一)

-- 查看状态分布
SELECT status, COUNT(*) 
FROM users 
GROUP BY status;
-- 结果:status=1 占 95%,status=0 占 5%

优化方案

方案 1:创建唯一索引

-- 手机号应该是唯一的
CREATE UNIQUE INDEX idx_phone ON users(phone);

-- 验证
EXPLAIN SELECT * FROM users 
WHERE phone = '13800138000' AND status = 1;

-- 新输出:
-- type: const(最优!)
-- key: idx_phone
-- rows: 1
-- Extra: Using where

效果验证:

-- 执行时间对比
-- 优化前:1.2 秒
-- 优化后:0.001 秒
-- 提升:1200 倍!

方案 2:联合索引优化

-- 如果 status 过滤也很重要
CREATE INDEX idx_phone_status ON users(phone, status);

-- 验证
EXPLAIN SELECT * FROM users 
WHERE phone = '13800138000' AND status = 1;

-- type: ref
-- key: idx_phone_status
-- rows: 1

经验总结

问题根因:

优化要点:

  1. 登录凭证(手机号/邮箱)应创建唯一索引
  2. 高选择性列优先建索引
  3. const 类型查询性能最优

注意事项:

-- 手机号登录时,确保使用字符串
SELECT * FROM users WHERE phone = '13800138000';  -- ✅
SELECT * FROM users WHERE phone = 13800138000;    -- ❌ 隐式转换

案例 3:日志系统分页优化

业务背景

系统日志表分页查询,深度分页时性能极差。

表结构:

CREATE TABLE system_logs (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT,
    action VARCHAR(50),
    ip_address VARCHAR(50),
    create_time DATETIME,
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB;

-- 数据量:5000 万行

问题描述

慢查询 SQL:

-- 深度分页
SELECT * FROM system_logs 
ORDER BY create_time DESC 
LIMIT 1000000, 20;
-- 执行时间:15 秒

性能指标:

分析诊断

1. EXPLAIN 分析:

EXPLAIN SELECT * FROM system_logs 
ORDER BY create_time DESC 
LIMIT 1000000, 20;

-- 输出:
-- type: index
-- key: idx_create_time
-- rows: 1000020
-- Extra: Using index

问题诊断:

2. 分页原理:

深度分页 = 扫描 (offset + limit) 行 + 丢弃 offset 行 + 返回 limit 行

LIMIT 1000000, 20
= 扫描 1000020 行 + 丢弃 1000000 行 + 返回 20 行

优化方案

方案 1:延迟关联(推荐)

-- 先通过覆盖索引获取 ID,再回表查询
SELECT l.* FROM system_logs l
INNER JOIN (
    SELECT id FROM system_logs 
    ORDER BY create_time DESC 
    LIMIT 1000000, 20
) tmp ON l.id = tmp.id;

-- 执行时间:0.5 秒(提升 30 倍)

原理:

  1. 子查询使用覆盖索引,只扫描索引树(更快)
  2. 获取 20 个 ID 后,再通过主键回表(精确查询)
  3. 避免了大量数据的回表操作

方案 2:游标分页(最佳)

-- 记录上一页最后一条的 create_time 和 id
-- 假设上一页最后一条:create_time='2024-08-01 12:00:00', id=5000000

SELECT * FROM system_logs 
WHERE create_time < '2024-08-01 12:00:00' 
   OR (create_time = '2024-08-01 12:00:00' AND id < 5000000)
ORDER BY create_time DESC, id DESC
LIMIT 20;

-- 执行时间:0.01 秒(提升 1500 倍)

优点:

缺点:

方案 3:限制最大页码

-- 业务层面限制最大页码
-- 例如:最多允许访问前 10000 条

-- 应用层逻辑
$max_offset = 10000;
$offset = min($page * $page_size, $max_offset);

-- SQL
SELECT * FROM system_logs 
ORDER BY create_time DESC 
LIMIT $offset, 20;

经验总结

问题根因:

优化方案对比:

方案性能适用场景实现复杂度
延迟关联⭐⭐⭐⭐任意分页
游标分页⭐⭐⭐⭐⭐连续翻页
限制页码⭐⭐⭐浅分页

最佳实践:

-- 1. 避免深度分页(业务限制)
-- 2. 使用游标分页(性能最优)
-- 3. 延迟关联(兼容性好)

-- 游标分页示例代码(Java)
public List<Log> getNextPage(LocalDateTime lastTime, Long lastId, int limit) {
    return jdbcTemplate.query(
        "SELECT * FROM system_logs " +
        "WHERE create_time < ? OR (create_time = ? AND id < ?) " +
        "ORDER BY create_time DESC, id DESC " +
        "LIMIT ?",
        new Object[]{lastTime, lastTime, lastId, limit}
    );
}

案例 4:报表系统聚合查询优化

业务背景

日报表查询慢,影响数据导出功能。

表结构:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    category_id INT,
    amount DECIMAL(10,2),
    status TINYINT,
    create_time DATETIME,
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB;

-- 数据量:2000 万行

问题描述

慢查询 SQL:

-- 按类目统计每日销售额
SELECT 
    DATE(create_time) as date,
    category_id,
    COUNT(*) as order_count,
    SUM(amount) as total_amount
FROM orders 
WHERE create_time >= '2024-01-01' 
  AND create_time < '2024-02-01'
GROUP BY DATE(create_time), category_id;
-- 执行时间:8 秒

分析诊断

1. EXPLAIN 分析:

EXPLAIN SELECT 
    DATE(create_time) as date,
    category_id,
    COUNT(*) as order_count,
    SUM(amount) as total_amount
FROM orders 
WHERE create_time >= '2024-01-01' 
  AND create_time < '2024-02-01'
GROUP BY DATE(create_time), category_id;

-- 输出:
-- type: range
-- key: idx_create_time
-- rows: 1500000
-- Extra: Using where; Using temporary; Using filesort

问题诊断:

2. 问题根因:

优化方案

方案 1:优化 GROUP BY

-- 避免在 GROUP BY 中使用函数
SELECT 
    DATE(create_time) as date,
    category_id,
    COUNT(*) as order_count,
    SUM(amount) as total_amount
FROM orders 
WHERE create_time >= '2024-01-01' 
  AND create_time < '2024-02-01'
GROUP BY create_time, category_id;  -- 直接按 create_time 分组

-- 执行时间:5 秒(提升有限)

方案 2:创建联合索引

-- 创建 (create_time, category_id) 联合索引
CREATE INDEX idx_time_category ON orders(create_time, category_id);

-- 验证
EXPLAIN SELECT 
    DATE(create_time) as date,
    category_id,
    COUNT(*) as order_count,
    SUM(amount) as total_amount
FROM orders 
WHERE create_time >= '2024-01-01' 
  AND create_time < '2024-02-01'
GROUP BY DATE(create_time), category_id;

-- 新输出:
-- type: range
-- key: idx_time_category
-- rows: 1500000
-- Extra: Using where; Using index(覆盖索引!)

效果验证:

-- 执行时间对比
-- 优化前:8 秒
-- 优化后:2 秒
-- 提升:4 倍

方案 3:预聚合(最佳)

-- 创建日报表
CREATE TABLE daily_category_stats (
    stat_date DATE,
    category_id INT,
    order_count INT,
    total_amount DECIMAL(15,2),
    PRIMARY KEY (stat_date, category_id)
) ENGINE=InnoDB;

-- 每天凌晨统计前一天的数据
INSERT INTO daily_category_stats 
SELECT 
    DATE(create_time) as stat_date,
    category_id,
    COUNT(*) as order_count,
    SUM(amount) as total_amount
FROM orders 
WHERE create_time >= CURDATE() - INTERVAL 1 DAY
  AND create_time < CURDATE()
GROUP BY DATE(create_time), category_id;

-- 查询时直接查统计表
SELECT * FROM daily_category_stats 
WHERE stat_date >= '2024-01-01' 
  AND stat_date < '2024-02-01';

-- 执行时间:0.01 秒(提升 800 倍)

经验总结

问题根因:

优化层次:

  1. 索引优化(4 倍提升)
  2. SQL 改写(避免函数)
  3. 预聚合(800 倍提升)⭐

最佳实践:

-- 报表系统通用方案
-- 1. 实时查询:索引优化
-- 2. T+1 报表:预聚合
-- 3. 复杂报表:数据仓库/OLAP

-- 预聚合表设计原则
-- - 按时间维度(天/周/月)
-- - 按业务维度(类目/地区/渠道)
-- - 定时任务更新
-- - 保留明细表用于钻取

案例 5:社交系统关联查询优化

业务背景

社交 Feed 流查询慢,影响用户体验。

表结构:

-- 用户表
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    avatar_url VARCHAR(255)
);

-- 关注表
CREATE TABLE follows (
    follower_id BIGINT,  -- 关注者
    followee_id BIGINT,  -- 被关注者
    create_time DATETIME,
    PRIMARY KEY (follower_id, followee_id)
);

-- 动态表
CREATE TABLE posts (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    content TEXT,
    create_time DATETIME,
    INDEX idx_user_time (user_id, create_time)
);

-- 数据量:
-- users: 500 万
-- follows: 5000 万
-- posts: 1 亿

问题描述

慢查询 SQL:

-- 查询关注人的动态(Feed 流)
SELECT p.*, u.username, u.avatar_url
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN (
    SELECT followee_id FROM follows 
    WHERE follower_id = 10086
)
ORDER BY p.create_time DESC
LIMIT 0, 20;
-- 执行时间:5 秒

分析诊断

1. EXPLAIN 分析:

EXPLAIN SELECT p.*, u.username, u.avatar_url
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN (
    SELECT followee_id FROM follows 
    WHERE follower_id = 10086
)
ORDER BY p.create_time DESC
LIMIT 0, 20;

-- 输出:
-- 子查询:type: ALL(全表扫描 follows)
-- 主查询:type: ALL(全表扫描 posts)

2. 问题诊断:

优化方案

方案 1:改为 JOIN

-- 将 IN 子查询改为 JOIN
SELECT p.*, u.username, u.avatar_url
FROM posts p
JOIN follows f ON p.user_id = f.followee_id
JOIN users u ON p.user_id = u.id
WHERE f.follower_id = 10086
ORDER BY p.create_time DESC
LIMIT 0, 20;

-- 执行时间:1 秒(提升 5 倍)

方案 2:创建索引

-- follows 表需要索引
CREATE INDEX idx_follower_time ON follows(follower_id, create_time);

-- posts 表已有索引,但需要优化
-- 现有:idx_user_time (user_id, create_time)

-- 验证
EXPLAIN SELECT p.*, u.username, u.avatar_url
FROM posts p
JOIN follows f ON p.user_id = f.followee_id
JOIN users u ON p.user_id = u.id
WHERE f.follower_id = 10086
ORDER BY p.create_time DESC
LIMIT 0, 20;

-- 新输出:
-- follows: type: ref, key: idx_follower_id
-- posts: type: ref, key: idx_user_time
-- users: type: eq_ref, key: PRIMARY

效果验证:

-- 执行时间对比
-- 优化前:5 秒
-- 优化后:0.1 秒
-- 提升:50 倍

方案 3:预计算 Feed 流(最佳)

-- 创建用户 Feed 流表
CREATE TABLE user_feed (
    user_id BIGINT,
    post_id BIGINT,
    post_time DATETIME,
    PRIMARY KEY (user_id, post_time, post_id)
) ENGINE=InnoDB;

-- 当用户发 post 时,插入到所有粉丝的 feed 表
INSERT INTO user_feed (user_id, post_id, post_time)
SELECT followee_id, ?, ? FROM follows 
WHERE follower_id = ?;

-- 查询 feed 流
SELECT p.*, u.username, u.avatar_url
FROM user_feed f
JOIN posts p ON f.post_id = p.id
JOIN users u ON p.user_id = u.id
WHERE f.user_id = 10086
ORDER BY f.post_time DESC
LIMIT 0, 20;

-- 执行时间:0.01 秒(提升 500 倍)

经验总结

问题根因:

优化方案对比:

方案性能写放大适用场景
IN 子查询小数据量
JOIN⭐⭐⭐中等数据量
预计算 Feed⭐⭐⭐⭐⭐大数据量,读多写少

最佳实践:

-- Feed 流系统架构选择
-- 1. 推模式(写扩散):适合大 V 少的场景
-- 2. 拉模式(读扩散):适合大 V 多的场景
-- 3. 混合模式:普通用户推模式,大 V 拉模式

-- 索引设计原则
-- - JOIN 列必须有索引
-- - 优先使用主键关联
-- - 大表避免多表 JOIN

总结

优化流程总结

graph TD
    A[发现慢查询] --> B[EXPLAIN 分析]
    B --> C{问题类型?}
    C -->|全表扫描 | D[添加索引]
    C -->|索引失效 | E[改写 SQL]
    C -->|深度分页 | F[延迟关联/游标]
    C -->|JOIN 复杂 | G[优化关联]
    D --> H[验证效果]
    E --> H
    F --> H
    G --> H
    H --> I[上线监控]

核心要点

  1. 先分析后优化:EXPLAIN 是必备工具
  2. 索引是基础:80% 的问题可以通过索引解决
  3. SQL 改写:避免函数、类型转换等
  4. 架构优化:预聚合、读写分离等

经验法则

下一步

索引优化篇完结!后续将学习:

参考资料


分享这篇文章到:

上一篇文章
Optional 深度解析
下一篇文章
Stream API 深度解析