数据库优化手册
以MySQL为例的数据库优化手册
数据库优化手册
本文使用MySQL 5.7.35为例
请重点关注文档中的加粗部分
介绍 § Introduction
天下苦数据库优化久矣, 本文以读者已熟悉基本的MySQL增删改查语句为前提, 介绍以索引, Explain语句与慢查询日志三项为主的优化方法, 希望对您有所帮助
还有如事务, 分库分表之类的做法可能会在日后完善
索引 § Index
介绍
数据库可以通过索引来提高数据的检索速度
在查询语句中的WHERE部分使用到索引字段时, 就会使用索引进行查询
使用
普通索引
若存在表user如下
1 | -- 创建一个用户表 |
方法一, 可以通过如下语句来创建一个将
username作为索引字段的索引1
2-- 以用户名作为索引字段, 给用户表建立索引
CREATE INDEX `username_index` ON `user` (`username`);方法二, 或者在创建表的同时创建索引
1
2
3
4
5
6
7CREATE TABLE `user` (
`id` INT PRIMARY KEY,
`username` VARCHAR(32) COMMENT '用户名',
`password` VARCHAR(32) COMMENT '密码',
-- 在创建表的同时创建索引
INDEX `username_index` (`username`)
);方法三, 也可以在表创建后修改表内容来创建索引
1
ALTER TABLE `user` ADD INDEX `username_index`(`username`);
查看表的索引
可以通过如下语句来查看一个表有哪些索引
1 | -- 显示用户表的索引 |
显示结果
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| user | 0 | PRIMARY | 1 | id | A | 0 | null |
null |
BTREE | |||
| user | 1 | username_index | 1 | username | A | 0 | null |
null |
YES | BTREE |
可以看到一条默认存在的以id作为索引字段的索引和刚创建的以username作为索引字段的username_indesx
字段解释
Table: 表名Non_unique: 是否是唯一索引, 0为唯一索引,PRIMARY就是唯一索引Key_Name: 索引名Squ_in_index: 列的序列号, 标识该列在该索引中的顺序- 普通索引只有一个列, 故序号为1, 下文提及的联合索引会有所不同
Column_name: 列名Collation: 列的存储字符序- 值可以为:
A(升序)或NULL(无分类)
- 值可以为:
Cardinality: 基数, 代表表中此列数据为唯一值的估计值若该值较低, 则需要重新思考是否有这条索引存在的必要
此处为零因为数据库中暂无数据
Sub_part: 前缀索引的前缀长度若索引非前缀索引, 或字段并未取前缀, 则为
null前缀索引的概念见下
Packed: 索引字段的压缩方式, 若未压缩则为nullNULL: 列中是否允许存在null值- 作为索引的列不允许存在
null值, 所以这里的username作为索引是不符合规范的 - 这里可以为
username列添加NOT NULL来避免这一情况
- 作为索引的列不允许存在
Index_type: 索引类型- 可能值
FULLTEXT: 全表检索索引HASH: 哈希值索引, 类似键值对的形式存储索引, 效率极高InnoDB引擎不支持HASH类型Index_type
BTREE: 一种树形索引, 默认且最常用RTREE: 一种树形索引, 较少用, 比BTREE的优势在于范围查找
- 可能值
Comment与Index_comment: 注释
查看语句使用到的索引
详细解释请阅读下文 执行计划 § Explain 部分
可以通过在查询语句前加上关键字EXPALIN来查看这条语句所使用到的索引
1 | -- 查看这条查询语句所用到的索引 |
显示结果
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user | null |
ref | username_index | username_index | 99 | const | 1 | 100.00 | null |
可以看到, 这里用到了username_index这个索引
其他索引
前缀索引
当列的类型为BLOB或TEXT一类或者长度很长的VARCHAR时, 将列的完整数据作为索引时不合适的, 这样会导致索引效率慢
当遇到这种情况时, 可以在创建索引时, 给列添加前缀长度限制, 这种索引成为前缀索引
1 | -- 该索引只取用户名的前2字符作为索引字段 |
联合索引
此处可在阅读下文 执行计划 § Explain 部分介绍之后再阅读
在普通索引之后还有联合索引, 这种索引比普通索引有更少的时间与空间复杂度, 并以索引树的方式存储
可以将多列同时作为索引字段来创建联合索引, 这样创建的索引与普通索引有所不同
1 | -- 创建一个3列的联合索引 |
这样相当于创建了
column1的单列索引column1与column2的联合索引column1,column2与column3的联合索引
最左匹配原则
联合索引从最左开始匹配, 最左指索引创建时的顺序最左
1 | -- 以column2与column3作为条件, 不会用到联合索引 |
举例
以上述用户表为例, 若实际场景中经常有需要通过username与password共同判断来查询的内容的情况, 那么可以创建联合索引
创建方式也有如上多种, 此处不赘述
1 | -- 创建一个id与用户名的联合索引 |
查看user的索引
1 | SHOW INDEX FROM `user`; |
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| user | 0 | PRIMARY | 1 | id | A | 0 | null |
null |
BTREE | |||
| user | 1 | username_password_index | 1 | username | A | 0 | null |
null |
BTREE | |||
| user | 1 | username_password_index | 2 | password | A | 0 | null |
null |
BTREE |
此处可以看到, 同一索引名产生了两条索引, 它们的索引顺序与对应列名不同
接下来进行Explain分析
1 | -- 通过用户名与密码查询用户 |
显示结果
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user | null |
range | username_password_index | username_password_index | 198 | null |
2 | 25.00 | Using index condition |
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user | null |
range | username_password_index | username_password_index | 99 | null |
2 | 100.00 | Using index condition |
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user | null |
ALL | null |
null |
null |
null |
4 | 25.00 | Using where |
对比三条语句所用到的结果, 可以发现
- 同时将
username与password作为判断条件时, 用到了索引username_password_index - 只将
username作为判断条件时, 用到了索引username_password_index - 只将
password作为判断条件时, 没有用到索引
也就证明了, 存在username的单列索引, 存在username与password的联合索引, 但是不存在password的单列索引
执行计划 § Explain
介绍
在查询语句前加上Explain关键字, 会进行语句的执行计划分析, 会检查这条语句用到了哪些检索, 是否有进行全表扫描等
使用
测试
若存在表user与表user_type如下
1 | -- 用户表 |
并存在数据
user
| id | username | password | type_id |
|---|---|---|---|
| 1 | Token1 | null |
1 |
| 2 | Token2 | null |
2 |
| 3 | User1 | null |
1 |
| 4 | User2 | null |
2 |
user_type
| id | typename |
|---|---|
| 1 | user |
| 2 | admin |
使用Explain语句对多表关联语句进行分析
1 | -- 查询用户表中用户名为T开头, 且类型为管理员的用户 |
显示结果
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t | null |
ref | PRIMARY,typename_index | typename_index | 99 | null |
1 | 100.00 | Using index |
| 1 | SIMPLE | u | null |
range | username_index | username_index | 99 | const | 2 | 25.00 | Using index condition; Using where; Using join buffer (Block Nested Loop) |
字段解释
id:SELECT的查询顺序序列号select_type: 查询类型table: 表名partition: 匹配的分区type: 对表的查询方式- 常用的类型(性能排序):
ALL<index<range<ref<eq_ref<const<system<NULL
- 常用的类型(性能排序):
possible_keys: 能用的索引, 但不一定被用到keys: 实际用到的索引key_len: 索引字段的最大可能长度ref: 使用常量查询则显示const, 连接查询则显示关联字段rows: 估算的结果行数- 只是预估行数, 并非准确行数
filtered: 数据经过过滤后满足条件的数据的百分比Extra: 查询的其他详细信息常见信息
Using index: 使用了索引Using where: 不用读取表中所有信息,仅通过索引就可以获取所需数据Using join buffer: 说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果- 一般出现这句话可以通过添加索引来优化
Using filesort: 说明MySQL无法通过索引来进行排序, 需要通过外部排序, 这种排序就是文件排序- 出现这句话需要注意, 数据量大的情况下可能会拖累性能, 需要及时优化
Using temporary: 说明使用了临时表保存中间结果来进行排序- 需要马上优化
Impossible where: 说明存在会导致无结果的WHERE
慢查询日志 § Slow Query Log
介绍
慢查询日志开启后会记录执行时间大于某指定时间长度的查询
可以通过查看慢查询记录, 配合Explain关键字进行SQL语句优化
配置与使用
开启慢查询日志记录
以下为Linux的开启方法, Windows类似
方法一, 通过配置文件开启
Windows系统中配置文件为my.ini, 在MySQL安装过程中手动创建Linux系统中配置文件为my.cnf, 在/etc或/etc/mysql中1
2
3
4
5
6
7[mysqld]
# 开启慢查询日志, 值也可为1
slow_query_log = on
# 指定日志文件, 需要手动创建文件
sloq_query_log_file = /var/log/mysql/slow_query.log
# 指定慢查询时间, 单位秒
long_query_time = 2方法二, 在
MySQL中通过指令开启1
2-- 开去慢查询日志, 其他属性配置方式与上配置类似
SET GLOBAL slow_query_log = on;需要通过
bash重启mysql服务1
service mysqld restart
注意点
CentOS需要配置日志文件的所有者, 执行chown mysql:mysql /var/log/mysql/slow_query.log可以将日志文件所有者设置为mysqlCentOS或其他Linux系统环境若在如上所有配置完之后依然不能开启慢查询日志, 则还需要配置SELinux(安全增强型Linux)信息步骤
- 查找 & 安装工具包
- 执行
yum provides /usr/sbin/semanage查询指令所在包 - 执行
yum install -y xxx进行安装, xxx是上面一条指令所提供的包, 一般完整包名以policycoreutil-python开头
- 执行
- 执行
semanage fcontext -a -t mysqld_log_t /var/log/mysql/slow_query.log, 这条指令将日志文件的默认安全类型设定为mysql_log_t - 执行
restorecon -Rv /var/log/mysql/slow_query.log, 这条指令将回复日志文件的安全上下文至刚设定的状态
- 查找 & 安装工具包
查看慢查询日志记录
bash中执行如下语句可以查看慢查询记录, 也可以通过cat指令查看
1 | mysqldumpslow -a /var/log/mysql/slow_query.log |
在查询前可以通过如下SQL语句来手动添加一条慢查询语句:
1 | -- 休眠3秒 |
查询结果为:
1 |
|
其他优化手段与细节 § Details
任何地方都不要使用星号
*来进行查询, 使用具体的字段来代替它, 不要返回用不到的字段注意缓存的使用
MySQL会产生SQL缓存, 重复查询时会使用缓存中的数据, 在表内容更新后消失在调试过程中若重复执行统一语句, 产生结果的时间可能会受缓存影响, 所以在必要时需要加上关键字
SQL_NO_CACHE1
2
3
4
5-- 可能会受缓存影响
SELECT * FROM `user`;
-- 不会受到缓存影响
SELECT SQL_NO_CACHE * FROM `user`;测试过程中使用
SQL_NO_CACHE并不代表数据库不适用缓存, 只是该语句不使用模糊查询时不要前置百分号
%1
2-- 不要这么做
SELECT `id` FROM `user` WHERE `username` LIKE '%T%';使用事务
当一次请求涉及到多条
SQL修改操作, 若失败时也需要按顺序回滚, 这时可以使用到事务使用
BEGIN,COMMIT与ROLLBACK指令进行事务处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17-- 开始事务
BEGIN;
-- 数据操作
INSERT INTO ...
-- 数据操作
UPDATE ...
-- 完成事务, 数据操作成功
COMMIT;
-- 开始事务
BEGIN;
-- 数据操作
INSERT INTO ...
-- 数据操作
UPDATE ...
-- 回滚事务, 数据将会回滚至BEGIN时的状态
ROLLBACK;分库分表
介绍
分库: 将单个数据库中的表拆分到多个数据库中
分表: 将单个表中的数据拆分到多个表中
原因
- 性能方面
- 表过多将增大数据库的连接压力, 分库将连接压力分散到多个数据库上
- 单表数据量过大将降低CRUD性能, 分表将提高每个表的性能
- 可用性方面
- 单数据库或表在发生意外时容易同时丢失所有数据, 分库分表后若发生意外还可以继续维持一部分服务
- 使用: 什么是分库分表,为什么要分库分表?
总结 § Summary
- 使用索引, 可以的话使用联合索引, 要注意大字段使用前缀索引
- 开启慢查询日志, 记录慢查询, 一般在测试或生产环境中使用
- 使用
Explain来分析慢查询的SQL语句, 是否有用到索引, 是否时全表检查等 SQL分析时注意MySQL的缓存- 注意基本
SQL的书写细节, 不要用星号*进行查询, 不要在LIKE内容的最前面加百分号%, 合理使用事务功能, 在表数量过多或数据量过大时进行分库分表