跳转到内容

SQL调优

💡

SQL调优是后端开发的核心技能。数据库数据存储在磁盘中,每次查询都需要IO操作,编写高效的SQL减少IO次数是关键。底层B+树也是为提高查询效率而设计的。本文档从索引、SQL编写、架构设计等维度系统梳理SQL优化技巧。


一、索引优化

1.1 索引的本质

📌

索引就类似于书本的目录,建立索引可以实现快速查找,避免全表扫描。索引是SQL优化最基础、最重要的手段。

1.2 创建索引的规则

  1. 索引数量控制:单表索引不超过6个。过多的索引会降低INSERT和UPDATE操作,因为增删改操作可能需要重建索引,B+树需要进行裂变
  2. 高区分度优先:如性别(sex)这种区分度极低的字段,建立索引几乎无效(只有男女两个值),不值得建索引
  3. 联合索引:多个条件经常一起查询时,建立联合索引提高效率。遵循最左前缀原则
  4. 使用EXPLAIN分析:通过EXPLAIN查看SQL语句的执行计划,判断是否走索引、扫描行数、使用的索引等

1.3 EXPLAIN 执行计划解读

关键字段

含义

type

访问类型,从好到差:const(主键/唯一索引常量匹配) → eq_ref(唯一join) → ref(非唯一索引) → range(范围扫描) → index(全索引扫描) → ALL(全表扫描,最差需避免)

key

实际使用的索引名(NULL表示未使用索引)

rows

MySQL估算需要扫描的行数(越小越好)

Extra

Using index(覆盖索引,好) / Using where / Using filesort(需排序优化) / Using temporary(需临时表优化)

key_len

使用的索引长度,可判断联合索引用了几个字段


二、避免索引失效

💡

索引失效是SQL性能问题的头号原因。以下6种常见场景会导致索引失效。

2.1 左模糊查询

  • 失效原因:SELECT * FROM table WHERE name LIKE '%周'——%放在最前面,B+树无法利用索引有序性
  • 解决:大量模糊搜索需求推荐使用ElasticSearch,基于倒排索引,专为全文搜索设计
  • 注意:LIKE '周%'可以使用索引(右模糊),因为索引有序

2.2 使用OR连接

  • 失效原因:OR连接后面的字段如果没有建立索引,整个查询可能不走索引
  • 解决:OR连接的所有字段都要有索引;或用UNION ALL替代OR查询

2.3 索引列存在计算/函数操作

  • 失效原因:对索引列做函数计算或表达式计算,如 WHERE DATE(create_time) = '2025-01-01'
  • 解决:将计算移到等号右边:WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'
  • 同理:WHERE age + 10 = 30 不会走索引,改为 WHERE age = 20

2.4 数据类型不匹配(隐式类型转换)

当查询条件中的值类型与索引列的数据类型不匹配时,数据库会自动进行隐式类型转换。索引是基于原始数据类型构建的,类型转换意味着数据库必须对所有记录逐一扫描完成转换,无法直接利用索引。

  • 典型场景:列是INT类型,查询写成 WHERE id = '123'(字符串)→ MySQL会自动将字符串转数字,不会失效。但反过来,列是VARCHAR,WHERE phone = 13800138000(数字)→ 字符串索引会失效
  • 解决:传参时保持类型一致,后端代码中传String就用引号,传Integer就不加引号

2.5 不等于/范围查询

  • 使用 != 或 <> 可能导致索引失效;范围查询(>, <, BETWEEN)在数据量大时索引效率降低
  • 联合索引中,范围查询后的字段索引失效(如联合索引(a,b,c),WHERE a=1 AND b>10 AND c=5,c不走索引)

三、SQL编写优化

3.1 避免SELECT *

📌

SELECT * 不会走覆盖索引,存在大量回表操作,导致SQL性能降低。需要哪些列就查哪些列。

覆盖索引 (Covering Index)

  • 定义:查询语句中需要的所有列都能从索引中获取,不需要回表(访问实际数据表)
  • 优势:只读索引即可返回结果,减少一次IO回表操作
  • 示例:联合索引(user_id, user_name, age),SELECT user_id, user_name FROM user WHERE user_id = 1 → Using index,直接从索引获取,不需要回表

回表详解

  • 二级索引叶子节点存储的是索引列 + 主键值。若查询列不全在索引中,需用主键去聚簇索引中查完整行记录——这就是回表
  • 索引覆盖可以完全避免回表,但索引列越长则索引文件越大

3.2 小表驱动大表

📌

MySQL表关联使用Nest Loop Join算法,以驱动表的结果集作为循环基础数据,一条一条地去关联表中查询匹配数据。

  • 原则:用小表(结果集小的表)作为驱动表,减少外层循环次数
  • 例如:140条记录的小表作为驱动表去关联20万条记录的大表,外层循环140次;反过来20万次外层循环性能差很多
  • 实践:LEFT JOIN时,左表是驱动表;INNER JOIN时,MySQL优化器通常自动选小表做驱动表

3.3 使用LIMIT优化

  • 单条查询:查询某条件是否存在时,用 LIMIT 1 查到第一条即返回,不用扫描全部满足条件的数据
  • 分批处理:大批量操作时用LIMIT分批次执行,避免长事务和锁表

3.4 连接查询代替子查询

  • 原因:子查询会产生临时表,查询结束后需删除。如果临时表数据量大,性能和内存消耗严重
  • 推荐:能用JOIN解决的尽量用JOIN,MySQL对JOIN的优化更成熟

3.5 UNION ALL 代替 UNION

对比

说明

UNION

合并两个SELECT结果并自动去重(需额外排序和去重计算),性能较低

UNION ALL

合并所有结果保留重复行,不需要去重计算,性能更高

  • 推荐:如果业务逻辑上不需要去重(或数据本身不重复),优先使用UNION ALL

3.6 优化 GROUP BY

  • HAVING vs WHERE:WHERE在分组前过滤数据(减少分组数据量),HAVING在分组后过滤。能用WHERE的先WHERE,缩小数据量再GROUP BY
  • 示例:GROUP BY user_id HAVING user_id <= 200 → 改为 WHERE user_id <= 200 GROUP BY user_id(先缩小范围再分组,效率更高)

四、深度分页优化

4.1 问题分析

💡

LIMIT 10000000, 100 会扫描前面全部1000万条数据,然后丢弃,只取最后100条。越往后翻页越慢,这就是深度分页问题。

4.2 游标分页(推荐)

  • 原理:WHERE id > 上次最后一条的id ORDER BY id LIMIT 10(利用主键索引快速定位)
  • 前提:主键自增或有序,适合无限滚动场景。缺点:不支持跳页

4.3 子查询优化

📌

核心思路:把条件转移到主键索引树,减少回表操作。先在二级索引上拿到主键ID,省去大量回表,再根据主键ID查数据。

4.4 延迟关联优化

📌

核心思路与子查询优化相同:把条件转移到主键索引树,减少回表。区别是使用INNER JOIN代替子查询。


五、批量操作优化

5.1 批量INSERT

  • 问题:每条数据都执行一次INSERT,每次都需要建立网络连接、事务开启/提交、索引更新
  • 方案:批量INSERT,一次插入多条记录。建议每次500条左右执行一次批量插入
  • 注意:不能过度批量(如一次插入几万条),会导致长事务、锁等待、undo log膨胀

六、SQL调优总结清单

#

优化手段

关键点

1

建合理索引

高区分度字段、联合索引最左前缀、不超过6个

2

避免索引失效

禁止左模糊、OR全索引、列上计算、隐式转换

3

避免SELECT *

只查需要的列,利用覆盖索引减少回表

4

小表驱动大表

JOIN时小结果集做驱动表

5

连接代替子查询

避免大量临时表创建

6

UNION ALL代替UNION

不需要去重时避免额外排序开销

7

LIMIT优化

配合索引,单条查询用LIMIT 1

8

优化GROUP BY

WHERE前置过滤缩小分组范围

9

深度分页优化

游标分页 / 子查询 / 延迟关联,条件转移到主键索引

10

批量操作

批量INSERT减少网络IO和事务开销

11

EXPLAIN分析

关注type/rows/Extra,避免ALL全表扫描


核心原则:SQL调优的底层逻辑是减少磁盘IO——索引让查询快速定位数据页,覆盖索引减少回表IO,合理的SQL编写避免不必要的扫描和临时表创建。一切优化围绕"少读数据、快读数据"展开。