Mysql 高级学习笔记
文章目录
- Mysql 高级学习笔记
- 一、Mysql 基础
- 1. 聚合函数
- 2. having
- 3. sql 的执行顺序
- 4. 约束
- 5. 试图
- 二、Mysql 高级
- 1. MySQL中的SQL的执行流程
- 2. 存储引擎介绍
- 2. 索引
- 3. 性能分析工具的使用
- 4. 索引优化与查询优化
- 5、关联查询优化
- 6、事务及日志
- 6、MVCC
一、Mysql 基础
1. 聚合函数
什么是聚合函数?
- 聚合函数作用域于一组数据,并对一组数据返回一个值。
常用的聚合函数:
- AVG():若值为NULL,不进行计算。比如三个人,A:30 B:30 C:NULL 使用AVG则为30,C不算。其实应该是计算的,所有应该用SUM(字段) / COUNT(1)
- SUM()
- MAX()
- MIN()
- COUNT()
- COUNT(*)
- COUNT(1)
- COUNT(具体字段):不一定对,计算指定字段出现的次数时,是不计算NULL值的。
2. having
- 如果过滤条件中使用了聚合函数,则必须使用HAVING 来替换WHERE。否则,报错。且需要sql在后面,通常在GROUP BY 后面。比如:SELECT department_id,MAX(salary) FROM employee WHERE MAX(salary) > 10000 GROUP BY department_id。正确写法:SELECT department_id,MAX(salary) FROM employee GROUP BY department_id HAVING MAX(salary) > 10000
- 当过滤条件总有聚合函数时,则此过滤条件必须声明在HAVNING中。
- 当过滤条件中没有聚合函数是,则此过滤条件声明在WHERE中或HAVING中都可以。但是,建议用WHERE,因为执行效率更高。
- 从适用范围上来讲,HAVING的适用范围更广。
3. sql 的执行顺序
4. 约束
约束分类:
NOT NULL
非空约束,规定某个字段不能为空UNIQUE
唯一约束,规定某个字段在整个表中是唯一的PRIMARY KEY
主键(非空且唯一)约束FOREIGN KEY
外键约束CHECK
检查约束DEFAULT
默认值约束
约束等级:
Cascade方式
:在父表上update/delete记录时,同步update/delete掉子表的匹配记录。Set null方式
:在父表上update/delete记录时,将子表上匹配记录的列设为null,但是要注意子 表的外键列不能为not null。No action方式
:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作。Restrict方式
:同no action, 都是立即检查外键约束。Set default方式
: (在可视化工具SQLyog中可能显示空白):父表有变更时,子表将外键列设置 成一个默认的值,但Innodb不能识别x
如果没有指定等级,就相当于Restrict方式。 对于外键约束,最好是采用: ON UPDATE CASCADE ON DELETE RESTRICT 的方式。
外键约束:
-
添加外键约束(建表时):
create table 主表名称(
字段1 数据类型 primary key,
字段2 数据类型
);
create table 从表名称(
字段1 数据类型 primary key,
字段2 数据类型,
[CONSTRAINT <外键约束名称>] FOREIGN KEY(从表的某个字段) references 主表名(被参考字段)
);
#(从表的某个字段)的数据类型必须与主表名(被参考字段)的数据类型一致,逻辑意义也一样
#(从表的某个字段)的字段名可以与主表名(被参考字段)的字段名一样,也可以不一样
– FOREIGN KEY: 在表级指定子表中的列
– REFERENCES: 标示在父表中的列
create table dept( #主表
did int primary key, #部门编号
dname varchar(50) #部门名称
);
create table emp(#从表
eid int primary key, #员工编号
ename varchar(5), #员工姓名
deptid int, #员工所在的部门
foreign key (deptid) references dept(did) #在从表中指定外键约束
#emp表的deptid和和dept表的did的数据类型一致,意义都是表示部门的编号
);
说明:
(1)主表dept必须先创建成功,然后才能创建emp表,指定外键成功。
(2)删除表时,先删除从表emp,再删除主表dept -
添加外键约束(建表后):一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不 过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那 么,就要用修改表的方式来补充定义。
格式:ALTER TABLE 从表名 ADD [CONSTRAINT 约束名] FOREIGN KEY (从表的字段) REFERENCES 主表名(被引用字段) [on update xx][on delete xx];
举例:ALTER TABLE emp1
ADD [CONSTRAINT emp_dept_id_fk] FOREIGN KEY(dept_id) REFERENCES dept(dept_id);
阿里开发规范:
强制 】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学 生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于 单 机低并发 ,不适合 分布式 、 高并发集群 ;级联更新是强阻塞,存在数据库 更新风暴 的风险;外键影响 数据库的 插入速度 。
为什么建表时,加 not null default ‘’ 或 default 0
- 不想让表中出现null值。
为什么不想要 null 的值
- 不好比较。null是一种特殊值,比较时只能用专门的is null 和 is not null来比较。碰到运算符,通 常返回null。
- 效率不高。影响提高索引效果。因此,我们往往在建表时 not null default ‘’ 或 default 0
5. 试图
视图概述:
- 视图是一种
虚拟表
,本身是不具有数据
的,占用很少的内存空间,它是 SQL 中的一个重要概念。 - 视图建立在已有表的基础上, 视图赖以建立的这些表称为基表。
视图的创建和删除只影响视图本身,不影响对应的基表。
但是当对视图中的数据进行增加、删除和 修改操作时,数据表中的数据会相应地发生变化
,反之亦然。- 视图提供数据内容的语句为 SELECT 语句,
可以将视图理解为存储起来的 SELECT 语句
- 在数据库中,
视图不会保存数据,数据真正保存在数据表中
。当对视图中的数据进行增加、删 除和修改操作时,数据表中的数据会相应地发生变化;反之亦然。 - 视图,是向用户提供基表数据的另一种表现形式。通常情况下,小型项目的数据库可以不使用视 图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我 们
把经常查询的结果集放到虚拟表中,提升使用效率。
理解和使用起来都非常方便。
创建视图:
-
在 CREATE VIEW 语句中嵌入子查询
CREATE VIEW 视图名称 AS 查询语句
-
创建单表视图
# 方式一: CREATE VIEW empvu80 AS SELECT employee_id, last_name, salary FROM employees WHERE department_id = 80; # 方式二: CREATE VIEW empsalary8000(emp_id, NAME, monthly_sal) # 小括号内字段个数与SELECT中字段个数相同 AS SELECT employee_id, last_name, salary FROM employees WHERE salary > 8000;
-
查询视图:
SELECT * FROM salvu80;
-
创建多表联合视图
#方式一 CREATE VIEW empview AS SELECT employee_id emp_id,last_name NAME,department_name FROM employees e,departments d WHERE e.department_id = d.department_id; #方式二 CREATE VIEW dept_sum_vu (name, minsal, maxsal, avgsal) AS SELECT d.department_name, MIN(e.salary), MAX(e.salary),AVG(e.salary) FROM employees e, departments d WHERE e.department_id = d.department_id GROUP BY d.department_name;
-
基于视图创建视图。当我们创建好一张视图之后,还可以在它的基础上继续创建视图。
CREATE VIEW emp_dept_ysalary AS SELECT emp_dept.ename,dname,year_salary FROM emp_dept INNER JOIN emp_year_salary ON emp_dept.ename = emp_year_salary.ename;
-
查看视图
-
查看数据库的表对象、视图对象
SHOW TABLES;
-
查看视图的结构
DESC / DESCRIBE 视图名称;
-
查看视图的属性信息
# 查看视图信息(显示数据表的存储引擎、版本、数据行数和数据大小等) SHOW TABLE STATUS LIKE '视图名称'\G
-
查看视图的详细定义信息
SHOW CREATE VIEW 视图名称;
-
-
更新视图的数据。MySQL支持使用INSERT、UPDATE和DELETE语句对视图中的数据进行插入、更新和删除操作。当视图中的 数据发生变化时,数据表中的数据也会发生变化,反之亦然。
#举例:UPDATE操作 UPDATE emp_tel SET tel = '13789091234' WHERE ename = '孙洪亮'; #举例:DELETE操作 DELETE FROM emp_tel WHERE ename = '孙洪亮';
-
修改、删除视图。
-
使用CREATE OR REPLACE VIEW 子句修改视图
CREATE OR REPLACE VIEW empvu80 (id_number, name, sal, department_id) AS SELECT employee_id, first_name || ' ' || last_name, salary, department_id FROM employees WHERE department_id = 80;
-
ALTER VIEW
ALTER VIEW 视图名称 AS 查询语句
-
删除视图。
DROP VIEW IF EXISTS 视图名称; #举例: DROP VIEW empvu80;
-
说明:基于视图a、b创建了新的视图c,如果将视图a或者视图b删除,会导致视图c的查询失败。这 样的视图c需要手动删除或修改,否则影响使用。
-
视图的优点:
操作简单。
将经常使用的查询操作定义为视图,可以使开发人员不需要关心视图对应的数据表的结构、表与表之间的关联关系,也不需要关心数据表之间的业务逻辑和查询条件,而只需要简单地操作视图即可,极大简化了开发人员对数据库的操作。减少数据冗余。
视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语 句来获取结果集。而视图本身不存储数据,不占用数据存储的资源,减少了数据冗余。
-数据安全。
MySQL将用户对数据的 访问限制 在某些数据的结果集上,而这些数据的结果集可以使用视图来实现。用 户不必直接查询或操作数据表。这也可以理解为视图具有 隔离性 。视图相当于在用户和实际的数据表之间加了一层虚拟表。
同时,MySQL可以根据权限将用户对数据的访问限制在某些视图上,用户不需要查询数据表,可以直接通过视图获取数据表中的信息。这在一定程度上保障了数据表中数据的安全性。适应灵活多变的需求。
当业务系统的需求发生变化后,如果需要改动数据表的结构,则工作量相对较 大,可以使用视图来减少改动的工作量。这种方式在实际工作中使用得比较多。能够分解复杂的查询逻辑。
数据库中如果存在复杂的查询逻辑,则可以将问题进行分解,创建多个视图 获取数据,再将创建的多个视图结合起来,完成复杂的查询逻辑。
视图的不足:
- 如果我们在实际数据表的基础上创建了视图,那么,如果
实际数据表的结构变更了,我们就需要及时对相关的视图进行相应的维护。
特别是嵌套的视图(就是在视图的基础上创建视图),维护会变得比较复杂,可读性不好 ,容易变成系统的潜在隐患。
因为创建视图的 SQL 查询可能会对字段重命名,也可能包含复杂的逻辑,这些都会增加维护的成本。
实际项目中,如果视图过多,会导致数据库维护成本的问题。
所以,在创建视图的时候,你要结合实际项目需求,综合考虑视图的优点和不足,这样才能正确使用视图,使系统整体达到最优。
二、Mysql 高级
1. MySQL中的SQL的执行流程
流程图:
-
查询缓存:
Server 如果在查询缓存中发现了这是SQL语句,就会直接将结果返回给客户端;如果没有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在MySQL8.0之后就抛弃了这个功能。- MySQL 拿到一个查询请求后,会先到查询缓存看看,
之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是查询的结果。
如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。
所以,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接放回结果,这个效率会很高。 - 大多数情况下查询缓存就是鸡肋,为什么呢?
- 查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的
鲁棒性大大降低,
只有相同的查询操作 才会命中查询缓存。
两个查询请求在任何字符上的不同(例如:空格、注释、大小写,同时一些函数不同时刻调,也是无法缓存命中的,比如说now()),都会导致缓存不会命中。因此MySQL 的查询缓存命中不高。
- 此外,还会出现
缓存失效
的情况下,比如我我刚缓存了一次查询及其结果。但是后面我删除了数据或者更改饿了表结构,那么查询缓存的数据是不对的。 总之,因为查询缓存往往弊大于利,查询缓存的失效非常频繁。
- 查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的
- MySQL 拿到一个查询请求后,会先到查询缓存看看,
-
解析器:在解析器中进行SQL的语法分析、语义分析。
没有命中查询缓存,则开始进行SQL语句的分析,分为词法分析与语法分析。
- 分析器先做
“词法分析”
。你输入的是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么。MySQL从你输入的“select”这个关键字识别出来,这是一个查询语句。它也要吧字符串“T”识别成“表名T”,把字符串“ID”识别成“列ID”。 - 接着要做
“语法分析”
。根据词法分析的结果,语法分析器(比如:Bison)会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
- 分析器先做
-
优化器:
在优化器中会确定SQL语句的执行路径,比如是根据全表检索
,还是根据索引检索
。- 经过了解析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
一条查询可以有很多中执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
- 比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序,还是表达式简化、子查询转为连接、外连接转为内连接。
- SQL优化分为两个部分:
逻辑查询优化和物理查询优化。
- 物理查询优化是通过
索引和表连接方式
等技术来进行优化,这里重点需要掌握索引的使用。 - 逻辑查询优化是通过SQL
等价变换
提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。
- 物理查询优化是通过
- 经过了解析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
-
执行器:
截止到现在,还没有真正去读写真实的表,仅仅是产出了一个执行计划。于是就进入了执行器阶段
。- 执行之前需要判断该客户是否
具备权限
。如果没有,就会返回权限错误。如果具备权限,就执行SQL查询并返回结果。在MySQL8.0以下版本,如果设置了查询缓存,这时会将查询结果进行缓存。 - 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,调用存储API对表进行读写。存取引擎API只是抽象接口,下面还有存储引擎层,具体实现还是要看表选择的存储引擎。
- 执行之前需要判断该客户是否
-
总结:SQL语句在MySQL中的流程是:
SQL语句 ->查询缓存 ->解析器 ->优化器->执行器
2. 存储引擎介绍
InnoDB 引擎:具备外键支持功能的事务存储引擎
- MySQL从3.23.34a开始就包含InnoDB存储引擎。
大于等于5.5之后,默认采用InnoDB引擎 。
InnoDB是MySQL的 默认事务型引擎
,它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。- 除了增加和查询外,还需要更新、删除操作,那么,应优先选择InnoDB存储引擎。 除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。
- 数据文件结构:
- 表名.frm 存储表结构(MySQL8.0时,合并在表名.ibd中)
- 表名.ibd 存储数据和索引
- InnoDB是 为处理巨大数据量的最大性能设计 。
- 在以前的版本中,字典数据以元数据文件、非事务表等来存储。现在这些元数据文件被删除 了。比如: .frm , .par , .trn , .isl , .db.opt 等都在MySQL8.0中不存在了。
- 对比MyISAM的存储引擎,
InnoDB写的处理效率差一些 ,并且会占用更多的磁盘空间以保存数据和索引。
MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较 高 ,而且内存大小对性能有决定性的影响。
MyISAM 引擎:主要的非事务处理存储引擎
- MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但
MyISAM不支持事务、行级 锁、外键 ,
有一个毫无疑问的缺陷就是崩溃后无法安全恢复
。 5.5之前默认的存储引擎。
- 优势是
访问的速度快 ,对事务完整性没有要求或者以SELECT、INSERT为主的应用。
- 针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高 数据文件结构。
- 表名.frm 存储表结构。
- 表名.MYD 存储数据 (MYData)。
- 表名.MYI 存储索引 (MYIndex)。
- 应用场景:只读应用或者以读为主的业务
2. 索引
索引概述:
- 索引(Index)是
帮助MySQL高效获取数据的数据结构。
索引是数据结构
。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。 这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法。索引是在存储引擎中实现的
,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时,存储引擎可以定义每个表的 最大索引数和 最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。
优点:
- 类似大学图书馆建书目索引,提高数据检索的效率,降低 数据库的IO成本 ,这也是创建索引最主 要的原因。
- 通过创建唯一索引,可以保证数据库表中每一行 数据的唯一性 。
- 在实现数据的 参考完整性方面,可以 加速表和表之间的连接 。换句话说,对于有依赖关系的子表和父表联合查询时, 可以提高查询速度。
- 在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。
缺点:
创建索引和维护索引要 耗费时间 ,并 且随着数据量的增加,所耗费的时间也会增加。
索引需要占 磁盘空间
,除了数据表占数据空间之 外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文 件更快达到最大文件尺寸。- 虽然索引大大
提高了查询速度
,同时却会降低更新表的速度
。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
因此,选择使用索引时,需要综合考虑索引的优点和缺点。
注:索引可以提高查询的速度,但是会影响插入记录的速度。这种情况下,最好的办法是先删除表中的索引,然后插入数据,插入完成后再创建索引。
常见索引概念:
-
聚簇索引
:聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在了叶子结点
),也就是所谓的 索引即数据,数据即索引。-
特点:使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
页内 的记录
是按照主键的大小顺序排成一个单向链表
。- 各个存放 用户
记录的页
也是根据页中用户记录的主键大小顺序排成一个双向链表 。
- 存放 目录项记录的页 分为不同的层次,在
同一层次中的页
也是根据页中目录项记录的主键大小顺序排成一个双向链表 。
-
B+树的 叶子节点 存储的是完整的用户记录
。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。 -
我们把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX 语句去创建, InnDB 存储引擎会 自动 的为我们创建聚簇索引。
-
优点:
- 数据访问更快 ,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
- 聚簇索引对于主键的 排序查找 和 范围查找 速度非常快
- 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多 个数据块中提取数据,所以 节省了大量的io操作 。
-
缺点:
- 插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
- 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
- 二级索引访问需要两次索引查找 ,第一次找到主键值,第二次根据主键值找到行数据。
-
-
二级索引(辅助索引、非聚簇索引)
:如果我们想以别的列作为搜索条件该怎么办?肯定不能是从头到尾沿着链表依次遍历记录一遍。我们可以多建几颗B+树,不同的B+树中的数据采用不同的排列规则。比方说我们用c2列的大小作为数据页、页中记录的排序规则,再建一课B+树。其中叶子节点不在记录完整的记录,而是非聚簇索引值和聚簇索引(一般情况下为主键值)值。回表
:我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。
也就 是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树!- 为什么我们还需要一次 回表 操作呢?直接把完整的用户记录放到叶子节点不OK吗?
- 如果把完整的用户记录放到叶子结点是可以不用回表。但是太占地方了,相当于每建立一课B+树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。
- 因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引,或者辅助索引。由于使用的是c2列的大小作为B+树的排序规则,所以我们也称这个B+树为c2列简历的索引。
- 非聚簇索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个非聚簇索引。
-
小结:聚簇索引与非聚簇索引的原理不同,在使用上也有一些区别:
- 聚簇索引的叶子节点存储的就是我们的数据记录, 非聚簇索引的叶子节点存储的是数据位置。非聚簇索引不会影响数据表的物理存储顺序。
一个表只能有一个聚簇索引
,因为只能有一种排序存储的方式,但可以有多个非聚簇索引
也就是多个索引目录提供数据检索。- 使用聚簇索引的时候,数据的查询效率高,但如果对数据进行插入,删除,更新等操作,效率会比非聚簇索引低。
-
联合索引:
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按 照 c2和c3列 的大小进行排序,这个包含两层含义:先把各个记录和页按照c2列进行排序,在记录的c2列相同的情况下,采用c3列进行排序
- 如图所示,我们需要注意以下几点:
- 每条目录项都有c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。
- B+树叶子节点处的用户记录由c2、c3和主键c1列组成。
- 注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引 ,本质上也是一个二级索引。它的意 思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
- 建立 联合索引 只会建立如上图一样的1棵B+树。
- 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。
- 如图所示,我们需要注意以下几点:
索引的分类:
- MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。从 功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。按照 作用字段个数 进行划分,分成单列索引和联合索引。
索引的创建:
-
创建普通索引
:在book表中的year_publication字段上建立普通索引,SQL语句如下:CREATE TABLE book( book_id INT , book_name VARCHAR(100), authors VARCHAR(100), info VARCHAR(100) , comment VARCHAR(100), year_publication YEAR, INDEX(year_publication) );
-
创建唯一索引
:该语句执行完毕之后,使用SHOW CREATE TABLE查看表结构:SHOW INDEX FROM test1 \GCREATE TABLE test1( id INT NOT NULL, name varchar(30) NOT NULL, UNIQUE INDEX uk_idx_id(id) );
-
主键索引
:设定为主键后数据库会自动建立索引,innodb为聚簇索引,语法:-
随表一起建索引:
CREATE TABLE student ( id INT(10) UNSIGNED AUTO_INCREMENT , student_no VARCHAR(200), student_name VARCHAR(200), PRIMARY KEY(id) );
-
删除主键索引:
ALTER TABLE student drop PRIMARY KEY;
-
修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引
-
-
创建单列索引
CREATE TABLE test2( id INT NOT NULL, name CHAR(50) NULL, INDEX single_idx_name(name(20)) );
-
创建组合索引:
创建表test3,在表中的id、name和age字段上建立组合索引,SQL语句如下:CREATE TABLE test3( id INT(11) NOT NULL, name CHAR(30) NOT NULL, age INT(11) NOT NULL, info VARCHAR(255), INDEX multi_idx(id,name,age) );
-
创建全文索引
:FULLTEXT全文索引可以用于全文检索,并且只为 CHAR 、VARCHAR 和 TEXT 列创建索引。索引总是对整个列进行,不支持局部 (前缀) 索引。-
1举例:创建表test4,在表中的info字段上建立全文索引,SQL语句如下:在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。
CREATE TABLE test4( id INT NOT NULL, name CHAR(30) NOT NULL, age INT NOT NULL, info VARCHAR(255), FULLTEXT INDEX futxt_idx_info(info) ) ENGINE=MyISAM;
-
举例2:
CREATE TABLE articles ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR (200), body TEXT, FULLTEXT index (title, body) ) ENGINE = INNODB;
-
举例3:
CREATE TABLE `papers` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(200) DEFAULT NULL, `content` text, PRIMARY KEY (`id`), FULLTEXT KEY `title` (`title`,`content`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-
不同于like方式的的查询:
SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’;
-
全文索引用match+against方式查询:
SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);
-
明显的提高查询效率。注意点:
- 使用全文索引前,搞清楚版本支持情况;
全文索引比 like + % 快 N 倍,但是可能存在精度问题;
- 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
-
在已经存在的表上创建索引: 在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句。
-
使用ALTER TABLE语句创建索引 ALTER TABLE语句创建索引的基本语法如下:
ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],...) [ASC | DESC]
-
使用CREATE INDEX创建索引 CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中, CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ON table_name (col_name[length],...) [ASC | DESC]
删除索引:
-
使用ALTER TABLE删除索引 ALTER TABLE删除索引的基本语法格式如下:
ALTER TABLE table_name DROP INDEX index_name;
-
使用DROP INDEX语句删除索引 DROP INDEX删除索引的基本语法格式如下:
DROP INDEX index_name ON table_name;
-
删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除。
适合创建索引的情况:
-
字段的数值有唯一性的限制。
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba) 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。 -
频繁作为 WHERE 查询条件的字段
。某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在 数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。 -
经常 GROUP BY 和 ORDER BY 的列。
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引 。如果待排序的列有多个,那么可以在这些列上建立组合索引 。 -
UPDATE、DELETE 的 WHERE 条件列。
对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。 -
DISTINCT 字段需要创建索引。
有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。 -
多表 JOIN 连接操作时,创建索引注意事项。首先, 连
接表的数量尽量不要超过 3 张
,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增 长会非常快,严重影响查询的效率。其次,对 WHERE 条件创建索引
,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下, 没有 WHERE 条件过滤是非常可怕的。最后,对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致
。比如 course_id 在 student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。 -
使用列的类型小的创建索引。
-
使用字符串前缀创建索引。
-
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引。
create table shop(address varchar(120) not null); alter table shop add index(address(12));
-
先看一下字段在全部数据中的选择度:
select count(distinct address) / count(*) from shop
-
通过不同长度去计算,与全表的选择性对比:公式:(越接近于1越好,说明越有区分度)
count(distinct left(列名, 索引长度))/count(*)
-
例如:
select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度 count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度 count(distinct left(address,20)) / count(*) as sub12, -- 截取前20个字符的选择度 count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度 from shop;
-
引申另一个问题:索引列前缀对排序的影响:如果使用了索引列前缀,比方说前边只把address列的 前12个字符 放到了二级索引中,下边这个查询可能就有点尴尬了:
SELECT * FROM shop ORDER BY address LIMIT 12;
-
因为二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用
索引列前缀的方式 无法支持使用索引排序 ,只能使用文件排序。
-
区分度高(散列性高)的列适合作为索引。
- 列的基数 指的是某一列中不重复数据的个数,比方说某个列包含值 2, 5, 8, 2, 5, 8, 2, 5, 8,虽然有9条记录,但该列的基数却是3。也就是说
在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。
这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列简历索引,为基数太小的列的简历索引效果可能不好。 - 可以使用公式select count(distinct a) / count(*) from t1 计算区分度,越接近1越好,一般超过33%就算比较高效的索引了。
- 扩展:联合索引把区分度搞(散列性高)的列放在前面。
- 列的基数 指的是某一列中不重复数据的个数,比方说某个列包含值 2, 5, 8, 2, 5, 8, 2, 5, 8,虽然有9条记录,但该列的基数却是3。也就是说
-
-
使用最频繁的列放到联合索引的左侧。
这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。 -
在多个字段都要创建索引的情况下,联合索引优于单值索引
哪些情况不适合创建索引:
在where中使用不到的字段,不要设置索引。
数据量小的表最好不要使用索引。
如果表记录太少,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引 对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。有大量重复数据的列上不要建立索引。
避免对经常更新的表创建过多的索引。
不建议用无序的值作为索引。
删除不再使用或者很少使用的索引。
不要定义夯余或重复的索引。
- `冗余索引。举例:我们知道,通过 idx_name_birthday_phone_number 索引就可以对 name 列进行快速搜索,再创建一 个专门针对 name 列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有 什么好处。
CREATE TABLE person_info( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, birthday DATE NOT NULL, phone_number CHAR(11) NOT NULL, country varchar(100) NOT NULL, PRIMARY KEY (id), KEY idx_name_birthday_phone_number (name(10), birthday, phone_number), KEY idx_name (name(10)) );
- 重复索引。另一种情况,我们可能会对某个列 重复建立索引 ,比方说这样:我们看到,col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就 会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。
CREATE TABLE repeat_index_demo ( col1 INT PRIMARY KEY, col2 INT, UNIQUE uk_idx_c1 (col1), INDEX idx_c1 (col1) );
- `冗余索引。举例:我们知道,通过 idx_name_birthday_phone_number 索引就可以对 name 列进行快速搜索,再创建一 个专门针对 name 列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有 什么好处。
3. 性能分析工具的使用
查看系统性能参数:
-
在MySQL中,可以使用
SHOW STATUS
语句查询一些MySQL数据库服务器的性能参数、执行频率。SHOW STATUS语句语法如下:SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
-
一些常用的性能参数如下:
Connections
:连接MySQL服务器的次数。SHOW STATUS LIKE 'Connections';
Uptime
:MySQL服务器的上线时间。Slow_queries
:慢查询的次数。Innodb_rows_read
:Select查询返回的行数。Innodb_rows_inserted
:执行INSERT操作插入的行数。Innodb_rows_updated
:执行UPDATE操作更新的 行数。Innodb_rows_deleted
:执行DELETE操作删除的行数。Com_select
:查询操作的次数。Com_insert
:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。Com_update
:更新操作 的次数。Com_delete
:删除操作的次数。
统计SQL的查询成本: last_query_cost:
-
一条SQL查询语句在执行前需要查询执行计划,如果存在多种执行计划的话,MySQL会计算每个执行计划所需要的成本,从中选择成本最小的一个作为最终执行的执行计划。如果我们想要查看某条SQL语句的查询成本,可以在执行完这条SQL语句之后,通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。它通常也是我们评价一个查询的执行效率的一个常用指标。这个查询成本对应的是SQL 语句所需要读取的读页的数量。
-
我们使用 student_info 表为例:
CREATE TABLE `student_info` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `student_id` INT NOT NULL , `name` VARCHAR(20) DEFAULT NULL, `course_id` INT NOT NULL , `class_id` INT(11) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
如果我们想要查询 id=900001 的记录,然后看下查询成本,我们可以直接在聚簇索引上进行查找:
SELECT student_id, class_id, NAME, create_time FROM student_info WHERE id = 900001;
-
运行结果(1 条记录,运行时间为 0.042s ),然后再看下查询优化器的成本,实际上我们只需要检索一个页即可:
mysql> SHOW STATUS LIKE 'last_query_cost'; +-----------------+----------+ | Variable_name | Value | +-----------------+----------+ | Last_query_cost | 1.000000 | +-----------------+----------+
-
如果我们想要查询 id 在 900001 到 9000100 之间的学生记录呢?
SELECT student_id, class_id, NAME, create_time FROM student_info WHERE id BETWEEN 900001 AND 900100;
-
运行结果(100 条记录,运行时间为 0.046s ):然后再看下查询优化器的成本,这时我们大概需要进行 20 个页的查询。
mysql> SHOW STATUS LIKE 'last_query_cost'; +-----------------+-----------+ | Variable_name | Value | +-----------------+-----------+ | Last_query_cost | 21.134453 | +-----------------+-----------+
-
你能看到页的数量是刚才的 20 倍,但是查询的效率并没有明显的变化,实际上这两个 SQL 查询的时间 基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。虽然 页 数量(last_query_cost)增加了不少 ,但是通过缓冲池的机制,并 没有增加多少查询时间 。
-
SQL查询时一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:
-
位置决定效率。
如果页就在数据库 缓冲池 中,那么效率是最高的
,否则还需要从 内存 或者 磁盘 中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。 -
批量决定效率。
如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取
,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。 -
所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是进程使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。
-
EXPLAIN各列作用:
-
table:不论我们的查询语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所 以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该 表的表名(有时不是真实的表名字,可能是简称)。
-
这个查询语句只涉及对s1表的单表查询,所以 EXPLAIN 输出中只有一条记录,其中的table列的值为s1,表明这条记录是用来说明对s1表的单表访问方法的。下边我们看一个连接查询的执行计划。可以看出这个连接查询的执行计划中有两条记录,这两条记录的table列分别是s1和s2,这两条记录用来分别说明对s1表和s2表的访问方法是什么。
-
id:正常来说一个select 一个id ,也有例外的可能,查询优化器做了优化
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
-
查询优化器优化:查询优化器可能对涉及子查询的查询语句进行重写,转变为多表查询的操作,如上图
-
Union去重
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
-
union all 不去重。所以不需要放在临时表里面
EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;
-
小结:
- id如果相同,可以认为是一组,从上往下顺序执行。
- 在所有组中,id值越大,优先级越高,越先执行
- 关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好
-
select_type
-
一条大的查询语句里边可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句,而每个SELECT关键字的FROM子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个SELECT关键字中的表来说,它们的id值是相同的。MySQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,我们看一下 select_type都能取哪些值,请看官方文档:
- SIMPLE。
- 查询语句中不包含
UNION
或者子查询的查询都算作是SIMPLE
类型。EXPLAIN SELECT * FROM s1。 - 连接查询也算是
SIMPLE
类型。EXPLAIN SELECT * FROM s1 INNER JOIN s2。
- 查询语句中不包含
- SIMPLE。
-
PRIMARY 与 UNION与 UNION RESULT
- UNION RESULT:MySQL选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT,例子上边有。
-
对于包含
UNION
或者UNION ALL
或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type
值就是PRIMARY
-
对于包含
UNION
或者UNION ALL
的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询
以外,其余的小查询的select_type
值就是UNION
-
MySQL
选择使用临时表来完成UNION
查询的去重工作,针对该临时表的查询的select_type
就是UNION RESULT
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;
-
- UNION RESULT:MySQL选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT,例子上边有。
-
type:执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,又称"访问类型”,其中的type列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type列的值是ref,表明MySQL即将使用ref访问方法来执行对s1表的查询。完整的访问方法如下: system , const , eq_ref , ref , fulltext , ref_or_null ,index_merge , unique_subquery , index_subquery , range , index , ALL 。
-
rows:rows 值越小,代表,数据越有可能在一个页里面,这样io就会更小。
-
4. 索引优化与查询优化
都有哪些维度可以进行数据库调优?简言之:
- 索引失效、没有充分利用到索引——建立索引
- 关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化
- 服务器调优及各个参数设置(缓冲、线程数等)——调整my.cnf
- 数据过多——分库分表
- 虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。
- 物理查询优化是通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
- 逻辑查询优化就是通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法效率可能更高。
索引失效案例:
-
全值匹配我最爱。
- 系统中经常出现的sql语句如下:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd' ;
- 建立索引前执行:(关注执行时间)
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd '; Empty set,1 warning (0.28 sec)
- 建立索引
CREATE INDEX idx_age oN student( age ) ; CREATE INDEX idx_age_classid ON student(age , classId ); CREATE INDEX idx_age_classid_name ON student( age ,classId , name ) ;
- 建立索引后执行:
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd '; Empty set,1 warning (0.01 sec)
- 可以看到,创建索引前的查询时间是0.28秒,创建索引后的查询时间是0.01秒,索引帮助我们极大的提高了查询效率。
- 系统中经常出现的sql语句如下:
-
最佳左前缀法则。
-
在MySQL建立联合索引时会遵守最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
-
举例1:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd' ;
-
举例2:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student . name = 'abcd';
-
举例3: 索引idx_age_classid_name还能否正常使用?
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid=4 AND student.age=30 AND student.name= 'abcd'
-
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student. name =' abcd ' ;
-
虽然可以正常使用,但是只有部分被使用到了。
mysq1>EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name ='abcd ' ;
-
完全没有使用上索引。
-
结论:MySQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。
-
-
主键插入顺序。对于一个使用InnoDB存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在聚簇索引的叶子节点的。而记录又是存储在数据页中的,数据页和话录又是按照记录主键值从小到大的顺序进行排序,所以如果我们插入的记录的主键值是依次增大的话,那我们每插满一个数据页就换到下一个数据页继续插,而如果我们插入的主键值忽大忽小的话,就比较麻烦了,假设某个数据页存储的记录已经满了,它存储的主键值在1~100之间:
如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:
可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录
移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量
避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。
所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 。 -
计算、函数、类型转换(自动或手动)导致索引失效。
-
类型转换导致索引失效。
-
范围条件右边的列索引失效
- 如果系统经常出现的sql如下:
ALTER TABLE student DROP INDEX idx_name; ALTER TABLE student DROP INDEX idx_age; ALTER TABLE student DROP INDEX idx_age_classid; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
- 那么索引 idx_age_classid_name这个索引还能正常使用么?
- 不能,范围右边的列不能使用。比如: (<) (<=) (>)(>=)和between等。如果这种sql出现较多,应该建立:
create index idx_age_name_classid on student(age,name,classid);
- 将范围查询条件放置语句最后:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 ;
- 应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后。
- 效果
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 ANDstudent.name = 'abc ' ;
- 如果系统经常出现的sql如下:
-
不等于(!= 或者<>)索引失效
-
为name字段创建索引。
CREATE INDEX idx_name oN student(NAME);
-
查看索引是否失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc' ;
-
-
is null可以使用索引,is not null无法使用索引。
-
like以通配符%开头索引失效。
-
OR 前后存在非索引的列,索引失效
-
数据库和表的字符集统一使用utf8mb4:统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的 字符集 进行比较前需要进行 转换 会造成索引失效。
5、关联查询优化
-
采用左外连接。
-
下面开始 EXPLAIN 分析。
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
-
结论:type 有All,添加索引优化
ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描 EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
-
可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引 。
ALTER TABLE `type` ADD INDEX X (card); #【驱动表】,无法避免全表扫描 EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
-
-
采用内连接:
drop index X on type; drop index Y on book;(如果已经删除了可以不用再执行该操作)
-
换成 inner join(MySQL自动选择驱动表)
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
-
添加索引优化
ALTER TABLE book ADD INDEX Y ( card); EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
ALTER TABLE type ADD INDEX X (card); EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
-
接着:
DROP INDEX X ON `type`; EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;
-
接着:
ALTER TABLE `type` ADD INDEX X (card); EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
-
接着:
#向表中再添加2日条记录 INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) ); INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND() * 20) ) ); INSERT INTO book(card) VALUES(FLOOR( 1 +(RAND() * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) ) ; INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) ); INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) ); INSERT INTO book(card) VALUES(FLOOR(1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ; INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ); INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ) ; ALTER TABLE book ADD INDEX Y ( card) ; EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;
-
图中发现,由于type表数据大于book表数据,MySQL选择将type作为被驱动表。也就是小表驱动大表。
-
-
join语句原理
- join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。
- 驱动表和被驱动表:驱动表就是主表,被驱动表就是从表、非驱动表。
- 对于内连接来说:
SELECT * FROM A JOIN B ON ...
- A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。通过explain关键字可以查看。
- 对于外连接来说:
SELECT * FROM A LEFT JOIN B ON ... #或 SELECT * FROM B RIGHT JOIN A ON ...
- 通常,大家会认为A就是驱动表,B就是被驱动表。但也未必。
- 对于内连接来说:
- Block Nested-Loop Join(块嵌套循环连接)
-
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和joinbuffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。
-
注意:这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让joinbuffer中可以存放更多的列。
-
- 永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量) (小的度量单位指的是表行数*每行大小)
- 增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)
- 减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)
- 为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)
-
子查询优化
-
子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子
查询的执行效率不高。原因:- 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
- 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会受到一定的影响。
- 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
-
在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询 不需要建立临时表 ,其 速度比子查询 要快 ,如果查询中使用索引的话,性能就会更好。
-
-
排序优化:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
-
优化建议:
- SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫 描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
- 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
- 无法使用 Index 时,需要对 FileSort 方式进行调优。
- 使用 order by时,需要加上 limit, 否则索引失效。
-
GROUP BY优化
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接
使用索引。 - group by 先排序再分组,遵照索引建的最佳左前缀法则
- where效率高于having,能写在where限定的条件就不要写在having中了。
- 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接
-
优化分页查询
- 优化思路一:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;
- 优化思路二:该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
- 优化思路一:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
-
优先考虑覆盖索引
-
什么是覆盖索引?
- 理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它
不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数
据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。 - 理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
- 理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它
-
覆盖索引的利弊
- 好处:
- 避免Innodb表进行索引的二次查询(回表)
- 可以把随机IO变成顺序IO加快查询效率
- 弊端:
- 索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。
- 好处:
-
-
索引下推
-
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优
化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。 -
使用前后对比:
- Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的优化方式。
- 如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE后面的条件是否保留行。
- 启用ICP后,如果部分WHERE条件可以仅使用索引中的列进行筛选,则mysql服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。
- 好处:ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数。
- 但是,ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例。
-
ICP的使用条件:
- 只能用于二级索引(secondary index)。
- explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。
- 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
- ICP可以用于MyISAM和InnnoDB存储引擎
- MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
- 当SQL使用覆盖索引时,不支持ICP优化方法。
-
ICP使用案例
SELECT * FROM tuser WHERE NAME LIKE '张%' AND age = 10 AND ismale = 1;
-
普通索引 vs 唯一索引:
-
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
-
查询过程:
- 假设,执行查询的语句是 select id from test where k=5。
- 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一
个不满足k=5条件的记录。 - 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检
索。 - 那么,这个不同带来的性能差距会有多少呢?答案是, 微乎其微 。
-
更新过程:
- 为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下change buffer。
- 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中 ,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
- 将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。除了 访问这个数据页 会触发merge外,系统有 后台线程会定期 merge。在 数据库正常关闭(shutdown) 的过程中,也会执行merge操作。
- 如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且,
数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存 ,提高内存利用率。 - 唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用。
-
change buffer的使用场景:
- 普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对 更新性能 的影响。所以,建议你 尽量选择普通索引 。
- 在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于 数据量大 的表的更新优化还是很明显的。
- 如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么你应该 关闭change buffer 。而在其他情况下,change buffer都能提升更新性能。
其它查询优化策略:
-
EXISTS 和 IN 的区分,不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?
- 索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。
- 比如下面这样:
SELECT * FROM A WHERE cc IN (SELECT cc FROM B) SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
- 当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:
for i in A for j in B if j.cc == i.cc then
- 当B小于A时用IN,因为实现的逻辑类似于:
for i in B for j in A if j.cc == i.cc then ...
- 哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。
-
COUNT(*)与COUNT(具体字段)效率
- 在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和 SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?
- COUNT()和COUNT(1)都是对所有结果进行COUNT,COUNT()和COUNT(1)本质上并没有区别(二者执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的)。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。
- 如果是MyISAM存储引擎,统计数据表的行数只需要O(1)的复杂度,这是因为每张MyISAM的数据表都有一个meta 信息存储了row_count值,而一致性则由表级锁来保证。
- 如果是InnoDB存储引擎,因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MyISAM一样,维护一个row_count变量,因此需要采用扫描全表,进行循环+计数的方式来完成统计,是O(N)级别复杂度。
- 在InnoDB引擎中,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。
- 如果有多个二级索引,会使用key_len 小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
-
关于SELECT(*)
- 在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
- MySQL 在解析的过程中,会通过 查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。
- 无法使用 覆盖索引
- 在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
-
LIMIT 1 对优化的影响
- 针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
- 如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。
-
多使用COMMIT:只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
- COMMIT 所释放的资源:
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
- redo / undo log buffer 中的空间
- 管理上述 3 种资源中的内部花费
- COMMIT 所释放的资源:
6、事务及日志
事务的四大特性(ACID)
- 原子性(Atomicity): 事务开始后所有操作,要么全部成功,要么全部失败,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
- 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
- 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
事务的并发问题:
- 脏读: 在一个事务处理过程里读取了另一个未提交的事务中的数据。
- 不可重复读: 是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读: 第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入或删除一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
事务的四种隔离级别:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
读已提交 (read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 是 |
- MySQL默认的是 可重复读(repeatable-read),而其它的数据库默认的是 读已提交 (read-committed)
事务的七大传播机制:
类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
- 事务的隔离性由 锁机制 实现。
- 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG与UNDO LOG:
-
REDO LOG:重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持
久性。是存储引擎层(innodb)生成的日志,记录的是"物理级别"上的页修改操作,比如页号xx、偏移量yyy写入了’zzz’数据。主要为了保证数据的可靠性。redo log是存储引擎层产生的 -
UNDO LOG:回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。是存储引擎层(innodb)生成的日志,记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚(undo log记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚行记录到某种特定的版本—MVCC,即多版本并发控制)。bin log是数据库层产生的
REDO LOG 的作用:
- InnoDB 引擎是以页为单位来管理存储空间的、在我们访问之前,需要把磁盘中的数据页缓存到内存中的 Buffer Pool 之后,才可以访问。所有的数据变更,都是先更新在 Buffer Pool 中的数据,然后在以一定频率刷到磁盘上,这里的一定的频率使用的chekpoint机制。但是这里如果在刷入磁盘之前发生宕机了,数据就会丢失,所以为了解决这个问题,引入了REDO日志,在我们更新Buffer Pool之前,先将要修改过程记录到REDO日志中,在刷磁盘。这里的REDO日志写成功了,才算事务的提交成功。
- Redo Log 可简单分为俩个部分,重做日志的缓冲 (redo log buffer)保存在内存中,是易失的。重做日志文件 (redo log file) :保存在硬盘中,是持久的。
- redo的整体流程
- redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以 一 定的频率 刷入到真正的redo log file 中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。
- 注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存 (page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。
- InnoDB给出了支持三种策略的参数:innodb_flush_log_at_trx_commit
- 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
- 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
- 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。
Undo日志的作用:
- edo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log
- MySQL把这些为了回滚而记录的这些内容称之为撤销日志或者回滚日志(即undo log)。注意,由于查询操作( SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。
- undo log 会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护
- 回滚数据
- MVCC
6、MVCC
什么事MVCC?
- 多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
快照读与当前读:
-
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读 。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
-
快照读:快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:
SELECT * FROM player WHERE ...
- 之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
-
当前读:当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁 SELECT * FROM student FOR UPDATE; # 排他锁 INSERT INTO student values ... # 排他锁 DELETE FROM student WHERE ... # 排他锁 UPDATE student SET ... # 排他锁
隐藏字段、Undo Log版本链:
- undo日志的版本链,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
- trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给trx_id 隐藏列。
- roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
- 每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个 roll_pointer 属性( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志都连起来,串成一个链表:
- 对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当前记录最新的值。
- 每个版本中还包含生成该版本时对应的 事务id 。
MVCC实现原理之ReadView:
-
MVCC 的实现依赖于:隐藏字段、Undo Log、Read View。
-
在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到ReadView了,它帮我们解决了行的可见性问题。ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(“活跃"指的就是,启动了但还没提交)。
-
使用 READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。使用 SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
-
这个ReadView中主要包含4个比较重要的内容,分别如下:
- creator_trx_id ,创建这个 Read View 的事务 ID。
- trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
- up_limit_id ,活跃的事务中最小的事务 ID。
- low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
-
举例:trx_ids为trx2、trx3、trx5和trx8的集合,系统的最大事务ID(low_limit_id)为trx8+1(如果之前没有其他的新增事务),活跃的最小事务ID (up_limit_id)为trx2。
ReadView的规则:
- 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
MVCC整体操作流程:
- 了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
- 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
- InnoDB中,MVCC是通过Undo Log + Read View进行数据读取,Undo Log保存了历史快照,而Read Vview规则帮我们判断当前版本的数据是否可见。
READ COMMITTED隔离级别下:
-
每次读取数据前都生成一个ReadView。
-
比如,系统里有两个 事务id 分别为 10 、 20 的事务在执行:
# Transaction 10 BEGIN; UPDATE student SET name="李四" WHERE id=1; UPDATE student SET name="王五" WHERE id=1; # Transaction 20 BEGIN; # 更新了一些别的表的记录 ...
-
此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:
-
假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:
# 使用REPEATABLE READ隔离级别的事务 BEGIN; # SELECT1:Transaction 10、20未提交 SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
-
这个SELECT1的执行过程如下:
- 步骤1:在执行SELECT语句时会先生成一个ReadView,ReadView的trx_ids列表的内容就是[10,20],up_limit_id为10,low_limit_id为21, creator_trx_id为0。
- 步骤2:然后从版本链中挑选可见的记录,从图中看出,最新版本的列name的内容是’王五’,该版本的trx_id值为10,在trx_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
- 步骤3:下一个版本的列name的内容是’李四’,该版本的trx_id值也为10,也在trx_ids列表内,所以也不符合要求,继续跳到下一个版本。
- 步骤4∶下一个版本的列name的内容是’张三’,该版本的trx_id值为8,小于ReadView中的up_limit_id值10,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为‘张三’的记录。
-
之后,我们把 事务id 为 10 的事务提交一下,就像这样:
# Transaction 10 BEGIN; UPDATE student SET name="李四" WHERE id=1; UPDATE student SET name="王五" WHERE id=1; COMMIT;
-
然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:
# Transaction 20 BEGIN; # 更新了一些别的表的记录 ... UPDATE student SET name="钱七" WHERE id=1; UPDATE student SET name="宋八" WHERE id=1;
-
此刻,表student 中 id 为 1 的记录的版本链长这样:
-
然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:
# 使用REPEATABLE READ隔离级别的事务 BEGIN; # SELECT1:Transaction 10、20均未提交 SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三' # SELECT2:Transaction 10提交,Transaction 20未提交 SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为'张三'
-
SELECT2的执行过程如下:
- 步骤1:因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的trx_ids列表的内容就是[10,20],up_limit_id为10, low_limit_id为21, creator_trx_id为0。
- 步骤2:然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是‘宋八’,该版本的trx_id值为20,在trx_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
- 步骤3:下一个版本的列name的内容是’钱七’,该版本的trx_id值为20,也在trx_ids列表内,所以也不符合要求,继续跳到下一个版本。
- 步骤4∶下一个版本的列name的内容是’王五’,该版本的trx_id值为10,而trx_ids列表中是包含值为10的事务id的,所以该版本也不符合要求,同理下一个列name的内容是’李四’的版本也不符合要求。继续跳到下一个版本。
- 步骤5∶下一个版本的列name的内容是’张三’,该版本的trx_id值为80,小于ReadView中的up_limit_id值10,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为‘张三’的记录。
如何解决幻读:
-
假设现在表 student 中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的 undo log 如下图所示。
-
假设现在有事务 A 和事务 B 并发执行, 事务 A 的事务 id 为 20 , 事务 B 的事务 id 为 30 。
- 步骤1:事务 A 开始第一次查询数据,查询的 SQL 语句如下。
select * from student where id >= 1;
- 在开始查询之前,MySQL 会为事务 A 产生一个 ReadView,此时 ReadView 的内容如下: trx_ids= [20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。
- 由于此时表 student 中只有一条数据,且符合 where id>=1 条件,因此会查询出来。然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务 A 的 ReadView 里 up_limit_id,这表示这条数据是事务 A 开启之前,其他事务就已经提交了的数据,因此事务 A 可以读取到。
- 结论:事务 A 的第一次查询,能读取到一条数据,id=1。
- 步骤2:接着事务 B(trx_id=30),往表 student 中新插入两条数据,并提交事务。
insert into student(id,name) values(2,'李四'); insert into student(id,name) values(3,'王五');
- 此时表student 中就有三条数据了,对应的 undo 如下图所示:
- 步骤3:接着事务 A 开启第二次查询,根据可重复读隔离级别的规则,此时事务 A 并不会再重新生成ReadView。此时表 student 中的 3 条数据都满足 where id>=1 的条件,因此会先查出来。然后根据ReadView 机制,判断每条数据是不是都可以被事务 A 看到。
- 1)首先 id=1 的这条数据,前面已经说过了,可以被事务 A 看到。
- 2)然后是 id=2 的数据,它的 trx_id=30,此时事务 A 发现,这个值处于 up_limit_id 和 low_limit_id 之间,因此还需要再判断 30 是否处于 trx_ids 数组内。由于事务 A 的 trx_ids=[20,30],因此在数组内,这表示 id=2 的这条数据是与事务 A 在同一时刻启动的其他事务提交的,所以这条数据不能让事务 A 看到。
- 3)同理,id=3 的这条数据,trx_id 也为 30,因此也不能被事务 A 看见。
- 结论:最终事务 A 的第二次查询,只能查询出 id=1 的这条数据。这和事务 A 的第一次查询的结果是一样的,因此没有出现幻读现象,所以说在 MySQL 的可重复读隔离级别下,不存在幻读问题。
- 步骤1:事务 A 开始第一次查询数据,查询的 SQL 语句如下。