1. SQL优化实战:索引策略与Explain分析的深度解析
刚处理完一个生产环境的慢查询问题,查询响应时间从12秒降到0.2秒。这让我想起五年前第一次面对SQL优化时的茫然——当时连执行计划都看不懂,现在却能通过索引策略和Explain分析快速定位瓶颈。今天就把这些年积累的实战经验系统梳理出来,特别要分享那些官方文档不会告诉你的"野路子"技巧。
SQL优化本质上是在解决数据库的"沟通效率"问题。就像快递员送包裹,索引是导航地图,执行计划是配送路线,而Explain就是路线规划说明书。当查询变慢时,我们需要通过索引策略调整"地图精度",通过Explain分析找出"绕路路段"。下面我会用电商、社交、物联网三个典型场景的案例,拆解索引设计的思维过程和Explain的深度解读方法。
1.1 为什么优化总从索引开始?
去年双十一压测时,我们有个商品搜索接口在1000QPS时CPU直接打满。检查发现这个LIKE查询竟然全表扫描了2000万行数据:
SELECT * FROM products WHERE name LIKE '%智能%' AND status = 1 ORDER BY sales DESC当时紧急加了(status, sales)的复合索引,但效果甚微。后来改成(status, name, sales)的索引,性能提升80%。这里有个关键认知:索引不仅是加速查询的工具,更是改变执行路径的开关。通过索引我们其实是在告诉优化器:"数据在这条路上走更快"。
关键认知:索引顺序必须匹配查询的"筛选漏斗"——把过滤性最强的条件放最左。上例中status=1能过滤掉70%数据,比name的模糊匹配更高效。
2.1 Explain执行计划的黑盒破解
很多人看Explain只关注type列是不是"index",这就像看病只量体温。去年我们有个订单查询出现诡异现象:
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND create_time > '2023-01-01'显示用了(user_id, create_time)索引,但实际扫描行数却是50万。原来是因为:
- 该用户是测试账号,历史订单占比极高
- 索引第二列的范围查询导致后续索引失效
这种情况需要"索引跳跃扫描"技巧:
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);通过引入低基数的status字段(如已支付/未支付),让范围查询落在索引第三列。这是B+树索引的特性决定的——就像查字典时不能先按第2个字母检索。
2.1.1 执行计划中的隐藏信号
这几个关键指标90%的人会忽略:
filtered列:显示条件过滤的实际效率Using index condition:是否用到索引下推Using filesort的真实代价(内存排序还是磁盘临时表)
去年我们通过监控Using filesort的sort_buffer_size使用情况,发现一个分页查询竟然用了800MB排序内存。后来通过optimizer_switch调整了优先使用索引排序的策略。
3.1 复合索引设计的黄金法则
在社交平台的feed流场景中,我们设计过这样一个索引:
ALTER TABLE posts ADD INDEX idx_geo_tag_time ( geo_hash_prefix, tag_id, is_del, create_time DESC );这个设计包含三个层级策略:
- 空间维度:用geo_hash前缀快速定位同城内容
- 内容维度:按标签二次过滤
- 时间维度:保证新内容优先
特别注意is_del这个看似多余的字段——实际能过滤掉30%的已删除内容。这种"索引包含查询"的设计,避免了回表操作带来的随机IO。
3.1.1 索引维护的实战技巧
有个容易踩的坑:线上直接添加大表索引导致锁表。我们现在的标准操作流程:
- 先在从库用
ALGORITHM=INPLACE测试添加耗时 - 使用pt-online-schema-change工具
- 在业务低峰期分批创建(特别是文本索引)
去年一个VARCHAR(255)字段的全文索引,在2000万数据量下创建耗时从4小时优化到40分钟,关键就是调整了innodb_sort_buffer_size参数。
4.1 Explain的进阶玩法
大多数教程只教基础执行计划解读,但实战中我们需要关注:
4.1.1 代价估算的准确性验证
通过EXPLAIN FORMAT=JSON可以获取更详细的成本计算:
{ "query_cost": "1023.76", "cost_info": { "eval_cost": "200.00", "io_cost": "823.76" } }曾经有个查询优化器误判了JOIN顺序,导致选择了比实际慢5倍的执行计划。通过optimizer_trace功能,我们发现是因为统计信息过期,手动执行ANALYZE TABLE后解决了问题。
4.1.2 索引合并的陷阱
看到Using union(idx_a,idx_b)别高兴太早——这可能是设计缺陷的信号。我们遇到过一个案例:
SELECT * FROM users WHERE mobile = '13800138000' OR email = 'admin@example.com'优化器选择了索引合并,但实际性能还不如全表扫描。最终解决方案是:
- 建立(mobile,email)的复合索引
- 业务层拆分成两个查询UNION ALL
5.1 特殊场景的优化策略
5.1.1 分页查询的终极方案
"深分页"是经典难题。我们对比过三种方案:
- 常规分页:
LIMIT 10000,20- 问题:需要先读取10020行再丢弃
- 延迟关联:
SELECT * FROM users u JOIN (SELECT id FROM users WHERE status=1 ORDER BY id LIMIT 10000,20) tmp ON u.id = tmp.id- 优势:内层查询只需走索引
- 游标分页:
SELECT * FROM users WHERE status=1 AND id > 上次最后ID ORDER BY id LIMIT 20- 适合无限滚动场景
实测在1000万数据量下,方案3比方案1快300倍。
5.1.2 JSON数据的高效查询
随着MySQL 8.0的JSON支持增强,我们总结出这些技巧:
- 对高频查询的JSON路径建立虚拟列+索引
- 使用
JSON_CONTAINS替代LIKE '%value%' - 多值查询时
MEMBER OF()比JSON_OVERLAPS更高效
有个物联网项目,设备上报的JSON数据经过优化后,查询速度从1200ms降到80ms。
6.1 监控与持续优化
我们团队现在使用这套监控体系:
- 慢查询实时捕获:通过
pt-query-digest分析模式变化 - 索引使用统计:定期检查
sys.schema_unused_indexes - 执行计划基线:用
optimizer_use_plan_baselines防止计划回退
上个月刚通过这个体系发现一个新增索引完全未被使用,及时进行了清理。这里有个经验值:单表索引数超过5个就需要警惕,特别是存在冗余索引时。
最后分享一个真实案例:某核心接口TP99从800ms降到90ms的完整过程。通过EXPLAIN发现虽然走了索引,但需要回表查8个字段。解决方案是:
- 创建覆盖索引
(a,b,c)包含所有查询字段 - 使用
FORCE INDEX临时锁定执行计划 - 重构业务代码减少查询字段数
这个案例让我深刻认识到:优化不是一次性的工作,而是需要建立持续监控、快速响应的完整机制。