作为一名长期与MySQL打交道的开发者,我处理过数百个性能问题案例,其中80%的慢查询都与索引使用不当有关。今天我将系统梳理索引优化的核心方法论,包含慢SQL排查、执行计划解读、索引设计原则等实战经验,这些都是面试中高频出现的问题,也是实际工作中必须掌握的技能。
提示:本文所有案例基于MySQL 8.0版本,部分特性在低版本可能表现不同
慢查询日志是排查性能问题的第一道防线。我通常使用动态设置方式(无需重启服务):
sql复制-- 开启慢日志记录(生产环境建议长期开启)
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(单位:秒,根据业务特点调整)
SET GLOBAL long_query_time = 1;
-- 记录未使用索引的查询(重要!)
SET GLOBAL log_queries_not_using_indexes = ON;
-- 日志文件路径(建议单独挂载高速磁盘)
SET GLOBAL slow_query_log_file = '/var/lib/mysql/mysql-slow.log';
配置后,所有执行时间超过long_query_time的SQL都会被记录。建议配合pt-query-digest工具分析日志,它能自动统计最耗时的查询类型。
拿到慢SQL后,EXPLAIN是分析执行计划的核心工具。以下是需要重点关注的字段及其实际意义:
type字段(访问类型,性能从优到劣)
key_len计算原理
每个索引字段的存储长度计算公式:
例如联合索引(a INT, b VARCHAR(20)):
除了慢日志,这些命令能实时捕捉性能问题:
sql复制-- 查看当前运行线程
SHOW PROCESSLIST;
-- 查看锁等待情况
SELECT * FROM sys.innodb_lock_waits;
-- 查看索引统计信息
SHOW INDEX FROM table_name;
经典案例:手机号字段定义为VARCHAR但用数字查询
sql复制-- 索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- 正确写法(参数与字段类型一致)
SELECT * FROM users WHERE phone = '13800138000';
经验:所有字符串类型字段比较时,参数必须显式加引号
sql复制-- 索引失效
SELECT * FROM orders WHERE DATE_FORMAT(create_time,'%Y-%m') = '2023-01';
-- 优化方案(使用范围查询)
SELECT * FROM orders
WHERE create_time >= '2023-01-01'
AND create_time < '2023-02-01';
假设有联合索引(a,b,c):
sql复制-- 有效:a、a,b、a,b,c
WHERE a=1 AND b=2 AND c=3
-- 部分有效:a,c(只用到了a)
WHERE a=1 AND c=3
-- 无效:b,c
WHERE b=2 AND c=3
sql复制-- 只能用到a,b索引(c失效)
SELECT * FROM table WHERE a=1 AND b>2 AND c=3;
-- 优化方案:调整查询顺序或索引顺序
ALTER TABLE table ADD INDEX idx_a_c_b(a,c,b);
计算字段区分度的标准方法:
sql复制SELECT
COUNT(DISTINCT column_name)/COUNT(*) AS selectivity
FROM table_name;
选择策略:
覆盖索引能减少90%以上的回表操作。设计原则:
示例:
sql复制-- 原始查询
SELECT id, name, status FROM users WHERE phone='13800138000';
-- 最优索引(覆盖所有查询字段)
ALTER TABLE users ADD INDEX idx_phone_cover(phone, name, status);
MySQL 8.0新特性:
sql复制-- MySQL 8.0可能使用跳跃扫描
SELECT * FROM table WHERE b=2 AND c=3;
通过一个实际案例说明:假设存储1000万条记录,每页16KB
B树存储特点:
B+树存储特点:
以(a,b,c)联合索引为例:
code复制叶子节点内容:
| a值 | b值 | c值 | 主键值 |
|-----|-----|-----|-------|
| 1 | 10 | 'A' | 1001 |
| 1 | 10 | 'B' | 1002 |
| 1 | 20 | 'C' | 1003 |
...
排序规则:
执行过程差异:
COUNT(*):遍历最小二级索引,不取值COUNT(1):与COUNT(*)完全等价COUNT(主键):遍历聚簇索引,取主键值但不计算COUNT(列):必须检查列值是否为NULL大表COUNT优化:
sql复制-- 使用覆盖索引(比主键索引更快)
SELECT COUNT(*) FROM table USE INDEX(idx_cover);
-- 使用汇总表(实时性要求不高时)
CREATE TABLE stats (
table_name VARCHAR(100),
row_count BIGINT,
PRIMARY KEY(table_name)
);
定期检查索引碎片率:
sql复制SELECT
table_name,
index_name,
ROUND(data_free/(data_length+index_length)*100,2) AS frag_ratio
FROM information_schema.tables
WHERE data_free > 0;
整理方法:
sql复制-- Online DDL方式(MySQL 5.6+)
ALTER TABLE table_name ENGINE=InnoDB;
-- pt-online-schema-change工具(不影响业务)
pt-online-schema-change --alter="ENGINE=InnoDB" D=database,t=table
通过performance_schema查看索引命中率:
sql复制SELECT
object_schema,
object_name,
index_name,
rows_selected,
rows_inserted,
rows_updated,
rows_deleted
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL;
关键指标:rows_selected/rows_inserted比值越大,索引价值越高
在实际工作中,我总结出一条黄金法则:每个索引都应该有明确的查询场景支撑。盲目添加索引不仅不能提升性能,反而会导致写入性能下降和存储空间浪费。建议每季度进行一次索引使用情况审计,删除三个月内未被使用过的索引。