数据库优化手册
以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
: 索引字段的压缩方式, 若未压缩则为null
NULL
: 列中是否允许存在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
可以将日志文件所有者设置为mysql
CentOS
或其他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_CACHE
1
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
内容的最前面加百分号%
, 合理使用事务功能, 在表数量过多或数据量过大时进行分库分表