1. 表优化

字符集

数据库和表的字符集统一使用 UTF-8,使用 charset=utf8mb4。

字段类型

字段类型选择的原则是使用可以正确存储数据的最小数据类型。

例如,可以用 int 时不要用 bigint,字段长度固定时用 char,长度不固定时用 varchar 代替 char,最大长度超过 varchar 限制时才使用 text。

NULL

字段尽量使用 NOT NULL 并提供默认值,因为 NULL 类型的存储需要额外的标记来表示,查询优化也会存在性能问题。

大文件

不要用 MySQL 的字段存储大文件,而是要将其存在磁盘中或网络对象中,通过字段记录文件的访问地址。

外键、触发器、视图

不建议使用外键、触发器、视图,会降低代码的可读性,带来性能、维护和复杂性的问题,数据完整性校验应该由程序逻辑完成。

修复损坏的表

# 检查表是否损坏
CHECK TABLE user;

# 修复损坏的表
REPAIR TABLE user;

# 重建表,可以修复损坏、减少索引和数据碎片
ALTER TABLE user ENGINE=InnoDB;

维护和优化表

# 更新表的统计信息,有助于优化器生成更高效的执行计划
ANALYSE TABLE user;

# 重建表,优化存储空间,减少碎片
OPTIMIZE TABLE user;

2. 索引优化

整型自增主键

使用整型自增字段作为主键,减少数据插入时频繁的 B+ 树页面分裂造成的性能损耗。

`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY

去除冗余索引

不要创建太多的不必要的索引,每个索引都需要维护一个 B+ 树空间,且增删改查的时候都会多一个选择索引和更新索引的时间代价。对于同样顺序的一至多列,如果创建了主键索引就没必要创建唯一索引了,同理创建了唯一索引就没必要创建普通索引了。

索引只包含必要列

只为用于搜索、排序、分组的列创建索引,出现在 select 列表的其他列不需要创建在索引内。

列的区分度

考虑索引列的值的区分度,选用区分度高的字段。如果一个列的不重复值占全部记录数的比例太低,每一列包含太多重复值,通过二级索引查询可能会执行过多的回表操作。

数据类型选择尽量小

索引列的数据类型尽量小。数据类型越小,索引站的存储空间就越少,一个数据页内就能存放更多的记录,从而减小磁盘 I/O 的性能消耗。特别是对于主键,因为除了聚簇索引,所有二级索引都会存储记录对应的主键值。如主键能选择 MEDIUMINT 就不要用 INT,能选择 INT 就不要用 BIGINT。

联合索引的字段选择

对多个字段建立联合索引时,把区分度高的字段放在前面。

存在等值判断和范围判断的条件时,将等值判断的列放在前面。

查询条件有两个列进行范围查询,将无法全用到索引。

索引覆盖

利用索引覆盖,当查询所需的列较少时,可以将查询条件的列和读取的列用来创建联合索引,该查询只需要读取该二级索引,无需再执行回表操作。

最左前缀原则

冗余索引会带来额外的存储空间要求和数据维护要求,应该避免。基于最左前缀原则,只要查询满足一个索引的前缀部份字段,也能利用该索引进行查询,也就不需要创建冗余的索引了。如已有联合索引 (k1, k2, k3),通过 k1 或 k1 + k2 进行查询也能使用该索引,无需再为具体查询字段创建额外索引。已有主键索引或唯一索引,也无需再对同样的字段创建普通索引。

有时候出于查询性能的考虑需要冗余联合索引的前缀作为索引,如同时保留索引 (k1, k2)(k1),因为 k2 可能过大,导致只用 k1 查询时,走联合索引明显慢于走独立的索引。

前缀索引

对字符串列创建索引时,值可能会很大,导致在索引占用的存储空间很大,可以以列的前缀建立索引。索引中的 key 只会是列值的对应长度前缀,减少了索引的存储空间,而查询确定值的时候就以前缀为匹配去索引中查找。但该方式无法对该列排序时使用索引,将会退化为全表扫描。注意前缀索引无法用于 GROUP BY 和 ORDER BY,也无法用于覆盖扫描。

# 以字符串列创建索引
ALTER TABLE user ADD INDEX idx_key(name);

# 以字符串列的长度为 10 的前缀创建索引
ALTER TABLE user ADD INDEX idx_key(name(10));

如果字符串列前缀的区分度不够,前缀索引的查询效率不高,还可以尝试将该列倒过来存储,这样就是以后缀倒叙作为前缀了。或者新建一个字段,存储字符串字段的哈希值,以该哈希值字段创建索引。

不要用表达式、函数、类型转换

查询条件中的字段如果以表达式、函数、类型转换的方式来筛选,即使有索引也会进行全表扫描,为了使用索引执行,需要以列名的形式单独出现在查询条件中。

# 全表扫描
SELECT * FROM user where age + 20 > 60;

# 使用索引
SELECT * FROM user where age > 40;

排序

排序的字段应该放在联合索引的最后。

无法使用索引的场景

负向条件 !=、<>、not in、not exists、not like 无法

is null、is not null 无法使用索引。

like 语句模糊查询的前导模糊如 like ‘%XX’ 查询无法使用索引,可以使用搜索引擎来解决。

3. SQL优化

select 字段

查询时,select 的字段只列出用到的字段,不要盲目 select *,因为这会返回过多字段,给磁盘 IO 带来额外的压力。

优化 WHERE 条件

优先使用 = 或 IN,避免使用 != 或 NOT IN。

大分页查询

在查询一个表根据某个字段排序的分页数据,但 offset 较大时,除了从业务上优化避免出现较大分页外,还可以优化 SQL。

例如如下查询,表的 age 字段有索引,该查询会扫描并跳过前十万行数据。

SELECT * FROM user where age > 40 ORDER BY age OFFSET 100000 LIMIT 10;

通过只查询所需要的字段,为这些字段创建一个覆盖索引,避免回表操作。

CREATE INDEX idx_covering on user (age, name, sex);

SELECT name, sex FROM user where age > 40 ORDER BY age OFFSET 100000 LIMIT 10;

或者使用延迟关联,通过覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的行。

SELECT * FROM user
JOIN (
  SELECT id FROM user ORDER BY age LIMIT 100000, 10
) x on user.id = x.id;

表连接

表连接时使用小表驱动大表,这里的小表和大表不是指表本身的数据量,而是对各表根据条件过滤后的行数相对而言。

确保表连接的字段有索引,右表连接的字段为主键更好。

使用表连接代替子查询,在大多数场景都更高效,因为表连接可以更好地被优化,可以并行查询,而子查询很可能会创建临时表,查询也只能串行执行。

# 子查询,性能差
SELECT A.* FROM A WHERE A.id not in (SELECT B.id FROM B);

# 表连接,更高效
SELECT A.* FROM A LEFT JOIN B on A.id = B.id where B.id IS NULL;

不要对太多张表连接查询,最多只能 3 - 4 个表进行连接。

OR

OR 条件可能会导致索引失效和全表扫描,可以改为 UNION,以更好利用索引和并行处理能力。

UNION

当确认两部份数据没有重复数据或不需要去重时,将 UNION 换为 UNION ALL。

只查询一行

如果只需要查询一行或者确定查询只有一行,可以加上 LIMIT 1,让数据库更好利用索引,优化查询行为。

事务

尽量缩短事务执行时间,避免长事务,尝试将长事务拆分为多个小事务,减少资源占用和锁竞争。

写操作分批

对于数量较大的写操作如 INSERT、DELETE、UPDATE,一次执行会花费很多时间,还会产生大量的锁,本身变成了一个大事务。应该将数据分批执行,如 100 行数据一批地写。

4. 配置优化

服务器配置

max_connections 配置服务器最大连接数量,默认为 151,可以调高以支持更多连接。

wait_timeout 配置客户端超时时间,默认为 28800 秒即 8 小时,可以调低以及时释放不活动连接。

slow_query_log、slow_query_log_file、long_query_time 打开慢查询日志,对慢日志进行分析以针对性优化 SQL。

客户端配置

客户端连接池大小通常是在应用程序中进行配置的,复用连接可以减少和服务器建立连接的额外开销,但连接池不是越大越好,盲目加大很可能会加大服务端性能开销降低效率,一个推荐的连接池大小是 CPU 核心数 * 2 + 1。

5. 架构优化

主从读写分离

在很多读多写少的业务场景,可以配置主库和从库,主库负责处理插入删除更新的写操作,主库和从库都可以接受查询的读操作,提升数据库的读性能。

还可以让主库只接受写操作,从库只接受读操作,实现读写分离。从库支持水平扩展,可以根据实际需求配置一到多个从库,分散读请求的压力和提升性能,降低单点故障风险,提升系统的可用性。

不过这时候需要关注主从数据备份的时间差,差距过大会使从库数据的时效性降低。

分库分表

垂直分库,按业务逻辑拆分为不同的数据库,减轻请求的并发压力。这样做两个数据库之间的表就不能直接做连接查询了,需要根据业务场景进行拆分。

垂直分表,将列比较多的表按照经常使用的字段(热字段)和不常用到的字段(冷字段)拆分为不同的表,提升查询速度。

水平分表,将单张表的数据拆分为多个表,可以解决存储的瓶颈。可以创建 MySQL 分区表来讲数据拆分到多个分区。也可以按照某个字段将数据保存在不同的表,如根据日期将数据写到对应日期的表 user_data_20250101、user_data_20250102、user_data_20250103 等。或者使用自带分区的分布式数据库如 TDSQL。

缓存

在服务和数据库之间,通过增加一层缓存如 Redis,针对频繁访问的热点数据,可以实现更快速的查询,同时可以降低数据库的读取压力。

这种方式需要考虑好缓存更新和淘汰的策略,避免数据大量失效过期对底层数据库的骤增压力。

消息队列

在同一时间访问数据库的请求过大,会对数据库造成很大的压力。如果是可以异步处理的请求,如数据扫描和同步,可以先将请求发给消息队列,再按照实际处理能力从消息队列获取请求进行处理,以对流量削峰。