提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 1.insert优化
- 2.主键优化
- [3.orderd by的优化](#3.orderd by的优化)
- [4.group by的优化](#4.group by的优化)
- 5.limit分页查询
- 6.count
- [7.update 优化](#7.update 优化)
1.insert优化
(1)批量插入
- 批量插入
sql
INSERT INTO tb_test VALUES (1,'Tom'), (2,'Cat'), (3,'Jerry');
- 手动提交事务
sql
START TRANSACTION;
INSERT INTO tb_test VALUES (1,'Tom'), (2,'Cat'), (3,'Jerry');
INSERT INTO tb_test VALUES (4,'Tom'), (5,'Cat'), (6,'Jerry');
INSERT INTO tb_test VALUES (7,'Tom'), (8,'Cat'), (9,'Jerry');
COMMIT;
- 主键顺序插入:顺序插入可以减少页分裂,提高插入效率。
(2)大批量插入数据
如果一次性需要插入大批量数据,使用 INSERT 语句性能较低,此时可以使用 MySQL 提供的 LOAD 指令进行插入。操作步骤如下:
sql
-- 客户端连接服务端时,加上参数 --local-infile
mysql --local-infile -u root -p
-- 设置全局参数 local_infile 为 1,开启从本地加载文件导入数据的开关
SET GLOBAL local_infile = 1;
-- 执行 load 指令将准备好的数据加载到表结构中
LOAD DATA LOCAL INFILE '/root/sql1.log' INTO TABLE tb_user
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n';
2.主键优化
(1)数据组织方式
在 InnoDB 存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(Index Organized Table, IOT)。
存储结构层次:表空间 → 段 → 区 → 页 → 行

(2)页分裂
如果是乱序插入,例如叶子节点已有序,此时插入数据 50,但 1 号数据页空间不足,就会产生页分裂。


(3)页合并
当删除一行记录时,实际上记录并没有被物理删除,只是被标记(flagged)为删除,其空间允许被其他记录使用。
当页中删除的记录达到 MERGE_THRESHOLD(默认为页大小的 50%)时,InnoDB 会开始寻找最靠近的页(前或后),尝试将两个页合并以优化空间使用。


参数 MERGE_THRESHOLD 表示合并页的阈值,可以自行设置,在创建表或创建索引时指定。
(4)主键设计原则
- 满足业务需求的情况下,尽量降低主键的长度(因为二级索引中会存储主键值)。
- 插入数据时,尽量选择顺序插入,推荐使用 AUTO_INCREMENT 自增主键(顺序插入可防止页分裂)。
- 尽量不要使用 UUID 做主键或其他自然主键(如身份证号),因为它们是乱序的。
- 业务操作中,避免对主键的修改。
3.orderd by的优化
MySQL 中排序的两种方式如下,
- Using filesort : 通过表的索引或全表扫描读取满足条件的数据行,然后在排序缓冲区(sort buffer)中完成排序。所有不是通过索引直接返回排序结果的排序都称为 filesort。
- Using index : 通过有序索引顺序扫描直接返回有序数据,不需要额外排序,效率高。
默认创建索引时,索引列按升序(ASC)排列。
sql
-- 未创建索引时,根据 age, phone 排序(Using filesort)
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age, phone;
-- 创建索引(age, phone 均升序)
CREATE INDEX idx_user_age_phone_aa ON tb_user(age, phone);
-- 此时排序使用索引(Using index)
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age, phone;
-- 改变排序顺序(phone, age),Using index + Using filesort
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY phone, age;
-- 全部降序,使用 Backward index scan + Using index
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age DESC, phone DESC;
-- 一升一降,Using index + Using filesort
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age ASC, phone DESC;
-- 创建 age 升序、phone 降序的索引
CREATE INDEX idx_user_age_pho_ad ON tb_user(age ASC, phone DESC);
-- 此时排序使用索引(Using index)
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age ASC, phone DESC;
优化总结
- 根据排序字段建立合适的索引,多字段排序时也遵循最左前缀法则。
- 尽量使用覆盖索引。
- 多字段排序时,如果一个升序一个降序,需要注意联合索引在创建时的规则(指定 ASC / DESC)。
- 如果不可避免出现 filesort,在大数据量排序时可以适当增大排序缓冲区大小 sort_buffer_size(默认 256 KB)。
4.group by的优化
- 在分组操作时,可以通过索引来提高效率。
- 分组操作时,索引的使用也是满足最左前缀法则的。
sql
-- 未创建索引时,根据 profession 分组(Using temporary)
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY profession;
-- 创建联合索引
CREATE INDEX idx_user_pro_age_sta ON tb_user(profession, age, status);
-- 根据 profession 分组,使用索引(Using index)
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY profession;
-- 根据 age 分组,不符合最左前缀,Using index + Using temporary
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY age;
-- 根据 profession, age 分组,使用索引(Using index)
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY profession, age;
5.limit分页查询
对于LIMIT 2000000, 10操作来说,此时 MySQL 需要排序前 2000010 条记录,然后仅返回第 2000000 到 2000010 条,其他记录被丢弃,查询排序的代价非常大。
sql
-- 原始分页查询(效率低)
SELECT * FROM tb_sku ORDER BY id LIMIT 9000000, 10;
-- 改进后:使用覆盖索引 + 子查询的方式
SELECT s.*
FROM tb_sku s
JOIN (SELECT id FROM tb_sku ORDER BY id LIMIT 9000000, 10) a
ON s.id = a.id;
优化思路:先通过子查询获取主键(覆盖索引避免回表),再与原表关联获取完整数据。
6.count
sql
explain select count(*) from tb_user ;
- 对于上面的代码不同存储引擎之间存在差异,优化思路为自己手动计数(例如创建统计表)。
- MyISAM:把一个表的总行数存储在磁盘上,执行 COUNT(*) 时直接返回,效率很高。
- InnoDB:执行 COUNT(*) 时需要把数据一行一行地从引擎中读出来,然后累积计数。
count的几种用法
- count(主键):InnoDB 引擎会遍历整张表,把每一行的 主键id 值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加(主键不可能为null)。
- count(字段):
- 没有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加。
- 有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。
- count(1):InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字 "1" 进去,直接按行进行累加。
- count(*):InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。
按照效率排序的话,count(字段)<count(主键 id) <count(1) ≈ count(),所以尽量使用 count()。
7.update 优化
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁。如果条件中的索引失效,行锁会升级为表锁。
- 在 UPDATE 操作中,尽量使用带索引的列作为条件,避免索引失效导致表锁。
- 如果无法避免,应尽量缩小锁范围或优化业务逻辑。
现在有下面这张表结构
sql
+----+--------+
| id | name |
+----+--------+
| 1 | javaEE |
| 2 | PHP |
| 3 | MySQL |
| 4 | Kafka |
+----+--------+
sql
begin;
update course set name ='SpringBoot' where name ='PHP';
sql
begin;
update course set name ='Kafka 2' where id = 4;
对于上面的例子,由于name没有索引,因此此时加的是表锁,另一个事务会被阻塞,直到 COMMIT 后才能执行。(行锁升级为表锁的问题)
添加name的索引后不会阻塞其他事务(此时加的是行锁)