深入理解MySQL学习记录

SQL优化

定位慢SQL

两种解决方案:
1、 查看慢查询日志确定已经执行完的慢查询
2、 show processlist查看正在执行的慢查询

慢查询日志

Mysql的慢查询日志用来记录在Mysql中响应时间超过参数long_query_time(单位秒,默认10)设置的值并且扫描记录数不小于min_examined_row_limit(默认值0)的语句

默认情况慢查询日志不会记录管理语句,可通过设置log_slow_admin_statements=on让管理语句中的慢查询也记录到慢查询日志中
默认情况也不会记录查询时间不超过long_query_time但是不使用索引的语句,可通过设置log_queries_not_using_indexes=on让不使用用索引的SQL记录到慢查询日志中
  1. 开启慢查询日志
set global slow_query_log=on;
  1. 设置慢查询时间阈值
set global long_query_time=1;
  1. 确定慢查询日志路径
show global variables like "datadir";
  1. 确定慢查询日志文件名
show global variables like "slow_query_log_file";

通过show processlist

如果不适用full关键字,在info字段中只显示每个语句的前100个字符。如果想看语句的全部内容可以使用full修饰(show full processlist)
show processlist\G;

使用explain分析慢查询

在查询的语句前面加上explain运行就行

需要重点关注的项

select_type   查询类型:显示本行是简单还是复杂查询
type          本次查询的表连接类型
key           实际选择的索引
rows          预计需要扫描的函数,对innoDB来说,这个值是估值,并不一定准确
extra         附加信息

show profile分析慢查询

通过profile,能够更清楚的了解SQL执行过程的资源使用情况,能让我们知道到底慢在哪个环节

可通过配置参数profiling=1来启用SQL分析。该参数可以在全局和session级别来设置。对于全局级别则作用于整个MySQL实例,而session级别仅影响当前session。改参数开启后,后续执行的SQL语句都将记录其资源开销,如IO、上下文切换、CPU、Memory等等
  1. 确定是否支持profile
select @@have_profiling;
  1. 查看profiling是否关闭
select @@profiling;
  1. 开启profile
set profiling=1;
  1. 执行SQL语句
  2. 确定SQL的query id
show profiles;
  1. 查询SQL执行详情
show profile for query 1;

trace分析SQL

只建议分析问题时临时开启

  1. 开启trace
set session optimizer_trace="enabled=on",end_markers_in_json=on;
  1. 执行SQL语句
  2. 分析SQL语句
select * from information_schema.OPTIMIZER_TAECE\G
  1. 关闭trace
set session optimizer_trace="enabled=off";

条件字段有索引,但是不走索引的情况

函数操作

select *from t1 where date(c)='2020-07-16';

对条件字段做函数操作走不了索引。优化

select * from t1 where c>='2020-07-16 00:00:00' and c<='2020-07-16 23:59:59';

隐式转换

当操作符与不同类型的操作对象一起使用时,就会发生类型转换以使操作兼容。某些转换是隐式的
示例:字段a类型为char,查询语句select * from t1 where a=12。MySQL内部会先将a转换为int类型,再去判断,select * from t1 where cast(a as aigned int)=12。优化:应该将12加上单引号

模糊查询

尽量不要使用模糊查询(比如like,like查询不能以%开头)。建议使用ElasticSearch或者其他搜索服务器

范围查询

优化器会根据检索比例、表大小、I/O块大小等进行评估是否使用索引。比如单次查询数据量过大,优化器将不走索引

select * from t1 where b>=1 and b <=2000;

优化后:

select * from t1 where b>=1 and b <=1000;
select * from t1 where b>=1001 and b <=2000;
实际这种范围查询而导致使用不了索引的场景经常出现,比如按照时间段抽取全量数据,每条SQL抽取一个月的;或者某张业务表历史数据的删除。遇到此类操作时,应该在执行之前对SQL做explain分析,确定能走索引,再进行操作,否则不但可能导致操作缓慢,在做更新或者删除时,甚至会导致表所有记录锁住,十分危险。

计算操作

select * from t1 where b-1 =1000;

优化后:

select * from t1 where b =1000 + 1;
一般需要对条件字段做计算时,建议通过程序代码实现,而不是通过MySQL实现。如果在MySQL中计算的情况避免不了,那必须把计算放在等号后面。

如何优化数据导入

  1. 建议有大批量导入时,推荐一条insert语句插入多行数据
  2. 关闭自动提交。autocommit开启时会为每个插入执行提交。可以在innoDB导入数据时,关闭自动提交
set autocommit=0;
多条插入语句
commit;
  1. 参数调整
    影响MySQL写入速度的主要两个参数:innodb_flush_log_at_trx_commit、sync_binlog
    innodb_flush_log_at_trx_commit:控制重做日志刷新到磁盘的策略
0:master线程每秒把redo log buffer写到操作系统缓存,再刷到磁盘
1:每次提交事务都把redo log buffer写到操作系统缓存,再刷到磁盘
2:每次事务提交都把redo log buffer写到操作系统缓存,由操作系统来管理刷盘

sync_binlog:控制binlog的刷盘时机

0:二进制日志从不同步到磁盘,依赖OS刷盘机制
1:二进制日志每次提交都会刷盘
n(n>1):每n次提交落盘一次

如果对数据库安全性要求不高(比如你的测试环境),可以尝试都设置为0后(当然这种情况可能会丢数据)再导入数据,能大大提升导入速度。

让order by、group by查询更快

order by原理

MySQL排序方式(使用 explain 来查看该排序 SQL 的执行计划,重点关注 Extra 字段)

  1. 通过有序索引直接返回有序数据
  2. 通过Filesort进行排序

Filesort是在内存还是磁盘完成排序

取决于排序的数据大小和sort_buffer_size配置的大小
“排序的数据大小” “排序的数据大小”>sort_buffer_size:磁盘排序
查看Filesort排序方式
使用trace进行分析,重点关注number_of_tmp_files。如果等于0,则表示在内存中完成排序;如果大于0,则表示在磁盘中完成排序

Filesort排序模式

双路排序(又叫回表排序模式):首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行ID,然后在sort buffer中进行排序,排序完成后需要再次取回其它需要的字段
单路排序:一次性取出满足条件行的所有字段,然后在sort buffer中进行排序
打包数据排序模式:与单路排序相似,区别是将char和varchar字段存在sort buffer中时更加紧缩
max_length_for_sort_data > 查询字段字节的总长度:单路排序
max_length_for_sort_data < 查询字段字节的总长度:双路排序

MySQL常见字段类型及所占字节

字段类型      字节
int            4
bigint         8
decimal(m,d)   m+2
datetime       8
timestamp      4
char(m)        m
varchar(m)     m

order by优化

  1. 添加索引(采用有序索引排序)
select id,a,b from t1 order by a,b;
a. 在排序字段添加索引或者联合索引(对于多字段排序)(a、b)
select id,a,b from t1 where a=1000 order by b;
b. 对于先等值查询再排序的语句,可通过在条件字段和排序字段添加联合索引来优化(a、b)
  1. 去掉不需要的返回字段
  2. 修改参数
max_length_for_sort_data:如果觉得排序效率比较低,可适当加大,让优化器优先选择全字段排序,当然不能设置过大,可能导致CPU利用率过低或者I/O过高
sort_buffer_size:适当加大,尽可能让排序在内存中完成。但不能设置过大,可能导致数据库服务器Swap
  1. 几种无法利用索引排序的情况
a. 使用范围查询再排序
select id,a,b from t1 where a>9000 order by b;

		a   1  1  2  3
		b   3  1  2  3

如上述情况,对a、b联合索引的表,对a字段范围查询,b字段整体是无序的,则会使用Filesort排序

b. ASC和DESC混合使用无法使用索引
select id,a,b from t1 order by a asc,b desc;

group by优化

默认情况,会对 group by 字段排序,因此优化方式与 order by 基本一致,如果目的只是分组而不用排序,可以指定 order by null 禁止排序。

分页查询优化

根据自增且连续主键排序的分页查询

select * from t1 limit 99000,2;

优化后:
满足条件:

  1. 主键自增且连续
  2. 结果是按照主键排序的
select * from t1 where id >99000 limit 2;

查询根据非主键字段排序的分页查询

select * from t1 order by a limit 99000,2;

扫描整个索引并查找到没索引的行的成本比扫描全表的成本更高,所以优化器放弃使用索引.关键让排序时返回的字段尽可能少

select * from t1 f inner join (select id from t1 order by a limit 99000,2)g on f.id = g.id;

join语句优化

关联查询算法

Nested-Loop Join算法

在MySQL被驱动表的关联字段(a)有索引,才会使用此算法

select * from t1 inner join t2 on t1.a = t2.a;

如果没固定连接方式(比如没加 straight_join)优化器会优先选择小表做驱动表
执行过程:

1. 从表t2(驱动表)中读取一行数据
2. 从第1步的数据中,取出关联字段a,到表t1(被驱动表)中查找
3. 取出表t1中满足条件的行,跟t2中获取到的结果合并,作为结果返回给客户端
4. 重复上面3步

Block Nested-Loop Join算法

把驱动表的数据读入到join_buffer中,然后扫描被驱动表,把被驱动表每一行取出来跟join_buffer中的数据做对比,如果满足join条件,则返回结果给客户端

b字段没有索引
select * from t1 inner join t2 on t1.b = t2.b;

执行过程:

1. 把t2的所有数据放到join_buffer中
2. 把表t1中每一行取出来,跟join_buffer中的数据作对比
3. 返回满足join条件的数据

Batched Key Access算法(前两种算法的结合体)

1. 将驱动表中相关列放入到join_buffer中
2. 批量将关联字段的值发送到Multi-Range Read(MRR)接口
3. MRR通过接收到的值,根据其对应的主键ID进行排序,然后再进行数据的读取和操作
4. 返回结果给客户端

MRR相关知识

当表很大并且没有存储在缓存中时,使用辅助索引上的范围扫描读取行可能导致对表有很多随机访问。而Multi-Range Read优化的设计思路是:查询辅助索引时,对查询结果先按照主键进行排序,并按照主键排序后的顺序,进行顺序查找,从而减少随机访问磁盘的次数。
使用MRR时,explain输出的Extra列显示的是Using MRR。
optimizer_switch中mrr_cost_based参数的值会影响MRR。
如果mrr_cost_based=on,表示优化器尝试在使用和不使用MRR之间进行基于成本的选择
如果mrr_cost_based=off,表示一直使用MRR

开启BKA

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

优化关联查询

  1. 在被驱动表的关联字段上添加索引,让BNL变成NLJ或者BKA
  2. 小表作为驱动表,小表驱动大表,减少扫描行数
    straight_join 可以固定连接方式,让前面的表为驱动表
select * from t2 straight_join t1 on t2.a = t1.a;
  1. 当遇到BNL的join语句,如果不方便在关联字段上添加索引,不妨尝试创建临时表,然后在临时表中的关联字段上添加索引,然后通过临时表来做关联查询

为何count(*)这么慢

重新认识count()

  1. count(a)和count()的区别
    count(a):不会统计a字段为null的记录
    count(
    ):会统计所有结果集
  2. MyISAM引擎和InnoDB引擎count()的区别
    MyISAM:MyISAM引擎会把表的总行数存在磁盘上。如果没有where字句,也没检索其他列,那么count(
    )会直接读取磁盘上的行数
    InnoDB:InnoDB并不会保留表的行数,因为并发事务可能同时读取到不同的行数。所以count(*)时临时计算的
  3. MySQL 5.7.18前后count()区别
    MySQL 5.7.18前:InnoDB通过扫描聚簇索引来处理count(
    )
    MySQL 5.7.18开始:通过遍历最小的可用二级索引来处理count()。如果不存在二级索引,则扫描聚簇索引(InnoDB二级索引树的叶子节点存放的是主键,而主键索引的叶子节点存放的是整行数据。所有count(主键)没有count()快)
  4. count(1)与count(*)统计结果没差别

加快count()

  1. show table status
    Rows表示这张表的行数。但是这个值是估算值,可能与实际值相差
  2. 用Redis做计数器
    a. 先执行一次精确计数
select count(*) from t1;

b. 将查询的总数存入Redis

set t1_count 10003
···
c. 插入数据或删除数据时更新Redis
d. 需要查找t1数据量时,直接读取Redis
```Redis
get t1_count

Redis计数补充

INCR t1_count表示键t1_count数值加1
DECR t1_count表示键t1_count数值减1

增加、删除多行
INCRBY t1_count 10
DECRBY t1_count 10

当MySQL数据已经加1,但是Redis还没有来得及加1,此时另一个session读取Redis中的t1_count的值,此时的t1_count就不准确了
3. 增加计数表
按照Redis做计数器的方式,用MySQL中的一张InnoDB表代替。在数据写入和计数操作都在一个事务中,这样可以避免计数不准确的情况

MySQL索引

索引提高查询速度的原因

跟索引相关的算法

  1. 二分查找法
    将记录按顺序排序,查找时先以有序列的中心位置为比较对象,如果要找的元素值小于该中点元素,则将查询范围缩小为左半部分;如果要找的元素值大于该中点元素,则将查询范围缩小为右半部分。依次类推,直到查到需要的值
  2. 二叉查找树
    二叉查找树中,左子树的键值总是小于根的键值,右子树的键值总是大于根的键值,并且每个节点最多只有两颗子树
    https://img1.sycdn.imooc.com/5d3ad35700016ad406250420.png
  3. 平衡二叉树
    满足二叉查找树的定义,另外必须满足任何节点的两个子树的高度差最大为1
    https://img1.sycdn.imooc.com/5d3ad37900019e3605020368.png
  4. B树
    B树和平衡二叉树稍有不同的是B树属于多叉树,可以理解为一个节点可以拥有多于2个子节点的平衡多叉树
    https://img1.sycdn.imooc.com/5d3ad3bd0001164505200262.png
  5. B+树
    B+树是B树的变体,定义基本与B树一致,与B树不同点:
所有叶子节点中包含了全部关键字的信息
各叶子节点用指针进行连接
非叶子节点上只存储key的信息,这样相对于B树,可以增加每一页中存储key的数量
B树是纵向扩展,最终变为一个“廋高个”,而B+树是横向扩展的,最终会变成一个“矮胖子”

https://img1.sycdn.imooc.com/5d3ad48900010aee05790308.png

B+树索引

在数据库中,B+树的高度一般都在2~4层,所以查找某一行数据最多只需要2到4次IO。而没有索引,需要逐行扫描。B+树索引并不能找到一个给定键值的具体行,B+树索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入缓冲池(buffer pool)中,在内存中通过二分查找法进行查找
InooDB中B+树索引分为聚集索引和辅助索引

聚集索引

InnoDB的数据是按照主键顺序存放的,而聚集索引就是按照每张表的主键构造的一颗B+树,它的叶子节点存放的是整行数据。
InnoDB的主键一定是聚集索引。如果没有定义主键,聚集索引可能是第一个不允许为null的唯一索引,也有可能是row id
由于实际的数据页只能按照一颗B+树进行排序,因此每张表只能有一个聚集索引(TokuDB引擎除外)。查找优化器倾向于采用聚集索引,因为聚集索引能够在B+树索引的叶子节点上直接找到数据
聚集索引对于主键的排序查找和范围查找速度非常快
https://img1.sycdn.imooc.com/5d3ad4d800016bf406160335.png

辅助索引

InnoDB存储引擎辅助索引的叶子节点并不会放整行数据,而存放的是键值和主键ID
当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引树来查找对应记录的主键,然后通过主键索引来找到对应的行数据

哪些情况需要添加索引

  1. 数据检索
    在条件字段添加索引
  2. 聚合函数
    对max()、min()等聚合函数条件添加索引
  3. 排序
  4. 避免回表
select b,c from t9_1 where b=90000;

如果条件字段(b)和需要查询的字段(b,c)有联合索引,通过辅助索引查询后,不会再回表去查询整条数据行
5. 关联查询

普通索引和唯一索引

Insert Buffer

对于非聚集索引的插入时,先判断插入的非聚集索引页是否在缓冲池中。如果在,则直接插入;如果不在,则放入Insert Buffer中,然后再以一定频率和情况进行Insert Buffer和辅助索引页子节点的merge操作。这时通常能够将多个插入合并到一个操作中
Insert Buffer的好处

减少磁盘的离散读取
将多次插入合并为一次操作

使用Insert Buffer需要满足的条件

索引是辅助索引
索引不是唯一

Change Buffer

影响参数:

innodb_change_buffering:确定哪些场景使用Change Buffer,包含none、inserts、deletes、changes、purges、all。默认为all
innodb_change_buffer_max_size:控制Change Buffer最大使用内存占总buffer pool的百分比。默认25

使用Change Buffer需要满足的条件

索引是辅助索引
索引不是唯一

为什么唯一索引的更新不使用Change Buffer
原因:唯一索引必须要将数据页读入内存才能判断是否违反唯一性约束。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用Change Buffer

普通索引和唯一索引的区别

有普通索引的字段可以写入重复的值,而有唯一索引的字段不可以写入重复的值
数据修改时,普通索引可以使用Change Buffer,而唯一索引不行
数据修改时,唯一索引在RR隔离级别下,更容易出现死锁
查询数据时,普通所有查找到满足条件的第一条记录还需要继续查找下一个记录,而唯一索引查找到第一个记录就可以直接返回结果了,但是普通所有多出的查找次数所消耗的资源多数情况可以忽略不计

普通索引和唯一索引如何选择

如果业务要求某一个字段唯一,但是代码不能完全保证写入唯一值,则添加唯一索引,让这个字段唯一,该字段新增重复数据时,则会报错
如果代码确定某个字段不会有重复的数据写入时,则可以选择添加普通索引。因为普通索引可以使用Change Buffer,并且出现死锁的概率比唯一索引低

联合索引

认识联合索引

对a、b做联合索引,存储到B+树结构中,会出项(1,1)(1,2)(1,3)(2,1)(2,2)(2,3)的排序。因此对于a、b两个字段都做条件时,查询是可以走索引的;对于单独a字段查询也是可以走索引的,但是对于b字段单独查询就走不了索引

where条件中,经常同时出现的列放在联合索引中
把选择性最大的列放在联合索引的最左边

联合索引使用分析

CREATE TABLE `t11` ( /* 创建表t11 */
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(20) DEFAULT NULL,
`b` int(20) DEFAULT NULL,
`c` int(20) DEFAULT NULL,
`d` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_a_b_c` (`a`,`b`,`c`)
) ENGINE=InnoDB CHARSET=utf8mb4 ;
  1. 可以完整用到联合索引的情况
联合索引各字段都做为条件时,各字段的位置不会影响联合索引的使用
select * from t11 where a=1 and b=1 and c=1;
select * from t11 where c=1 and b=1 and a=1;
联合索引前面的字段使用范围查询,后面的字段做为条件时仍然可以使用完整的联合索引
select * from t11 where a=2 and b in (1,2) and c=2;
联合索引前面的字段做为条件时,对后面的字段做排序可以使用完整的联合索引
select * from t11 where a=1 and b=2 order by c;
select * from t11 where a=1 order by b,c;
联合索引的字段同时做排序时(但是排序的三个字段顺序要跟联合索引中三个字段的顺序一致),可以完整用到联合索引
select a,b,c from t11 order by a,b,c;
  1. 只能使用部分联合索引的情况
当条件只包含联合索引的前面部分字段时,可以用到部分联合索引
select * from t11 where a=1 and b=1;
对于联合索引 idx_a_b_c(a,b,c) ,如果条件中只包含 a 和 c,则只能用到联合索引中 a 的索引。c 这里是用不了索引的。联合索引 idx_a_b_c(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c) 三种索引,称为联合索引的最左原则。
select * from t11 where a=1 and c=1;
联合索引前面的字段使用了范围查询,对后面的字段排序使用不了索引排序
select * from t11 where a=2  and b in (3,4) order by c;
  1. 可以用到覆盖索引的情况
    从辅助索引就可以查询到结果,不需要回表查询聚集索引中的记录
select b,c from t11 where a=3;
select c from t11 where a=1 and b=1 ;
select id from t11 where a=1 and b=1 and c=1;
  1. 不能使用联合索引的情况
只使用联合索引后面的字段做为条件查询,则使用不了联合索引
select * from t11 where b=1;
select * from t11 where c=1;
select * from t11 where b=1 and c=1;
对联合索引后面的字段做排序操作,也使用不了联合索引
select * from t11 order by b;

为什么MySQL会选错索引

show index的使用

show index from t13;

几个重要字段的解释:

Non_unique:如果是唯一索引,值为0;如果可以重复,则值为1
Key_name:索引名字
Seq_in_index:索引中的列序号
Column_name:字段名
Collation:字段在索引中的排序方式。A为升序,NULL表示未排序
Cardinality:索引中不重复记录数量的预估值
Sub_part:如果是前缀索引,则会显示索引字符的数量;如果是对整列进行索引,则该字段为NULL
Null:如果列可能包含空值,则该字段为YES;如果不包含空值,则该字段值为''
Index_tyoe:索引类型,包括BTREE、FULLTEXT、HASH、RTREE等

Cardinality取值

Cardinality表示该索引不重复记录数量的预估值。如果该值比较小,那就应该考虑是否还有必要创建这个索引。比如性别这种类型的字段,即使加了索引,Cardinality 值比较小,使用性别做条件查询数据时,可能根本用不到已经添加的索引
Cardinality 统计信息的更新发生在两个操作中:INSERT 和 UPDATE。当然也不是每次 INSERT 或 UPDATE 就更新的,其更新时机为:

表中 1/16 的数据已经发生过变化
表中数据发生变化次数超过 2000000000

InnoDB表取出 B+ 树索引中叶子节点的数量,记为 a;随机取出 B+ 树索引中的 8 个(这个数量有参数 innodb_stats_transient_sample_pages 控制,默认为 8)叶子节点,统计每个页中不同记录的个数(假设为 b1,b2,b3,…,b8)。则 Cardinality 的预估值为:(b1 + b2 + b3 + … b8)* a/8
Cardinality涉及的几个参数:

innodb_stats_transient_sample_pages:设置统计 Cardinality 值时每次采样页的数量,默认值为 8
innodb_stats_method:用来判断如何对待索引中出现的 NULL 值记录,默认为 nulls_equal,表示将 NULL 值记录视为相等的记录。另外还有 nulls_unequal 和 nulls_ignored。nulls_unequal 表示将 NULL 视为不同的记录,nulls_ignored 表示忽略 NULL 值记录
innodb_stats_persistent:是否将 Cardinality 持久化到磁盘。好处是:比如数据库重启,不需要再计算 Cardinality 的值
innodb_stats_on_metadata:当通过命令 show table status、show index 及访问 information_chema 库下的 tables 表和 statistics 表时,是否需要重新计算索引的 Cardinality。目的是考虑有些表数据量大,并且辅助索引多时,执行这些操作可能会比较慢,而使用者可能并不需要更新 Cardinality

统计信息不准确导致选错索引

在MySQL中,优化器控制着索引的选择,一般情况下,优化器会考虑扫描行数、是否使用临时表、是否排序等因素,然后选择一个最优方案去执行SQL语句
MySQL扫描行数并不会每次执行语句都去计算一次,是通过统计信息来预估扫描行数。可以看成show index中的Cardinality。可以使用下面命令重新统计信息:

analyze table t13;

单次选取的数据量过大导致选错索引

使用force index强制走索引idx_a

select a from t13 force index(idx_a) where a>70000 limit 1000;

MySQL锁

全局锁和表锁

全局锁

MySQL全局锁会关闭所有打开的表,并使用全局读锁锁住所有表

FLUSH TABLES WITH READ LOCK;
简称FTWRL

解锁

UNLOCK TABLES;

当执行FTWRL后,所有表都变成只读状态,数据更新或者字段更新会被阻塞
全局锁一般用在整个库做备份。mysqldump 包含一个参数 --single-transaction,可以在一个事务中创建一致性快照,然后进行所有表的备份。因此增加这个参数的情况下,备份期间可以进行数据修改。但是需要所有表都是事务引擎表

表级锁

表级锁有两种:表锁和元数据锁

表锁

使用场景:

1. 事务需要更新某张大表的大部分或者全部数据。如果使用默认的行锁,不仅事务执行效率低,而且可能造成其它事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高事务执行速度
2. 事务涉及多个表,比较复杂,可能会引起死锁,导致大量事务回滚,可以考虑表锁避免死锁

表锁又分为表读锁和表写锁

lock tables t14 read;
lock tables t14 write;

对表执行 lock tables xxx read (表读锁)时,本线程和其它线程可以读,本线程写会报错,其它线程写会等待
对表执行 lock tables xxx write (表写锁)时,本线程可以读写,其它线程读写都会阻塞

元数据锁

在MySQL中,DDL不属于事务范畴。如果事务和DDL并行执行同一张表,可能会出项事务特性被破坏、binlog顺序错乱等问题。为了解决这类问题,从MySQL5.5.3引入了元数据锁,解决同一张表上事务和DDL并行执行时可能导致数据不一致的问题
在开发中,应该尽量避免慢查询、尽量保证事务及时提交、避免大事务等,也应该尽量避免业务高峰期执行DDL操作

InnoDB行锁

InnoDB替代MyISAM的最主要原因:

InnoDB支持事务:适合在并发条件下要求数据一致的场景
InnoDB支持行锁:有效降低由于删除或者更新导致的锁定

两阶段锁

分为加锁阶段和解锁阶段,并且保证加锁阶段和解锁阶段不相交

InnoDB行锁模式

共享锁(S):允许一个事务去读一行,阻止其它事务获得相同数据集的排他锁
排他锁(X):允许获得排他锁的事务更新数据,阻止其它事务获得相同数据集的共享读锁和排他写锁

对于普通 select 语句,InnoDB 不会加任何锁,事务可以通过以下语句显式给记录集加共享锁或排他锁

共享锁(S):select * from table_name where ... lock in share mode
排他锁(X):select * from table_name where ... for update

InnoDB行锁算法

Record Lock:单个记录上的索引加锁。
Gap Lock:间隙锁,对索引项之间的间隙加锁,但不包括记录本身。
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。

事务隔离级别

Read Uncommitted(读未提交,简称:RU):在该隔离级别,所有事务都可以看到其它未提交事务的执行结果。可能会出现脏读
Read Committed(读已提交,简称:RC):一个事务只能看见已经提交事务所做的改变。因为同一个事务的其它实例在该实例处理期间可能会有新的commit,所以可能出现幻读
Repeatable Read(可重复读,简称:RR):这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。消除了脏读、不可重复读,默认也不会出现幻读
Serializable(串行):这是最高的隔离级别,它通过强制事务的排序,使之不可能相互冲突,从而解决幻读问题

对于 RU 隔离级别,会导致脏读,从性能上看,也不会比其它隔离级别好太多,因此生产环境不建议使用。
对于 RC 隔离级别,相比 RU 隔离级别,不会出现脏读;但是会出现幻读,一个事务中的两次执行同样的查询,可能得到不一样的结果。
对于 RR 隔离级别,相比 RC 隔离级别,不会出现幻读,但是相对于 RC,锁的范围可能更大了。
对于 Serializable 隔离级别,因为它强制事务串行执行,会在读取的每一行数据上都加锁,因此可能会导致大量的超时和锁争用的问题。生成环境很少使用。
因此总的来说,建议在 RC 和 RR 两个隔离级别中选一种,如果能接受幻读,需要并发高点,就可以配置成 RC,如果不能接受幻读的情况,就设置成 RR 隔离级别。

RC隔离级别下的行锁实验

通过非索引字段查询

由于条件字段没有索引,因此只能走聚集索引,进行全表扫描。聚集索引上的所有记录都被加上X锁,这是因为MySQL中,如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由server层进行过滤
在没有索引的情况下,InnoDB的当前读会对所有记录都加锁。所以在工作中应该特别注意InnoDB这一特性,否则可能回产生大量的锁冲突

通过唯一索引查询

由于条件字段a是唯一索引,因此select * from t16 where a=1 for update语句会选择走a列的索引进行条件过滤,在找到a=1的记录后,会将唯一索引上a=1索引记录上加上X锁,同时,会根据读取到的id列,回到聚集索引,将id=1对应的聚集索引项加X锁
如果查询的条件是唯一索引,那么SQL需要在满足条件的唯一索引上加锁,并且会在对应的聚集索引上加锁

通过非唯一索引查询

在a字段非唯一索引上,满足c=3的所有记录,都被加锁。同时,对应的主键索引上的记录也都加上了锁。与通过唯一索引查询的情况相比,唯一索引查询最多有一行记录被锁,而非唯一索引将会把满足条件的所有记录都加上锁
如果查询条件是非唯一索引,那么SQL需要在满足条件的非唯一索引上都加上锁,并且会在它们对应的聚集索引上加锁

RR隔离级别下的行锁实验

通过非唯一索引查询

与RC隔离级别下非唯一索引查询相比,不仅会在满足条件的非唯一索引上加锁和对应的聚集索引上加锁,而且由于B+树索引是有序的,会在满足条件的非唯一索引之间(包括满足条件的开始和结尾)加上Gap锁

通过非索引字段查询

非索引字段做条件的当前读不但会把每条记录都加上锁,还会把每个GAP加上GAP锁。到commit之前,除了不加锁的快照读,其它任何加锁的SQL都会等待

通过唯一索引当前读是否会用到GAP锁

GAP 锁的目的是:为了防止同一事务两次当前读,出现幻读的情况。如果能确保索引字段唯一,那其实一个等值查询,最多就返回一条记录,而且相同索引记录的值,一定不会再新增,因此不会出现 GAP 锁
以唯一索引为条件的当前读,不会有 GAP 锁。所以 RR 隔离级别下的唯一索引当前读加锁情况与 RC 隔离级别下的唯一索引当前读加锁情况一致

死锁

认识死锁

死锁是指两个或者多个事务在同一资源相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象
InnoDB解决死锁的两种方式:

1. 检测到死锁的循环依赖,立即返回一个错误,将参数innodb_deadlock_detect设置为on表示开启这个逻辑
2. 等查询的时间达到锁等待超时的设定后放弃请求。这个超时时间由innodb_lock_wait_timeout来控制。默认是50秒

出现死锁的几种情况

  1. 同一张表中
  2. 不同表之间
  3. 事务隔离级别

降低死锁概率

1. 更新SQL的where条件尽量用索引
2. 基于primary或者unique key更新数据
3. 减少范围更新,尤其非主键、非唯一索引上的范围更新
4. 加锁顺序一致,尽可能一次性锁定所有需要行
5. 将RR隔离级别调整为RC隔离级别

分析死锁方法

1. 通过show engine innodb status;命令查看最后一个死锁的信息
2. 设置innodb_print_all_deadlocks=on可以在err log中记录全部死锁信息

事务

数据库忽然断电会丢失数据吗

什么是事务

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功的对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或者其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败
一个良好的事务处理系统,必须具备ACID特性:

atomicity(原子性):要么全执行,要么全都不执行
consistency(一致性):在事务开始和完成时,数据都必须保持一致状态
isolation(隔离性):事务处理过程中的中间状态对外部是不可见的
durability(持久性):事务完成之后,它对于数据的修改是永久性的

InnoDB采用redo log机制来保证事务更新的一致性和持久性

Redo log

Redo log称为重做日志,用于记录事务操作变化,记录的是数据被修改之后的值
Redo log由两部分组成:

内存中的重做日志缓冲(redo log buffer)
重做日志文件(redo log file)

每次数据更新会先更新redo log buffer,然后根据innodb_flush_log_at_trx_commit来控制redo log buffer更新到redo log file的时机。innodb_flush_log_at_trx_commit有三个只可选:

0:事务提交时,每秒触发一次redo log buffer写磁盘操作,并调用操作系统fsync刷新IO缓存。如果数据库崩溃,最后1秒的redo log可能会由于未及时写入磁盘文件而丢失,这种方式尽管效率高,但是最不安全
1:事务提交时,InnoDB立即将缓存中的redo日志写入到日志文件,并调用操作系统faync刷新IO缓存。这是默认值,每个事务提交的时候都会从log buffer写更新记录到日志文件,而且会刷新磁盘缓存,这完全满足事务持久化的要求,是最安全的,但是这样会有比较大的性能损失
2:事务提交时,InnoDB立即将缓存中的redo日志写入到日志文件中,但不是马上调用fsync刷新IO缓存,而是每秒只做一次磁盘IO缓存刷新操作。如果数据库崩溃,由于已经执行了重做日志写入到磁盘的操作,只是没有做磁盘IO刷新操作,因此只要不发生操作系统崩溃,数据就不会丢失,这种方式是对性能和安全的一种折中处理

Bin log

二进制日志记录了所有DDL(数据定义语句)和DML(数据操纵语句),但是不包括select和show这类操作。Binlog作用:

恢复:数据恢复时可以使用二进制日志
复制:通过传输二进制日志到从库,然后进行恢复,以实现主从同步
审计:可以通过二进制日志进行审计数据的变更操作

通过参数sync_binlog来控制累计多少个事务后才将二进制日志fsync到磁盘

sync_binlog=0,表示每次提交事务都只write,不fsync
sync_binlog=1,表示每次提交事务都会执行fsync
sync_binlog=N(N>1),表示每次提交事务都write,累积N个事务后才fsync

要加快写入数据的速度或者机器磁盘IO瓶颈时,可以将sync_binlog设置成大于1的值,但是如果数据库崩溃,可能会丢失最近N个事务的binlog

怎样确保数据库突然断电不丢失数据

只要将innodb_flush_log_at_trx_commit和sync_binlog都设置为1,就能确保MySQL机器断电重启后,数据不丢失

MVCC

MVCC,即多版本并发控制。MVCC的实现,是通过保存数据在某个时间点的快照来实现的,也就是说,不管需要执行多长时间,每个事务看到数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的

Undo log

undo log是逻辑日志,将数据库逻辑的恢复到原来的样子,所有修改都被逻辑的取消了。undo log的另一个作用是MVCC,InnoDB存储引擎中MVCC的实现是通过undo来实现的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo log读取之前的行版本信息,以此实现非锁定读取

MVCC实现原理

InnoDB每一行数据都有一个隐藏的回滚指针,用于指向改行修改前的最后一个历史版本,这个历史版本存放到undo log中。如果要执行更新操作,会将原记录放入undo log中,并通过隐藏的回滚指针指向undo log中的原纪录。其它事物此时需要查询时,就是查询undo log中这行数据的最后一个历史版本

MVCC的优势

读不加锁,读写不冲突,极大增加了MySQL的并发性。通过MVCC保证了事务ACID中的隔离性

分布式事务

分布式事务是指一个大的事务由很多小操作组成,小操作分布在不同的服务器上或者不同的应用程序上,分布式事务需要保证这些小操作要么成功,要么全部失败
分布式事务使用两阶段提交协议:

第一阶段:所有分支事务都开始准备,告诉事务管理器自己已经准备好了
第二阶段:确定是rollback还是commit,如果有一个节点不能提交,则所有节点都要回滚

与本地事务不同点在于:分布式事务需要多一次prepare操作,等收到所有节点的确定信息后,再进行commit或者rollback

MySQL自带的分布式事务

启动分支事务

xa start 'a','a_1';
'a','a_1'表示xid
a表示gtrid,为分布式事务标识符,相同的分布式事务使用相同的gtrid
a_1表示bqual,为分支限定符,分布式事务中每个分支事务的bqual必须不同

结束分支事务

xa end 'a','a_1';

进入准备状态

xa prepare 'a','a_1';

提交分支事务

xa commit 'a','a_1';

回滚分支事务

xa rollbakc 'a','a_1';

返回当前数据库中处于prepare状态的分支事务的详细信息

xa recover;

结合中间件实现分布式

订单业务程序处理完增加订单的操作后,将减库存操作发送到消息队列中间件中(比如:Rocketmq),订单业务程序完成提交。然后库存业务程序检查到消息队列有减对应商品库存的信息,就开始执行减库存操作。库存业务执行完减库存操作,再发送一条消息给消息队列中间件:内容是已经减掉库存

MySQL中一些其他经验

预防SQL注入

通过程序验证、转义用户的输入

主键自增

主键和聚集索引的关系

在InnoDB中,聚集索引不一定是主键,但是主键一定是聚集索引;原因是如果没有定义主键,聚集索引可能是第一个不允许为null的唯一索引,如果也没有这样的唯一索引,InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引
InnoDB的数据是按照主键顺序存放的,而聚集索引就是按照每张表的主键构造的一颗B+树,它的叶子节点存放的是整行数据。
每张InnoDB表都有一个聚集索引,但是不一定有主键

主键是否需要自增

聚集索引是按照每张表的主键构造一颗 B+ 树的,而 B+ 树中,所有记录节点都是按键值的大小顺序存放在同一层叶子节点上。如果每次插入的数据都是在聚集索引树的后面,聚集索引不需要分裂就可以存入数据。但是如果插入的数据值在聚集索引树的中间部分,由于要保证插入后叶子节点中的记录依然排序,就可能需要聚集索引树分裂来保证键值的有序性。
如果业务输入的主键都是随机数字,那么写入数据时很可能会导致数据页频繁分裂,从而影响写入效率。
如果设置主键是自增,那么每一次都是在聚集索引的最后增加,当一页写满,就会自动开辟一个新页,不会有聚集索引树分裂这一步,效率会比随机主键高很多。这也是很多建表规范要求主键自增的原因。

MySQL缓存

如果线上环境中 99% 以上都是只读,很少更新,可以考虑全局开启 QC

读写分离

MySQL读写分离是指:对于修改操作在主库上执行,而对于查询操作,在从库上执行。主要目的是分担主库的压力

主从复制原理

MySQL异步复制

MySQL异步复制原理如下:

在主库开启binlog的情况下
如果主库有增删改的语句,会记录到binlog中
主库通过IO线程把binlog里面的内容传到从库的中继日志(relay log)中
主库给客户端返回commit成功(这里不会管从库是否已经收到事务的binlog)
从库的SQL线程负责读取它的relay log里的信息并应用到从库数据库中

在主库上并行运行的更新SQL,由于从库只有单个SQL线程去消化relay log,因此更新的SQL在从库只能串行执行。这也是很多情况下,会出现主从延迟的原因

MySQL半同步复制

在主库开启binlog的情况下
如果主库有增删改的语句,会记录到binlog中
主库通过IO线程把binlog里面的内容传到从库的中继日志(relay log)中
从库收到binlog后,发送给主库一个ACK,表示收到了
主库收到这个ACK以后,才能给客户端返回commit成功
从库的SQL线程负责读取它的relay里的信息并应用到从库数据库中

常见的读写分离方式

通过程序

开发通过配置程序来决定修改操作走主库,查询操作走从库。这种方式直连数据库,优点是性能会好点,缺点是配置麻烦

通过中间件

拿MyCAT举例
在schema.xml文件中,dataHost标签balance属性的值,决定了是否启用读写分离。balance各个值及对应的读写方法如下:

0:不开启读写分离,读操作发送到writehost
1:全部的readhost与stand by writehost参与select语句的负载均衡
2:所有读操作都随机在writehost、readhost上分发
3:所有读请求随机分发到writehost对应的readhost执行,writehost不负担读压力

出现主从延迟的情况

大表 DDL
大事务
主库 DML 并发大
从库配置差
表上无主键
等等

读写分离怎样应对主从延迟

判断主从是否延迟

查询时先判断主从是否存在延迟,如果存在延迟,则查询落在主库,如果没延迟,则查询语句落在从库
判断主从延迟的方法:

  1. 判断Seconds_Behind_Master是否等于0
    如果Seconds_Behind_Master=0,则查询从库,如果大于0,则查询主库
Seconds_Behind_Master 是在从库上执行 show slave status 时返回的其中一项,表示从库延迟的秒数。Seconds_Behind_Master 并不一定准确。比如网络中断时,Seconds_Behind_Master = 0 ,并不能代表主从无延迟。
  1. 对比位点或者GTID
    如果Master_Log_File跟Relay_Master_Log_File相等,并且Read_Master_Log_Pos跟Exec_Master_Log_Pos相等,则可以把读请求放到从库,否则读请求放到主库
几个参数均是通过show slave status返回的参数,用来查询主从复制的状态
Mastr_Log_File:IO线程正在读取的主库binlog文件名
Relay_Master_Log_File:SQL线程最近执行的事务对应的主库binlog文件名
Read_Master_Log_Pos:IO线程正在读取的主库binlog文件中的位点
Exec_Master_Log_Pos:SQL线程最近读取和执行事务对应的主库binlog文件中的位点

如果开启了GTID复制,则可以对比Retrieved_Gtid_Set和Executed-Gtid_Set是否相等,相等则把读请求放到从库,有差异则读请求放到主库

开启GTID两个参数才会有值
Retrieved_Gtid_Set:从库收到的所有日志的GTID集合
Executed_Gtid_Set:从库已经执行完的GTID集合

采用半同步复制

半同步复制跟传统的异步复制相比,半同步复制保证了所有给客户端发送过确认提交的事务,从库都已经收到这个日志了。因此出现延迟的概率会小很多,当然实际生产应用时,建议结合上面讲的位点或 GTID 判断

等待同步完成

依然采用判断主从是否延迟的方法,只是应对的方式不一样,比如存在延迟,则将情况发聩给程序,在前端页面提醒用户数据未完全同步,如果没有延迟,则查询从库

分库分表

MySQL分库分表是指:把MySQL数据库物理的拆分到多个实例或者机器上去,从而降低单台MySQL实例的负载

MySQL分库分表拆分方法

  1. 垂直拆分
有多个业务,每个业务单独分到一个实例里面
在同一个实例中有多个库,把这些库分别放到单独的实例中
在同一个库中存在过多的表,把这些表拆分到多个库中
把字段过多的表拆分成多个表,每张表包含一部分字段
  1. 水平拆分
    水平拆分,就是把同一张表分为多张表结构相同的表,每张表里存储一部分数据。而拆分的算法也比较多,常见的就是取模、范围、和全局表等

哪些情况需要考虑分库分表

1. 数据量过大,影响了运维操作
2. 把修改频繁的字段拆分出来
3. 把大字段拆分出来
4. 增长比较快
5. 降低不同库或者表的相互影响

分库分表的实现

通过程序

垂直拆分,将不同的业务表放在不同的业务库中,程序只要每个业务配置不同的database即可
水平拆分,通过多种拆分算法实现

通过数据库中间件

安全高效删除大量无用数据

共享表空间和独立表空间

共享表空间

表的数据放在系统共享表空间,也就是跟数据字典放一起。文件名为ibdata1。通过参数innodb_data_file_path设置。在my.cnf中配置:

[mysqld]
innodb_data_file_path=ibdata1:1G;ibdata2:1G:autoextend

表示用两个文件(ibdata1和ibdata2)组成表空间,文件ibdata1大小为1G,文件ibdata2大小为1G,autoectend表示用完1G可以自动增长

独立表空间

每个innoDB表数据存储在一个以.idb为后缀的文件中。由参数innodb_file_per_table控制,设置为on表示使用独立表空间,设置为off表示使用共享表空间
一般情况下建议设置为独立表空间。如果某张表被drop掉,回直接删除掉该表对应的文件,如果放在共享表空间,即使执行了drop table操作,空间还是不能回收

几种数据删除形式

删除表

出于安全考虑,建议步骤如下:

1. 首选将表明改为t29_bak_20200730
alter table t29 rename t29_bak_20200730;
2. 然后等待半个月,观察是否有程序因为找不到表t29而报错
3. 如果没有跟表t29相关的报错,则半个月后直接drop掉调t29_bak_20200730
drop table t29_bak_20200730;

清空表

建议步骤如下:

1. 创建一张与t29表结构相同的临时表
create table t29_bak_20200730 like t29;
2. 并将数据拷贝到临时表
insert into t29_bak_20200730 select * from t29;
3. 再清空该表
truncate table t29;
4. 如果空间不够,观察半个月后,考虑转移t29_bak_20200730的数据到备份机器上,然后删除表t29_bak_20200730
drop table t29_bak_20200730;

清空表建议使用truncate,而不使用delete

非分区表删除部分记录

通过delete删除某条记录,InnoDB引擎会把这条记录标记为删除,但是磁盘文件的大小并不会缩小。如果之后要在这中间插入一条数据,则可以复用这个位置,如果一直没有数据插入,就会形成一个“空洞”。因此delete命令是不能回收空间的,这也是delete后表文件大小没有变化的原因
比如删除2017年前的数据,建议步骤如下:

1. 首先备份全表
2. 确保date字段有索引,如果没有索引,则需要添加索引(目的是避免执行删除命令时,全表扫描)
3. 如果要删除的数据比较多,建议写一个循环,每次删除满足条件记录的1000条(目的是避免大事务),删完为止
delete from table_name where date<'2017-01-01' limit 1000;
4. 最后重建表(目的是释放表空间,但是会锁表,建议在业务低峰执行)
alter table table_name engine=InnoDB;或者optimize table table_name;

分区表删除部分分区

对于需要经常删除历史数据的表,建议配置问分区表,可以通过直接drop掉对应的分区

使用MySQL时,应用层优化

使用连接池

MySQL 如果频繁创建和断开连接,那 MySQL 的开销会比较大,可能会占用过多的服务器内存资源,甚至导致响应时间变慢。此时就可以考虑使用连接池来改进性能。
大致原理:

1. 当进程启动时,创建相应的数据库连接池对象
2. 如果程序需要请求数据库,则直接从连接池获取到一个连接
3. 数据库情况完成后,释放数据库连接池

减少对MySQL的访问

避免对同一行数据做重复检索。比如需要查询同一行数据不同字段的值,将多条SQL合并为一条,减少部分建立连接所花费的内存和时间

增加Redis缓存层

几种Redis减少MySQL压力的场景

  1. 计数器
    在Redis中执行计数器加1:INCR t1_count 缓解MySQL执行update的压力
  2. K-V数据缓存
    在MySQL中,如果某个字段会被频繁查询,而该字段内容变化的概率又不是很大,就可以考虑使用Redis缓存
  3. 消息队列
    生产者通过 lpush 将消息放在 list 中,消费者通过 rpop 取出该消息。

单表过大及时归档

单张表过大,可能会有下面影响:

1. 在修改表结构时导致长时间主从延迟
2. 备份时间过久
3. 查询速度可能会变慢

可以考虑对历史数据归档(比如日志数据),控制单表的数据量

代码层读写分离

提前规划表的索引

在查询操作的条件字段添加合适索引,增加查询速度

MySQL整体优化思路

硬件相关优化

CPU

  1. 关闭CPU节能,设定为最大性能模式
  2. 配置合理的CPU核数和选择合适的CPU主频

内存

InnoDB 使用 InnoDB buffer pool 缓存数据、索引等内容,从而加快访问速度

磁盘

  1. 使用SSD或者Pcie SSD设备
  2. 尽可能选择RAID 10
  3. 设置阵列写策略为Write Back

系统层面优化

调整I/O调度算法

I/O 调度算法建议使用:deadline/noop,尽量不使用 CFQ

文件系统选择

优选选择xfs或者ext4,坚决不用ext3

调整内核参数

vm.swappiness ≤ 10
降低使用 swap 的概率,但是尽量不要设置为 0,可能引起 OOM
vm.dirty_ratio ≤ 5
指定了当文件系统缓存脏页数量达到系统内存百分之多少时(如10%),系统不得不开始处理缓存脏页(因为此时脏页数量已经比较多,为了避免数据丢失需要将一定脏页刷入外存);在此过程中很多应用进程可能会因为系统转而处理文件 IO 而阻塞
vm.dirty_background_ratio ≤ 10
指定了当文件系统缓存脏页数量达到系统内存百分之多少时,就会触发 pdflush/flush/kdmflush 等后台回写进程运行,将一定缓存的脏页异步地刷入外存。

MySQL层优化

参数优化

innodb_buffer_pool_size:该参数控制 InnoDB 缓存表和索引数据的内存区域大小。对性能影响非常大,建议设置为机器内存的 50-80%
innodb_flush_log_at_trx_commit:InnoDB 的 redo 日志刷新方式
sync_binlog:控制累积多少个事务后才将二进制日志 fsync 到磁盘
innodb_file_per_table:开启独立表空间
max_connection:最大连接数。不能设置的过小,防止客户端连接失败;也不能设置的过大,防止数据库内存资源过多消耗。
long_query_time:慢查询时间阀值
query_cache_type、query_cache_size:建议这两个参数都设置为 0

MySQL设计优化

  1. 使用InnoDB存储,不建议使用MyISAM存储引擎
  2. 预估表数据量和访问量,如果数据量或者访问量比较大,则需要提前考虑分库分表
  3. 指定合适的数据库规范,在设计表、执行SQL语句时按照数据库规范进行

MySQL操作规范

命名规范

  1. 表名建议使用有业务意义的英文词汇,必要时可加数字和下划线,并以英文字母开头
  2. 库、表、字段全部采用小写
  3. 避免用 MySQL 的保留字
  4. 命名(包括表名、列名)禁止超过 30 个字符
  5. 临时库、表名必须以 tmp 为前缀,并以日期为后缀,如:tmp_shop_info_20190404
  6. 备份库、表必须以 bak 为前缀,并以日期为后缀,如:bak_shop_info_20190404
  7. 非唯一索引必须按照“idx_字段名称”进行命名;唯一索引必须按照“uniq_字段名称”进行命名

设计规范

  1. 主键
    表必须有主键;
    不使用更新频繁的列做主键;
    尽量不选择字符串列做主键;
    不使用 UUID MD5 HASH 做主键;
    默认使用非空的唯一键。
  2. 如无特殊要求,建议都使用 InnoDB 引擎
  3. 默认使用 utf8mb4 字符集,数据排序规则使用 utf8mb4_general_ci
  4. 所有表、字段都需要增加 comment 来描述此表、字段所表示的含义
    5 如无说明,表必须包含 create_time 和 update_time 字段,即表必须包含记录创建时间和修改时间的字段
  5. 用尽量少的存储空间来存数一个字段的数据
    能用 int 的就不用 char 或者 varchar;
    能用 tinyint 的就不用 int;
    使用 UNSIGNED 存储非负数值;
    只存储年使用 YEAR 类型;
    只存储日期使用 DATE 类型。
  6. 存储精确浮点数必须使用 DECIMAL 替代 FLOAT 和 DOUBLE
  7. 尽可能不使用 TEXT、BLOB 类型
    如果实在有某个字段过长需要使用 TEXT、BLOB 类型,则建议独立出来一张表,用主键来对应,避免影响原表的查询效率。
  8. 禁止在数据库中存储明文密码
  9. 索引设计规范
a. 需要添加索引的字段
UPDATE、DELETE 语句的 WHERE 条件列
ORDER BY、GROUP BY、DISTINCT 的字段
多表 JOIN 的字段
b. 单表索引建议控制在 5 个以内
c. 适当配置联合索引
d. 业务上具有唯一性的字段,添加成唯一索引
e. 在 varchar 字段上建立索引时,建议根据实际文本区分度指定索引长度
f. 索引禁忌
不在低基数列上建立索引,例如:性别字段
不在索引列进行数学运算和函数运算
  1. 不建议使用外键
  2. 禁止使用存储过程、视图、触发器、Event
  3. 单表列数目建议小于 30

SQL语句规范

  1. 避免隐式转换
  2. 尽量不使用select *,只 select 需要的字段
  3. 禁止使用 INSERT INTO t_xxx VALUES (xxx),必须显示指定插入的列属性
  4. 尽量不使用负向查询;比如 not in/like
  5. 禁止以 % 开头的模糊查询
  6. 禁止单条 SQL 语句同时更新多个表
  7. 统计记录数使用 select count(*),而不是 select count(primary_key)或者 select count(普通字段名);
  8. 建议将子查询转换为关联查询
  9. 建议应用程序捕获 SQL 异常,并有相应处理
  10. SQL 中不建议使用 sleep()
  11. 避免大表的 join

行为规范

  1. 批量导入、导出数据必须提前通知 DBA 协助观察;
  2. 有可能导致 MySQL QPS 上升的活动,提前告知DBA;
  3. 同一张表的多个 alter 合成一次操作;
  4. 不在业务高峰期批量更新、查询数据库;
  5. 删除表或者库要求尽量先 rename,观察几天,确定对业务没影响,再 drop

你可能感兴趣的:(sql)