1. 从3秒到50毫秒的SQL优化实战
去年双十一大促期间,我们团队接手了一个棘手的案例:某头部电商平台的订单查询接口在高峰期响应时间飙升至3秒以上,直接导致用户流失率上升1.8个百分点。经过72小时紧急优化,最终将查询耗时稳定控制在50毫秒内,成功支撑了单日1.2亿次查询请求。这个案例让我深刻认识到,SQL优化不是纸上谈兵的理论,而是直接影响业务存亡的关键技能。
2. 索引策略的深度实践
2.1 复合索引的最左匹配实战
在用户订单查询场景中,我们遇到这样一个典型问题:
SELECT * FROM orders WHERE user_id = 100 AND create_time > '2023-01-01'最初这张表只有单列索引,导致查询需要全表扫描120万行记录。我们通过以下步骤进行优化:
- 索引设计:创建复合索引
(user_id, create_time) - 执行计划验证:EXPLAIN显示
type=range,Extra=Using index condition - 性能对比:
- 扫描行数:120万 → 2000行
- 执行时间:3.2秒 → 0.04秒
关键经验:复合索引的字段顺序必须严格遵循最左前缀原则。当遇到范围查询时,范围条件字段应该放在索引最后。
2.2 覆盖索引的极致优化
商品搜索接口存在严重的I/O瓶颈:
SELECT * FROM products WHERE name LIKE '智能手机%'优化方案:
- 分析发现查询结果只需要id和name字段
- 创建覆盖索引
(name, id) - 改写SQL为:
SELECT id, name FROM products WHERE name LIKE '智能手机%'优化效果:
- EXPLAIN显示
Extra=Using index - 磁盘I/O减少85%
- 执行时间:1.8秒 → 0.12秒
2.3 前缀索引的空间优化技巧
用户表的address字段存在存储浪费:
CREATE INDEX idx_address ON users(address) -- 原始索引优化步骤:
- 分析字段内容分布:
SELECT COUNT(DISTINCT LEFT(address,10))/COUNT(*) AS ratio10, COUNT(DISTINCT LEFT(address,15))/COUNT(*) AS ratio15 FROM users - 确定前15字符可保证99%唯一性
- 创建前缀索引:
CREATE INDEX idx_address_prefix ON users(address(15))优化效果:
- 索引大小减少70%
- 查询性能保持稳定
- 模糊查询时间:2.5秒 → 0.3秒
3. 查询改写的高级技巧
3.1 分页查询的性能陷阱
传统分页写法在深度分页时性能急剧下降:
SELECT * FROM orders LIMIT 10000, 10优化方案:
SELECT * FROM orders WHERE id > (SELECT id FROM orders ORDER BY id LIMIT 10000, 1) LIMIT 10性能对比:
- 原查询:扫描10010行,耗时2.8秒
- 优化后:扫描10行,耗时0.03秒
踩坑记录:曾遇到MySQL 5.7版本下子查询性能问题,改用JOIN写法解决:
SELECT t1.* FROM orders t1 JOIN (SELECT id FROM orders ORDER BY id LIMIT 10000,1) t2 WHERE t1.id >= t2.id LIMIT 10
3.2 JOIN操作的索引黄金法则
多表关联查询优化案例:
SELECT u.username, o.order_id FROM users u JOIN orders o ON u.id = o.user_id WHERE o.create_time > '2023-01-01'优化步骤:
- 确保
users.id有主键索引 - 为orders表创建复合索引
(user_id, create_time) - 执行计划显示
type=eq_ref
优化效果:
- 执行时间:4.5秒 → 0.15秒
- 扫描行数:80万 → 200行
3.3 函数陷阱与解决方案
日期查询的常见错误:
SELECT * FROM orders WHERE YEAR(create_time) = 2023 -- 导致索引失效正确写法:
SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01'性能对比:
- 原查询:全表扫描,5.8秒
- 优化后:索引范围扫描,0.2秒
4. EXPLAIN执行计划精读
4.1 访问类型性能图谱
通过分析100+个慢查询案例,我总结出type字段的性能分级:
| 访问类型 | 扫描方式 | 性能等级 |
|---|---|---|
| system | 系统表单行 | ★★★★★ |
| const | 主键等值查询 | ★★★★★ |
| eq_ref | 关联主键匹配 | ★★★★☆ |
| ref | 普通索引等值 | ★★★☆☆ |
| range | 索引范围扫描 | ★★★☆☆ |
| index | 全索引扫描 | ★★☆☆☆ |
| ALL | 全表扫描 | ★☆☆☆☆ |
4.2 Extra字段的预警信号
在商品分类查询优化中,我们发现:
EXPLAIN SELECT category,COUNT(*) FROM products GROUP BY category显示Using temporary; Using filesort,说明存在性能瓶颈。
优化方案:
- 为category字段添加索引
- 使用固定排序:
SELECT category,COUNT(*) FROM products GROUP BY category ORDER BY category优化后Extra字段变为Using index。
5. 索引维护的工程实践
5.1 索引数量的平衡艺术
在某金融系统优化中,我们发现:
- 表字段数:32个
- 索引数:12个
- 写入性能下降40%
优化措施:
- 通过性能模式识别冗余索引:
SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 'db_name'- 删除3个从未使用的索引
- 合并2个功能重叠的索引
优化效果:
- 写入TPS提升35%
- 查询性能保持稳定
5.2 索引碎片化监控方案
我们建立的自动化监控体系:
- 每周执行碎片率检查:
SELECT table_name, index_name, round(stat_value * @@innodb_page_size / 1024 / 1024, 2) size_mb, stat_description FROM mysql.innodb_index_stats WHERE stat_name = 'size' AND database_name = 'db_name'- 当碎片率>30%时触发OPTIMIZE TABLE
- 大表采用在线重建:
ALTER TABLE orders ENGINE=InnoDB5.3 索引创建的最佳时段
在电商系统中,我们制定索引维护时间窗:
- 业务低峰期(凌晨2:00-4:00)
- 采用在线DDL工具:
pt-online-schema-change \ --alter "ADD INDEX idx_new(column)" \ D=database,t=table \ --execute- 监控长事务避免锁冲突
6. 真实案例中的避坑指南
在最近一个物流系统优化中,我们遇到一个隐蔽的性能问题:即使有理想索引,查询仍然很慢。最终发现是字符集不匹配导致索引失效:
-- 表结构 CREATE TABLE waybills ( id INT PRIMARY KEY, waybill_no VARCHAR(20) CHARACTER SET utf8mb4, INDEX idx_waybill(waybill_no) ) -- 查询语句(应用使用utf8字符集) SELECT * FROM waybills WHERE waybill_no = CONVERT('快递单号' USING utf8)解决方案:
- 统一字符集为utf8mb4
- 使用强制索引提示:
SELECT * FROM waybills FORCE INDEX(idx_waybill) WHERE waybill_no = '快递单号'这个案例让我养成了检查字符集和排序规则的习惯:
SHOW CREATE TABLE table_name; SHOW VARIABLES LIKE 'character_set%';在索引优化这条路上,每个性能问题背后都可能藏着意想不到的细节。我建议每个DBA都应该建立自己的检查清单,包括但不限于:字符集一致性、隐式类型转换、统计信息准确性、隔离级别影响等。只有系统化的排查方法,才能确保优化效果持久稳定。