1. 索引优化的底层逻辑与实战策略
1.1 索引的本质与工作原理
索引的本质是数据库中的一种特殊数据结构,它通过建立数据表中某列或多列的快速访问路径,显著提升查询效率。想象一下图书馆的目录系统——没有索引时找书需要遍历整个书架(全表扫描),而索引就像按作者/书名分类的卡片柜,能直接定位到目标位置。
B+树是MySQL最常用的索引结构,其优势在于:
- 三层结构即可支撑千万级数据(根节点常驻内存)
- 叶子节点形成有序链表,适合范围查询
- 非叶子节点只存键值,单个节点可存储更多指针
注意:索引不是越多越好。每个索引需要占用存储空间,且增删改操作需同步维护索引,会导致写性能下降。我的经验是OLTP系统单表索引最好控制在5个以内。
1.2 索引选择的核心原则
高选择性原则:优先为区分度高的列建索引。可通过以下SQL评估列区分度:
sql复制SELECT
COUNT(DISTINCT column_name)/COUNT(*) AS selectivity
FROM table_name;
当结果大于0.2时适合建索引。例如用户表的手机号字段选择性通常接近1,而性别字段只有0.5(男/女)则不适合单独建索引。
最左前缀原则:联合索引(a,b,c)实际相当于建立了(a)、(a,b)、(a,b,c)三个索引。查询条件必须包含最左列才能命中索引。我曾遇到一个典型案例:某查询条件为WHERE b=? AND c=?,虽然表上有(a,b,c)索引但完全失效,改为(a,c,b)顺序后性能提升300倍。
覆盖索引优化:当索引包含所有查询字段时,引擎可直接从索引获取数据而无需回表。例如:
sql复制-- 需要回表
SELECT * FROM users WHERE username='张三';
-- 覆盖索引(假设有(username,age)索引)
SELECT username, age FROM users WHERE username='张三';
1.3 索引失效的六大陷阱
- 隐式类型转换:
WHERE phone=13800138000(phone是varchar类型)会导致全表扫描 - 使用函数操作:
WHERE DATE(create_time)='2023-01-01'会使索引失效 - 不当的LIKE使用:
WHERE name LIKE '%张'无法使用索引,LIKE '张%'可以 - OR条件未全覆盖:
WHERE a=1 OR b=2,若a有索引而b没有,则全表扫描 - !=或<>操作符:多数情况下无法使用索引
- 联合索引顺序错误:如前述最左前缀原则案例
实战技巧:通过EXPLAIN查看执行计划时,若type列为"ALL"或key为NULL,说明索引未命中。我曾用这个方法发现某核心接口95%的查询都在全表扫描,优化后RT从800ms降到50ms。
2. 查询重写的艺术
2.1 从EXISTS到JOIN的蜕变
很多开发习惯使用EXISTS子查询,但实际测试表明JOIN通常效率更高。对比以下两种写法:
sql复制-- 写法1:EXISTS
SELECT * FROM orders o
WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id=o.user_id AND u.vip_level>3
);
-- 写法2:JOIN
SELECT o.* FROM orders o
JOIN users u ON o.user_id=u.id
WHERE u.vip_level>3;
在百万级数据测试中,JOIN版本比EXISTS快40%。这是因为:
- EXISTS需要对外层每条记录执行子查询
- JOIN可以先过滤users表再关联,处理数据量更少
- 现代优化器对JOIN的处理更成熟
2.2 分页查询的终极优化
常见的分页写法存在严重性能问题:
sql复制SELECT * FROM large_table
ORDER BY id LIMIT 1000000, 10;
这种写法会先读取1000010条记录再丢弃前100万条。优化方案:
方案1:延迟关联
sql复制SELECT t.* FROM large_table t
JOIN (
SELECT id FROM large_table
ORDER BY id LIMIT 1000000, 10
) tmp ON t.id=tmp.id;
方案2:游标分页(适合有序字段)
sql复制-- 第一页
SELECT * FROM large_table
ORDER BY id LIMIT 10;
-- 后续页(传上一页最后一条记录的id)
SELECT * FROM large_table
WHERE id > last_id
ORDER BY id LIMIT 10;
实测在1000万数据量下,方案2比传统分页快200倍以上。
2.3 聚合查询的优化技巧
案例:统计每个分类的商品数量
sql复制-- 低效写法
SELECT category_id, COUNT(*)
FROM products
GROUP BY category_id;
-- 优化写法(利用覆盖索引)
SELECT category_id, COUNT(*)
FROM products
FORCE INDEX(idx_category)
GROUP BY category_id;
关键点:
- 确保GROUP BY字段上有索引
- 使用FORCE INDEX提示避免优化器误判
- 考虑使用物化视图预计算高频统计
3. 执行计划深度解析
3.1 EXPLAIN全景解读
执行计划中的关键字段解析:
- type:从优到差依次为 system > const > eq_ref > ref > range > index > ALL
- possible_keys:可能使用的索引
- key:实际使用的索引
- rows:预估扫描行数(与实际偏差大时需要analyze table)
- Extra:重要提示如"Using filesort"(需要优化)
3.2 真实案例诊断
问题SQL:
sql复制SELECT u.name, o.order_no
FROM users u
JOIN orders o ON u.id=o.user_id
WHERE u.register_time>'2023-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
执行计划分析:
- 发现对register_time的筛选率只有5%,但使用了全表扫描
- ORDER BY导致filesort(内存或磁盘排序)
- 两表关联使用嵌套循环效率低
优化方案:
- 为users表添加(register_time,id,name)联合索引
- 为orders表添加(user_id,create_time,order_no)联合索引
- 改写为:
sql复制SELECT u.name, o.order_no
FROM (
SELECT id, name FROM users
WHERE register_time>'2023-01-01'
) u
JOIN orders o ON u.id=o.user_id
ORDER BY o.create_time DESC
LIMIT 100;
优化后查询时间从1.2s降至80ms。
4. 高级优化策略
4.1 索引跳跃扫描
MySQL 8.0新增的优化手段,即使不满足最左前缀原则,也可能使用索引。例如有索引(a,b):
sql复制SELECT * FROM table WHERE b=1;
当a列的区分度很低时(如性别),优化器会先扫描a的所有可能值,再查找b=1的记录。虽然比直接索引查询慢,但远优于全表扫描。
4.2 直方图统计信息
MySQL 8.0引入的直方图功能,为非索引列提供数据分布统计:
sql复制ANALYZE TABLE products
UPDATE HISTOGRAM ON price WITH 100 BUCKETS;
这能帮助优化器对没有索引的列做出更好的选择,比如决定是否使用某个索引。
4.3 代价模型优化
通过调整优化器成本常数可以影响执行计划选择:
sql复制UPDATE mysql.server_cost
SET cost_value=0.5
WHERE cost_name='row_evaluate_cost';
UPDATE mysql.engine_cost
SET cost_value=2.0
WHERE cost_name='io_block_read_cost';
这在SSD服务器上特别有用,可以降低随机IO的成本估值。
5. 实战避坑指南
5.1 隐式排序陷阱
sql复制SELECT * FROM table WHERE a=1 AND b=2;
即使有(a,b)索引,如果a=1的记录很多,实际执行可能变成:
- 用索引找到所有a=1的记录
- 对这些记录在内存中过滤b=2
解决方案:确保高区分度列在前,或使用FORCE INDEX。
5.2 临时表与排序优化
当出现"Using temporary; Using filesort"时:
- 增大sort_buffer_size
- 考虑使用索引优化排序
- 对于GROUP BY,尝试添加ORDER BY NULL避免排序
5.3 连接缓冲区优化
对于无法使用索引的JOIN操作:
sql复制SET join_buffer_size=256*1024; -- 默认128KB
但要注意:
- 每个连接都会分配独立缓冲区
- 设置过大会导致内存浪费
6. 性能监控体系
6.1 慢查询日志深度配置
ini复制# my.cnf配置
slow_query_log=1
slow_query_log_file=/var/log/mysql-slow.log
long_query_time=0.5 # 超过500ms的记录
log_queries_not_using_indexes=1
log_throttle_queries_not_using_indexes=60 # 每分钟最多记录60条
6.2 Performance Schema实战用法
查询最耗时的SQL:
sql复制SELECT digest_text, count_star,
avg_timer_wait/1000000000 as avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;
6.3 可视化监控方案
推荐组合:
- Prometheus + Grafana监控数据库指标
- pt-query-digest分析慢日志
- MySQL Workbench性能仪表板
7. 真实案例复盘
7.1 电商订单查询优化
原始情况:
- 订单表5000万数据
- 查询商家近3个月订单需要8秒
- 有(shop_id,create_time)索引但未使用
问题诊断:
- 查询使用
WHERE shop_id=? AND create_time BETWEEN ? AND ? - 发现create_time是字符串类型(yyyy-MM-dd HH:mm:ss)
- 存在隐式类型转换导致索引失效
解决方案:
- 修改字段类型为TIMESTAMP
- 重建索引
- 查询时间降至200ms
7.2 社交平台Feed流优化
挑战:
- 用户关注关系表2亿条数据
- 需要实时获取关注用户的动态
- 传统JOIN方式超时
最终方案:
- 使用Redis维护用户关注列表
- 动态数据写入时同步到粉丝的收件箱
- 查询时直接获取预聚合数据
- 99分位响应时间从3s降至200ms