MySQL 索引与 SQL 优化
一、索引核心知识
1. 索引本质与作用
-
本质:是一个有序的数据结构,类似数据库表中数据的 “目录”,基于 B+ 树(主流)、哈希等结构实现,核心目的是避免全表扫描。
-
优缺点:
-
✅ 优点:
-
加速查询(减少磁盘 IO 次数,直接定位数据位置);
-
辅助排序(利用索引有序性,避免全表排序,降低 CPU 消耗);
-
-
❌ 缺点:
-
降低 DML 效率(INSERT/UPDATE/DELETE 时需同步维护索引结构,额外消耗资源);
-
占用额外存储空间(索引文件独立于数据文件,过量索引会浪费磁盘)。
-
-
2. 索引分类(按存储结构 / 功能)
(1)按存储结构分:聚簇索引 vs 二级索引(InnoDB核心)
| 类型 | 特点 | 关联关系 |
|---|---|---|
| 聚簇索引 | 数据与索引共存,叶子节点存储完整数据;一张表仅 1 个聚簇索引 | 1. 主键索引默认是聚簇索引;2. 无主键时,InnoDB 会选第一个唯一非空索引作为聚簇索引;3. 无上述索引时,自动生成隐藏聚簇索引(row_id) |
| 二级索引 | 叶子节点存储聚簇索引键(主键值) ;一张表可多个二级索引 | 查询时需通过二级索引找到主键,再回表查询聚簇索引获取完整数据(回表查询) |
(2)按功能分:常用索引类型
| 索引类型 | 特点 | 适用场景 |
|---|---|---|
| 主键索引 | 非空 + 唯一,默认是聚簇索引(数据与索引共存) | 主键查询(WHERE id=100)、关联查询(JOIN ON id) |
| 唯一索引 | 字段值唯一(可存 NULL),属于二级索引;加速查询 + 数据去重 | 唯一字段查询(手机号、邮箱,WHERE tel='138xxx') |
| 普通索引 | 无约束,属于二级索引;仅用于加速查询 | 高频查询字段(姓名、年龄、部门 ID,WHERE dept_id=1) |
| 联合索引 | 多字段组合(如 idx_name_age(name,age)),属于二级索引;遵循 “最左前缀原则” | 多字段组合查询(WHERE name='张三' AND age=25)、排序(ORDER BY name,age) |
| 全文索引 | 支持长文本关键词匹配(如文章、详情);MySQL 5.6+ 支持 InnoDB 全文索引 | 模糊搜索(MATCH(content) AGAINST('MySQL 优化')) |
(3)单列索引 vs 联合索引(核心对比)
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 单列索引 | 仅包含单个字段的索引;维护成本低,灵活性高,但多字段查询时可能触发回表 / 索引合并 | 单字段高频查询场景(如仅按 dept_id 查询用户) |
| 联合索引 | 包含多个字段的索引;可利用最左前缀法则,支持多字段查询,减少回表次数 | 多字段组合查询 / 排序场景(如 WHERE name='张三' AND age=25、ORDER BY dept_id, age) |
| 核心对比 | 1. 单列索引多字段查询可能触发 “索引合并”(如 idx_name + idx_age),效率低于联合索引;2. 联合索引可实现覆盖索引,单列索引无法做到;3. 联合索引维护成本高于单列索引 | 优先选联合索引(覆盖高频组合查询),避免创建过多单列索引导致 DML 性能下降 |
(4)前缀索引(针对长字符串优化)
-
定义:对字符串字段的前 N 个字符创建索引,而非完整字符串,减少索引存储空间。
-
适用场景:长字符串字段(如地址、简介),完整索引占用空间大,且前 N 个字符已能区分大部分数据。
-
核心:计算选择性:
-
选择性 = 不重复的数量 / 总行数(越接近 1,代表索引项越接近UNIQUE,索引效率越高)。
-
计算 SQL:
-- 计算 name 字段不同前缀长度的选择性(目标:找到最小 N 使选择性接近 1) SELECT COUNT(DISTINCT LEFT(name, 1))/COUNT(*) AS len1, COUNT(DISTINCT LEFT(name, 2))/COUNT(*) AS len2, COUNT(DISTINCT LEFT(name, 3))/COUNT(*) AS len3 FROM user;
-
-
创建语法:
-- 对 name 字段前 3 个字符创建前缀索引 CREATE INDEX idx_name_prefix ON user(LEFT(name, 3)); -- 等价写法 ALTER TABLE user ADD INDEX idx_name_prefix (name(3));
注意:前缀索引的叶节点中只有前缀和主键,所以就算是仅仅查原文也是要回表查询的,而且回表之后拿到的也只是前缀相同的行,需要再比对完整行
3. 索引创建 / 删除语法
(1)创建索引
-- 1. 创建表时指定索引
CREATE TABLE `user` (
`id` INT PRIMARY KEY, -- 主键索引(聚簇索引)
`tel` VARCHAR(11) UNIQUE, -- 唯一索引
`name` VARCHAR(20),
`age` TINYINT,
INDEX `idx_name_age` (`name`, `age`), -- 联合索引
FULLTEXT INDEX `ft_content` (`content`), -- 全文索引
INDEX `idx_name_prefix` (`name`(3)) -- 前缀索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 给已有表添加索引
ALTER TABLE `user` ADD INDEX `idx_dept_id` (`dept_id`); -- 普通索引
ALTER TABLE `user` ADD UNIQUE INDEX `idx_email` (`email`); -- 唯一索引
ALTER TABLE `user` ADD FULLTEXT INDEX `ft_intro` (`intro`); -- 全文索引
-- 3. 直接创建索引(等价于 ALTER TABLE)
CREATE INDEX `idx_gender` ON `user` (`gender`);
-- 查看表中索引
SHOW INDEX FROM 表名;
(2)删除索引
-- 1. ALTER TABLE 删除
ALTER TABLE `user` DROP INDEX `idx_name_age`; -- 普通/唯一/联合/全文/前缀索引
ALTER TABLE `user` DROP PRIMARY KEY; -- 主键索引(需先删除外键约束)
-- 2. DROP INDEX 删除(不能删主键索引)
DROP INDEX `idx_dept_id` ON `user`;
4. 核心索引规则(避坑关键)
(1)最左前缀法则(联合索引核心)
-
定义:联合索引(如
idx_a_b_c(a,b,c))仅支持 “从左到右” 的字段组合查询,中间跳过字段则后续索引失效。 -
注意:与 WHERE 中位置无关,只与是否存在有关。
-
生效 / 失效示例:
SQL 条件 索引使用情况 WHERE a=1生效(使用 a 字段索引) WHERE a=1 AND b=2生效(使用 a+b 字段索引) WHERE a=1 AND b=2 AND c=3生效(使用 a+b+c 字段索引) WHERE b=2失效(跳过左前缀 a) WHERE a=1 AND c=3仅 a 字段生效,c 字段失效(跳过 b)
(2)覆盖索引与回表查询
-
回表查询:通过二级索引(如普通索引)查询时,仅能获取主键值,需再次查询聚簇索引才能拿到完整数据(2 次索引查询)。
-
覆盖索引:查询的字段恰好是索引包含的字段(如联合索引
idx_name_age(name,age),查询SELECT name,age FROM user WHERE name='张三'),无需回表,效率极高。 -
优化建议:高频查询的字段可纳入联合索引,避免回表(如
idx_dept_id_name(dept_id,name)支持SELECT name FROM user WHERE dept_id=1覆盖查询)。
(3)SQL 提示(强制控制索引使用)
-
作用:手动干预 MySQL 优化器的索引选择逻辑,解决 “优化器选错索引” 问题。
-
常用提示:
提示类型 语法示例 作用 USE INDEX SELECT * FROM user USE INDEX(idx_name) WHERE name='张三'提示优化器优先使用指定索引(仅建议,优化器可忽略) IGNORE INDEX SELECT * FROM user IGNORE INDEX(idx_name) WHERE name='张三'提示优化器忽略指定索引 FORCE INDEX SELECT * FROM user FORCE INDEX(idx_name) WHERE name='张三'强制优化器使用指定索引(优先级最高)
(4)索引失效场景(高频踩坑)
-
对索引字段使用函数 / 表达式(如
WHERE DATE(create_time)='2024-01-01');- 失效原因:索引存储的是字段原始值,函数 / 表达式会改变字段原始值的有序性,MySQL 无法通过索引直接匹配计算后的值,只能全表扫描后再计算。
-
字段类型不匹配,隐式转换时(如
tel是 VARCHAR,查询WHERE tel=13800138000未加引号);- 失效原因:MySQL 会对字段做隐式类型转换(如把 VARCHAR 型的
tel转为数字),转换后破坏了索引的有序性,导致索引失效,退化为全表扫描。
- 失效原因:MySQL 会对字段做隐式类型转换(如把 VARCHAR 型的
-
模糊查询带
%时,前缀失效(后缀不失效) ;WHERE name LIKE '%三'失效WHERE name LIKE '三%'可用- 失效原因:B+ 树索引是按字段前缀有序排列的,前缀
%会导致无法定位索引起始位置,只能全表匹配;后缀%可利用索引的前缀有序性,定位到以 “三” 开头的索引范围,再匹配后续内容。
-
OR连接非索引字段时,涉及到的索引均失效;- 失效原因:MySQL 优化器无法同时利用 “索引字段” 和 “非索引字段” 的索引,为了统一执行逻辑,会放弃所有索引,直接全表扫描。
-
联合索引不满足最左前缀法则时;
- 失效原因:联合索引的 B+ 树按 “最左字段→次左字段→后续字段” 的顺序有序排列,跳过左前缀字段后,无法利用后续字段的有序性,只能全表扫描。
-
NOT IN/IS NOT NULL/!=可能失效,视数据量而定,优先用IN/IS NULL/BETWEEN;- 失效原因:
NOT IN/!=属于 “否定式查询”,MySQL 优化器判断这类查询大概率需要扫描大部分数据,使用索引的收益低于全表扫描,因此放弃索引;IS NOT NULL同理,索引无法快速定位 “非空” 的全部数据范围。
- 失效原因:
-
联合索引中使用范围查询(
>/</>=/<=/BETWEEN),范围查询字段后的索引列失效;-
示例(联合索引
idx_name_age_score(name,age,score)):SQL 条件 索引使用情况 WHERE name='张三' AND age>20 AND score=90仅 name+age生效,score失效WHERE name='张三' AND age=25 AND score>90name+age+score全生效(等值在前,范围在后) -
失效原因:范围查询会获取一个无序的结果集(如
age>20包含 21、22、23… 等任意值),后续索引列(如score)的有序性依赖前序字段的等值匹配,无序的范围结果会导致后续索引列无法被利用,索引被截断。
-
5. B+ 树页分裂与页合并(主键设计的核心依据)
(1)页分裂(Page Split)
-
背景:InnoDB 中数据和索引以 “页” 为单位存储(默认页大小 16KB),聚簇索引(主键)的叶子节点按主键有序排列,每个页存储一定数量的行数据。
-
触发条件:插入新数据时,目标页已存满(达到页填充因子,默认 15/16),需将该页拆分为两个新页,重新分配数据。
-
影响:
-
额外消耗 IO 和 CPU(拆分页、调整索引指针);
-
页分裂后页填充率降低(可能从 90% 降至 50%),浪费存储空间;
-
频繁页分裂会导致索引碎片增多,查询效率下降。
-
(2)页合并(Page Merge)
-
触发条件:删除数据后,相邻两个页的空闲空间总和超过阈值(默认页大小的 50%),InnoDB 会将两个页合并为一个页。
-
影响:
-
合并操作消耗资源(调整索引结构);
-
频繁删除 + 合并会导致索引结构频繁变动,影响查询稳定性。
-
(3)主键设计原则(基于页分裂 / 合并的优化)
| 设计原则 | 核心逻辑 | 反例 / 正例 |
|---|---|---|
| 主键自增(有序) | 新数据插入到当前页末尾,极少触发页分裂;页填充率高,存储空间利用率优 | 反例:UUID 作为主键(无序,插入随机位置,频繁页分裂);正例:INT/BIGINT 自增主键(id INT AUTO_INCREMENT) |
| 主键类型最小化 | 减小主键占用空间,提升页存储行数(主键越小,每页存的行越多,IO 次数越少),二级索引叶子节点存储主键值,主键越短,二级索引体积越小、查询 IO 也越少 | 反例:VARCHAR (36) 存 UUID(占 36 字节);正例:BIGINT(8 字节)/INT(4 字节) |
| 主键唯一且非空 | 聚簇索引要求主键唯一,避免 InnoDB 生成隐藏 row_id(额外开销) | 反例:无主键 / 主键允许 NULL;正例:id INT PRIMARY KEY NOT NULL AUTO_INCREMENT |
| 避免主键更新 | 主键更新会触发索引重构(删除旧行 + 插入新行,可能引发页分裂) | 反例:用手机号作为主键(可能修改);正例:自增 ID 作为主键(永不修改) |
| 慎用复合主键 | 复合主键会增大主键长度,降低页存储效率,且二级索引叶子节点存储完整主键,占用更多空间 | 反例:PRIMARY KEY (dept_id, user_id);正例:单独自增 ID 作为主键,dept_id 建普通索引 |
6. 索引设计原则(核心:为谁建索引)
(1)为 “高频查询字段” 建索引
- 优先给
WHERE条件、JOIN关联、ORDER BY/GROUP BY字段建索引; - 低频查询(如每日仅 1 次的统计查询)无需建索引,避免浪费存储空间和 DML 性能。
(2)为 “高选择性字段” 建索引,尽量建立UNIQUE索引
- 选择性 = 唯一值数量 / 总行数,选择性越高,索引过滤效果越好;
- 反例:性别字段(仅男 / 女)选择性极低,建索引无意义;
- 正例:手机号、身份证号字段选择性接近 1,建索引收益高。
(3)为 “小表” 最好不建索引
- 数据量<1000 行的小表,全表扫描效率可能高于索引查询(索引 IO 开销>扫描开销);
- 小表优先优化 SQL 逻辑,而非创建索引。
(4)避免 “冗余索引”
- 冗余索引示例:已建
idx_name_age(name,age),再建idx_name(name)属于冗余; - 定期清理冗余索引(通过
SHOW INDEX或第三方工具如 pt-duplicate-key-checker)。
(5)长字符串优先建 “前缀索引”
- 如地址、简介等长字符串字段,无需建完整索引,通过计算选择性选择合适的前缀长度(如前 5 个字符)。
(6)优先选联合索引(覆盖高频组合查询)
- 原因见2.(3)
(7)若索引列不能为NULL,用NOT NULL约束
- 可以让优化器更有效地选择索引
7. 优化工具(核心补充)
(1)SHOW PROFILE(SQL 执行耗时分析)
-
作用:精准分析 SQL 执行过程中各阶段的耗时(如 CPU、IO、锁等待等),定位性能瓶颈。
-
使用步骤:
-
开启功能(默认关闭):
SET profiling = ON; -- 会话级开启,仅当前连接有效 SET profiling_history_size = 100; -- 保留最近100条执行记录 -
执行待分析的 SQL:
SELECT * FROM user WHERE dept_id=1 ORDER BY age DESC LIMIT 10; -
查看执行记录列表:
SHOW PROFILES; -- 显示所有SQL的执行ID、耗时、语句 -
查看指定 SQL 的详细耗时(替换
query_id为实际 ID):SHOW PROFILE FOR QUERY query_id; -- 基础耗时分析 SHOW PROFILE CPU, BLOCK IO FOR QUERY query_id; -- 含CPU、IO的详细分析
-
-
关键指标:
Sending data:数据传输耗时(过长可能是结果集过大或索引失效);Waiting for table lock:表锁等待(MyISAM 常见,需优化锁逻辑);Creating tmp table:创建临时表(子查询 / 排序 / 分组导致,需优化 SQL)。
(2)SQL 执行频率统计(SHOW STATUS)
-
作用:统计数据库整体执行频率,定位高频操作(如慢查询、高频更新)。
-
常用命令:
-- 查看所有状态变量(按关键字过滤) SHOW GLOBAL STATUS LIKE 'Com_%'; -- 统计各类SQL执行次数 SHOW GLOBAL STATUS LIKE 'Innodb_%'; -- 统计InnoDB引擎相关指标 -
核心指标解读:
指标 含义 优化方向 Com_selectSELECT 执行次数 过高需优化查询(加索引、精简条件) Com_insert/update/delete增删改执行次数 过高需优化批量操作,减少 IO Innodb_rows_readInnoDB 读取行数 远大于 Com_select说明全表扫描多Innodb_rows_updatedInnoDB 更新行数 结合业务看是否有无效更新 Slow_queries慢查询次数 非 0 需分析慢查询日志
(3)慢查询日志(定位慢 SQL)
-
作用:记录执行时间超过阈值的 SQL,是优化慢查询的核心工具。
-
配置与使用:
-
开启慢查询日志(临时生效,重启失效):
SET GLOBAL slow_query_log = ON; -- 开启慢查询日志 SET GLOBAL long_query_time = 1; -- 阈值:执行时间>1秒记录(默认10秒) SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未使用索引的SQL(慎用,易刷屏) -
查看日志路径:
SHOW VARIABLES LIKE 'slow_query_log_file'; -- 默认为 /var/lib/mysql/localhost-slow.log -
分析慢查询日志(用 mysqldumpslow 工具):
# 查看最慢的10条SQL mysqldumpslow -s t -t 10 /var/lib/mysql/localhost-slow.log # 查看按执行次数排序的SQL mysqldumpslow -s c -t 10 /var/lib/mysql/localhost-slow.log
-
-
关键参数:
slow_query_log:是否开启慢查询日志(生产建议开启);long_query_time:慢查询阈值(建议设 1-5 秒,按需调整);log_output:日志输出方式(FILE 或 TABLE,推荐 FILE)。
(4)EXPLAIN(执行计划分析)
-
作用:分析 SQL 执行计划,查看索引使用、扫描行数、连接方式、查询类型等核心信息,是定位 SQL 性能问题的核心工具。
-
使用方式:
EXPLAIN + 待分析SQL(如EXPLAIN SELECT * FROM user WHERE dept_id=1;);进阶用法EXPLAIN ANALYZE + SQL(MySQL 8.0+ 支持,可输出实际执行耗时)。 -
核心字段解读:
| 字段 | 含义 | 优化目标 / 解读要点 |
|---|---|---|
id | 表示查询中执行子查询 / 操作的顺序;1. id 越大,执行优先级越高;2. id 相同,执行顺序从上到下;3. id 为 NULL,代表结果集(如衍生表、临时表) | 无优化目标,仅用于理解执行顺序 |
select_type⭐ | 核心:见下单独描述 | 无优化目标,用于区分查询类型,如 DERIVED 可能伴随临时表,需关注性能 |
table | 表示当前行执行的操作对应的表名(或衍生表标记如 derived2) | 确认操作是否命中目标表,排查关联查询的表顺序问题 |
type⭐ | 核心:见下单独描述 | 至少达到 range,最优 eq_ref/const;杜绝 ALL(全表扫描)和 index(全索引扫描) |
possible_keys⭐ | MySQL 优化器认为可能适用的索引列表(仅为候选,不一定实际使用) | 若为空,说明无可用索引,需新增索引;若有值但 key 为空,说明优化器选错索引 |
key ⭐ | 实际执行时使用的索引名称;若为 NULL,说明未使用索引 | 非 NULL 表示使用索引;需确认是否为最优索引(可结合 SQL 提示 调整) |
key_len | 实际使用的索引长度(字节);联合索引中,可通过该值判断使用了索引的哪些字段(如 idx_name_age 中 key_len=20 表示仅用了 name 字段) | 长度越短越好(索引利用率高);联合索引需确认是否用到全部目标字段 |
ref | 与索引匹配的列 / 常量;如 const(常量)、user.dept_id(表字段) | 若为 func,说明使用了函数计算,可能导致索引失效 |
rows | MySQL 优化器预估的扫描行数(非实际行数) | 行数越少越好;数值过大需优化索引或 SQL 条件 |
filtered⭐ | 过滤率(百分比),表示扫描行中满足条件的比例;公式:filtered = 最终结果行数/读取总行数 × 100% | 过滤率越高越好;过低(如 <10%)说明 WHERE 条件过滤效果差,需优化条件 |
Extra⭐ | 核心:见下单独描述 | 避免 Using filesort/Using temporary/Using join buffer;优先出现 Using index |
补充说明:
-
select_type字段核心值详解:-
SIMPLE:简单查询(无子查询、无 UNION); -
PRIMARY:主查询(外层查询); -
SUBQUERY:子查询(内层查询,不依赖外层); -
DERIVED:衍生查询(FROM 中的子查询); -
UNION:UNION 中第二个及以后的查询; -
UNION RESULT:UNION 的结果集
-
-
type字段核心值详解:-
ALL:全表扫描,需遍历整张表,性能最差; -
index:全索引扫描(遍历整个索引树),比ALL略优,相当于有索引的全表扫描; -
range:范围扫描(如>/</IN/BETWEEN),仅扫描索引范围内的数据; -
ref:非唯一索引等值匹配(如普通索引idx_dept_id匹配dept_id=1); -
eq_ref:唯一索引等值匹配(如主键 / 唯一索引匹配id=100),返回条数为一行,性能极佳; -
const/system:常量匹配(如主键匹配id=1,数据可直接缓存),返回条数为一行,性能最优。
-
-
Extra字段核心值详解:-
字段优先级:
Using index(覆盖索引)>Using where(仅过滤)>Using filesort/Using temporary(需优化)。 -
Using filesort:需额外排序(未用索引排序),需优化; -
Using temporary:需创建临时表(如 GROUP BY 无索引),需优化; -
Using index:覆盖索引(无需回表),最优; -
Using where:使用 WHERE 条件过滤; -
Using join buffer:连接缓存,关联查询效率低; -
Impossible WHERE:WHERE 条件恒假(如 1=0)
-
二、SQL 优化实战
1. 查询语句优化(核心场景)
(1)避免全表扫描
-
禁用
SELECT *:只查询需要的字段(减少数据传输 + 支持覆盖索引);-- 错误(全字段查询,无法使用覆盖索引) SELECT * FROM user WHERE dept_id=1; -- 正确(仅查需要字段,若有 idx_dept_id_name 索引则触发覆盖查询) SELECT id, name FROM user WHERE dept_id=1; -
必加
WHERE条件:无过滤条件会扫描全表(如统计部门 1 用户数,需加dept_id=1); -
用
JOIN代替子查询:子查询易生成临时表,JOIN效率更高(MySQL 对JOIN优化更成熟);-- 错误(子查询生成临时表) SELECT * FROM user WHERE dept_id IN (SELECT id FROM dept WHERE name='技术部'); -- 正确(JOIN 效率更高) SELECT u.* FROM user u JOIN dept d ON u.dept_id=d.id WHERE d.name='技术部';
(2)条件优化(避免索引失效)
-
不用函数操作索引字段;
-
字段类型严格匹配;
-
模糊查询少用前缀
%; -
强制使用指定索引(SQL 提示);
(3)分页优化(大偏移量问题)
-
大偏移量
LIMIT效率低(如LIMIT 100000, 10需扫描前 100010 条数据),优化方案:-- 方案1:主键过滤(适用于主键自增) SELECT * FROM user WHERE id > 100000 ORDER BY id LIMIT 10; -- 方案2:联合索引覆盖查询(避免回表) SELECT id, name FROM user WHERE dept_id=1 ORDER BY id LIMIT 100000, 10; -- 方案3:延迟关联(先查主键,再关联查完整数据) SELECT u.* FROM user u JOIN ( SELECT id FROM user WHERE dept_id=1 ORDER BY id LIMIT 100000, 10 ) t ON u.id=t.id;
(4)排序 / 分组优化
-
排序字段加索引:
ORDER BY字段若有索引,可避免全表排序(Using filesort); -
若不可避免
Using filesort,可以适当增大sort_buffer_size; -
联合索引包含排序字段:如
idx_dept_id_age(dept_id,age),支持WHERE dept_id=1 ORDER BY age(利用索引有序性); -
避免
GROUP BY无索引:GROUP BY字段加索引,减少临时表生成(Using temporary);-- 优化前(无索引,生成临时表+文件排序) SELECT dept_id, COUNT(*) FROM user GROUP BY dept_id ORDER BY COUNT(*); -- 优化后(idx_dept_id 索引,避免临时表) SELECT dept_id, COUNT(*) FROM user GROUP BY dept_id ORDER BY COUNT(*);
(6)count优化
-
可自己额外引入一个变量,自己记录表的总数目;
-
效率:
count(字段)<count(id)<count(1)≈count(*)- 原因是因为要取值
(7)insert优化
-
批量插入可减少连接次数
-
手动事务提交,将多条语句用一次事务提交,减少提交次数
-
主键顺序插入
-
大批量插入时使用load(离线导入的一种方式,非重点)
2. 表结构优化
-
字段类型最小化:按实际需求选择最小类型(如年龄用
TINYINT(0-255)代替INT,手机号用VARCHAR(11)代替VARCHAR(20)); -
避免
NULL值:给字段设默认值(如gender DEFAULT '未知',avatar DEFAULT 'default.png'),NULL会影响索引效率和查询判断; -
拆分大表(垂直拆分):将字段多的表拆分为 “高频访问表” 和 “低频访问表”(如
user拆分为user_base(id、name、tel)和user_extend(intro、address、create_time)); -
拆分大表(水平拆分):按时间 / 地域 / 用户 ID 拆分(如订单表
order拆分为order_202401、order_202402,用户表按id%10拆分为 10 个分表); -
合理分区:时间序列数据(订单表、日志表)按时间分区,查询仅扫描指定分区(如
ORDER BY create_time BETWEEN '2024-01-01' AND '2024-01-31'仅扫描 202401 分区); -
用
INT代替VARCHAR存枚举值:如性别用TINYINT(1)(0 = 女,1 = 男)代替VARCHAR(2),查询 / 排序效率更高。
3. 批量操作优化
-
批量插入代替单条插入:减少网络 IO 和事务提交次数(推荐一次插入 1000-5000 条);
-- 错误(单条插入,100次 IO) INSERT INTO user(name, tel) VALUES ('张三','138xxx'); INSERT INTO user(name, tel) VALUES ('李四','139xxx'); -- 正确(批量插入,1次 IO) INSERT INTO user(name, tel) VALUES ('张三','138xxx'), ('李四','139xxx'), ('王五','137xxx'); -
批量更新代替循环单条更新:用
CASE WHEN或JOIN实现,减少事务开销;-- 批量更新多个用户年龄 UPDATE user SET age = CASE id WHEN 1 THEN 25 WHEN 2 THEN 30 WHEN 3 THEN 28 END WHERE id IN (1,2,3); -
批量删除优化:避免
DELETE FROM user(全表删除,锁表时间长),用LIMIT分批删除;-- 错误(全表删除,锁表) DELETE FROM user WHERE create_time < '2023-01-01'; -- 正确(分批删除,每次删1000条) WHILE EXISTS (SELECT 1 FROM user WHERE create_time < '2023-01-01') DO DELETE FROM user WHERE create_time < '2023-01-01' LIMIT 1000; END WHILE;
4. 事务与锁优化
-
事务尽量短小:长事务会占用锁资源,导致并发下降,甚至死锁(如避免在事务中执行非数据库操作,如调用第三方接口);
-
统一锁顺序:多表操作时,按固定顺序加锁(如先锁
user表,再锁order表),避免死锁; -
用行锁代替表锁:InnoDB 行锁基于索引,查询需命中索引(否则退化为表锁);
-- 错误(未命中索引,退化为表锁) UPDATE user SET age=25 WHERE name='张三'; -- name 无索引 -- 正确(命中索引,行锁) UPDATE user SET age=25 WHERE id=100; -- id 是主键索引 -
避免幻读:用
SELECT ... FOR UPDATE加行锁(InnoDB Repeatable Read 隔离级别下),或提升隔离级别到SERIALIZABLE(不推荐,影响并发); -
禁用
FOR UPDATE无索引查询:会导致表锁,如SELECT * FROM user FOR UPDATE(无 WHERE 条件)。