MySQL复习(二):MySQL锁、MySQL事务、SQL优化、数据库分库分表

五、MySQL锁

根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类

1、全局锁

全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock。当需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句

全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本

但是让整个库都只读,可能出现以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的binlog,会导致主从延迟
    在可重复读隔离级别下开启一个事务能够拿到一致性视图

官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的

2、表级锁

MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)

表锁的语法是lock tables … read/write。可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象

如果在某个线程A中执行lock tables t1 read,t2 wirte;这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作。连写t1都不允许

另一类表级的锁是MDL。MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做了变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定不行

在MySQL5.5版本引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁

  • 读锁之间不互斥,因此可以有多个线程同时对一张表增删改查
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行

给一个表加字段,或者修改字段,或者加索引,需要扫描全表的数据。在对大表操作的时候,需要特别小心,以免对线上服务造成影响

在这里插入图片描述

session A先启动,这时候会对表t加一个MDL读锁。由于session B需要的也是MDL读锁,因此可以正常执行。之后sesession C会被blocked,是因为session A的MDL读锁还没有释放,而session C需要MDL写锁,因此只能被阻塞。如果只有session C自己被阻塞还没什么关系,但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了

事务中的MDL锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放

1.如果安全地给小表加字段?

首先要解决长事务,事务不提交,就会一直占着DML锁。在MySQL的information_schema库的innodb_trx表中,可以查到当前执行的事务。如果要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者kill掉这个长事务

2.如果要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而又不得不加个字段,该怎么做?

在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后再通过重试命令重复这个过程

3、行锁

行锁就是针对数据表中行记录的锁。比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新

1)、两阶段锁协议

在这里插入图片描述

事务A持有的两个记录的行锁都是在commit的时候才释放的,事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议

如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

假设要实现一个电影票在线交易业务,顾客A要在影院B购买电影票。业务需要涉及到以下操作:

1.从顾客A账户余额中扣除电影票价

2.给影院B的账户余额增加这张电影票价

3.记录一条交易日志

为了保证交易的原子性,要把这三个操作放在一个事务中。如何安排这三个语句在事务中的顺序呢?

如果同时有另外一个顾客C要在影院B买票,那么这两个事务冲突的部分就是语句2了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。根据两阶段锁协议,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果把语句2安排在最后,比如按照3、1、2这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度

2)、一致性非锁定读

一致性非锁定读是指InnoDB通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或update操作,这时读取操作不会因此去等待行上排它锁的释放。相反地,InnoDB会去读取行的一个快照数据

在这里插入图片描述

非锁定读机制极大地提高了数据库的并发性。在InnoDB的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录有不止一个快照数据,行多版本的并发控制称为多版本并发控制(MVCC)

在事务隔离级别为读提交和可重复读下,InnoDB使用非锁定的一致性读。对于快照数据的定义却不相同。在读提交隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在可重复读隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本

3)、一致性锁定读

InnoDB支持两种一致性的锁定读操作:

  • select … for update
  • select … lock in share mode

select … for update对读取的行记录加一个排它锁,其他事务不能对已锁定的行加上任何锁。select … lock in share mode对读取的行记录加一个共享锁,其他事务可以向北锁定的行加共享锁,但是如果加排它锁,则会被阻塞

4)、行锁的3种算法

InnoDB有3种行锁的算法,分别是:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

1)幻读问题

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行

InnoDB默认的事务隔离级别是可重复读,在该隔离级别下采用Next-Key Lock的方式来加锁。而在事务隔离级别为读提交下,仅采用Record Lock

订单表中有id为1、2、5的三条数据,当隔离级别为读提交的时候会出现幻读的问题,过程如下:

操作 会话A 会话B
1 set session transaction isolation level read committed;
2 begin;
3 select * from order where id>2 for update;(查询结果:5)
4 begin;
5 insert into order(id,order_no,order_name) values(4,4,‘订单4’);
6 commit;
7 select * from order where id>2 for update;(查询结果:4,5)

在可重复隔离级别下,select * fromorderwhere id>2 for update锁住的不是id为5的这条记录,而是对(2, +supremum]这个范围加了排它锁。因此任何对于这个范围的插入操作都是不被允许的,操作5将会被阻塞,从而避免了幻读的问题

2)间隙锁

建表和初始化语句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

这个表除了主键id外,还有一个索引c

为了解决幻读问题(在读提交隔离级别下,不存在间隙锁),InnoDB引入了间隙锁,锁的就是两个值之间的空隙

在这里插入图片描述

当执行select * from t where d=5 for update的时候,就不止是给数据库中已有的6个记录加上了行锁,还同时加了7个间隙锁。这样就确保了无法再插入新的记录

行锁分成读锁和写锁

在这里插入图片描述

跟间隙锁存在冲突关系的是往这个间隙中插入一个记录这个操作。间隙锁之间不存在冲突关系

在这里插入图片描述

这里sessionB并不会被堵住。因为表t里面并没有c=7会这个记录,因此sessionA加的是间隙锁(5,10)。而sessionB也是在这个间隙加的间隙锁。它们用共同的目标,保护这个间隙,不允许插入值。但它们之间是不冲突的

间隙锁和行锁合称Next-Key Lock,每个Next-Key Lock是前开后闭区间。表t初始化以后,如果用select * from t for update要把整个表所有记录锁起来,就形成了7个Next-Key Lock,分别是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。因为+∞是开区间,在实现上,InnoDB给每个索引加了一个不存在的最大值supremum,这样才符合都是前开后闭区间

间隙锁和Next-Key Lock的引入,解决了幻读的问题,但同时也带来了一些困扰

间隙锁导致的死锁

在这里插入图片描述

1.sessionA执行select … for update语句,由于id=9这一行并不存在,因此会加上间隙锁(5,10)

2.sessionB执行select … for update语句,同样会加上间隙锁(5,10),间隙锁之间不会冲突

3.sessionB试图插入一行(9,9,9),被sessionA的间隙锁挡住了,只好进入等待

4.sessionA试图插入一行(9,9,9),被sessionB的间隙锁挡住了

两个session进入互相等待状态,形成了死锁

间隙锁的引入可能会导致同样的语句锁住更大的范围,这其实是影响并发度的

3)Next-Key Lock加锁规则

  • 原则1:加锁的基本单位是Next-Key Lock,Next-Key Lock是前开后闭区间
  • 原则2:查找过程中访问到的对象才会加锁
  • 优化1:索引上的等值查询,给唯一索引加锁的时候,Next-Key Lock退化为行锁
  • 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,Next-Key Lock退化为间隙锁
  • 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止

这个规则只限于MySQL5.x系列<=5.7.24,8.0系列<=8.0.13

5)、死锁和死锁检测

在并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁

在这里插入图片描述

事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有两种策略:

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑

在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的

正常情况下还是要采用主动死锁检查策略,而且innodb_deadlock_detect的默认值本身就是on。主动死锁监测在发生死锁的时候,是能够快速发现并进行处理的,但是它有额外负担的。每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁

如果所有事务都要更新同一行的场景,每个新来的被堵住的线程都要判断会不会由于自己的加入导致死锁,这是一个时间复杂度是O(n)的操作

怎么解决由这种热点行更新导致的性能问题?

1.如果确保这个业务一定不会出现死锁,可以临时把死锁检测关掉

2.控制并发度

3.将一行改成逻辑上的多行来减少锁冲突。以影院账户为例,可以考虑放在多条记录上,比如10个记录,影院的账户总额等于这10个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成员原来的1/10,可以减少锁等待个数,也就减少了死锁检测的CPU消耗

六、MySQL事务

1、事务的特性

  • 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 隔离性:数据库允许多个并发事务同时对数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

2、隔离级别

1)、当数据库上有多个事务同时执行的时候,就可能出现脏读、不可重复读、幻读的问题

  • 脏读:B事务读取到了A事务尚未提交的数据
  • 不可重复读:一个事务读取到了另一个事务中提交的update的数据
  • 幻读/虚读:一个事务读取到了另一个事务中提交的insert的数据

2)、事务的隔离级别包括:读未提交、读提交、可重复读和串行化

  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到(解决脏读,Oracle默认的隔离级别)
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,而且未提交变更对其他事务也是不可见的(解决脏读和不可重复读,MySQL默认的隔离级别)
  • 串行化:对于同一行记录,写会加写锁,读会加读锁,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行(解决脏读、不可重复读和幻读)

安全性依次提交,性能依次降低

3)、案例

假设数据表T中只有一列,其中一行的值为1

create table T(c int) engine=InnoDB;
insert into T(c) values(1);

下面是按照时间顺序执行两个事务的行为:

在这里插入图片描述

  • 若隔离级别是读未提交,则V1是2。这时候事务B虽然还没提交,但是结果已经被A看到了。V2、V3都是2
  • 若隔离级别是读提交,则V1是1,V2是2。事务B的更新在提交后才能被A看到。V3也是2
  • 若隔离级别是可重复读,则V1、V2是1,V3是2。之所以V2是1,遵循的是事务在执行期间看到的数据前后必须是一致的
  • 若隔离级别是串行化,V1、V2值是1,V3是2

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。读未提交隔离级别下直接返回记录上的最新值,没有视图概念;在读提交隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的;在可重复读隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图;而串行化隔离级别下直接用加锁的方式来避免并行访问(InnoDB会对每个select语句后自动加上lock in share mode)

3、事务启动的方式

MySQL的事务启动方式有以下几种:

  • 显示启动事务语句,begin或start transaction。提交语句是commit,回滚语句是rollback
  • set autocommit=0,这个命令将这个线程的自动提交关掉。意味着如果只执行一个select语句,这个事务就启动了,而且不会自动提交事务。这个事务持续存在直到主动执行commit或rollback语句,或者断开连接

建议使用set autocommit=1,通过显示语句的方式来启动事务

可以在information_schema库中的innodb_trx这个表中查询长事务,如下语句查询持续时间超过60s的事务

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

4、事务隔离的实现(以可重复读为例)

redo log恢复提交事务修改的页操作,而undo log回滚行记录到某个特定版本。redo log是物理日志,记录的是页的屋里修改操作;undo log是逻辑日志,根据每行记录进行记录

在MySQL中,每条记录在更新的时候都会同时记录一条回滚操作(undo log)。记录上的最新值,通过回滚操作,都可以得到前一个状态的值

假设一个值从1被按顺序改成了2、3、4,在undo log里面就会有类似下面的记录:

在这里插入图片描述

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-viewA,要得到1,就必须将当前值一次执行图中所有的回滚操作得到

即使现在有另外一个事务正在将4改成5,这个事务跟read-view A、B、C对应的事务是不会冲突的

系统会判断,当没有事务再需要用到这些undo log时,回收已经使用并分配的undo页

下面是一个只有两行的表的初始化语句:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

事务A、B、C的执行流程如下,采用可重复读隔离级别

在这里插入图片描述

begin/start transaction命令:不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动,一致性视图是在执行第一个快照读语句时创建的

start transaction with consistent snapshot命令:马上启动一个事务,一致性视图是在执行这条命令时创建的

按照上图的流程执行,事务B查到的k的值是3,而事务A查到的k的值是1

1)、快照在MVCC里是怎么工作的?

在可重复读隔离级别下,事务启动的时候拍了个快照。这个快照是基于整个库的,那么这个快照是如何实现的?

InnoDB里面每个事务有一个唯一的事务ID,叫做transaction id。它在事务开始的时候向InnoDB的事务系统申请,是按申请顺序严格递增的

每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记作row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本,每个版本有自己的row trx_id

下图是一个记录被多个事务连续更新后的状态:

在这里插入图片描述

语句更新生成的undo log(回滚日志)就是上图中的是哪个虚线箭头,而V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3、U2算出来的

按照可重复读的定义,一个事务启动的时候,能够看到所以已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前在启动了但还没提交的所有事务ID。数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。这个视图数组和高水位就组成了当前事务的一致性视图。而数据的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的

这个视图数组把所有的row trx_id分成了几种不同的情况

在这里插入图片描述

对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:

1)如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的

2)如果落在红色部分,表示这个版本是由将来启动的事务生成的,肯定不可见

3)如果落在黄色部分,那就包括两种情况

  • 若row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见
  • 若row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见

InnoDB利用了所有数据都有多个版本的这个特性,实现了秒级创建快照的能力

2)、为什么事务A的查询语句返回的结果是k=1?

假设:

1.事务A开始时,系统里面只有一个活跃事务ID是99

2.事务A、B、C的版本号分别是100、101、102

3.三个事务开始前,(1,1)这一行数据的row trx_id是90

这样,事务A的是数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]

在这里插入图片描述

从上图中可以看到,第一个有效更新是事务C,从数据从(1,1)改成了(1,2)。这时候,这个数据的最新版本的row trx_id是102,而90这个版本已经成为了历史版本

第二个有效更新是事务B,把数据从(1,2)改成了(1,3)。这时候,这个数据的最新版本是101,而102又成为了历史版本

在事务A查询的时候,其实事务B还没提交,但是它生成的(1,3)这个版本已经变成当前版本了。但这个版本对事务A必须是不可见的,否则就变成脏读了

现在事务A要读数据了,它的视图数组是[99,100]。读数据都是从当前版本读起的。所以,事务A查询语句的读数据流程是这样的:

  • 找到(1,3)的时候,判断出row trx_id=101,比高水位大,处于红色区域,不可见
  • 接着,找到上一个历史版本,一看row trx_id=102,比高水位大,处于红色区域,不可见
  • 再往前找,终于找到了(1,1),它的row trx_id=90,比低水位小,处于绿色区域,可见

虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到这行数据的结果都是一致的,我们称之为一致性读

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  • 版本未提交,不可见
  • 版本已提交,但是是在视图创建后提交的,不可见
  • 版本已提交,而且是在视图创建前提交的,可见

事务A的查询语句的视图数组是在事务A启动的时候生成的,这时候:

  • (1,3)还没提交,属于情况1,不可见
  • (1,2)虽然提交了,但是是在视图数组创建之后提交的,属于情况2,不可见
  • (1,1)是在视图数组创建之前提交的,可见

3)、为什么事务B的查询语句返回的结果是k=3?

在这里插入图片描述

事务B要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set k=k+1是在(1,2)的基础上进行的操作

更新数据都是先读后写的,而这个读,只能读当前的值,称为当前读。除了update语句外,select语句如果加锁,也是当前读

假设事务C不是马上提交的,而是变成了下面的事务C’,会怎么样?

在这里插入图片描述

上图中,事务C更新后没有马上提交,在它提交前,事务B的更新语句先发起了。虽然事务C还没提交,但是(1,2)这个版本也已经生成了,并且是当前的最新版本

这时候涉及到了两阶段锁协议,事务C没提交,也就是说(1,2)这个版本上的写锁还没释放。而事务B是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务C释放这个锁,才能继续它的当前读

在这里插入图片描述

七、SQL优化

1、通过EXPLAIN分析SQL执行计划

在这里插入图片描述

下面对图示中的每一个字段进行说明:

1)id:每个执行计划都有一个id,如果是一个联合查询,这里还将有多个id

2)select_type:表示SELECT查询类型,常见的有SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION 中后面的查询)、SUBQUERY(子查询)等

3)table:当前执行计划查询的表,如果给表起别名了,则显示别名信息

4)partitions:访问的分区表信息

5)type:表示从表中查询到行所执行的方式,查询方式是SQL优化中一个很重要的指标,结果值从好到差依次是:system > const > eq_ref > ref > range > index > ALL

  • system/const:表中只有一行数据匹配,此时根据索引查询一次就能找到对应的数据
  • eq_ref:使用唯一索引扫描,常见于多表连接中使用主键和唯一索引作为关联条件
  • ref:非唯一索引扫描,还可见于唯一索引最左原则匹配扫描
  • range:索引范围扫描,比如:<,>,between等操作
  • index:索引全表扫描,此时遍历整个索引树
  • ALL:表示全表扫描,需要遍历全表来找到对应的行

6)possible_keys:可能使用到的索引

7)key:实际使用到的索引

8)key_len:当前使用的索引的长度

9)ref:关联 id 等信息

10)rows:查找到记录所扫描的行数

11)filtered:查找到所需记录占总扫描记录数的比例

12)Extra:额外的信息

2、数据库设计方面

1)对查询进行优化,应尽量避免全表扫描,首先应考虑在 whereorder by 涉及的列上建立索引

2)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num = 0

3)并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用

4)索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insertupdate 的效率,因为 insertupdate 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要

5)应尽可能的避免更新索引数据列,因为索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新索引数据列,那么需要考虑是否应将该索引建为索引

6)尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了

7)尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些

3、SQL语句方面

1)应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描

2)应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num=10 or num=20 可以这样查询: select id from t where num=10 union all select id from t where num=20

3)innot in 也要慎用,否则会导致全表扫描,如: select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了: select id from t where num between 1 and 3

4)下面的查询也将导致全表扫描: select id from t where name like ‘%abc%’

5)如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描: select id from t where num=@num 可以改为强制查询使用索引: select id from t with(index(索引名)) where num=@num

6)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如: select id from t where num/2=100 应改为: select id from t where num=100*2

7)应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如: select id from t where substring(name,1,3)=’abc’–name以abc开头的id,select id from t where datediff(day,createdate,’2005-11-30′)=0–‘2005-11-30’生成的id 应改为: select id from t where name like ‘abc%’ select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′

8)不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引

9)很多时候用 exists 代替 in 是一个好的选择: select num from a where num in(select num from b) 用下面的语句替换: select num from a where exists(select 1 from b where num=a.num)

10)任何地方都不要使用 select * from t ,用具体的字段列表代替*

4、优化分页查询

通常是使用limit M,N+合适的order by来实现分页查询,这种实现方式在没有任何索引条件支持的情况下,需要做大量的文件排序操作(file sort),性能将会非常得糟糕。如果有对应的索引,通常刚开始的分页查询效率会比较理想,但越往后,分页查询的性能就越差

这是因为在使用limit的时候,偏移量M在分页越靠后的时候,值就越大,数据库检索的数据也就越多。例如limit 10000,10这样的查询,数据库需要查询10010条记录,最后返回10条记录。也就是说将会有10000条记录被查询出来没有被使用到

在这里插入图片描述

在这里插入图片描述

通过索引覆盖扫描,使用子查询的方式来优化分页查询:

在这里插入图片描述

在这里插入图片描述

5、不同count的用法

count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值

1)对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给Server层。Server层拿到id后,判断是不可能为空的,就按行累加

2)对于count(1)来说,InnoDB引擎遍历整张表,但不取值。Server层对于返回的每一行,放一个数字1进入,判断是不可能为空的,按行累加

3)对于count(字段)来说,如果这个字段是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;如果这个字段定义允许为null的话,那么执行的时候,判断到有可能是null,还要把值取出来在判断一下,不是null才累加

4)对于count(*)来说,并不会把全部字段取出来,而是专门做了优化。不取值,count(*)肯定不是null,按行累加

按照效率排序count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)

八、数据库分库分表

1、业务分库

业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上

在这里插入图片描述

业务分库能够分散存储和访问压力,但也带来了新的问题

1)join操作问题

业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL的join查询

2)事务问题

原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改

2、分表

单表数据拆分有两种方式:垂直分表水平分表

在这里插入图片描述

垂直分表:表记录相同但包含不同的列。例如,上图的垂直拆分,会把表切分成两个表,一个表包含ID、name、age、sex列,另外一个表包含ID、nickname、description列

水平分表:表的列相同但包含不同的行数据。例如,上图的水平拆分,两个表都包含ID、name、age、sex、nickname、description列,但是一个表包含的是ID从1到999999的行数,另一个表包含的是ID从1000000到9999999

单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的

1)、垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去

垂直分表引入的复杂性主要体现在表操作的数量要增加

2)、水平分表

水平分表适合表行数特别大的表

水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:

1)路由

水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性

常见的路由算法有:

范围路由:选取有序的数据列作为路由的条件,不同分段分散到不同的数据库表中

范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后字表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题

范围路由的优点是可以随着数据的增加平滑地扩充新的表,例如现在的用户是100万,如果增加到1000万,只需要增加新的标就可以了,原有的数据不需要动

缺点是分布不均匀,假如按照1000万来进行分表,有可能某个分段实际存储的数据量只有100条,另外一个分段实际存储的数据量有900万条

Hash路由:选取某个列(或者某几个列组合)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。以用户ID为例,假如一开始就规划了10个数据库表,路由算法可以简单地用user_id%10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的子表中

Hash路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布

配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。以用户ID为例,新增一张user_router表,这个表包含user_id和table_id两列,根据user_id就可以查询对应的table_id

配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了

配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大,性能同样可能成为瓶颈

2)join操作

水平分表后,数据分散在多个表中,如果需要与其他表进行join查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并

3)count()操作

count()相加:在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低

记录数表:新建一张表,假如表名为记录数表,包含table_name、row_count两个字段,每次插入或者删除子表数据成功后,都更新记录数表

4)order by操作

水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序

发布了177 篇原创文章 · 获赞 407 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/104091177