【Mysql】SQL优化最佳实践
可以把“慢 SQL 优化”理解成一个固定套路:
先找到慢 SQL → 再判断慢在哪里 → 最后针对原因优化。
你这份总结内容比较全,我帮你按面试/学习逻辑重新解释一遍。
一、慢 SQL 怎么定位?
首先你得知道:哪条 SQL 慢。
常用方式是开启慢查询日志。
慢查询日志会记录执行时间超过阈值的 SQL,比如超过 1 秒、2 秒、5 秒的 SQL。
例如:
select*fromorderwhereuser_id=1001;如果它执行了 5 秒,就可能被记录到慢查询日志里。
定位到慢 SQL 后,下一步不是马上加索引,而是先分析它为什么慢。
二、慢 SQL 为什么会慢?
慢 SQL 的原因通常分几类。
1. 数据量太大
比如一张订单表有几千万数据,你查:
select*fromorderwherestatus=1;如果status = 1的数据非常多,即使有索引,也可能很慢。
因为数据库要扫描很多行、返回很多数据。
这类问题可能需要:
加索引 分页 分库分表 冷热数据分离 归档历史数据2. 并发量太大
有时候 SQL 本身不算特别慢,但同时有几百、几千个请求都在查数据库,数据库扛不住。
比如秒杀、首页推荐、热门商品详情页。
这类问题不一定只靠 SQL 优化解决,还需要:
缓存 读写分离 限流 异步处理 数据库扩容3. 索引没用上,或者用得不好
这是慢 SQL 最常见的原因。
比如表中有 100 万条数据:
select*fromuserwherephone='13000000001';如果phone没有索引,就可能全表扫描。
也有些情况是建了索引但没用上,比如:
select*fromuserwhereleft(phone,3)='130';对索引字段使用函数,可能导致索引失效。
4. SQL 写法不合理
比如:
select*fromuser;不管业务是否需要所有字段,都全部查出来,会增加:
解析成本 网络传输成本 内存消耗 回表成本再比如深分页:
select*fromorderorderbyidlimit1000000,10;数据库要先扫描前 1000000 条,然后丢掉,只返回 10 条,所以很慢。
5. 表结构设计不合理
比如一个用户表设计成这样:
id name phone avatar_blob resume_text login_time ...如果里面有很多大字段,比如头像二进制、长文本,普通查询用户信息时也可能受到影响。
这时可以把大字段拆出去:
user 基础表 user_ext 扩展表常用字段放主表,不常用字段放扩展表。
6. 业务设计不合理
有些慢 SQL 不是数据库的问题,而是需求本身不合理。
比如用户非要查第 100000 页:
select*fromorderlimit999990,10;这种深分页本来就很慢。
又比如日报、月报、统计报表,每次页面打开都实时统计:
selectcount(*),sum(amount)fromorderwheredate='2026-05-26';如果数据量很大,实时算就会很慢。
更合理的方式是:提前在低峰期算好结果,页面直接查统计结果。
三、索引优化怎么做?
索引优化是慢 SQL 优化里最核心的一部分。
1. 不是索引越多越好
索引可以提高查询速度,但也有代价。
因为每次插入、更新、删除数据时,不仅要改表数据,还要维护索引树。
比如你有一张表:
user 表上面建了 10 个索引,那么插入一条用户数据时,MySQL 可能要维护 10 棵索引树。
所以索引多了会影响:
插入性能 更新性能 删除性能 磁盘空间 优化器选择执行计划的成本因此一般建议:单表索引不要太多,常见建议控制在 5 个左右。
这个不是绝对规则,而是经验值。
2. 联合索引怎么建?
联合索引不是随便把几个字段拼起来,而是要考虑查询条件。
例如 SQL:
SELECTage,city,nameFROMuserWHEREage=20ANDcity='Beijing'ORDERBYname;可以建立联合索引:
(age,city,name)为什么?
因为查询过程可以理解成:
先用age = 20定位一批数据,再用city = 'Beijing'缩小范围,最后数据在索引中已经按name排好序。
这样可以减少:
扫描行数 回表次数 额外排序 filesort3. 联合索引字段顺序怎么放?
你总结里写了两个原则:
区分度高的放左边 使用频繁的放左边这两个都对,但要补充一点:要结合具体 SQL 来看。
比如联合索引:
(a,b,c)可以命中:
wherea=1wherea=1andb=2wherea=1andb=2andc=3但是下面这种通常不能很好利用这个联合索引:
whereb=2wherec=3因为不满足最左前缀原则。
所以建联合索引时,优先看:
where 条件 order by 条件 group by 条件 字段区分度 字段使用频率四、SQL 语句优化
1. 尽量不要使用 select *
例如:
select*fromuserwhereid=1;如果只需要名字和手机号,应该写成:
selectname,phonefromuserwhereid=1;原因是:
第一,select *会查出很多无用字段。
第二,字段越多,网络传输越大。
第三,如果查询字段都在索引里,可以走覆盖索引;但select *往往需要回表。
例如有索引:
(name,phone)SQL:
selectname,phonefromuserwherename='Tom';可能直接从索引里拿到数据,不用回表。
但如果:
select*fromuserwherename='Tom';就可能需要根据索引找到主键,再回到聚簇索引中查完整数据。
2. join 时,小表驱动大表
比如:
A 表:1000 行 B 表:100 万行如果用 A 表驱动 B 表,大概是:
遍历 A 表 1000 行 每一行去 B 表查匹配数据如果 B 表相关字段有索引,效率比较高。
但如果反过来,用 B 表驱动 A 表:
遍历 B 表 100 万行 每一行去 A 表查匹配数据成本就大很多。
所以一般说:小表驱动大表。
不过严格来说,MySQL 优化器会根据统计信息自动选择驱动表,我们写 SQL 时要尽量让连接条件、索引设计更有利于优化器选择好方案。
3. union all 优先于 union
区别是:
union会合并结果并去重。
unionall只是简单合并,不去重。
例如:
selectnamefromuser_aunionselectnamefromuser_b;MySQL 要额外做去重,可能涉及排序或临时表。
如果业务允许重复,应该用:
selectnamefromuser_aunionallselectnamefromuser_b;效率更高。
五、表结构优化
1. 常一起查的字段放一张表
如果业务中经常同时查:
用户姓名 手机号 头像地址 注册时间这些可以放在一张用户基础表里,避免频繁 join。
但如果有些字段很少查,比如:
个人简介 证件照片 详细备注 扩展配置可以拆到扩展表里。
2. 适当反范式设计
数据库三范式强调减少冗余。
但是在高并发业务里,有时为了性能,会保留一些冗余字段。
例如订单表里保存:
user_id user_name虽然user_name可以通过用户表查到,但为了避免订单查询时频繁 join 用户表,可以冗余一份用户名。
这就是反范式设计。
它的代价是:数据一致性维护更复杂。
比如用户改名后,订单表里的user_name是否要同步更新?这要根据业务决定。
3. 选择合适的数据类型
字段类型越小,存储越省,索引也越小,查询通常越快。
比如年龄字段:
ageint其实没必要,使用:
agetinyint就够了。
再比如状态字段:
statusvarchar(20)如果状态只有 0、1、2,可以用:
statustinyint这样更节省空间。
4. 避免 TEXT、BLOB 大字段
大字段会让表变得很“重”。
比如图片、文件、长文本,不建议直接存在数据库表里。
更常见的做法是:
文件放 OSS / 文件服务器 数据库只存文件 URL如果必须使用大字段,也建议拆到扩展表里。
5. 尽量 NOT NULL
如果字段允许 NULL,MySQL 需要额外记录 NULL 标志位。
另外 NULL 在比较、计算、索引统计时也会更复杂。
例如:
selectavg(score)fromstudent;如果score有 NULL,MySQL 要特殊处理。
所以除非业务上确实需要表达“未知”或“不存在”,否则可以尽量设置为:
notnull并给默认值。
6. 不要用字符串存日期
不要这样设计:
create_timevarchar(20)更推荐:
create_timedatetime或者:
create_timetimestamp因为字符串日期有几个问题:
占用空间更大 比较效率低 容易格式不统一 不能方便使用日期函数 索引效果可能更差比如日期类型可以直接写:
wherecreate_time>='2026-05-01'也可以用日期函数做统计。
7. 单表字段不要太多
如果一张表有几十个、上百个字段,每次查询时行数据很大,会影响 IO 和缓存效率。
可以做冷热分离:
热字段:经常查,经常用 冷字段:很少查,不常用例如:
user 表:id、name、phone、status、create_time user_ext 表:address、intro、remark、extra_config六、业务优化
业务优化其实很重要,因为有些慢 SQL 靠加索引也救不了。
1. 深分页优化
普通分页:
select*fromorderorderbyidlimit1000000,10;很慢。
可以改成基于上一页最后一个 id 查询:
select*fromorderwhereid>1000000orderbyidlimit10;这种叫游标分页,性能更好。
2. 报表提前计算
比如 T+1 报表,意思是统计前一天的数据。
这种没必要用户打开页面时实时算。
可以每天凌晨定时任务算好,把结果放到报表表中:
order_daily_report页面打开时直接查结果表。
七、架构优化
当单纯 SQL 和索引优化不够时,就要考虑架构层面的优化。
1. 读写分离
主库负责写:
insert update delete从库负责读:
select适合读多写少的业务。
比如电商商品详情页,大量用户都在读商品信息,可以走从库。
2. 分库分表
当单表数据量非常大,比如订单表上亿数据,就可以考虑分库分表。
例如按用户 ID 分表:
order_0 order_1 order_2 ...优点是降低单表数据量。
缺点是复杂度明显增加:
跨表查询复杂 分页复杂 事务复杂 运维复杂 数据迁移复杂所以分库分表不是第一选择,通常是数据量确实很大时才考虑。
3. 缓存机制
对于热点数据,可以放到 Redis 中。
比如:
商品详情 首页推荐 用户权限信息 字典配置请求先查 Redis,Redis 没有再查数据库。
这样可以明显减少数据库压力。
不过缓存也有问题:
缓存一致性 缓存击穿 缓存穿透 缓存雪崩所以也要设计好过期时间、互斥锁、布隆过滤器等机制。
4. 分布式数据库
例如 TiDB 这类数据库,可以支持更大的数据规模和更高并发。
但它不是所有场景都适合,通常用于:
数据量大 并发高 需要水平扩展 传统单机 MySQL 不够用八、面试时可以这样回答
如果面试官问:你是怎么优化慢 SQL 的?
可以这样说:
我一般会先通过慢查询日志或者监控系统定位慢 SQL,然后用 EXPLAIN 分析执行计划,重点看 type、key、rows、Extra 等字段,判断是否走了索引、扫描行数是否过大、是否出现 filesort 或 temporary。
如果是索引问题,就根据 where、order by、group by 设计合适的索引,尤其是联合索引,注意最左前缀原则和字段区分度。
如果是 SQL 写法问题,就避免 select *、优化 join、用 union all 替代 union、避免深分页。
如果是表结构问题,就考虑字段类型优化、大字段拆分、冷热分离、适当反范式。
如果是业务或架构问题,就考虑缓存、读写分离、提前计算报表、分库分表等方案。
总的来说,慢 SQL 优化不是单纯加索引,而是从 SQL、索引、表结构、业务和架构多个层面综合分析。
你这份总结的核心可以记成一句话:
慢 SQL 优化的本质,就是减少扫描数据量、减少回表、减少排序、减少 join、减少数据库压力。
