MySQL

多表查询

概述

如果设计到多表查询,那么首先应该确定设计哪几张表,然后分析他们之间的连接条件,最后确定查询字段。

多表查询就是指从多张表中查询数据。

原来查询单表数据,执行的SQL形式为:select * from emp;

那么我们要执行多表查询,就只需要使用逗号分隔多张表即可,如: select * from emp(17条记录) , dept(6条记录) ;

此时,我们看到查询结果中包含了大量的结果集,总共102条记录,而这其实就是员工表emp所有的记录 (17) 与 部门表dept所有记录(6) 的所有组合情况,这种现象称之为笛卡尔积。接下来,就来简单 介绍下笛卡尔积。

笛卡尔积: 笛卡尔乘积是指在数学中,两个集合A集合 和 B集合的所有组合情况。

而在多表查询中,我们是需要消除无效的笛卡尔积的,只保留两张表关联部分的数据。

在SQL语句中,如何来去除无效的笛卡尔积呢? 我们可以给多表查询加上连接查询的条件即可

select * from emp , dept where emp.dept_id = dept.id;

分类

  • 连接查询

    • 内连接:相当于查询A、B交集部分数据

      • 隐式内连接

        1
        2
        SELECT 字段列表 FROM 表1 , 表2 WHERE 条件 ... ;

      • 显式内连接

        1
        SELECT 字段列表 FROM1 [ INNER ] JOIN2 ON 连接条件 ... ;
    • 外连接:

    • 左外连接:查询左表所有数据,以及两张表交集部分数据

      1
      SELECT 字段列表 FROM1 LEFT [ OUTER ] JOIN2 ON 条件 ... ;

      eg: 查询emp表的所有数据, 和对应的部门信息

      1
      select e.*, d.name from emp e left join dept d on e.dept_id = d.id;
    • 右外连接:查询右表所有数据,以及两张表交集部分数据

    • 自连接:当前表与自身的连接查询,自连接必须使用表别名

    1
    SELECT 字段列表 FROMA 别名A JOIN 表A 别名B ON 条件 ... ;

    eg:查询员工 及其 所属领导的名字

    1
    select a.name , b.name from emp a , emp b where a.managerid = b.id;(内连接)

    eg:查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来 -> 外连接 表结构: emp a , emp b

    1
    select a.name '员工', b.name '领导' from emp a left join emp b on a.managerid = b.id;
  • 子查询:SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询。

    1
    SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 ) 
    • 标量子查询(子查询结果为单个值)eg:查询 “销售部” 的所有员工信息
    1
    select * from emp where dept_id = (select id from dept where name = '销售部');
    • 列子查询(子查询结果为一列) eg: 查询 “销售部” 和 “市场部” 的所有员工信息
    1
    2
    select * from emp where dept_id in (select id from dept where name = '销售部' or
    name = '市场部');
    • 行子查询(子查询结果为一行) eg:查询与 “张无忌” 的薪资及直属领导相同的员工信息 ;
    1
    2
    select * from emp where (salary,managerid) = (select salary, managerid from emp
    where name = '张无忌');
    • 表子查询(子查询结果为多行多列) eg: 查询与 “鹿杖客” , “宋远桥” 的职位和薪资相同的员工信息
    1
    2
    select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left
    join dept d on e.dept_id = d.id ;

事务

事务四大特性

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立 环境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

上述就是事务的四大特性,简称ACID。

并发事务

  • 赃读:一个事务读到另外一个事务还没有提交的数据。

  • 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。

事务A两次读取同一条记录,但是读取到的数据却是不一样的.

  • 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据 已经存在,好像出现了 “幻影”

为什么会出现幻读?因为已经解决了上面的不可重复度,所以两次查询的结果是一样的,误以为没有数据,其实事务B已经插入了。

事务隔离级别

为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别。主要有以下几种:

  • 查看事务隔离级别
1
SELECT @@TRANSACTION_ISOLATION;
  • 设置事务隔离级别
1
2
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED |
READ COMMITTED | REPEATABLE READ | SERIALIZABLE }

事务隔离级别越高,数据越安全,但是性能越低

索引

索引是帮助MySQL高效获取数据的数据结构(有序)。

聚簇索引

聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引

聚簇索引(Clustered Index)一般指的是主键索引(如果存在主键索引的话),聚簇索引也被称之为聚集索引。

聚簇索引在 InnoDB 中是使用 B+ 树实现的,比如我们创建一张 student 表,它的构建 SQL 如下:

以上 student 表中有一个聚簇索引(也就是主键索引)id,和一个非聚簇索引 class_id。

聚簇索引 id 对应的 B+ 树如下图所示:

在聚簇索引的叶子节点的data区直接存储用户信息的内存地址,我们使用内存地址可以直接找到相应的行数据。

非聚簇索引

非聚簇索引在 InnoDB 引擎中,也叫二级索引,以上面 student 表为例,在 student 中非聚簇索引 class_id 对应 B+ 树如下图所示:

从上图我们可以看出,在非聚簇索引的叶子节点上的data存储的并不是真正的行数据,而是主键 ID,所以当我们使用非聚簇索引进行查询时,首先会得到一个主键 ID,然后再使用主键 ID 去聚簇索引上找到真正的行数据,我们把这个过程称之为回表查询。

总结:

在 MySQL 的 InnoDB 引擎中,每个索引都会对应一颗 B+ 树,而聚簇索引和非聚簇索引最大的区别在于叶子节点存储的数据不同,聚簇索引叶子节点存储的是行数据,因此通过聚簇索引可以直接找到真正的行数据;而非聚簇索引叶子节点存储的是主键信息,所以使用非聚簇索引还需要回表查询,因此我们可以得出聚簇索引和非聚簇索引的区别主要有以下几个:

  • 聚簇索引叶子节点data存储的是行数据;而非聚簇索引叶子节点data存储的是聚簇索引(通常是主键 ID)。
  • 聚簇索引查询效率更高,而非聚簇索引需要进行回表查询,因此性能不如聚簇索引。
  • 聚簇索引一般为主键索引,而主键一个表中只能有一个,因此聚簇索引一个表中也只能有一个,而非聚簇索引则没有数量上的限制。

索引失效

对索引使用左或者左右模糊匹配

当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。

比如下面的 like 语句,查询 name 后缀为「林」的用户,执行计划中的 type=ALL 就代表了全表扫描,而没有走索引。

1
2
// name 字段为二级索引
select * from t_user where name like '%林';

如果是查询 name 前缀为林的用户,那么就会走索引扫描,执行计划中的 type=range 表示走索引扫描,key=index_name 看到实际走了 index_name 索引:

1
2
// name 字段为二级索引
select * from t_user where name like '林%';

为什么 like 关键字左或者左右模糊匹配无法走索引呢?

因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。

举个例子,下面这张二级索引图是以 name 字段有序排列存储的。

假设我们要查询 name 字段前缀为「林」的数据,也就是 name like '林%',扫描索引的过程:

  • 首节点查询比较:林这个字的拼音大小比首节点的第一个索引值中的陈字大,但是比首节点的第二个索引值中的周字小,所以选择去节点2继续查询;
  • 节点 2 查询比较:节点2的第一个索引值中的陈字的拼音大小比林字小,所以继续看下一个索引值,发现节点2有与林字前缀匹配的索引值,于是就往叶子节点查询,即叶子节点4;
  • 节点 4 查询比较:节点4的第一个索引值的前缀符合林字,于是就读取该行数据,接着继续往右匹配,直到匹配不到前缀为林的索引值。

如果使用 name like '%林' 方式来查询,因为查询的结果可能是「陈林、张林、周林」等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。

对索引使用函数

有时候我们会用一些 MySQL 自带的函数来得到我们想要的结果,这时候要注意了,如果查询条件中对索引字段使用函数,就会导致索引失效。

比如下面这条语句查询条件中对 name 字段使用了 LENGTH 函数,执行计划中的 type=ALL,代表了全表扫描:

1
2
// name 为二级索引
select * from t_user where length(name)=6;

为什么对索引使用函数,就无法走索引了呢?

因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

举个例子,我通过下面这条语句,对 length(name) 的计算结果建立一个名为 idx_name_length 的索引。

1
alter table t_user add key idx_name_length ((length(name)));

然后我再用下面这条查询语句,这时候就会走索引了。

对索引进行表达式计算

在查询条件中对索引进行表达式计算,也是无法走索引的。

比如,下面这条查询语句,执行计划中 type = ALL,说明是通过全表扫描的方式查询数据的:

1
explain select * from t_user where id + 1 = 10;

但是,如果把查询语句的条件改成 where id = 10 - 1,这样就不是在索引字段进行表达式计算了,于是就可以走索引查询了。

为什么对索引进行表达式计算,就无法走索引了呢?

原因跟对索引使用函数差不多。

因为索引保存的是索引字段的原始值,而不是 id + 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。

对索引隐式类型转换

如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描。

我在原本的 t_user 表增加了 phone 字段,是二级索引且类型是 varchar。

然后我在条件查询中,用整型作为输入参数,此时执行计划中 type = ALL,所以是通过全表扫描来查询数据的。

1
select * from t_user where phone = 1300000001;

但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。

我们再看第二个例子,id 是整型,但是下面这条语句还是走了索引扫描的。

为什么第一个例子会导致索引失效,而第二例子不会呢?

要明白这个原因,首先我们要知道 MySQL 的数据类型转换规则是什么?就是看 MySQL 是会将字符串转成数字处理,还是将数字转换成字符串处理。

我在看《mysql45讲的时候》看到一个简单的测试方式,就是通过 select “10” > 9 的结果来知道MySQL 的数据类型转换规则是什么:

  • 如果规则是 MySQL 会将自动「字符串」转换成「数字」,就相当于 select 10 > 9,这个就是数字比较,所以结果应该是 1;
  • 如果规则是 MySQL 会将自动「数字」转换成「字符串」,就相当于 select “10” > “9”,这个是字符串比较,字符串比较大小是逐位从高位到低位逐个比较(按ascii码) ,那么”10”字符串相当于 “1”和“0”字符的组合,所以先是拿 “1” 字符和 “9” 字符比较,因为 “1” 字符比 “9” 字符小,所以结果应该是 0。

在 MySQL 中,执行的结果如下图:

上面的结果为 1,说明 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较

前面的例子一中的查询语句,我也跟大家说了是会走全表扫描:

1
2
//例子一的查询语句
select * from t_user where phone = 1300000001;

这是因为 phone 字段为字符串,所以 MySQL 要会自动把字符串转为数字,所以这条语句相当于:

1
select * from t_user where CAST(phone AS signed int) = 1300000001;

可以看到,CAST 函数是作用在了 phone 字段,而 phone 字段是索引,也就是对索引使用了函数!而前面我们也说了,对索引使用函数是会导致索引失效的

例子二中的查询语句,我跟大家说了是会走索引扫描:

1
2
//例子二的查询语句
select * from t_user where id = "1";

这时因为字符串部分是输入参数,也就需要将字符串转为数字,所以这条语句相当于:

1
select * from t_user where id = CAST("1" AS signed int);

可以看到,索引字段并没有用任何函数,CAST 函数是用在了输入参数,因此是可以走索引扫描的

联合索引非最左匹配

对主键字段建立的索引叫做聚簇索引,对普通字段建立的索引叫做二级索引。

那么多个普通字段组合在一起创建的索引就叫做联合索引,也叫组合索引。

创建联合索引时,我们需要注意创建时的顺序问题,因为联合索引 (a, b, c) 和 (c, b, a) 在使用的时候会存在差别。

联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。

比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

  • where a=1;
  • where a=1 and b=2 and c=3;
  • where a=1 and b=2;

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。

但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

  • where b=2;
  • where c=3;
  • where b=2 and c=3;

有一个比较特殊的查询条件:where a = 1 and c = 3 ,符合最左匹配吗?

MySQL 5.5 的话,前面 a 会走索引,在联合索引找到主键值后,开始回表,到主键索引读取数据行,Server 层从存储引擎层获取到数据行后,然后在 Server 层再比对 c 字段的值。

从 MySQL 5.6 之后,有一个索引下推功能,可以在存储引擎层进行索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,再返还给 Server 层,从而减少回表次数。

索引下推的大概原理是:截断的字段不会在 Server 层进行条件判断,而是会被下推到「存储引擎层」进行条件判断(因为 c 字段的值是在 (a, b, c) 联合索引里的),然后过滤出符合条件的数据后再返回给 Server 层。由于在引擎层就过滤掉大量的数据,无需再回表读取数据来进行判断,减少回表次数,从而提升了性能。

比如下面这条 where a = 1 and c = 0 语句,我们可以从执行计划中的 Extra=Using index condition 使用了索引下推功能。

为什么联合索引不遵循最左匹配原则就会失效?

原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。

也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。

WHERE 子句中的 OR

在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

举个例子,比如下面的查询语句,id 是主键,age 是普通列,从执行计划的结果看,是走了全表扫描。

1
select * from t_user where id = 1 or age = 18;

这是因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。

要解决办法很简单,将 age 字段设置为索引即可。

可以看到 type=index merge, index merge 的意思就是对 id 和 age 分别进行了扫描,然后将这两个结果集进行了合并,这样做的好处就是避免了全表扫描

总结

今天给大家介绍了 6 种会发生索引失效的情况:

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
  • 当我们在查询条件中对索引列使用函数,就会导致索引失效。
  • 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
  • MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

SQL优化

插入数据

insert

如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。

1
2
3
4
insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');
.....
  • 优化方案一

批量插入数据

1
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
  • 优化方案二

手动控制事务

1
2
3
4
5
begine;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry');
insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
commit;
  • 优化方案三

主键顺序插入,性能要高于乱序插入。

1
2
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89

大批量插入数据

如果一次性需要插入大批量数据(比如: 几百万的记录),使用insert语句插入性能较低,此时可以使 用MySQL数据库提供的load指令进行插入。操作如下:

可以执行如下指令,将数据脚本文件中的数据加载到表结构中:

1
2
3
4
5
6
7
8
9
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p

-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;

-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields
terminated by ',' lines terminated by '\n' ;

主键顺序插入性能高于乱序插入

演示:

  • 创建表结构
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `tb_user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(50) NOT NULL,
`name` VARCHAR(20) NOT NULL,
`birthday` DATE DEFAULT NULL,
`sex` CHAR(1) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_username` (`username`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 ;
  • 设置参数
1
2
3
4
5
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p

-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
  • load加载数据
1
2
load data local infile '/root/load_user_100w_sort.sql' into table tb_user
fields terminated by ',' lines terminated by '\n' ;

我们看到,插入100w的记录,17s就完成了,性能很好。

在load时,主键顺序插入性能高于乱序插入

主键优化

在上一小节,我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的 原因,然后再分析一下主键又该如何设计。

  • 数据组织方式

在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表 (index organized table IOT)。

行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:

在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。 那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不小,将会存储 到下一个页中,页与页之间会通过指针连接。

页分裂

页可以为空,也可以填充一半,也可以填充100%。每个页包含了2-N行数据(如果一行数据过大,会行 溢出),根据主键排列。

主键顺序插入效果:

  • 从磁盘中申请页, 主键顺序插入

  • 第一个页没有满,继续往第一页插入

  • 当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接

  • 当第二页写满了,再往第三页写入

主键乱序插入效果:

  • 加入1#,2#页都已经写满了,存放了如图所示的数据

  • 此时再插入id为50的记录,我们来看看会发生什么现象

会再次开启一个页,写入新的页中吗?

不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在47之后。

但是47所在的1#页,已经写满了,存储不了50对应的数据了。 那么此时会开辟一个新的页 3#。

上述的这种现象,称之为 “页分裂”,是比较耗费性能的操作。

页合并

目前表中已有数据的索引结构(叶子节点)如下:

当我们对已有数据进行删除时,具体的效果如下:

当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间 变得允许被其他记录声明使用。

当我们继续删除2#的数据记录

这个里面所发生的合并页的这个现象,就称之为 “页合并”。

知识小贴士: MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或者创建索引时指定。

索引设计原则

  • 满足业务需求的情况下,尽量降低主键的长度
  • 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
  • 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
  • 业务操作时,避免对主键的修改。

order by优化

MySQL的排序,有两种方式(explain分析时看extra字段):

  • Using filesort : 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。
  • Using index : 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要 额外排序,操作效率高。

对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序 操作时,尽量要优化为 Using index。

接下来,我们来做一个测试:

  • 数据准备

把之前测试时,为tb_user表所建立的部分索引直接删除掉

1
2
3
drop index idx_user_phone on tb_user;
drop index idx_user_phone_name on tb_user;
drop index idx_user_name on tb_user;
  • 执行排序SQL

explain select id,age,phone from tb_user order by age ;

explain select id,age,phone from tb_user order by age, phone ;

由于 age, phone 都没有索引,所以此时再排序时,出现Using filesort, 排序性能较低。

  • 创建索引
1
2
-- 创建索引
create index idx_user_age_phone_aa on tb_user(age,phone);
  • 创建索引后,根据age, phone进行升序排序
1
explain select id,age,phone from tb_user order by age;

1
explain select id,age,phone from tb_user order by age , phone;

建立索引之后,再次进行排序查询,就由原来的Using filesort, 变为了 Using index,性能 就是比较高的了。

E. 创建索引后,根据age, phone进行降序排序

1
explain select id,age,phone from tb_user order by age desc , phone desc ;

也出现 Using index, 但是此时Extra中出现了 Backward index scan,这个代表反向扫描索 引,因为在MySQL中我们创建的索引,默认索引的叶子节点是从小到大排序的,而此时我们查询排序 时,是从大到小,所以,在扫描时,就是反向扫描,就会出现 Backward index scan。 在 MySQL8版本中,支持降序索引,我们也可以创建降序索引。

  • 根据phone,age进行升序排序,phone在前,age在后。
1
explain select id,age,phone from tb_user order by phone , age;

排序时,也需要满足最左前缀法则,否则也会出现 filesort。因为在创建索引的时候, age是第一个 字段,phone是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort。

  • 创建联合索引(age 升序排序,phone 倒序排序)
1
create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);

升序/降序联合索引结构图示:

由上述的测试,我们得出order by优化原则:

  • 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。
  • 尽量使用覆盖索引。
  • 多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。
  • 果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)。

group by优化

分组操作,我们主要来看看索引对于分组操作的影响。

首先我们先将 tb_user 表的索引全部删除掉.

1
2
3
4
drop index idx_user_pro_age_sta on tb_user;
drop index idx_email_5 on tb_user;
drop index idx_user_age_phone_aa on tb_user;
drop index idx_user_age_phone_ad on tb_user;

接下来,在没有索引的情况下,执行如下SQL,查询执行计划:

1
explain select profession , count(*) from tb_user group by profession

使用临时表,性能较低

然后,我们在针对于 profession , age, status 创建一个联合索引。

1
create index idx_user_pro_age_sta on tb_user(profession , age , status);

紧接着,再执行前面相同的SQL查看执行计划。

1
explain select profession , count(*) from tb_user group by profession 

我们发现,如果仅仅根据age分组,就会出现 Using temporary ;而如果是 根据 profession,age两个字段同时分组,则不会出现 Using temporary。原因是因为对于分组操作, 在联合索引中,也是符合最左前缀法则的。

所以,在分组操作中,我们需要通过以下两点进行优化,以提升性能:

  • 在分组操作时,可以通过索引来提高效率。
  • 分组操作时,索引的使用也是满足最左前缀法则的。

count优化

select count(*) from tb_user

在之前的测试中,我们发现,如果数据量很大,在执行count操作时,是非常耗时的。

  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个 数,效率很高; 但是如果是带条件的count,MyISAM也慢。

  • InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出 来,然后累积计数。

如果说要大幅度提升InnoDB表的count效率,主要的优化思路:自己计数(可以借助于redis这样的数 据库进行,但是如果是带条件的count又比较麻烦了)。

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

update优化

我们主要需要注意一下update语句执行时的注意事项。

1
update course set name = 'javaEE' where id = 1 ;

当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。

但是当我们在执行如下SQL时。

1
update course set name = 'SpringBoot' where name = 'PHP' ;

当我们开启多个事务,在执行上述的SQL时,我们发现行锁升级为了表锁。 导致该update语句的性能 大大降低

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁 升级为表锁 。

所以我们应该给条件创建索引,不然行锁会升级为表锁,并发性能降低

limit优化

在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。

越往后,分页查询效率越低,这就是分页查询的问题所在。

因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记 录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。

优化思路: 一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查 询形式进行优化

1
2
explain select * from tb_sku t , (select id from tb_sku order by id
limit 2000000,10) a where t.id = a.id;

平时写SQL时该注意什么?有什么经验可谈?

  • 统计时尽量使用count(*)

  • 如果明确查找一条语句,请使用limit 1,因为找到一条符合条件的记录就不会继续查找了

  • 避免使用select * ,用到什么字段就具体写什么字段,原因除了select * 查询所有字段会多出网络开销之外,还有更重要的一点就是select * 无法使用覆盖索引。

  • 对于使用索引方面

    • 对索引字段做函数操作或者做运算操作,都不能使用上索引。
      所以针对这一点,除了我们索引的字段不要加函数之外。

    • 还要注意一些隐式转换,比如,交易日志表(tradelog),tradeid 的字段类型是 varchar(32),字段有索引,但是当你执行 select * from tradelog where tradeid=110717;语句,你发现走的还是全索引扫描。这是因为它其实做了类型转换,相当于这么执行mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;

    • 隐式字符编码转换

      如果两张表使用的编码集不一致,比如一个是utf8mb4,一个是utf8,那么这两个字段连接后,相当于做了一次CONVERT(traideid USING utf8mb4),这样也用不了索引。其实字符集不同只是条件之一,连接过程中要求在被驱动表的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因。

  • 如果设计到多表查询查询,那么首先应该确定设计哪几张表,然后分析他们之间的连接条件,最后确定查询字段。

MySQL执行计划分析(EXPLAIN)

优化 SQL 的第一步应该是读懂 SQL 的执行计划。本篇文章,我们一起来学习下 MySQL EXPLAIN 执行计划相关知识。

什么是执行计划?

执行计划 是指一条 SQL 语句在经过 MySQL 查询优化器 的优化会后,具体的执行方式。

执行计划通常用于 SQL 性能分析、优化等场景。通过 EXPLAIN 的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。

如何获取执行计划?

MySQL 为我们提供了 EXPLAIN 命令,来获取执行计划的相关信息。

需要注意的是,EXPLAIN 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。

EXPLAIN 执行计划支持 SELECTDELETEINSERTREPLACE 以及 UPDATE 语句。我们一般多用于分析 SELECT 查询语句,使用起来非常简单,语法如下:

1
EXPLAIN + SELECT 查询语句;

我们简单来看下一条查询语句的执行计划:

1
2
3
4
5
6
7
8
mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_emp GROUP BY emp_no HAVING COUNT(emp_no)>1);
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+
| 1 | PRIMARY | dept_emp | NULL | ALL | NULL | NULL | NULL | NULL | 331143 | 100.00 | Using where |
| 2 | SUBQUERY | dept_emp | NULL | index | PRIMARY,dept_no | PRIMARY | 16 | NULL | 331143 | 100.00 | Using index |
+----+-------------+----------+------------+-------+-----------------+---------+---------+------+--------+----------+-------------+

可以看到,执行计划结果中共有 12 列,各列代表的含义总结如下表:

如何分析 EXPLAIN 结果?

为了分析 EXPLAIN 语句的执行结果,我们需要搞懂执行计划中的重要字段。

id

SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。

id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL.

select_type

查询的类型,主要用于区分普通查询联合查询子查询等复杂的查询,常见的值有:

  • SIMPLE:简单查询,不包含 UNION 或者子查询。
  • PRIMARY:查询中如果包含子查询或其他部分,外层的 SELECT 将被标记为 PRIMARY。
  • SUBQUERY:子查询中的第一个 SELECT。
  • UNION:在 UNION 语句中,UNION 之后出现的 SELECT。
  • DERIVED:在 FROM 中出现的子查询将被标记为 DERIVED。
  • UNION RESULT:UNION 查询的结果。
table

查询用到的表名,每行都有对应的表名,表名除了正常的表之外,也可能是以下列出的值:

  • <unionM,N> : 本行引用了 id 为 M 和 N 的行的 UNION 结果
  • <derivedN> : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。
  • <subqueryN> : 本行引用了 id 为 N 的表所产生的的物化子查询结果。
type(重要)

查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

常见的几种类型具体含义如下:

  • system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。

  • const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。

  • eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。

  • ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。

  • index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。

  • range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。

  • index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。

  • ALL:全表扫描。

possible_keys

possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引;这种情况下,需要检查 WHERE 语句中所使用的的列,看是否可以通过给这些列中某个或多个添加索引的方法来提高查询性能.

key(重要)

key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。

key_len

key_len 列表示 MySQL 实际使用的索引的最大长度;当使用到联合索引时,有可能是多个列的长度和。在满足需求的前提下越短越好。如果 key 列显示 NULL ,则 key_len 列也显示 NULL 。

rows

rows 列表示根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好。

Extra(重要)

这列包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下:

  • Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。

  • Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY

  • Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。

  • Using index condition:表示查询优化器选择使用了索引条件下推这个特性。

  • Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。

  • **Using join buffer (Block Nested Loop)**:连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。

这里提醒下,当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。

概述

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、 RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有 效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个 角度来说,锁对数据库而言显得尤其重要,也更加复杂。

MySQL中的锁,按照锁的粒度分,分为以下三类:

  • 全局锁:锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据。

全局锁

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语 句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做**全库的逻辑备份(把表转为SQL文件)**,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

为什么全库逻辑备份,就需要加全就锁呢?

下个单总不能表A库存没扣减,表B多了一条订单记录吧。

加全局锁语法

  • 加全局锁
1
flush tables with read lock;
  • 数据备份
1
mysqldump -uroot –p1234 itcast(数据库名) > itcast.sql

也可以利用可视化软件直接备份

  • 释放锁
1
unlock tables;

特点

数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。

  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。

在InnoDB引擎中,我们可以在备份时加上参数 –single-transaction 参数来完成不加锁的一致 性数据备份。

1
mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql

表级锁

表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、 InnoDB、BDB等存储引擎中。

对于表级锁,主要分为以下三类:

  • 表锁
  • 元数据锁(meta data lock,MDL)
  • 意向锁

表锁

  • 表共享读锁(read lock)
  • 表独占写锁(write lock)

表锁语法

  • 加锁:lock tables 表名… read/write。
  • 释放锁:unlock tables / 客户端断开连接 。

特点

  • 读锁(表共享读锁)

对指定表加了读锁,不会影响自己和其他客户端的读,但是自己进行DML/DDL操作会报错,而其他客户端会阻塞

  • 写锁(表独占写锁)

对指定表加了写锁,不会影响自己的读写操作,但是会阻塞其他客户端的读写操作。

元数据锁(MDL,meta data lock)

meta data lock , 元数据锁,简写MDL。

MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维 护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML(增删改)与 DDL (Data Definition Language,即数据定义语言,例如建数据库、建表等,都属于数据定义语言) 冲突,保证读写的正确性。

这里的元数据,大家可以简单理解为就是一张表的表结构。 也就是说,某一张表涉及到未提交的事务 时,是不能够修改这张表的表结构的。

在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享)当对表结构进行变 更操作的时候,加MDL写锁(排他)。

常见的SQL操作时,所添加的元数据锁:

可以看到2个客户端交叉执行DML操作是没问题的,因为DML加的是共享锁。

当左侧执行一个DML语句,右侧执行一个DDL语句时,右侧操作被阻塞,因为DDL加的是DML写锁(与DML读锁互斥),直到左侧提交事务,右侧才能执行。

我们可以通过下面的SQL,来查看数据库中的元数据锁的情况:

1
2
select object_type,object_schema,object_name,lock_type,lock_duration from
performance_schema.metadata_locks ;

意向锁

假如没有意向锁,客户端一对表加了行锁后,客户端二如何给表加表锁呢,来通过示意图简单分析一 下:

首先客户端一,开启一个事务,然后执行DML操作,在执行DML语句时,会对涉及到的行加行锁。

当客户端二,想对这张表加表锁时,会检查当前表是否有对应的行锁,它的判断方式是每一行都检查一遍看是否有行锁,效率较低。(加了行锁之后是不能加表锁的)。

有了意向锁之后 :

客户端一,在执行DML操作时,会对涉及的行加行锁,同时也会对该表加上意向锁

而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而 不用逐行判断行锁情况了。

介绍

为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行 数据是否加锁,使用意向锁来减少表锁的检查。

分类

  • 意向共享锁(IS): 由语句select … lock in share mode添加 。 与 表锁-共享锁 (read)(读锁)兼容,与表锁-排他锁(write)(写锁)互斥。
  • 意向排他锁(IX): 由insert、update、delete、select…for update添加 。与表锁共 享锁(read)及排他锁(write)都互斥,意向锁之间不会互斥。

一旦事务提交了,意向共享锁、意向排他锁,都会自动释放。

可以通过以下SQL,查看意向锁及行锁的加锁情况:

select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

当客户端1对某条记录加上行锁并且给全表加上意向共享锁时,客户端2可以对表加上表读锁,但是当客户端2给表加上表写锁时,会阻塞。很好理解,线程1要读某条数据,客户端2也可以读,反正没影响,但是客户端2不能修改这条数据吧,客户端2修改了那我客户端1不是读了错的数据吗?


当客户端1使用DML语句时,给某条记录加上了行锁并且给全表加上了意向排他锁,此时客户端2无论是加表读锁还是表写锁都会阻塞,这也很好理解,我客户端1要修改某条数据,客户端1还未提交事务,数据还不确定,客户端2读取肯定是错的,那没必要读啊,那客户端2写就更不用说了,我客户端1都还没改完,你客户端2更别想改了(加表写锁)。

行级锁

介绍

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在 InnoDB存储引擎中。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的 锁。对于行级锁,主要分为以下三类:

  • 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在 RC、RR隔离级别下都支持。

  • 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事 务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。

  • 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。 在RR隔离级别下支持。

行锁

InnoDB实现了以下两种类型的行锁:

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

(共享锁与共享锁不冲突,共享锁与排他锁冲突,排他锁与排他锁冲突).

  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。

两种行锁的兼容情况如下:

常见的SQL语句,在执行时,所加的行锁如下:

默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁(临键锁)进行搜 索和索引扫描,以防止幻读。

  • 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
  • InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记 录加锁,此时 就会升级为表锁

因为行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记 录加锁,此时 就会升级为表锁,name没有索引,所以使用update会加上表锁,客户端2就无法使用更新操作了,会阻塞。

那么我们给name字段创建一个索引就不会升级为表锁了。create index xxx(名字) on table(name);

可以通过以下SQL,查看意向锁及行锁的加锁情况:

1
2
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;

间隙锁&临键锁

默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜 索和索引扫描,以防止幻读。

  • 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。

当给id为5的数据进行修改操作时,表中没有id为5的数据,那么会给id为3与8之间的间隙加间隙锁,当右侧客户端插入一条id为7的数据时,操作被阻塞。

  • 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁(间隙锁唯一的目的是防止其他事务插入间隙,间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁)。

我们知道InnoDB的B+树索引,叶子节点是有序的双向链表。 假如,我们要根据这个二级索引查询值 为18的数据,并加上共享锁,我们是只锁定18这一行就可以了吗? 并不是,因为是非唯一索引,这个 结构中可能有多个18的存在(非唯一普通索引),所以,在加锁时会继续往后找,找到一个不满足条件的值(当前案例中也 就是29)。此时会对18加临键锁,并对29之前的间隙加锁。

  • 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。

查询的条件为id>=19,并添加共享锁。 此时我们可以根据数据库表中现有的数据,将数据分为三个部 分:

  • [19]
  • (19,25]
  • (25,+∞]

所以数据库数据在加锁时,就是将19加了行锁,25的临键锁(包含25及25之前的间隙),正无穷的临 键锁(正无穷及之前的间隙)。

插入意向锁

插入意向锁 Insert Intention Locks,代表当前事务准备插入一行数据。使用INSERT语句会获得插入意向锁。(是指一段空间,比如(0,5])表示我要对这段空间进行插入了。

插入意向锁和插入意向锁之间是兼容的,只要插入的键值不同,就不会相互阻塞(注意,键值一定不能相同,否则阻塞)

但如果两个事务插入同一个key,那就会阻塞。

插入意向锁可以保证两个事务插入key不同的数据的时候不冲突,提升并发性。

但是间隙锁会阻塞插入意向锁!这也可以理解,因为InnoDB想在RR隔离级别就解决幻读问题。所以A事务用SELECT语句获取了一个间隙锁,自然不希望B事务在这个期间往这个间隙插入一条新的记录。(图错了,应该是for updata)

InnoDB存储引擎

逻辑存储结构

  • 表空间

表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在 8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空 间,用于存储记录、索引等数据。

段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段 (Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点, 索引段即为B+树的 非叶子节点。段用来管理多个Extent(区)。

区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一 个区中一共有64个连续的页。

页,是InnoDB 存储引擎磁盘管理的最小单元,每次查寻是读取一个页的数据到内存中,每个页的大小默认为 16KB。为了保证页的连续性, InnoDB 存储引擎每次从磁盘申请 4-5 个区。

行,InnoDB 存储引擎数据是按行进行存放的。

在行中,默认有两个隐藏字段:

Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。 Roll_pointer:每次对某条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个 隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

架构

MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发 中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

内存架构

先看左边的内存架构

在左侧的内存结构中,主要分为这么四大块儿: Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer。 接下来介绍一下这四个部分。

  • Buffer pool

InnoDB存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能 弥补这两者之间的I/O效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁 盘I/O。

在InnoDB的缓冲池中不仅缓存了索引页数据页,还包含了undo页、插入缓存、自适应哈希索引以及 InnoDB的锁信息等等。

缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增 删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频 率刷新到磁盘,从而减少磁盘IO,加快处理速度。

缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:

  • free page:空闲page,未被使用。

  • clean page:被使用page,数据没有被修改过。

  • dirty page:脏页,被使用page,数据被修改过,页中数据与磁盘的数据产生了不一致。

  • Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML(增删改)语句时,如果这些数据Page 没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer 中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

先来看一幅图,这个是二级索引的结构图:

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新 可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 Change Buffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

写缓冲区为什么仅适用于非唯一普通索引页?

如上图所示,如果是唯一索引时,是直接去磁盘加载的数据,并不涉及到change Buffer,理由如下。

这是因为如果对索引设置唯一性,在进行修改操作时,InnoDB必须要做唯一性校验,因此必须得查询磁盘,做一次IO操作。这样就会直接将记录查询到Buffer Pool中,然后在缓冲池修改,就不会在change Buffer中操作了。

  • Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持 hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在 进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需 要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。 InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度, 则建立hash索引,称之为自适应hash索引。 自适应哈希索引,无需人工干预,是系统根据情况自动完成。 参数: adaptive_hash_index

  • Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事 务,增加日志缓冲区的大小可以节省磁盘 I/O。

参数:

innodb_log_buffer_size:缓冲区大小

innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:

1: 日志在每次事务提交时写入并刷新到磁盘,默认值。

0: 每秒将日志写入并刷新到磁盘一次。

2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。

磁盘结构

再看右边的磁盘结构:

  • System TableSpace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建 的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)

  • File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索 引 ,并存储在文件系统上的单个数据文件中(xxx.idb)。

开关参数:innodb_file_per_table ,该参数默认开启。

那也就是说,我们每创建一个表,都会产生一个表空间文件

  • General Tablespaces(了解)

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空 间。

  • Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储 undo log日志。

  • Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

  • Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件 中,便于系统异常时恢复数据。

  • Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所 有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。 以循环方式写入Redo Log日志文件,涉及两个文件:

事务原理

  • 事务

事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系 统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

  • 特性
    • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
    • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。(不仅数据要保持一致状态,约束也要保持一致状态)
    • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环 境下运行。
    • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久性,实际上是由InnoDB中的 两份日志来保证的,一份是redo log日志,一份是undo log日志。 而隔离性是通过数据库的锁, 加上MVCC来保证的。

redo log

redo log是如何保证事务的持久性的呢?

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。

redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用 于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

脏页:当客户端对buffer pool进行DML时,没有找到相关数据页,buffer pool从磁盘读取相关数据页到buffer pool,再对数据进行修改,此时buffer pool里的数据页和磁盘的数据不一致,这就是脏页。

如果没有redo log,可能会存在什么问题的? 我们一起来分析一下。

我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数 据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果 缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中 的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘 中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后 将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却 没有持久化下来,这就出现问题了,没有保证事务的持久性。

那么,如何解决上述的问题呢? 在InnoDB中提供了一份日志 redo log,接下来我们再来分析一 下,通过redo log如何解决这个问题。

有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redolog buffer中的数据刷新到redo log磁盘文件中。 过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据 恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此 时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的.

那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新 到磁盘呢 ?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在 往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这 种先写日志的方式,称之为 WAL(Write-Ahead Logging)。

undo log

回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的 update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些 日志可能还用于MVCC。

Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含1024个undo log segment。

MVCC

基本概念

  • 当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加 锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读

事务1首先查询了一次数据,事务2对表进行了一次修改,事务1再查询,但是数据还是和第一次一样,因为当前的隔离级别是默认的可重复读,第一次和第二次的结果是一样的。

但是在查询语句后面加上lock in share mode的话就是当前读,数据是最新版本。

  • 快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据, 不加锁,是非阻塞读。

    • Read Committed:每次select,都生成一个快照读。
    • Repeatable Read:开启事务后第一个select语句才是快照读的地方。
    • Serializable:快照读会退化为当前读。

MVCC基本概念

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本, 使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需 要依赖于数据库记录中的三个隐式字段(也有可能是两个)undo log日志readView

隐式字段

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了 这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键, 如果有主键,则不会添加该隐藏字段。

进入服务器中的 /var/lib/mysql/itcast/ , 查看stu的表结构信息, 通过如下指令:

1
ibd2sdi stu.ibd
undolog

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。

而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即 被删除。

版本链

有一张原始数据为:

DB_TRX_ID : 代表最近修改事务的ID,记录插入这条记录或最后一次修改该记录的事务ID,是 自增的。

DB_ROLL_PTR :回滚指针,由于这条数据是才插入的,没有被更新过,所以该字段值为null。

然后,有四个并发事务同时在访问这张表。

第一步

当事务2执行第一条修改语句时,会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

第二步

当事务3执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记 录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

第三步

当事务4执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记 录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条 记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

readview

ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务 (未提交的)id。

ReadView中包含了四个核心字段:

  • m_ids : 当前活跃的事务ID集合
  • min_trx_id : 最小活跃事务ID
  • max_trx_id : 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
  • creator_trx_id : ReadView创建者的事务ID

而在readview中就规定了版本链数据的访问规则:

trx_id 代表当前undolog版本链对应事务ID。

不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
原理分析
RC隔离级别(读已提交)

RC隔离级别下,在事务中每一次执行快照读时生成ReadView。

我们就来分析事务5中,两次快照读读取数据,是如何获取数据的?

在事务5中,查询了两次id为30的记录,由于隔离级别为Read Committed,所以每一次进行快照读 都会生成一个ReadView,那么两次生成的ReadView如下。

那么这两次快照读在获取数据时,就需要根据所生成的ReadView以及ReadView的版本链访问规则, 到undolog版本链中匹配数据,最终决定此次快照读返回的数据。

  • 先来看第一次快照读具体的读取过程:

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  • 先匹配最上面这条记录,这条记录对应的 trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 , 都不满足,则继续匹配undo log版本链的下一条。
  • 再匹配第二条 ,这条 记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②不满足 ③不满足 ④也 不满足 ,都不满足,则继续匹配undo log版本链的下一条。
  • 再匹配第三条 ,这条记 录对应的trx_id为2,也就是将2带入右侧的匹配规则中。①不满足 ②满足 终止匹配,此次快照 读,返回的数据就是版本链中记录的这条数据。

本质就是当前事务操作的最早已经提交的事务的数据。


再来看第二次快照读具体的读取过程:

先不看答案,按照上面说的本质,答案应该是事务3修改后的数据。

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  • 先匹配最上面 这条记录,这条记录对应的 trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 , 都不满足,则继续匹配undo log版本链的下一条。
  • 再匹配第二条 ,这条 记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②满足 。终止匹配,此次 快照读,返回的数据就是版本链中记录的这条数据。
RR隔离级别(可重复读)

RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可 重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。

那MySQL是如何做到可重复读的呢? 我们简单分析一下就知道了

我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的。

所以呢,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。 而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。

MySQL主从复制

主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这 些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。

MySQL支持一台主库同时向多台从库进行复制, 从库同时也可以作为其他从服务器的主库,实现链状 复制。

MySQL 复制的优点主要包含以下三个方面:

  • 主库出现问题,可以快速切换到从库提供服务。
  • 实现读写分离,降低主库的访问压力。(主库DML,从库DQL)
  • 可以在从库中执行备份,以避免备份期间影响主库服务。

原理

MySQL主从复制的核心就是 二进制日志,具体的过程如下:

从上图来看,复制分成三步:

  • Master 主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
  • 从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。
  • slave重做中继日志中的事件,将改变反映它自己的数据。

MySQL
http://example.com/2023/04/13/MySQL/
Author
Posted on
April 13, 2023
Licensed under