在数据库性能调优领域,索引优化始终是DBA和开发者的必修课。我经历过太多因为索引不当导致的性能灾难——某次线上查询从毫秒级骤降到30秒,最终发现仅仅是因为漏加了一个联合索引。本章将分享我在MySQL索引优化中积累的实战经验,特别是那些教科书上不会写的"血泪教训"。
B+树索引的选择性计算公式为:
code复制选择性 = 基数(Cardinality) / 总行数
当选择性大于0.2时,通常认为该列适合建索引。但实际生产中还需要考虑数据分布是否均匀。我曾遇到一个案例:某性别字段虽然基数低,但因为90%查询都筛选女性用户,最终为该字段建立索引反而提升了30%性能。
联合索引的列顺序遵循"最左前缀原则",但如何确定最优顺序?我的经验法则是:
例如用户表查询场景:
sql复制WHERE gender = 'F' AND age > 18 AND city = 'Shanghai'
建议索引顺序为:(city, gender, age)。实测比(gender, city, age)快5倍。
当查询所需字段都包含在索引中时,MySQL可以直接从索引获取数据而无需回表。我曾通过改造索引将QPS从200提升到1200:
sql复制-- 原索引: INDEX(uid)
-- 优化后: INDEX(uid, name, avatar)
SELECT name, avatar FROM users WHERE uid = 123;
注意:不要过度追求覆盖索引,索引字段过多会导致写入性能下降。一般建议不超过5个字段。
MySQL排序分为两种方式:
当发生filesort时,可以通过调整sort_buffer_size参数(默认256KB)来优化。对于百万级数据排序,建议设置为4-8MB。但要注意:
sql复制-- 错误示范:混合ASC/DESC导致无法使用索引排序
ORDER BY create_time DESC, update_time ASC
-- 正确写法:统一排序方向
ORDER BY create_time DESC, update_time DESC
GROUP BY隐式排序可能带来性能损耗。某次慢查询分析发现:
sql复制-- 耗时1.2s
SELECT department, COUNT(*) FROM employees GROUP BY department
-- 优化后0.3s(添加ORDER BY NULL取消排序)
SELECT department, COUNT(*) FROM employees GROUP BY department ORDER BY NULL
深度分页是性能杀手。对于LIMIT 10000,10这样的查询,推荐方案:
sql复制-- 方案1:延迟关联
SELECT * FROM users INNER JOIN (
SELECT id FROM users ORDER BY score DESC LIMIT 10000,10
) AS t USING(id)
-- 方案2:记录位点(适合有序数据)
SELECT * FROM users WHERE id > 上次最后ID ORDER BY id LIMIT 10
原始查询(执行时间2.8s):
sql复制SELECT * FROM orders
WHERE user_id = 123
AND status IN (2,3)
ORDER BY create_time DESC
LIMIT 10
优化步骤:
sql复制SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE user_id = 123 AND status IN (2,3)
ORDER BY create_time DESC
LIMIT 10
) AS tmp USING(id)
最终优化到23ms。
典型分页查询场景:
sql复制-- 原始方案(页数越深越慢)
SELECT * FROM feeds
WHERE user_id IN (好友ID列表)
ORDER BY create_time DESC
LIMIT 1000,20
优化方案:
sql复制SELECT * FROM feeds
WHERE id IN (从Redis获取的ID分片)
ORDER BY create_time DESC
MySQL 8.0新增的Index Skip Scan特性,可以在某些场景下利用复合索引即使不满足最左前缀:
sql复制-- 索引: (gender, age)
-- MySQL 8.0+可以自动对gender做枚举扫描
SELECT * FROM users WHERE age > 20
使用INVISIBLE INDEX可以在不删除索引的情况下测试删除影响:
sql复制-- 测试索引必要性
ALTER TABLE users ALTER INDEX idx_name INVISIBLE
-- 确认无影响后再删除
DROP INDEX idx_name ON users
MySQL 8.0支持函数索引,解决JSON字段查询等场景:
sql复制-- 为JSON字段建立函数索引
CREATE INDEX idx_profile_age ON users( (profile->>'$.age') )
-- 查询优化
SELECT * FROM users WHERE profile->>'$.age' > 25
通过performance_schema查看索引使用情况:
sql复制SELECT * FROM sys.schema_index_statistics
WHERE table_schema = 'your_db'
AND table_name = 'your_table'
定期检查并优化表:
sql复制-- 查看碎片率
SELECT table_name, index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb,
ROUND(stat_value * 100 / table_rows, 2) AS frag_ratio
FROM mysql.innodb_index_stats
WHERE stat_name = 'size' AND database_name = 'your_db'
-- 优化表
OPTIMIZE TABLE your_table
每次设计新索引时,我都会问自己:
经过多年实践,我发现最有效的索引策略是:先用EXPLAIN验证,再用真实数据测试,最后通过慢查询日志持续监控。记住,索引不是越多越好,而是越精准越好。