慢SQL治理:索引优化实战指南——从定位到优化的完整解决方案

🎁 福利时间

如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。

知识库地址:https://farerboy.com/


引言

在生产环境中,慢SQL是导致系统响应缓慢、CPU飙升、甚至服务宕机的头号元凶。一个糟糕的SQL查询可能会:

  • 导致数据库CPU使用率飙升至100%
  • 占用大量连接资源,引发连接池耗尽
  • 锁表或锁行,阻塞其他正常查询
  • 拖慢整个应用集群的响应速度

然而,很多开发者对索引的理解停留在"加索引就能变快"的层面,实际应用中却经常出现索引失效、索引冗余、索引设计不合理等问题。

本文将从慢SQL的定位、索引原理、优化实战到治理体系,为你提供一套完整的索引优化解决方案。

一、慢SQL定位与分析

1.1 开启慢查询日志

MySQL配置

ini 复制代码
[mysqld]
# 开启慢查询日志
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log

# 设置慢查询阈值(秒)
long_query_time = 1

# 记录未使用索引的查询
log_queries_not_using_indexes = ON

动态开启

sql 复制代码
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 设置阈值
SET GLOBAL long_query_time = 1;

1.2 使用pt-query-digest分析

bash 复制代码
# 安装pt-query-digest
apt-get install percona-toolkit

# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt

# 分析最近1小时的慢查询
pt-query-digest --since=1h /var/log/mysql/slow.log

# 分析指定用户的慢查询
pt-query-digest --filter '$event->{User} =~ m/^app_user/' /var/log/mysql/slow.log

1.3 实时慢SQL监控

sql 复制代码
-- 查看当前正在执行的查询
SHOW FULL PROCESSLIST;

-- 查看长时间运行的事务
SELECT * FROM information_schema.INNODB_TRX 
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;

-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

1.4 使用EXPLAIN分析执行计划

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE user_id = 123 AND status = 'PAID' 
ORDER BY create_time DESC 
LIMIT 20;

关键字段解读

字段 说明 理想值
type 访问类型 system > const > eq_ref > ref > range > index > ALL
possible_keys 可能使用的索引 不为空
key 实际使用的索引 不为空
key_len 索引使用长度 越短越好
rows 预估扫描行数 越少越好
Extra 额外信息 避免出现Using filesort、Using temporary

二、索引核心原理

2.1 B+树索引结构

复制代码
                    [根节点]
                   /        \
              [内部节点]    [内部节点]
             /    |    \      |    \
         [叶子] [叶子] [叶子] [叶子] [叶子]
           |      |      |      |      |
         数据    数据    数据    数据    数据

B+树的特点

  1. 所有数据都在叶子节点:非叶子节点只存储索引值
  2. 叶子节点形成链表:便于范围查询
  3. 树高通常为3-4层:一次查询只需3-4次磁盘IO
  4. 自平衡:插入删除自动调整

2.2 聚簇索引 vs 二级索引

聚簇索引(Clustered Index)

复制代码
叶子节点直接存储行数据
┌────────────────────────────────┐
│ 索引值 │ 行数据(所有列)       │
├────────────────────────────────┤
│ 1      │ (1, 'Alice', 25, ...) │
│ 2      │ (2, 'Bob', 30, ...)   │
└────────────────────────────────┘
  • InnoDB的主键就是聚簇索引
  • 一张表只能有一个聚簇索引
  • 叶子节点存储完整的行数据

二级索引(Secondary Index)

复制代码
叶子节点存储主键值
┌────────────────────────┐
│ 索引值 │ 主键值         │
├────────────────────────┤
│ Alice  │ 1             │
│ Bob    │ 2             │
└────────────────────────┘
  • 可以有多个二级索引
  • 叶子节点存储主键值,需要回表查询
  • 回表:先查二级索引获取主键,再查聚簇索引获取行数据

2.3 回表与覆盖索引

回表查询

sql 复制代码
-- 假设name有二级索引
SELECT * FROM user WHERE name = 'Alice';
-- 1. 查name索引,得到主键id=1
-- 2. 用id=1查聚簇索引,获取完整行数据(回表)

覆盖索引

sql 复制代码
-- 假设(name, age)有联合索引
SELECT id, name, age FROM user WHERE name = 'Alice';
-- 直接通过索引返回结果,无需回表

覆盖索引是索引优化的重要手段,可以显著减少IO。

三、索引优化实战

3.1 最左前缀原则

联合索引 (a, b, c) 的生效规则:

sql 复制代码
-- 生效
SELECT * FROM table WHERE a = 1;                    -- 使用a
SELECT * FROM table WHERE a = 1 AND b = 2;          -- 使用a, b
SELECT * FROM table WHERE a = 1 AND b = 2 AND c = 3; -- 使用a, b, c
SELECT * FROM table WHERE a = 1 AND c = 3;          -- 使用a,c不生效

-- 不生效
SELECT * FROM table WHERE b = 2;                    -- 无a,不生效
SELECT * FROM table WHERE b = 2 AND c = 3;          -- 无a,不生效
SELECT * FROM table WHERE c = 3;                    -- 无a,不生效

实战案例

sql 复制代码
-- 原始索引
CREATE INDEX idx_user ON user(name, age, city);

-- 场景1:按城市查询(索引失效)
SELECT * FROM user WHERE city = 'Beijing';  -- 全表扫描

-- 优化:调整索引顺序
CREATE INDEX idx_city_name ON user(city, name);

3.2 索引失效的常见场景

场景1:函数操作导致索引失效

sql 复制代码
-- 索引失效
SELECT * FROM user WHERE YEAR(create_time) = 2024;

-- 优化:使用范围查询
SELECT * FROM user 
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

场景2:隐式类型转换导致索引失效

sql 复制代码
-- 假设phone是varchar类型
-- 索引失效(数值比较导致隐式转换)
SELECT * FROM user WHERE phone = 13800138000;

-- 优化:使用字符串
SELECT * FROM user WHERE phone = '13800138000';

场景3:LIKE前缀通配符导致索引失效

sql 复制代码
-- 索引失效
SELECT * FROM user WHERE name LIKE '%张%';

-- 索引生效(前缀匹配)
SELECT * FROM user WHERE name LIKE '张%';

-- 优化:使用全文索引
ALTER TABLE user ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM user WHERE MATCH(name) AGAINST('张');

场景4:OR条件导致索引失效

sql 复制代码
-- 可能索引失效(取决于优化器判断)
SELECT * FROM user WHERE name = 'Alice' OR age = 25;

-- 优化:拆分为UNION
SELECT * FROM user WHERE name = 'Alice'
UNION
SELECT * FROM user WHERE age = 25;

场景5:NOT、!=、<>导致索引失效

sql 复制代码
-- 索引失效
SELECT * FROM user WHERE status != 'deleted';

-- 优化:改为IN
SELECT * FROM user WHERE status IN ('active', 'pending');

3.3 索引设计最佳实践

实践1:选择区分度高的列

sql 复制代码
-- 区分度 = 不同值数量 / 总行数

-- 性别列区分度低(只有男女),不适合单独建索引
SELECT COUNT(DISTINCT gender) / COUNT(*) FROM user;  -- 约0.5

-- 手机号区分度高,适合建索引
SELECT COUNT(DISTINCT phone) / COUNT(*) FROM user;   -- 接近1.0

实践2:控制索引数量

sql 复制代码
-- 查询表的所有索引
SHOW INDEX FROM user;

-- 删除未使用的索引
DROP INDEX idx_unused ON user;

实践3:选择合适的索引类型

sql 复制代码
-- 普通索引
CREATE INDEX idx_name ON user(name);

-- 唯一索引
CREATE UNIQUE INDEX uk_email ON user(email);

-- 联合索引
CREATE INDEX idx_name_age ON user(name, age);

-- 前缀索引(适用于长字符串)
CREATE INDEX idx_name_prefix ON user(name(10));

-- 全文索引
ALTER TABLE article ADD FULLTEXT INDEX ft_content(content);

实践4:联合索引列的顺序

sql 复制代码
-- 原则:等值查询列在前,范围查询列在后

-- 场景:WHERE status = ? AND create_time > ? ORDER BY create_time
-- 索引设计
CREATE INDEX idx_status_time ON user(status, create_time);

3.4 覆盖索引优化

案例:分页查询优化

sql 复制代码
-- 优化前:回表查询大量数据
SELECT * FROM orders 
WHERE user_id = 123 
ORDER BY create_time DESC 
LIMIT 100000, 20;
-- 需要回表100020行,然后丢弃前100000行

-- 优化后:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders 
    WHERE user_id = 123 
    ORDER BY create_time DESC 
    LIMIT 100000, 20
) t ON o.id = t.id;
-- 子查询只通过索引获取id,再回表20行

3.5 索引条件下推(ICP)

MySQL 5.6引入的优化,可以在存储引擎层过滤数据。

sql 复制代码
-- 联合索引 (name, age)
SELECT * FROM user WHERE name = '张' AND age > 25;

-- ICP优化:在索引层就过滤掉age <= 25的数据
-- 减少回表次数

四、典型慢SQL优化案例

4.1 案例一:深分页优化

问题SQL

sql 复制代码
SELECT * FROM orders 
WHERE status = 'PAID' 
ORDER BY create_time DESC 
LIMIT 1000000, 20;
-- 执行时间:5.2秒

执行计划

复制代码
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 5000000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+---------+-----------------------------+

优化方案

sql 复制代码
-- 方案1:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders 
    WHERE status = 'PAID'
    ORDER BY create_time DESC 
    LIMIT 1000000, 20
) t ON o.id = t.id;

-- 需要添加索引
CREATE INDEX idx_status_time ON orders(status, create_time);

-- 方案2:记录上一页最后一条
-- 前端传递上一页最后的create_time和id
SELECT * FROM orders 
WHERE status = 'PAID' AND create_time < '2024-01-01 12:00:00'
ORDER BY create_time DESC 
LIMIT 20;

优化效果:5.2秒 → 0.05秒

4.2 案例二:GROUP BY优化

问题SQL

sql 复制代码
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders
WHERE create_time >= '2024-01-01'
GROUP BY user_id
ORDER BY total_amount DESC
LIMIT 100;
-- 执行时间:8.5秒

优化方案

sql 复制代码
-- 添加覆盖索引
CREATE INDEX idx_time_user_amount ON orders(create_time, user_id, amount);

-- 如果数据量巨大,考虑预聚合
CREATE TABLE order_stats_daily (
    stat_date DATE,
    user_id BIGINT,
    order_count INT,
    total_amount DECIMAL(10,2),
    PRIMARY KEY (stat_date, user_id)
);

-- 定时任务每日聚合
INSERT INTO order_stats_daily
SELECT DATE(create_time), user_id, COUNT(*), SUM(amount)
FROM orders
WHERE create_time >= CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(create_time), user_id;

-- 查询改为
SELECT user_id, SUM(order_count), SUM(total_amount)
FROM order_stats_daily
WHERE stat_date >= '2024-01-01'
GROUP BY user_id
ORDER BY SUM(total_amount) DESC
LIMIT 100;

优化效果:8.5秒 → 0.2秒

4.3 案例三:JOIN优化

问题SQL

sql 复制代码
SELECT u.*, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 执行时间:12秒

问题分析

  1. LEFT JOIN导致user表作为驱动表
  2. WHERE条件中的o.create_time使LEFT JOIN变成了INNER JOIN
  3. 缺少必要的索引

优化方案

sql 复制代码
-- 改为INNER JOIN(语义不变)
SELECT u.*, o.order_no, o.amount
FROM orders o
INNER JOIN user u ON u.id = o.user_id
WHERE u.status = 'active'
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;

-- 添加索引
CREATE INDEX idx_user_status ON user(status, id);
CREATE INDEX idx_orders_time_user ON orders(create_time, user_id);

优化效果:12秒 → 0.3秒

4.4 案例四:子查询优化

问题SQL

sql 复制代码
SELECT * FROM user
WHERE id IN (
    SELECT user_id FROM orders 
    WHERE amount > 1000
);
-- 执行时间:6秒

优化方案

sql 复制代码
-- 改为JOIN
SELECT DISTINCT u.*
FROM user u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;

-- 或者使用EXISTS(MySQL 5.6+已优化)
SELECT * FROM user u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.amount > 1000
);

-- 添加索引
CREATE INDEX idx_amount_user ON orders(amount, user_id);

五、索引治理体系

5.1 索引规范

创建规范

  1. 表必须建立主键,推荐使用自增ID或雪花算法ID
  2. 唯一键使用uk_前缀,普通索引使用idx_前缀
  3. 联合索引列不超过5个
  4. 字符串字段建议建立前缀索引,前缀长度根据区分度确定

命名规范

sql 复制代码
-- 主键
PRIMARY KEY (id)

-- 唯一索引
UNIQUE KEY uk_email (email)

-- 普通索引
KEY idx_name (name)

-- 联合索引
KEY idx_status_time (status, create_time)

5.2 索引评审流程

复制代码
需求开发 → SQL编写 → EXPLAIN分析 → 索引设计 → 代码评审 → 测试验证 → 上线

评审检查项

  • 是否进行了EXPLAIN分析
  • 是否有全表扫描(type=ALL)
  • 是否使用了filesort或temporary
  • 索引是否符合最左前缀原则
  • 是否存在索引冗余

5.3 定期索引清理

查询未使用的索引

sql 复制代码
-- MySQL 5.7+
SELECT 
    table_schema,
    table_name,
    index_name,
    seq_in_index,
    column_name
FROM information_schema.STATISTICS
WHERE table_schema = 'your_database'
AND index_name NOT IN ('PRIMARY')
AND index_name NOT IN (
    SELECT DISTINCT INDEX_NAME 
    FROM performance_schema.table_io_waits_summary_by_index_usage
    WHERE INDEX_NAME IS NOT NULL
    AND OBJECT_SCHEMA = 'your_database'
);

删除冗余索引

sql 复制代码
-- 安装pt-duplicate-key-checker
pt-duplicate-key-checker -u root -p password -d your_database

5.4 监控告警

慢SQL告警规则

yaml 复制代码
# Prometheus告警规则
groups:
  - name: slow_sql_alerts
    rules:
      - alert: SlowSQLDetected
        expr: rate(mysql_slow_queries[5m]) > 10
        for: 5m
        annotations:
          summary: "慢查询数量异常"
          description: "5分钟内慢查询数量超过10个"
      
      - alert: FullTableScan
        expr: mysql_handler_read_rnd_next_total > 1000000
        for: 2m
        annotations:
          summary: "检测到全表扫描"

六、高级优化技巧

6.1 索引下推优化

sql 复制代码
-- 联合索引 (name, age, city)
SELECT * FROM user 
WHERE name LIKE '张%' AND age > 25 AND city = 'Beijing';

-- ICP会在索引层过滤age和city,减少回表

6.2 MRR(Multi-Range Read)优化

sql 复制代码
-- 开启MRR
SET optimizer_switch = 'mrr=on,mrr_cost_based=off';

-- MRR可以将随机IO转换为顺序IO
SELECT * FROM user 
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);

6.3 BKA(Batched Key Access)优化

sql 复制代码
-- 开启BKA
SET optimizer_switch = 'mrr=on,batched_key_access=on';

-- BKA优化JOIN时的索引查找
SELECT u.*, o.*
FROM user u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active';

6.4 虚拟列与函数索引

MySQL 5.7+支持生成列,MySQL 8.0+支持函数索引:

sql 复制代码
-- MySQL 8.0 函数索引
CREATE INDEX idx_year ON user((YEAR(create_time)));

-- MySQL 5.7 生成列
ALTER TABLE user ADD COLUMN create_year INT 
GENERATED ALWAYS AS (YEAR(create_time)) STORED;
CREATE INDEX idx_create_year ON user(create_year);

七、总结

索引优化是慢SQL治理的核心,需要系统性地掌握以下要点:

  1. 理解原理:掌握B+树结构、聚簇索引、二级索引、回表等核心概念
  2. 熟练工具:善用EXPLAIN、pt-query-digest等工具定位问题
  3. 避免陷阱:熟悉索引失效的各种场景,避开常见坑点
  4. 规范先行:建立索引设计规范,从源头避免问题
  5. 持续治理:定期清理无用索引,监控慢SQL

优化心法

  • 少即是多:不是索引越多越好,冗余索引反而影响写入性能
  • 精准打击:针对具体SQL设计索引,不要盲目加索引
  • 覆盖为王:尽量使用覆盖索引,避免回表
  • 数据说话:优化前后一定要用EXPLAIN和执行时间验证效果

索引优化没有银弹,只有深入理解原理,结合实际场景,才能真正做到"对症下药"。希望本指南能帮助你在慢SQL治理的道路上少走弯路。


相关推荐
Aision_5 小时前
从工具调用到 MCP、Skill完整学习记录
java·python·gpt·学习·langchain·prompt·agi
zc.z9 小时前
JAVA实现:纯PCM格式音频转换成BASE64
java·音视频·pcm
mask哥9 小时前
力扣算法java实现汇总整理(上)
java·算法·leetcode
Aaswk11 小时前
Java Lambda 表达式与流处理
java·开发语言·python
是宇写的啊11 小时前
Spring AOP
java·spring
万邦科技Lafite11 小时前
京东item_get接口实战案例:实时商品价格监控全流程解析
java·开发语言·数据库·python·开放api·淘宝开放平台
Mr_pyx12 小时前
Spring AI 入门教程:Java开发者的AI应用捷径
java·人工智能·spring
Zephyr_013 小时前
Leedcode算法题
java·算法
苍煜13 小时前
Java开发IO零基础吃透:BIO、NIO、同步异步、阻塞非阻塞
java·python·nio