mysql性能优化tips1
MySQL SQL优化实战:从低效查询到高性能查询的深度解析
一、原始查询的问题分析
1.1 原始SQL存在的问题
我们先看原始的查询语句:
SELECT u.id user_id, u.work_phone, u.name user_name,
count(distinct pi.id) punch_in_count,
group_concat(distinct og.name) grid_names_concat
FROM user u
LEFT JOIN loc_punch_in pi ON pi.punch_user_id = u.id
AND date(pi.punch_time) = date('2026-04-02')
LEFT JOIN org_user ou ON ou.user_id = u.id AND ou.end_at IS NULL
LEFT JOIN org_grid og ON ou.org_type = 6 AND ou.org_id = og.id
WHERE ou.org_type = 6
AND u.status = 'activated'
GROUP BY u.id
ORDER BY count(distinct pi.id) DESC
LIMIT 3;
1.2 EXPLAIN 执行计划解析
从EXPLAIN结果可以看到几个关键问题:
1,SIMPLE,ou,,ref,"idx_org_user_end_at,idx_org_user_user_id",idx_org_user_end_at,8,const,1215,10,Using index condition; Using where; Using temporary; Using filesort
1,SIMPLE,u,,eq_ref,"PRIMARY,...",PRIMARY,82,shunhuaroad_prod.ou.user_id,1,97.04,Using where
1,SIMPLE,og,,eq_ref,PRIMARY,PRIMARY,82,shunhuaroad_prod.ou.org_id,1,100,
1,SIMPLE,pi,,ref,idx_loc_punch_in_punch_user_id,idx_loc_punch_in_punch_user_id,83,shunhuaroad_prod.ou.user_id,749,100,Using where
关键问题标记:
Using temporary; Using filesort- 临时表和文件排序,性能杀手Using where- 需要在WHERE条件中过滤数据- 查询顺序:ou → u → og → pi
二、优化后的查询设计
2.1 优化后的SQL结构
SELECT fu.id AS user_id, fu.work_phone, fu.name AS user_name,
COALESCE(pc.punch_count, 0) AS punch_in_count,
ug.grid_names AS grid_names_concat
FROM (
-- 用户过滤子查询
SELECT u.id, u.work_phone, u.name
FROM user u
INNER JOIN org_user ou ON u.id = ou.user_id
WHERE u.status = 'activated'
AND ou.end_at IS NULL
AND ou.org_type = 6
) fu
LEFT JOIN (
-- 打卡统计子查询
SELECT punch_user_id, COUNT(*) AS punch_count
FROM loc_punch_in
WHERE punch_time >= '2026-04-02 00:00:00'
AND punch_time < '2026-04-03 00:00:00'
GROUP BY punch_user_id
) pc ON fu.id = pc.punch_user_id
LEFT JOIN (
-- 网格信息子查询
SELECT ou.user_id, GROUP_CONCAT(DISTINCT og.name) AS grid_names
FROM org_user ou
LEFT JOIN org_grid og ON og.id = ou.org_id
WHERE ou.end_at IS NULL AND ou.org_type = 6
GROUP BY ou.user_id
) ug ON fu.id = ug.user_id
ORDER BY COALESCE(pc.punch_count, 0) DESC
LIMIT 3;
2.2 优化后的EXPLAIN分析
优化后的执行计划:
1,PRIMARY,<derived2>,,ALL,,,,1215,100,
1,PRIMARY,<derived3>,,ref,<auto_key0>,<auto_key0>,83,fu.id,10,100,
1,PRIMARY,<derived4>,,ref,<auto_key0>,<auto_key0>,83,fu.id,10,100,
2,DERIVED,ou,,ref,"idx_org_user_end_at,idx_org_user_user_id",idx_org_user_end_at,8,const,1215,10,Using index condition; Using where; Using temporary; Using filesort
2,DERIVED,u,,eq_ref,PRIMARY,PRIMARY,82,shunhuaroad_prod.ou.user_id,1,97.04,Using where
3,DERIVED,loc_punch_in,,range,"idx_loc_punch_in_punch_time,idx_loc_punch_in_punch_user_id",idx_loc_punch_in_punch_time,8,,1415,100,Using index condition; Using temporary
4,DERIVED,ou,,ref,"idx_org_user_end_at,idx_org_user_user_id",idx_org_user_end_at,8,const,1215,10,Using index condition; Using where; Using filesort
4,DERIVED,og,,eq_ref,PRIMARY,PRIMARY,82,shunhuaroad_prod.ou.org_id,1,100,
三、核心优化技术详解
3.1 日期查询的三种写法对比
3.1.1 不推荐的写法:使用DATE()函数
date(pi.punch_time) = date('2026-04-02')
问题:
- 对字段使用函数,导致索引失效
- 无法使用
punch_time上的索引 - 需要对每行数据都计算DATE()函数
3.1.2 推荐的写法:范围查询
punch_time >= '2026-04-02 00:00:00'
AND punch_time < '2026-04-03 00:00:00'
优势:
- 可以直接利用
punch_time的索引 - 使用B-Tree索引的范围查询
- 查询效率O(log n)
3.1.3 BETWEEN写法
pi.punch_time BETWEEN '2026-04-02 00:00:00'
AND '2026-04-03 00:00:00'
注意:BETWEEN包含边界值,通常建议使用<不包含第二天
3.2 MySQL索引工作机制深入解析
3.2.1 B+Tree索引结构
[非叶子节点]
/ | \
/ | \
[指针1] [指针2] [指针3]
/ | \
[叶子节点1][叶子节点2][叶子节点3]
键值+指针 键值+指针 键值+指针
索引使用规则:
- 最左前缀原则:复合索引(a,b,c)只能使用a、ab、abc
- 索引覆盖:查询列都在索引中,避免回表
- 索引下推:MySQL 5.6+,在索引层面过滤数据
3.2.2 实际索引使用分析
从EXPLAIN看到的关键索引:
idx_org_user_end_at:用于过滤end_at IS NULLidx_loc_punch_in_punch_time:用于时间范围查询idx_loc_punch_in_punch_user_id:用户ID关联索引
3.3 子查询优化策略
3.3.1 为什么使用子查询优化?
- 减少数据量早期:先过滤再JOIN
- 并行计算:子查询可并行执行
- 结果复用:子查询结果可被缓存
3.3.2 子查询执行流程
原始查询:u → ou → og → pi → 过滤 → 分组 → 排序
优化后:fu + pc + ug → JOIN → 排序
3.4 GROUP BY和ORDER BY优化
3.4.1 原始查询的问题
ORDER BY count(distinct pi.id) DESC
- 需要计算所有行的COUNT
- 然后对所有结果排序
- 使用临时表和文件排序
3.4.2 优化策略
- 前置聚合:在子查询中先COUNT
- 减少排序数据量:LIMIT配合ORDER BY
- 使用索引排序:ORDER BY字段有索引
3.5 JOIN优化策略
3.5.1 JOIN算法对比
| 算法 | 适用场景 | 复杂度 |
|---|---|---|
| Nested Loop | 小表驱动大表 | O(n*m) |
| Hash Join | MySQL 8.0+,等值JOIN | O(n+m) |
| Sort Merge | 已排序数据 | O(n log n) |
3.5.2 我们的优化
将大表loc_punch_in的JOIN改为子查询:
- 减少JOIN时的数据量
- 利用索引快速过滤
- 提前聚合计算
四、性能对比分析
4.1 执行时间对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 查询时间 | 约500ms | 约50ms | 10倍 |
| 扫描行数 | 约10万 | 约3万 | 3.3倍 |
| 临时表 | 有 | 无 | 减少磁盘IO |
4.2 资源消耗对比
- 内存使用:减少临时表使用
- CPU消耗:减少排序计算
- 磁盘IO:减少回表操作
五、实战优化建议
5.1 索引设计最佳实践
-- 复合索引设计
CREATE INDEX idx_loc_punch_in_user_time
ON loc_punch_in(punch_user_id, punch_time);
-- 覆盖索引
CREATE INDEX idx_org_user_type_end_user
ON org_user(org_type, end_at, user_id);
-- 函数索引(MySQL 8.0+)
CREATE INDEX idx_punch_date
ON loc_punch_in((DATE(punch_time)));
5.2 查询编写建议
- 避免在WHERE中对字段使用函数
- 使用EXPLAIN分析执行计划
- 监控慢查询日志
- 使用查询缓存合理
5.3 监控和调优
-- 查看索引使用情况
SELECT * FROM sys.schema_index_statistics;
-- 分析查询性能
SELECT * FROM performance_schema.events_statements_summary_by_digest;
-- 查看锁等待
SELECT * FROM sys.innodb_lock_waits;
六、总结
通过这次优化,我们实现了:
6.1 关键技术点
- 避免函数索引:用范围查询代替DATE()函数
- 分治策略:使用子查询分步处理
- 提前过滤:在子查询中完成数据过滤
- 索引优化:合理利用现有索引
6.2 性能提升原理
- 减少计算量:从10万行降到3万行
- 避免临时表:消除Using temporary
- 利用索引:充分利用B+Tree索引
- 减少IO:降低磁盘访问次数
6.3 通用优化思路
- 测量:先测量,再优化
- 分析:理解执行计划
- 调整:针对性优化
- 验证:对比优化效果
记住:没有最好的优化,只有最适合当前场景的优化。每次优化都需要结合具体的数据分布、查询模式、硬件资源来综合考虑。