- ">

- 1、MySQL 锁以及事务
- 1 表锁
- 2 行锁
- 行锁支持事务
- 并发事务处理带来的问题
- 事务隔离级别
- 什么是脏读?幻读?不可重复读?
· 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
· 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
· 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。- SQL 标准定义了四个隔离级别
- 2、索引
- Hash索引和B+树索引的区别?
- 为什么B+树比B树更适合实现数据库索引?
- 索引什么时候会失效?
- 3、回表查询
- 4、覆盖索引
- 5 、哪些场景可以利用索引覆盖来优化SQL?
- 6、MySQL执行慢分析
- 7、数据库的乐观锁和悲观锁是什么?
- 8、MyIASM和Innodb两种引擎所使用的索引的数据结构是什么?
- 9、数据库优化的思路
- MySQL的Limit有性能问题
- MVCC 实现原理
MySQL 面试题锦:https://learnku.com/articles/40216
https://www.jianshu.com/p/c82148473235
mysql索引:https://mp.weixin.qq.com/s/rS-zbxX2P_1-r0jE8HZQjw
[https://mp.weixin.qq.com/s/QduIxKOykMmoZp13UGF1XQ](https://mp.weixin.qq.com/s/QduIxKOykMmoZp13UGF1XQ)
1、MySQL 锁以及事务
- 锁的定义
锁是计算机协调多个进程或线程并发访问某一资源的机制。
在数据库中,除了传统的计算资源(如 CPU、RAM、I/O等)的争用以外,数据也是一种需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库需要解决的问题,锁冲突也是影响数据库并发性能的一个重要因素。
- 锁分类
- 从性能上分为乐观锁和悲观锁
- 从数据库操作的类型分为读锁和写锁
读锁:针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁:当前写操作没有完成前,它会阻断其他写锁和读锁
- 从对数据的操作粒度分为表锁和行锁
1 表锁
每次操作会锁住整张表。
优点:开销小,加锁快,不会出现死锁
缺点:锁的粒度大,发生锁冲突的概率高,并发度最低
--手动增加表锁lock table 表名称 read(write),表名称2 read(write);--查看表上加过的锁show open tables;--删除表锁unlock tables;行锁**
- 加读锁
lock table 表名 read;
当前session和其他session都可以读该表;
当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待
- 加写锁
lock table 表名 write;
当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞。
- MyISAM表分析
MyISAM在执行查询语句select前,会自动给涉及的所有表加读锁,在执行增删该操作前,会自动给涉及的表加写锁。
对MyISAM表的读操作(加读锁),不会阻碍其他进程对同一表的读请求,但会阻碍对同一表的写请求。只有当读锁释放后,才会执行其他操作的写操作。
对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其他进程的读写操作。
2 行锁
每次总锁住一行数据。
优点:锁粒度最小,发生锁冲突概率最低,并发度最高
缺点:开销大、加锁慢,会出现死锁;
行锁支持事务
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity):事务是一个原子操作单元,对数据的修改,要么全部执行,要么全部不执行。
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致的状态。意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构也都必须是正确的。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能保持。
并发事务处理带来的问题
- 脏读(Dirty Reads):一个事务读取到另外一个事务未提交的数据
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此进一步的处理,就会产生未提交的数据依赖关系。这种现象被称为“脏读”。
事务A读取到事务B已经修改但未提交的数据,还在这个数据基础上做了修改。此时,如果事务B回滚了,事务A的数据无效,不符合一致性要求。 - 不可重读(Non-Repetable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现起读出的数据已经发生了改变、或某些记录已经被删除。这种现象叫做“不可重读”。
事务A读取到了事务B已经提交的修改数据,不符合隔离性。 - 幻读(Phantom Reads):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
一个事务按照相同的查询条件读取以前检索过的数据,却发现某些事务插入了满足其查询条件的新数据,这种现象称为“幻读”。
事务A读取了事务B提交的新增数据,不符合隔离性。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除
事务隔离级别
脏读、不可重读和幻读,其实都是数据库读一致性的问题,必须由数据库提供一定的事务隔离机制来解决。
SQL标准定义的四个隔离级别为:
- read uncommited :读到未提交数据
- read committed:脏读,不可重复读
- repeatable read:可重读
- serializable :串行事物
什么是脏读?幻读?不可重复读?
· 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
· 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
· 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
SQL 标准定义了四个隔离级别
- READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别
事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。
2、索引
索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据
优点:
- 加快数据查找的速度
- 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
- 加快表与表之间连接的速度
缺点:
常见的MySQL主要有两种结构:Hash索引和B+Tree索引,我们使用的是InnoDB引擎,默认的是B+树索引具体采用的哪种数据结构呢?
搜索范围 因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ Tree是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。
B+ Tree索引和Hash索引区别 哈希索引适合等值查询,但是无法进行范围查询, 哈希索引没办法利用索引完成排序, 哈希索引不支持多列联合索引的最左匹配规则 如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
为什么采用B+树?这和Hash索引比较起来有什么优缺点吗?
B+ Tree的叶子节点都可以存哪些东西: InnoDB的B+ Tree可能存储的是整行数据,也有可能是主键的值。在 InnoDB 里,索引B+ Tree的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引。而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引。
面试官:那么,聚簇索引和非聚簇索引,在查询数据的时候有区别吗?
我:聚簇索引查询会更快?
面试官:为什么呢?
我:因为主键索引树的叶子节点直接就是我们要查询的整行数据了。而非主键索引的叶子节点是主键的值,查到主键的值以后,还需要再通过主键的值再进行一次查询。
面试官:刚刚你提到主键索引查询只会查一次,而非主键索引需要回表查询多次。(后来我才知道,原来这个过程叫做回表)是所有情况都是这样的吗?非主键索引一定会查询多次吗?
我:(额、这个问题我回答的不好,后来我自己查资料才知道,通过覆盖索引也可以只查询一次)
Hash索引和B+树索引的区别?
- 哈希索引不支持排序,因为哈希表是无序的。
- 哈希索引不支持范围查找。
- 哈希索引不支持模糊查询及多列索引的最左前缀匹配。
- 因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。
为什么B+树比B树更适合实现数据库索引?
- 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。
- B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。
- B+树的查询效率更加稳定,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
聚簇索引、覆盖索引
科普时间——覆盖索引 覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。 当一条查询语句符合覆盖索引条件时,MySQL只需要通过索引就可以返回查询所需要的数据,这样避免了查到索引后再返回表操作,减少I/O提高效率。 如,表covering_index_sample中有一个普通索引 idx_key1_key2(key1,key2)。当我们通过SQL语句:select key2 from covering_index_sample where key1 = ‘keytest’;的时候,就可以通过覆盖索引查询,无需回表。
在创建多列索引时,我们根据业务需求,where子句中使用最频繁的一列放在最左边,因为MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。所以当我们创建一个联合索引的时候,如(key1,key2,key3),相当于创建了(key1)、(key1,key2)和(key1,key2,key3)三个索引,这就是最左匹配原则。联合索引、最左前缀匹配
索引什么时候会失效?
- 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引
- 以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引
- 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效
- 判断索引列是否不等于某个值时
- 对索引列进行运算
- 查询条件使用or连接,也会导致索引失效
3、回表查询
这先要从InnoDB的索引实现说起,InnoDB有两大类索引:
- 聚集索引(clustered index)
- 普通索引(secondary index)
InnoDB聚集索引和普通索引有什么差异?
InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
(1)如果表定义了PK,则PK就是聚集索引;
(2)如果表没有定义PK,则第一个not NULL unique列是聚集索引;
(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
画外音:所以PK查询非常快,直接定位行记录。
InnoDB普通索引的叶子节点存储主键值。
画外音:注意,不是存储行记录头指针,MyISAM的索引叶子节点存储记录指针。
举个栗子,不妨设有表:
t(id PK, name KEY, sex, flag);
画外音:id是聚集索引,name是普通索引。
表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B

两个B+树索引分别如上图:
(1)id为PK,聚集索引,叶子节点存储行记录;
(2)name为KEY,普通索引,叶子节点存储PK值,即id;
既然从普通索引无法直接定位行记录,那普通索引的查询过程是怎么样的呢?
通常情况下,需要扫码两遍索引树。
例如:
select * from `t `where `name`='lisi';
是如何执行的呢?

如粉红色路径,需要扫码两遍索引树:
(1)先通过普通索引定位到主键值id=5;
(2)在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
4、覆盖索引
- 什么是索引覆盖:
借用一下SQL-Server官网的说法。

MySQL官网,类似的说法出现在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖。

不管是SQL-Server官网,还是MySQL官网,都表达了:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
- 如何实现索引覆盖
常见的方法是:将被查询的字段,建立到联合索引里去。
仍是之前中的例子:
create table user(id int primary key,name varchar(20),sex varchar(5),index(name))engine=innodb;
第一个SQL语句:

select id,name from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,通过name的索引树即可获取id和name,无需回表,符合索引覆盖,效率较高。
画外音,Extra:Using index。
第二个SQL语句:

select id,name,sex from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,但sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫码聚集索引获取sex字段,效率会降低。
画外音,Extra:Using index condition。
如果把(name)单列索引升级为联合索引(name, sex)就不同了。
create table user (id int primary key,name varchar(20),sex varchar(5),index(name, sex))engine=innodb;

可以看到:
select id,name ... where name='shenjian';select id,name,sex ... where name='shenjian';
都能够命中索引覆盖,无需回表。
画外音,Extra:Using index。
5 、哪些场景可以利用索引覆盖来优化SQL?
场景1:全表count查询优化

原表为:
user(PK id, name, sex);
直接:
select count(name) from user;
不能利用索引覆盖。
添加索引:
alter table user add key(name);
就能够利用索引覆盖提效。
场景2:列查询回表优化
select id,name,sex ... where name='shenjian';
这个例子不再赘述,将单列索引(name)升级为联合索引(name, sex),即可避免回表。
场景3:分页查询
select id,name,sex ... order by name limit 500,100;
将单列索引(name)升级为联合索引(name, sex),也可以避免回表。
参考博客:https://www.cnblogs.com/myseries/p/11265849.html
6、MySQL执行慢分析
数据库在刷新脏页我也无奈啊
当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。
不过,redo log 里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候 redo log 很快就会被写满了,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中去的,而这个时候,就会导致我们平时正常的SQL语句突然执行的很慢,所以说,数据库在在同步数据到磁盘的时候,就有可能导致我们的SQL语句执行的很慢了。
一个 SQL 执行的很慢,我们要分两种情况讨论:
1、大多数情况下很正常,偶尔很慢,则有如下原因
(1)、数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。
(2)、执行的时候,遇到锁,如表锁、行锁。
2、这条 SQL 语句一直执行的很慢,则有如下原因。
(1)、没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。
(2)、数据库选错了索引。
7、数据库的乐观锁和悲观锁是什么?
数据库的乐观锁和悲观锁是什么?
确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性,乐观锁和悲观锁是并发控制主要采用的技术手段。
- 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
- 在查询完数据的时候就把事务锁起来,直到提交事务
- 实现方式:使用数据库中的锁机制
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
- 在修改数据的时候把事务锁起来,通过version的方式来进行锁定
- 实现方式:使用version版本或者时间戳
悲观锁:
乐观锁:
8、MyIASM和Innodb两种引擎所使用的索引的数据结构是什么?
答案:都是B+树!
MyIASM引擎,B+树的数据结构中存储的内容实际上是实际数据的地址值。也就是说它的索引和实际数据是分开的,只不过使用索引指向了实际数据。这种索引的模式被称为非聚集索引。
Innodb引擎的索引的数据结构也是B+树,只不过数据结构中存储的都是实际的数据,这种索引有被称为聚集索引。
9、数据库优化的思路
SQL优化
在我们书写SQL语句的时候,其实书写的顺序、策略会影响到SQL的性能,虽然实现的功能是一样的,但是它们的性能会有些许差别。
因此,下面就讲解在书写SQL的时候,怎么写比较好。
①选择最有效率的表名顺序
数据库的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表将被最先处理
在FROM子句中包含多个表的情况下:
- 如果三个表是完全无关系的话,将记录和列名最少的表,写在最后,然后依次类推
- 也就是说:选择记录条数最少的表放在最后
如果有3个以上的表连接查询:
- 如果三个表是有关系的话,将引用最多的表,放在最后,然后依次类推。
- 也就是说:被其他表所引用的表放在最后
例如:查询员工的编号,姓名,工资,工资等级,部门名
emp表被引用得最多,记录数也是最多,因此放在form字句的最后面
select emp.empno,emp.ename,emp.sal,salgrade.grade,dept.dnamefrom salgrade,dept,empwhere (emp.deptno = dept.deptno) and (emp.sal between salgrade.losal and salgrade.hisal)
②WHERE子句中的连接顺序
数据库采用自右而左的顺序解析WHERE子句,根据这个原理,表之间的连接必须写在其他WHERE条件之左,那些可以过滤掉最大数量记录的条件必须写在WHERE子句的之右。
emp.sal可以过滤多条记录,写在WHERE字句的最右边
select emp.empno,emp.ename,emp.sal,dept.dnamefrom dept,empwhere (emp.deptno = dept.deptno) and (emp.sal > 1500)
③SELECT子句中避免使用*号
我们当时学习的时候,“*”号是可以获取表中全部的字段数据的。
- 但是它要通过查询数据字典完成的,这意味着将耗费更多的时间
- 使用*号写出来的SQL语句也不够直观。
④用TRUNCATE替代DELETE
这里仅仅是:删除表的全部记录,除了表结构才这样做。
DELETE是一条一条记录的删除,而Truncate是将整个表删除,保留表结构,这样比DELETE快
⑤多使用内部函数提高SQL效率
例如使用mysql的concat()函数会比使用||来进行拼接快,因为concat()函数已经被mysql优化过了。
⑥使用表或列的别名
如果表或列的名称太长了,使用一些简短的别名也能稍微提高一些SQL的性能。毕竟要扫描的字符长度就变少了。。。
⑦多使用commit
comiit会释放回滚点…
⑧善用索引
索引就是为了提高我们的查询数据的,当表的记录量非常大的时候,我们就可以使用索引了。
⑨SQL写大写
我们在编写SQL 的时候,官方推荐的是使用大写来写关键字,因为Oracle服务器总是先将小写字母转成大写后,才执行
⑩避免在索引列上使用NOT
因为Oracle服务器遇到NOT后,他就会停止目前的工作,转而执行全表扫描
①①避免在索引列上使用计算
WHERE子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描,这样会变得变慢
①②用 >= 替代 >
低效:SELECT * FROM EMP WHERE DEPTNO > 3首先定位到DEPTNO=3的记录并且扫描到第一个DEPT大于3的记录高效:SELECT * FROM EMP WHERE DEPTNO >= 4直接跳到第一个DEPT等于4的记录
①③用IN替代OR
select * from emp where sal = 1500 or sal = 3000 or sal = 800;select * from emp where sal in (1500,3000,800);
①④总是使用索引的第一个列
如果索引是建立在多个列上,只有在它的第一个列被WHERE子句引用时,优化器才会选择使用该索引。 当只引用索引的第二个列时,不引用索引的第一个列时,优化器使用了全表扫描而忽略了索引
create index emp_sal_job_idexon emp(sal,job);----------------------------------select *from empwhere job != 'SALES';上边就不使用索引了。
数据库结构优化
- 1)范式优化: 比如消除冗余(节省空间。。)
- 2)反范式优化:比如适当加冗余等(减少join)
- 3)拆分表: 垂直拆分和水平拆分
MySQL的Limit有性能问题
MySQL的分页查询通常通过limit来实现。
MySQL的limit基本用法很简单。limit接收1或2个整数型参数,如果是2个参数,第一个是指定第一个返回记录行的偏移量,第二个是返回记录行的最大数目。初始记录行的偏移量是0。
为了与PostgreSQL兼容,limit也支持limit # offset #。
问题:
对于小的偏移量,直接使用limit来查询没有什么问题,但随着数据量的增大,越往后分页,limit语句的偏移量就会越大,速度也会明显变慢。
优化思想:
避免数据量大时扫描过多的记录
解决:
子查询的分页方式或者JOIN分页方式。
JOIN分页和子查询分页的效率基本在一个等级上,消耗的时间也基本一致。
下面举个例子。一般MySQL的主键是自增的数字类型,这种情况下可以使用下面的方式进行优化。
下面以真实的生产环境的80万条数据的一张表为例,比较一下优化前后的查询耗时:
— 传统limit,文件扫描 [SQL]SELECT FROM tableName ORDER BY id LIMIT 500000,2; 受影响的行: 0 时间: 5.371s — 子查询方式,索引扫描 [SQL] SELECT FROM tableName WHERE id >= (SELECT id FROM tableName ORDER BY id LIMIT 500000 , 1) LIMIT 2; 受影响的行: 0 时间: 0.274s — JOIN分页方式 [SQL] SELECT FROM tableName AS t1 JOIN (SELECT id FROM tableName ORDER BY id desc LIMIT 500000, 1) AS t2 WHERE t1.id <= t2.id ORDER BY t1.id desc LIMIT 2; 受影响的行: 0 时间: 0.278s 复制代码
可以看到经过优化性能提高了将近20倍。
*优化原理:
子查询是在索引上完成的,而普通的查询时在数据文件上完成的,通常来说,索引文件要比数据文件小得多,所以操作起来也会更有效率。因为要取出所有字段内容,第一种需要跨越大量数据块并取出,而第二种基本通过直接根据索引字段定位后,才取出相应内容,效率自然大大提升。
因此,对limit的优化,不是直接使用limit,而是首先获取到offset的id,然后直接使用limit size来获取数据。
在实际项目使用,可以利用类似策略模式的方式去处理分页,例如,每页100条数据,判断如果是100页以内,就使用最基本的分页方式,大于100,则使用子查询的分页方式。
MVCC 实现原理
- MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。
- 作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。
- MVCC 实现原理如下:
- MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。
- DB_TRX_ID:当前事务id,通过事务id的大小判断事务的时间顺序。
- DB_ROLL_PRT:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。
- DB_ROLL_ID:主键,如果数据表没有主键,InnoDB会自动生成主键。
每条表记录大概是这样的:
使用事务更新行记录的时候,就会生成版本链,执行过程如下:
- 用排他锁锁住该行;
- 将该行原本的值拷贝到undo log,作为旧版本用于回滚;
- 修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。
- 下面举个例子方便大家理解。
- 1、初始数据如下,其中DB_ROW_ID和DB_ROLL_PTR为空。

2、事务A对该行数据做了修改,将age修改为12,效果如下:
3、之后事务B也对该行记录做了修改,将age修改为8,效果如下:
- 4、此时undo log有两行记录,并且通过回滚指针连在一起。
- 接下来了解下read view的概念。
- read view可以理解成将数据在每个时刻的状态拍成“照片”记录下来。在获取某时刻t的数据时,到t时间点拍的“照片”上取数据。
- 在read view内部维护一个活跃事务链表,表示生成read view的时候还在活跃的事务。这个链表包含在创建read view之前还未提交的事务,不包含创建read view之后提交的事务。
- 不同隔离级别创建read view的时机不同。
- read committed:每次执行select都会创建新的read_view,保证能读取到其他事务已经提交的修改。
- repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即可重复读。
- read view的记录筛选方式
- 前提:DATA_TRX_ID 表示每个数据行的最新的事务ID;up_limit_id表示当前快照中的最先开始的事务;low_limit_id表示当前快照中的最慢开始的事务,即最后一个事务。

- 如果DATA_TRX_ID < up_limit_id:说明在创建read view时,修改该数据行的事务已提交,该版本的记录可被当前事务读取到。
- 如果DATA_TRX_ID >= low_limit_id:说明当前版本的记录的事务是在创建read view之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的记录对当前事务的可见性。
- 如果up_limit_id <= DATA_TRX_ID < low_limit_i:
- 需要在活跃事务链表中查找是否存在ID为DATA_TRX_ID的值的事务。
- 如果存在,因为在活跃事务链表中的事务是未提交的,所以该记录是不可见的。此时需要通过版本链找到上一个版本,然后重新判断该版本的可见性。
- 如果不存在,说明事务trx_id 已经提交了,这行记录是可见的。
总结:InnoDB 的MVCC是通过 read view 和版本链实现的,版本链保存有历史版本记录,通过read view 判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。
