查看MySQL提供的所有存储引擎
mysql> show engines;
从上图我们可以查看出 MySQL 当前默认的存储引擎是 InnoDB,并且在 5.7 版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
查看MySQL当前默认的存储引擎
show table status like "table_name";
1.是否支持行级锁
MyISAM只有表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁
2.是否支持事物
MyISAM不支持事物
InnoDB提供事物支持,具有提交(commit)和回滚(rollback)事物的能力
3.是否支持外键
MyISAM不支持,InnoDB支持
补充:一般不建议使用外键,应用层面可以解决
4.是否支持数据库异常崩溃后的安全恢复
MyISAM不支持,InnoDB支持
使用InnoDB的数据库再异常崩溃后,数据库重新启动会保证数据恢复到崩溃前的状态,依赖于redo log
补充:
REPEATABLE-READ
可重复读)5.是否支持MVCC
MyISAM不支持,InnoDB支持
MyISAM和InnoDB存储引擎使用的锁
表级锁与行级锁对比
InnoDB存储的引擎的锁的算法
首先MySQL是典型的C/S架构,即Client/Server架构,服务器端程序使用的mysqld。
不论客户端进程和服务器进程是采用哪种方式进行通信,最后实现的效果都是:客户端进程向服务器进程发送一段文本(sql语句),服务器进程处理后再向客户端进程发送一段文本(处理结果)。
那服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?这里以查询请求为例展示:
下面具体展开一下:
mysql在存储引擎的架构上采用 插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取相分离 。这种架构可以根据业务的需求和实际需求选择核实的存储引擎
逻辑架构分层
Connectors,指的是不同语言中与SQL的交互。MySQL首先是一个网络程序,在TCP之上定义了自己的应用层协议。所以要使用MySQL,我们可以编写代码,跟MySQL Server’建立TCP连接’,之后按照其定义好的协议进行交互。或者比较方便的办法是调用SDK,比如Native c API、JDBC、PHP等各种语言MySQL Connector,或者通过ODBC。但通过SDK来访问MySQL,本质上还是在TCP连接上通过MySQL协议跟MySQL进行交互。
最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
系统(客户端)访问 MySQL 服务器前,做的第一件事就是建立 TCP 连接。
经过三次握手建立连接成功后, MySQL 服务器对 TCP 传输过来的账号密码做身份认证、权限获取。
用户名或密码不对,会收到一个Access denied for user错误,客户端程序结束执行
用户名密码认证通过,会从权限表查出账号拥有的权限与连接关联,之后的权限判断逻辑,都将依赖于此时读到的权限
TCP 连接收到请求后,必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池,去走后面的流程。每一个连接从线程池中获取线程,省去了创建和销毁线程的开销。
思考:一个系统只会和MySQL服务器建立一个连接吗只能有一个系统和MySQL服务器建立连接吗?
当然不是,多个系统都可以和MySQL服务器建立连接,每个系统建立的连接肯定不止一个。所以,为了解决TCP无限创建与TCP频繁创建销毁带来的资源耗尽、性能下降问题。MySQL服务器里有专门的TCP连接池限制连接数,采用长连接模式复用TCP连接,来解决上述问题。
TCP连接收到请求后,必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池,去走后面的流程。每个连接从线程池中获取线程,省去了创建和销毁线程的开销。
这些内容都归纳到MySQL的连接管理组件中。
所以连接管理的职责是负责认证、管理连接、获取权限信息。
第二层架构主要完成大多少的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序是否利用索引等,最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。
SQL Interface: SQL接口
Parser: 解析器
Optimizer:查询优化器
SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个执行计划 。
这个执行计划表明应该 使用哪些索引 进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
它使用“ 选取-投影-连接 ”策略进行查询。例如:
SELECT id,name FROM student WHERE gender = '女'
这个SELECT查询先根据WHERE语句进行 选取 ,而不是将表全部查询出来以后再进行gender过滤。 这个SELECT查询先根据id和name进行属性 投影 ,而不是将属性全部取出以后再进行过滤,将这两个查询条件 连接 起来生成最终查询结果。
Caches & Buffers: 查询缓存组件
存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。
和其它数据库相比,MysQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用,主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。同时开源的MySQL还允许开发人员设置自己的存储引擎。
这种高效的模块化架构为那些希望专门针对特定应用程序需求(例如数据仓库、事务处理或高可用性情况)的人提供了巨大的好处,同时享受使用一组独立于任何接口和服务的优势存储引擎。
插件式存储引擎层( Storage Engines),真正的负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。
所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在文件系统上,以文件的方式存在的,并完成与存储引擎的交互。当然有些存储引擎比如InnoDB,也支持不使用文件系统直接管理裸设备,但现代文件系统的实现使得这样做没有必要了。在文件系统之下,可以使用本地磁盘,可以使用DAS、NAS、SAN等各种存储系统
简化为三层结构:
连接层:客户端和服务器端建立连接,客户端发送 SQL 至服务器端;
SQL 层(服务层):对 SQL 语句进行查询处理;与数据库文件的存储方式无关;
存储引擎层:与数据库文件打交道,负责数据的存储和读取。
MySQL的查询流程:
1.查询缓存:Server如果在查询缓存中发现了这条SQL语句,就会直接将结果返回客户端;如果没有,就进入解析器阶段,需要说明的是,因为查询缓存的效率不高,所以在MySQL8.0之后就抛弃了缓存
MysQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句(必须要一模一样)。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key 是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。所以,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
大多数情况下查询缓存就是鸡肋,为什么?
查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的概率会大大降低,只有相同的查询操作才会命中查询缓存。两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中。因此MySQL的查询缓存命中率不高。
同时,如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如mysql 、information_schema、performance_schema数据库中的表,那这个请求就不会被缓存。以某些系统函数举例,可能同样的函数的两次调用会产生不一样的结果,比如函数NOW,每次调用都会产生最新的当前时间,如果在一个查询请求中调用了这个函数,那即使查询请求的文本信息都一样,那不同时间的两次查询也应该得到不同的结果,如果在第一次查询时就缓存了,那第二次查询的时候直接使用第一次查询的结果就是错误的!
此外,既然是缓存,那就有它缓存失效的时候。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使用了INSERT、UPDATE、DELETE、TRUNCATE TABLE、ALTER TABLE、DROP TABLE或 DROP DATABASE语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!对于更新压力大的数据库来说,查询缓存的命中率会非常低。
总之,因为查询缓存往往弊大于利,查询缓存的失效非常频繁
一般建议大家在静态表里使用查询缓存,什么叫静态表呢?就是一般我们极少更新的表。比如,一个系统配置表、字典表,这张表上的查询才适合使用查询缓存。好在MysQL也提供了这种°按需使用”的方式。你可以将my.cnf参数query_cache_type设置成DEMAND,代表当sql语句中有sQL_CACHE关键词时才缓存。比如:
#query_cache_type有3个值 0代表关闭查询缓存OFF、1代表开启ON、2(DEMAND)
query_cache_type=2
这样对于默认的SQL语句都不使用查询缓存。而对于确定的需要使用查询缓存的语句,可以显示启动。如:
select SQL_CACHE * from test where ID=5;
监控查询缓存的命中率
show status like'%Qcache%';
如果没有命中查询缓存,就要开始真正执行语句了。首先,MysQL需要知道你要做什么,因此需要对sQL语句做解析。SQL语句的分析分为词法分析与语法分析。
分析器先做“词法分析”。你输入的是由多个字符串和空格组成的一条sQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么。
MySQL从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T"识别成“表名T”,把字符串“ID”识别成“列ID”。
接着,要做“语法分析”。根据词法分析的结果,语法分析器(比如: Bison)会根据语法规则,判断你输入的这个sQL语句是否满足MySQL语法。
如果你的语句不对,就会收到"You have an error in your SQL syntax”的错误提醒,比如下面这个语句from写成了"rom"。
如果SQL语句正确,会生成一个语法树:
至此,我们解析器的工作任务基本完成。接下来进入优化器。
3.优化器:在优化器中会确定SQL语句的执行路径,比如是根据全表检索,还是根据索引检索等。
经过了优化器,MySQL就知道你要做什么了,在开始执行之前,还要经过优化器的处理。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有很多表关联(join)的时候,决定各个表的连接顺序,还有表达式简化、子查询转为连接、外连接转为内连接等。
举例:如下语句是执行连个表的join:
select * from test1 join test2 using(ID) where test1.name='zhangwei' and test2.name='mysql高级课程';
在查询优化器中,可以分为逻辑查询优化和物理查询优化阶段
逻辑查询优化就是通过改变SQL语句的内容来使得SQL查询更高效,同时为物理查询优化提供更多的候选执行计划。通常采用的方式是对SQL语句进行等价变换,对查询进行重写,而查询重写的数学基础就是关系代数。对条件表达式进行等价谓词重写、条件简化,对视图进行重写,对子查询进行优化,对连接语义进行了外连接消除、嵌套连接消除等。
物理查询优化是基于关系代数进行的查询重写,而关系代数的每一步都对应着物理计算,这些物理计算往往存在多种算法,因此需要计算各种物理路径的代价,从中选择代价最小的作为执行计划。在这个阶段里对于单表和多表连接的操作,需要高效地使用索引,提升查询效率。
4.执行器:
截止到现在,还没有去读写真实的表,仅仅只是产出了一个执行计划,就进入了执行器阶段
在执行之前需要判断该用户是否具有权限。如果没有,会返回权限错误,如果有,就执行SQL查询并返回结果。在MySQL8.0以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。
select * from test where id=1;
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,调用存储引擎API对表进行的读写。存储引擎API只是抽象接口,下面还有个存储引擎层,具体实现还是要看表选择的存储引擎。
比如表test中,ID字段没有索引,那么执行器的执行流程是:
调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是1,如果不是则跳过,如果是则将这行存在结果集中; 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句执行完成。对于有索引的表,逻辑也差不多。
SQL语句在MySQL中的流程是:
前面的结构图很复杂,我们需要抓取最核心的部分:sQL的执行原理。不同的DBMS的SQL的执行原理是相通的,只是在不同的软件中,各有各的实现路径。
既然一条SQL语句会经历不同的模块,那我们就来看下,在不同的模块中,SQL执行所使用的资源(时间)是怎样的。如何在MySQL中对一条sQL语句的执行时间进行分析。
了解查询语句底层执行的过程:select @@profiling;或者show variables like ’%profiling%‘
查看是否开启计划。开其他可以让MySQL收集在SQL执行时所使用的资源情况,命令如下:
mysql> select @@profiling;
mysql> show variables like 'profiling';
profiling=0代表关闭,需要把profiling打开,即设置为1;
mysql> set profiling=1;
profiling功能由MySQL会话变量:profiling控制,默认是OFF(关闭状态)。
执行一个SQL查询:
mysql> select * from employees;
查看当前会话所产生的所有profiles:
mysql> show profiles; # 显示最近的几次查询
4.查看profile
显示执行计划,查看程序的执行步骤:
mysql> show profile;
当然你也可以查询指定的 Query ID,比如:
mysql> show profile for query 7;
查询 SQL 的执行时间结果和上面是一样的。
此外,还可以查询更丰富的内容:
mysql> show profile cpu,block io for query 6;
继续:
mysql> show profile cpu,block io for query 7;
1.除了查看CPU、io阻塞等参数情况,还可以查看下列参数的利用情况。
2.发现两次查询当前情况都一致,说明没有缓存一说。
在8.0版本之后,MySQL不再支持缓存的查询。一旦有数据表更新,缓存都将清空,因此只有数据表是静态时,或者数据表很少发生变化时,使用缓存查询才有价值,否则如果数据表经常更新,反而增加了查询时间
上述操作在MySQL5.7中测试,发现前后两次相同的sql语句,执行的查询过程仍然是相同的。不是会使用 缓存吗?这里我们需要 显式开启查询缓存模式 。在MySQL5.7中如下设置:
在etc/my/cnf中新增一行:
query_cache_type=1
systemctl restart mysqld
由于重启过服务,需要重新执行如下指令,开启profiling
mysql> set profiling=1;
mysql> select * from locations;
mysql> select * from locations;
显示执行计划,查看程序的执行步骤:
mysql> show profile for query 1;
mysql> show profile for query 2;
结论不言而喻。执行编号2时,比执行编号1时少了很多信息,从截图中可以看出查询语句直接从缓存中获取数据。
注意1:SQL必须是一致的,否则,不能命中缓存。
例如:
#虽然查询结果一致,但并没有命中缓存
select * from mydb.mytbl where id=2;
select * from mydb,mydbl where id>1 and id<3;
注意2:
分别在MySQL5.7和 MySQL8.0执行以下命令:
mysql> show variables like '%query_cache%';
随着Mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同而动态调整执行顺序。
需求:查询每个部门年龄高于20岁的人数且高于20岁人数不能少于2人,显示人数最多的第一名部门信息
#7 select
#1 from
#3 join
#2 on
#4 where
#5 group by
#6 having
#8 distinct
#9 order by
#10 limit
InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。而磁盘 I/O 需要消耗的时间很多,而在内存中进行操作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS 会申请 占用内存来作为数据缓冲池 ,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访 问。
这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 I/O 的时间 。要知道,这种策略对提升 SQL 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。
缓冲池和查询缓存是一个东西吗?不是。
1. 缓冲池(Buffer Pool)
首先我们需要了解在 InnoDB 存储引擎中,缓冲池都包括了哪些。
在 InnoDB 存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种数据的缓存,如下图所示:
从图中,你能看到 InnoDB 缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应 Hash 和数据字典信息等。
缓冲池的重要性:
对于使用InnoDB作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。但是各位也都知道,磁盘的速度慢的跟乌龟一样,怎么能配得上“快如风,疾如电"的CPU呢?这里,缓冲池可以帮助我们消除cPU和磁盘之间的鸿沟。所以InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。
缓存原则:
“ 位置 * 频次 ”这个原则,可以帮我们对 I/O 访问效率进行优化。
首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。
其次,频次决定优先级顺序。因为缓冲池的大小是有限的,比如磁盘有 200G,但是内存只有 16G,缓冲池大小只有 1G,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会 优先对使用频次高的热数据进行加载 。
缓冲池的预读特性:
了解了缓冲池的作用之后,我们还需要了解缓冲池的另一个特性:预读。
缓冲池的作用就是提升I/O效率,而我们进行读取数据的时候存在一个“局部性原理”,也就是说我们使用了一些数据,大概率还会使用它周围的一些数据,因此采用“预读”的机制提前加载,可以减少未来可能的磁盘Ⅳ/O操作。
2.查询缓存
那么什么是查询缓存呢?
查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。因为命中条件苛刻,而且只要数据表发生变化,查询缓存就会失效,因此命中率低。
缓冲池服务于数据库整体的I/o操作,它们的共同点都是通过缓存的机制来提升效率。
缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在乐冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。
如果我们执行SQL语句的时候更新了缓冲池中的数据,那么这些数据会马上 同步到磁盘上吗
实际上,当我们对数据库中的记录进行修改的时候,首先会修改缓冲池中页里面的记录信息,然后数据库会以一定的频率刷新到磁盘上。注意并不是每次发生更新操作,都会立刻进行磁盘回写。缓冲池会采用一种叫做checkpoint的机制将数据回写到磁盘上,I这样做的好处就是提升了数据库的整体性能。
比如,当缓冲池不够用时,需要释放掉一些不常用的页,此时就可以强行采用checkpoint 的方式,将不常用的脏页回写到磁盘上,然后再从缓冲池中将这些页释放掉。这里脏页(dirty page)指的是缓冲池中被修改过的页,与磁盘上的数据页不一致。
如果使用的是MySQL MyISAM存储引擎,它缓存索引,不缓存数据,对应的键缓存参数为key_buffer_size,可以用它进行查看。
如果使用的是MySQL InnoDB存储引擎,可以通过查看innodb_buffer)pool_size变量来查看缓冲池大小。命令为:
show variables like 'innodb_buffer_poll_size';
能看到此时InnoDB的缓冲池大小只有134217728/1024=128MB。可以修改缓冲池大小,比如改为256MB,方法如下:
set global innodb_buffer_pool_size = 268435456;
或者:
[server]
innodb_buffer_pool_size = 268435456
Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的数据都需要加锁处理。在Buffer Pool特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool可能会影响请求的处理速度。所以在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表。所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。
我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数,比方说这样:
[server]
innodb_buffer_pool_instances = 2
这样就表明要创建两个Buffer Pool实例
查看缓冲池的个数,命令:
show variables like 'innodb_buffer_pool_instacnces';
那每个Buffer Pool实例实际占多少内存空间?使用这个公式算出来:
也就是总共的大小除以实例的个数,结果就是每个Buffer Pool实例占用的大小。
不过也不是说Buffer Pool实例创建的越多越好,分别管理各个Buffer Pool也是需要性能开销的,InnoDB规定:当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances的值修改为1。而我们鼓励在Buffer Pool大于或等于1G的时候设置多个Buffer Pool实例。
Buffer Pool是MySQL内存结构中的核心的一个组成,可以想象成一个黑盒子。
黑盒子下的更新数据流程
当我们查询数据的时候,会先去Buffer Pool中查询。如果Buffer Pool中不存在,存储引擎会先将数据从磁盘加载到Buffer Pool中,然后将数据返回给客户端;同理,当我们更新某个数据的时候,如果这个数据不存在于BufferPool,同样会先数据加载进来,然后修改修改内存的数据。被修改过的数据会在之后统一刷入磁盘。
这个过程看似没啥问题,实则是有问题的。假设我们修改Buffer Pool中的数据成功,但是还没来得及将数据刷入磁盘MySQL就挂了怎么办?按照上图的逻辑,此时更新之后的数据只存在于Buffer Pool中,如果此时MysQL宕机了,这部分数据将会永久地丢失;
再者,我更新到一半突然发生错误了,想要回滚到更新之前的版本,该怎么办?连数据持久化的保证、事务回滚都做不到还谈什么崩溃恢复?(redo log & undo log)
为了管理方便,人们把连接管理、查询缓存、语法解析、查询优化这些并不涉及真实数据存储的功能划分为MySQL server的功能,把真实存取数据的功能划分为存储引擎的功能。所以在MySQL server完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的API,获取到数据后返回给客户端就好了。
MysQL中提到了存储引擎的概念。简而言之,存储引擎就是指表的类型。其实存储引擎以前叫做表处理器,后来改名为存储引擎,它的功能就是接收上层传下来的指令,然后对表中的数据进行提取或写入操作。
show engines #查询mysql支持的存储引擎
show engines \G;
显示如下:
*************************** 1. row *************************** Engine: InnoDB Support: DEFAULT Comment: Supports transactions, row-level locking, and foreign keys Transactions: YES XA: YES Savepoints: YES *************************** 2. row *************************** Engine: MRG_MYISAM Support: YES Comment: Collection of identical MyISAM tables Transactions: NO XA: NO Savepoints: NO *************************** 3. row *************************** Engine: MEMORY Support: YES Comment: Hash based, stored in memory, useful for temporary tables Transactions: NO XA: NO Savepoints: NO *************************** 4. row *************************** Engine: BLACKHOLE Support: YES Comment: /dev/null storage engine (anything you write to it disappears) Transactions: NO XA: NO
2. 设置系统默认的存储引擎
查看默认的存储引擎:
Savepoints: NO *************************** 5. row *************************** Engine: MyISAM Support: YES Comment: MyISAM storage engine Transactions: NO XA: NO Savepoints: NO *************************** 6. row *************************** Engine: CSV Support: YES Comment: CSV storage engine Transactions: NO XA: NO Savepoints: NO *************************** 7. row *************************** Engine: ARCHIVE Support: YES Comment: Archive storage engine Transactions: NO XA: NO Savepoints: NO *************************** 8. row *************************** Engine: PERFORMANCE_SCHEMA Support: YES Comment: Performance Schema Transactions: NO XA: NO Savepoints: NO *************************** 9. row *************************** Engine: FEDERATED Support: NO Comment: Federated MySQL storage engine Transactions: NULL XA: NULL Savepoints: NULL
如果在创建表的语句中没有显式指定表的存储引擎的话,那就会默认使用 InnoDB 作为表的存储引擎。
如果我们想改变表的默认存储引擎的话,可以这样写启动服务器的命令行:
SET DEFAULT_STORAGE_ENGINE=MyISAM;
或者修改my.cnf文件:
default-storage-engine=MyISAM
# 重启服务
systemctl restart mysqld.service
存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为 不同的表设置不同的存储引擎 ,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。
我们之前创建表的语句都没有指定表的存储引擎,那就会使用默认的存储引擎 InnoDB 。如果我们想显式的指定一下表的存储引擎,那可以这么写:
CREATE TABLE 表名(
建表语句;
) ENGINE = 存储引擎名称;
如果表已经建好了,我们也可以使用下边这个语句来修改表的存储引擎:
ALTER TABLE 表名 ENGINE = 存储引擎名称;
比如我们修改一下 engine_demo_table 表的存储引擎:
mysql> ALTER TABLE engine_demo_table ENGINE = InnoDB;
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
这时我们再查看一下 engine_demo_table 的表结构:
mysql> SHOW CREATE TABLE engine_demo_table\G *************************** 1. row ***************************
Table: engine_demo_table
Create Table: CREATE TABLE `engine_demo_table` ( `i` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)
MySQL从3.23.34a开始就包含InnoDB存储引擎。 大于等于5.5之后,默认采用InnoDB引擎 。
InnoDB是MySQL的 默认事务型引擎 ,它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。
除了增加和查询外,还需要更新、删除操作,那么,应优先选择InnoDB存储引擎。
除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。
数据文件结构:(在《第02章_MySQL数据目录》章节已讲)
表名.frm 存储表结构(MySQL8.0时,合并在表名.ibd中)
表名.ibd 存储数据和索引
InnoDB是 为处理巨大数据量的最大性能设计 。
对比MyISAM的存储引擎, InnoDB写的处理效率差一些 ,并且会占用更多的磁盘空间以保存数据和 索引。
MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较高 ,而且内存大小对性能有决定性的影响。
MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM 不支持事务、行级锁、外键 ,有一个毫无疑问的缺陷就是 崩溃后无法安全恢复 。
5.5之前默认的存储引擎
优势是访问的 速度快 ,对事务完整性没有要求或者以SELECT、INSERT为主的应用
针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高
数据文件结构:(在《第02章_MySQL数据目录》章节已讲)
应用场景:只读应用或者以读为主的业务
MySQL5.5之前默认是MyIAM,之后是InnoDB
首先对于InnoDB存储引擎,提供了良好的事务管理、崩溃恢复和并发控制。因为InnoDb存储引擎支持事务,所以对于要求事务完整性的场合需要选择InnoDB,比如数据操作除了插入和查询外还包含很多更新、删除操作,像财务系统等对数据准确性要求较高的系统。缺点是读写效率差,占用内存空间相对比较大。
其次对于MyISAM存储引擎,如果是小型应用,系统以读操作和插入操作为主,只有很少的更新、删除操作,并且对事务的要求没有那么高,则可以选择这个存储引擎。MyISAM存储引擎的优势在于占用空间小,处理速度快;缺点是不支持事务的完整性和并发性。
InnoDB存储引擎在实际应用中拥有诸多优势,比如操作便利、提高了数据库的性能、维护成本低等。如果由于硬件或软件的原因导致服务器崩溃,那么在重启服务器之后不需要进行额外的操作。InnoDB崩溃恢复功能自动将之前提交的内容定型,然后撤销没有提交的进程,重启之后继续从崩溃点开始执行。 InnoDB存储引擎在主内存中维护缓冲池,高频率使用的数据将在内存中直接被处理。这种缓存方式应用于多种信息,加速了处理进程。
在专用服务器上,物理内存中高达80%的部分被应用于缓冲池。如果需要将数据插入不同的表中,可以设置外键加强数据的完整性。更新或者删除数据,关联数据将会被自动更新或删除。如果试图将数据插入从表,但在主表中没有对应的数据,插入的数据将被自动移除。如果磁盘或内存中的数据出现崩溃,在使用脏数据之前,校验和机制会发出警告。当每个表的主键都设置合理时,与这些列有关的操作会被自动优化。插入、更新和删除操作通过做改变缓冲自动机制进行优化。 InnoDB不仅支持当前读写,也会 缓冲改变的数据到数据流磁盘 。
InnoDB的性能优势不只存在于长时运行查询的大型表。在同一列多次被查询时,自适应哈希索引会提高查询的速度。使用InnoDB可以压缩表和相关的索引,可以 在不影响性能和可用性的情况下创建或删除索引 。对于大型文本和BLOB数据,使用动态行形式,这种存储布局更高效。通过查询INFORMATION_SCHEMA库中的表可以监控存储引擎的内部工作。在同一个语句中,InnoDB表可以与其他存储引擎表混用。即使有些操作系统限制文件大小为2GB,InnoDB仍然可以处理。 当处理大数据量时,InnoDB兼顾CPU,以达到最大性能 。
1.缓冲池缓冲池是主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的数据能够直接在内存中获得,从而提高速度。
2.更改缓存更改缓存是一个特殊的数据结构,当受影响的索引页不在缓存中时,更改缓存会缓存辅助索引页的更改。索引页被其他读取操作时会加载到缓存池,缓存的更改内容就会被合并。不同于集群索 引,辅助索引并非独一无二的。当系统大部分闲置时,清除操作会定期运行,将更新的索引页刷入磁 盘。更新缓存合并期间,可能会大大降低查询的性能。在内存中,更新缓存占用一部分InnoDB缓冲池。在磁盘中,更新缓存是系统表空间的一部分。更新缓存的数据类型由innodb_change_buffering配置项管理。
3.自适应哈希索引
自适应哈希索引将负载和足够的内存结合起来,使得InnoDB像内存数据库一样运行,不需要降低事务上的性能或可靠性。这个特性通过innodb_adaptive_hash_index选项配置,或者通过–skip-innodb_adaptive_hash_index命令行在服务启动时关闭。
4.重做日志缓存重做日志缓存存放要放入重做日志的数据。重做日志缓存大小通过innodb_log_buffer_size配置项配置。重做日志缓存会定期地将日志文件刷入磁盘。大型的重做日志缓存 使得大型事务能够正常运行而不需要写入磁盘。
5.系统表空间系统表空间包括InnoDB数据字典、双写缓存、更新缓存和撤销日志,同时也包括表和索引数据。多表共享,系统表空间被视为共享表空间。
6.双写缓存双写缓存位于系统表空间中,用于写入从缓存池刷新的数据页。只有在刷新并写入双写缓存后,InnoDB才会将数据页写入合适的位置。
7.撤销日志撤销日志是一系列与事务相关的撤销记录的集合,包含如何撤销事务最近的更改。如果其他事务要查询原始数据,可以从撤销日志记录中追溯未更改的数据。撤销日志存在于撤销日志片段中,这些片段包含于回滚片段中。
8.每个表一个文件的表空间每个表一个文件的表空间是指每个单独的表空间创建在自身的数据文件中,
而不是系统表空间中。这个功能通过innodb_file_per_table配置项开启。每个表空间由一个单独的.ibd数据文件代表,该文件默认被创建在数据库目录中。
9.通用表空间使用CREATE TABLESPACE语法创建共享的InnoDB表空间。通用表空间可以创建在MySQL数据目录之外能够管理多个表并支持所有行格式的表。
10.撤销表空间撤销表空间由一个或多个包含撤销日志的文件组成。撤销表空间的数量由 innodb_undo_tablespaces配置项配置。
11.临时表空间 用户创建的临时表空间和基于磁盘的内部临时表都创建于临时表空间。innodb_temp_data_file_path配置项定义了相关的路径、名称、大小和属性。如果该值为空,默认会在innodb_data_home_dir变量指定的目录下创建一个自动扩展的数据文件。
12.重做日志重做日志是基于磁盘的数据结构,在崩溃恢复期间使用,用来纠正数据。正常操作期间,重做日志会将请求数据进行编码,这些请求会改变InnoDB表数据。遇到意外崩溃后,未完成的更改会自动在初始化期间重新进行。
由于有些mysql不能使用full join,不过可以换种方法表示
A 的独有 + AB 共有 + B的独有
union本身就可以去重
所以可以这样使用
select * from tbl_emp a left join tbl_dept b on a.deptId = b.id
union
select * from tbl_emp a right join tbl_dept b on a.deptId = b.id;
上图中第7个的实现可以通过如下:
也就是A的独有+ B的独有
之后通过union进行合并
select * from tbl_emp a left join tbl_dept b on a.deptId = b.id where b.id is null
union
select * from tbl_emp a right join tbl_dept b on a.deptId = b.id where a.deptId is null;
索引是存储引擎用于快速找到数据记录的一种数据结构,就好比一本教课书的目录部分,通过目录中找到对应文章的页码,便可快速定位到需要的文章。MySQL中也是一样的道理,进行数据查找时,首先查看查询条件是否命中某条索引,符合则通过索引查找相关数据,如果不符合则需要全表扫描,即需要一条一条地查找记录,直到找到与条件符合的记录。
如上图所示,数据库没有索引的情况下,数据分布在硬盘不同的位置上面,读取数据时,摆臂需要前后摆动查找数据,这样操作非常消耗时间。如果数据顺序摆放,那么也需要从1到6行按顺序读取,这样就相当于进行了6次Io操作,依旧非常耗时。如果我们不借助任何索引结构帮助我们快速定位数据的话,我们查找Col 2= 89这条记录,就要逐行去查找、去比较。从Col2=34开始,进行比较,发现不是,继续下一行。我们当前的表只有不到10行数据,但如果表很大的话,有上千万条数据,就意味着要做很多很多次磁盘I/0才能找到。现在要查找col2=89这条记录。CPU必须先去磁盘查找这条记录,找到之后加载到内存,再对数据进行处理。这个过程最耗时间的就是磁盘l/o(涉及到磁盘的旋转时间((速度较快)、磁头的寻道时间(速度慢、费时))
假如给树使用二叉树这样的数据结构进行存储,如图:
对字段Col2添加了索引,就相当于在硬盘上为col 2维护了一个索引的数据结构,即这个二叉搜索树。二叉搜索树的每个结点存储的是**〈K,V)结构**,key是Col 2, valde是该key 所在行的文件指针(地址)。比如:该二叉搜索树的根节点就是:(34,0x07)。现在对Col 2添加了索引,这时再去查找Col2=89这条记录的时候会先去查找该二叉搜索树(二叉树的遍历查找)。读34到内存,89>34;继续右侧数据,读89到内存,89.=- 89;找到数据返回。找到之后就根据当前结点的value快速定位到要查找的记录对应的地址。我们可以发现,只需要查找两次就可以定位到记录的地址,查询速度就提高了。
这就是建立索引的目的:减少磁盘IO的次数,加快查询速率。
什么是索引
索引是排好序的快速查找的数据结构,是数据库维护的一个满足特定查找算法的数据结构,以某种方式指向数据,这种数据结构就是索引
重点:索引会影响到mysql查找(WHERE的查询条件)、排序(ORDER BY)两大功能
索引本身也很大,不可能全部存储在内存中,因此往往存储在以索引文件的形式存储在磁盘上
平时所说的索引,如果没有特别说明,都使指B树(多路搜索树,并不一定是二叉),结构组织的索引,其中聚集索引、次要索引、覆盖索引、复合索引、前缀索引、唯一索引默认使用B+树索引,统称索引,除了B+树这种数据结构的索引外,还有哈希索引。
索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同。同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。
索引的优势和劣势
优势:
劣势:
提示:
索引可以提高查询的速度,但是会影响插入记录的速度。这种情况下,最好的办法是先删除表中的索引,然后再插入数据,然后再创建索引
先来看一个精确匹配的例子:
SELECT [列名列表] FROM 表明 WHERE 列名 = xxx;
假设目前表中的记录比较少,所有的记录可以被存到一个页中,在查找记录时可以根据搜索条件的不同分为两种情况:
以主键为搜索条件
可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录可快速找到指定的记录
以其他列为搜索条件
因为在数据页中并没有对非主键列建立所谓的页目录,所以我们无法通过二分法快速定位相应的槽。
这种情况下只能从最小记录开始依次遍历单链表中的每条记录,然后对比每条记录是不是符合搜索条件。很显然,这种查找的效率是非常低的。
大部分情况下我们表中存放的记录都是非常多的,需要好多的数据页来存储这些记录。在很多页中查找记录的话可以分为两个步骤:
1.定位到记录所在的页
2.从所在的页内中查找相应的记录
在没有索引情况下,不论是根据主键列或其他列的值进行查找,由于并不能快速定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中根据我们上面的查找方式去查找记录。因为要遍历所有的数据页,所以这种方式显示超级耗时的,因此诞生了索引。
建一个表:
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
这个新建的index_demo表中有2个INT类型的。1个CHAR(1)类型的列,而且规定了c1列是主键,这个表使用Compact行格式来实际存储记录的。这里简化了index_demo表的行格式示意图:
我们只在示意图里展示记录的这几个部分:
将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果:
把一些记录放到页里的示意图:
我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以不得不依次遍历所有的数据页。所以如果我们 想快速的定位到需要查找的记录在哪些数据页 中该咋办?我们可以为快速定位记录所在的数据页而 建立一个目录 ,建这个目录必须完成下边这些事:
下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值
假设:每个数据页最多能存放3条记录(实际上一个数据页非常大,可以存放下好多记录)。有了这个假设之后我们向index_demo表插入3条记录:
那么这些记录已经按照逐渐知道大小串联成一个单项链表,如图:
从图中可以看出,index_demo表中的3条件记录都被插入到编号为10的数据项中,此时再插入一条记录:
因为页10最多只能放3条记录,所以不得不再分一个新页:
注意,新分配的数据页编号可能并不是连续的。它们只是通过维护着上一个页和下一个页的编号而建立了链表关系。另外,页10中用户记录最大的主键值是5,而页28中有一条记录的主键值是4,因为5>4,所以这就不符合下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求,所以在插入主键值为4的记录的时候需要伴随着一次记录移动,也就是把主键值为5的记录移动到页28中,然后再把主键值为4的记录插入到页10中,这个过程的示意图如下:
这个过程表明了在对页中的记录进行增删改操作的过程中,我们必须通过一些诸如记录移动的操作来始终保证这个状态一直成立:下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。这个过程我们称为页分裂。
给所有页建立一个目录项。
由于数据页的编号可能不是连续的,所以在向index_demo表中插入许多记录后,是这样的:
因为这些16KB的页在物理存储上是不连续的,所以如果想从这么多页中根据主键值快速定位某些记录所在的页,我们需要给他们做个目录,每个页对应一个目录项,每个目录项包括下边两个部分:
以页28为例,它对应目录项2,这个目录项中包含着该页的页号 28 以及该页中用户记录的最小主键值 5 。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找某条记录的功能。比如:查找主键值为20的记录,具体查找过程分两步:
至此,针对数据页做的简易目录就完成,这个目录也就是索引。
1.迭代一次:目录项记录的页
上边称为一个简易的索引方案,是因为我们为了在根据主键值进行查找时使用二分法快速定位具体的目录项而假设所有目录项都可以在物理存储器上连续存储,但是这样做有几个问题:
所以,我们需要一种可以灵活管理所有目录项的方式。我们发现目录项其实长得跟我们的用户记录差不多,只不过目录项中的两个列是主键和页号而已,为了和用户记录做一下区分,我们把这些用来表示目录项的记录称为目录项记录。那InnoDB怎么区分一条记录是普通的田尸记录还是目录项己录呢?使用记录头信息里的
rcord_type属性,它的各个取值代表的意思如下:
从图中可以看出来。我们新分配一个编号为30的页来专门存储目录项记录。这里再次强调目录项记录和普通的用户记录的不同点:
相同点:两者用的是一样的数据页,都会为主键值生成Page Directory(页目录),从而在按照主键值进行查找时可以使用二分法来加快查询速度。
现在以查找主键为20的记录为例,根据某个主键值去查找记录的步骤就可以大致分为下边两步:
2.迭代2次:多个目录项记录的页
虽然说目录项记录中只存储主键值和对应的页号,比用户记录需要的存储空间小多了,但是不论怎么说一个页只有16KB大小,能存放的目录项记录也是有限的,那如果表中的数据太多,以至于一个数据页不足以存放所有的目录项记录,如何处理呢?
这里我们假设一个存储目录项记录的页最多只能存放4条目录项记录,所以如果此时我们再向上图中插入一条主键值为320的用户记录的话,那就需要分配一个新的存储目录项记录的页:
从图中可以看出,我们插入了一条主键为320的用户记录之后需要两个新的数据页:
现在因为存储目录项记录的页不止一个,所以如果我们想要根据主键值查找一条用户记录大致需要三个步骤:
确定目录项记录页
我们现在的存储目录项记录的页有两个,即页30和页32,又因为页30表示的目录项的主键值的范围是[1,320),页32表示的目录项的主键值不小于320,所以主键值为20的记录对应的目录项记录在页30中。
通过目录项记录页确定用户记录真实所在的页
在一个存储目录项记录的页中通过主键值定位一条目录项记录的方式
在真实存储用户记录的页中定位到具体记录
3.迭代3次:目录项记录页的目录页
问题来了,在这个查询步骤的第1步中我们需要定位存储目录项记录的页,但是这些页是不连续的,如果我们表中的数据非常多则会产生很多存储目录项记录的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?那就为这些存储目录项记录的页再生成一个更高级的目录,就像是一个多级目录一样,大目录里嵌套小目录,小目录里才是实际的数据,所以现在各个页的示意图就是这样子:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PnP35bgZ-1657248623280)(MySQL/1652626601115.png)]
如图,我们生成了一个存储更高级目录项的页33,这个页中的两条记录分别代表页3o和页32,如果用户记录的主键值在**[1,320)之间,则到页30中查找更详细的目录项记录,如果主键值不小于320**的话,就到页32中查找更详细的目录项记录。
随着表中记录的增加,这个目录的层级会继续增加,如果简化一下,那么我们可以用下边这个图来描述它:
这个数据结构,它的名称是B+树
B+Tree
不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到B+树这个数据结构中了,所以我们也称这些数据页为节点。从图中可以看出,我们的实际用户记录其实都存放在B+树的最底层的节点上,这些节点也被称为叶子节点,其余用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上边的那个节点也称为根节点。
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第0层,之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页最多存放3条记录,存放目录项记录的页最多存放4条记录。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么:
你的表里能存放 100000000000 条记录吗?所以一般情况下,我们 用到的B+树都不会超过4层 ,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的 Page Directory (页目录),所以在页面内也可以通过 二分法 实现快速定位记录。
索引按照物理实现方式,索引可以分为2种:聚簇索引(聚集)和非聚簇(非聚集)索引。我们把非聚集索引称为二级索引或者辅助索引。
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录存储在叶子节点),也就是所谓的索引即数据,数据即索引
术语“聚簇”表示数据行和相邻的键值聚簇的存储在一起
特点:
使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
B+树的 叶子节点 存储的是完整的用户记录。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
我们把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点。这种聚簇索引并不需要我们在MySQL语句种显式的使用INDEX语句去创建,InnoDB存储引擎会自动为我们创建聚簇索引。
优点:
缺点:
插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新
限制:
上边介绍的聚簇索引只能在搜索条件是主键时才发挥作用,因为B+树中的数据都是按照主键进行排序的。那么如果想以别的列作为搜索条件该怎么办?肯定不能从头到尾沿着链表依次遍历一遍
答案:我们可以多建几颗B+树,不同的B+树中的数据采用不同的排序规则。比方说我们用c2列的大小作为数据页、页中记录的排序规则,再建一颗B+树,效果图如下:
这个B+树与上边介绍的聚簇索引有几处不同:
使用记录c2列的大小进行记录和页的排序,包括三个方面的含义:
B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值
目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配
所以如果我们现在想通过c2列的值查找某些记录的话就可以使用刚刚键好的B+树。以查找c2列的值为4的记录为例,查找过程如下:
确定目录项记录页
根据根页面,也就是页44,可以快速定位到目录项所在的页为页42(2<4<9)。
通过目录项记录页确定用户记录真实所在的页
在页42中可以快速定位到实际存储用户记录的页,但是由于c2列并没有唯一性约束,所以c2列值为4的记录可能分布在多个数据页中,又因为2< 4≤4,所以确定实际存储用户记录的页在页34和页35中。
在真实存储用户记录的页中定位到具体的记录。
到页34和页35中定位到具体的记录。
但是这个B+树的叶子节点中的记录只存储了c2和c1(也就是主键)两个列,所以必须再次根据主键值去聚簇索引在查找一遍完整的用户记录
概念:回表
我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到聚簇索引中再查一遍,这个过程称为回表。也就是根据c2列的值查询一条完整的用户记录需要使用到2棵B+树!
问题:为什么我们还需要一次回表操作?直接把完整记录放进叶子节点不行吗?
回答:
如果把完整的用户记录放到叶子节点是可以不用回表。但是太占地方了,相当于每建立一棵B+树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。
因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引(英文名secondary index ),或者辅助索引。由于我们使用的是c2列的大小作为B+树的排序规则,所以我们也称这个B+树是为c2列建立的索引。
非聚簇索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个非聚簇索引。
小结:聚簇索引与非聚簇索引的原理不同,在使用上也有一些区别:
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照c2和c3列的大小进行排序,这个包含两层含义:
如图所示,我们需要注意几点:
注意一点,以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。他的意思是与分别为c2和c3列分别建立索引的表述不同,不同点如下:
根页面万年不动窝
我们前边介绍B+树索引的时候,为了大家理解上的方便,先把存储用户记录的叶子节点都画出来,然后接着画存储目录项记录的内节点,实际上B+树的形成过程是这样的:
这个过程特别注意的是:一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引l的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引。
内节点中目录项记录的唯一性
我们知道B+树索引的内节点中目录项记录的内容是索引列+页号的搭配,但是这个搭配对于二级索引来说有点不严谨。还拿index_demo表为例:
如果二级索引中目录项记录的内容只是索引列+页号的搭配的话,那么为c2列建立索引后的B+树应该是:
如果我们想新插入一行记录,其中c1、c2、c3的值分别是:9、1、 ‘c’,那么在修改这个为c2列建立的二级索引对应的B+树时便碰到了个大问题:由于页3中存储的目录项记录是由c2列+页号的值构成的,页3中的两条目录项记录对应的c2列的值都是1,而我们新插入的这条记录的c2列的值也是1,那我们这条新插入的记录到底应该放到页4中,还是应该放到页5中啊?答案是:对不起,懵了。
为了让新插入记录能找到自己在哪个页里,我们需要保证在B+树的同一层内节点的目录项记录除了页号这个字段以外是唯一的。所以对于二级索引的内节点的目录项记录的内容实际上是由三个部分构成的:
也就是我们把主键值也添加到二级索引内节点中的目录项记录,这样就能保证B+树每一层节点中各条目录项记录除页号这个字段外是唯一的,所以为c2列建立索引后是:
这样我们再插入记录(9,1, ‘c’)时,由于页3中存储的目录项记录是由c2列+主键+页号的值构成的,可以先把新记录的c2列的值和页3中各目录项记录的c2列的值作比较,如果c2列的值相同的话,可以接着比较主键值,因为B+树同一层中不同目录项记录的c2列+主键的值肯定是不一样的,所以最后肯定能定位唯一的一条目录项记录,在本例中最后确定新记录应该被插入到页5中。
一个页面最少存储2条记录
一个B+树只需要很少的层级就可以轻松存储数亿条记录,查询速度相当不错! 这是因为B+树本质上就是一个大的多层级目录,每经过一个目录时都会过滤掉许多无效的子目录,直到最后访问到存储真实数据的目录。那如果一个大的目录中只存放一个子目录是个啥效果呢?那就是目录层级非常非常非常多,而且最后的那个存放真实数据的目录中只能存放一条记录。费了半天劲只能存放一条真实的用户记录?所以InnoDB的一个数据页至少可以存放两条记录。
即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。Innodb和MylSAM默认的索引是Btree索引;而Memory默认的索引是Hash索引。
MylSAM引擎使用B+Tree作为索引结构,叶子节点的data域存放的是数据记录的地址。
下图是MyISAM索引的原理图
我们知道InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了,而MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储:
这里设表一共有三列,假设我们以col1为主键,上图是一个MyISAM表的主索引(Primary key)示意可以看出MylISAM的索引文件仅仅保存数据记录的地址。在MylSAM中,主键索引和二级索引(Secondary key)在结构上没有任何区别,只是主键索引要求key是唯一的,而二级索引的key可以重复。如果我们在col2上建立一个二级索引,则此索引的结构如下图所示:
同样也是一棵B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为:首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。
MyISAM的索引方式都是”非聚簇“的,与InnoDB包含一个聚簇索引不同,小结两种引擎中索引的区别:
1.在InnoDB存储引擎中,我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,而在MyISAM:却需要进行一次回表操作,意味着MyISAM中建立的索引相当于全部都是二级索引。
2.InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。
3.InnoDB的非聚簇索引data域存储相应记录主键的值,而MyISAM索引记录的是地址。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
4.MyISAM的回表操作是十分快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
小结:
了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助。比如:
举例1:知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有二级索引都引用主键索引,过长的主键索引会令二级索引变得过大。
举例2:用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一棵B+Tree,非单调的主键会造成在插入新记录时,数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
索引是个好东西,但不能乱建,它在空间和时间上都会有消耗:
空间上的代价
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
时间上的代价
每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位
页面分裂、页面回收等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
一个表上索引键的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。为了能建立又好又少的索引,我们得学会这些索引在哪些条件下起作用
从MySQL的角度讲,不得不考虑一个现实问题就是磁盘lo。如果我们能让索引的数据结构尽量减少硬盘的IO操作,所消耗的时间也就越小。可以说,磁盘的I/O操作次数对索引的使用效率至关重要。
查找都是索引操作,一般来说索引非常大,尤其是关系型数据库,当数据量比较大的时候,索引的大小有可能几个G甚至更多,为了减少索引在内存的占用,数据库索引是存储在外部磁盘上的。当我们利用索引查询的时候,不可能把整个索引全部加载到内存,只能逐一加载,那么MysQL衡量查询效率的标准就是磁盘lo次数。
显而易见,效率非常差
Hash本身是一个函数,又被称为散列函数,它可以帮助我们大幅提升检索数据的效率。
Hash算法是通过某种确定性的算法(比如MD5、SHA1、SHA2、SHA3)将输入转变为输出。相同的输入永远可以得到相同的输出 ,假设输入内容有微小偏差,在输出中通常会有不同的结果。
举例:如果你想要验证两个文件是否相同,那么你不需要把两份文件直接拿来比对,只需要让对方把Hash 函数计算得到的结果告诉你即可,然后在本地同样对文件进行Hash函数的运算,最后通过比较这两个Hash 函数的结果是否相同,就可以知道这两个文件是否相同。
加速查找速度的数据结构,常见的有两类:
采用Hash进行检索效率非常高,基本上一次检索就可以找到数据,而B+树需要自顶向下依次查找多次访问节点才能找到数据,中间需要多次Io操作,从效率来说 Hash比 B+树更快。
在哈希的方式下,一个元素k处于h(k)中,即利用哈希函数h,根据关键字k计算出槽的位置。函数h将关键字域映射到哈希表T[o…m-1]的槽位上。
上图中哈希函数h有可能将两个不同的关键字映射到相同的位置,这叫做碰撞,在数据库中一般采用链接法来解决。在链接法中,将散列到同一槽位的元素放在一个链表中,如下图所示:
实验:体会数组和hash表查找方面的效率区别
// 算法复杂度为 O(n)
@Test
public void test1(){
int[] arr = new int[100000];
for(int i = 0;i < arr.length;i++){
arr[i] = i + 1;
}
long start = System.currentTimeMillis();
for(int j = 1; j<=100000;j++){
int temp = j;
for(int i = 0;i < arr.length;i++){
if(temp == arr[i]){
break;
}
}
}
long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); //time: 823
}
//算法复杂度为 O(1)
@Test
public void test2(){
HashSet<Integer> set = new HashSet<>(100000);
for(int i = 0;i < 100000;i++){
set.add(i + 1);
}
long start = System.currentTimeMillis();
for(int j = 1; j<=100000;j++) {
int temp = j;
boolean contains = set.contains(temp); }
long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); //time: 5
}
Hash结构效率高,为什么索引结构要设计成树型?
原因1: Hash索引仅能满足(=)(◇)和IN查询。如果进行范围查询,哈希型的索引,时间复杂度会退化为o(n);而树型的“有序"”特性,依然能够保持o(log2N)的高效率。
原因2: Hash索引还有一个缺陷,数据的存储是没有顺序的,在ORDER BY的情况下,使用Hash索引还需要对数据重新排序。
原因3∶对于联合索引的情况,Hash值是将联合索引键合并后一起来计算的,无法对单独的一个键或者几个索引键进行查询。
原因4:对于等值查询来说,通常Hash索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效率就会降低。这是因为遇到Hash 冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字,非常耗时。所以, Hash索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。
Hash适用存储引擎如表所示:
Hash索引的适用性:
Hash索引存在着很多限制,相比之下在数据库中B+树索引的使用面会更广,不过也有一些场景采用Hash索引效率更高,比如在键值型((Key-value)数据库中,Redis存储的核心就是Hash表。
MysQL中的Memory存储引擎支持Hash存储,如果我们需要用到查询的临时表时,就可以选择Memory存储引擎,把某个字段设置为Hash索引,比如字符串类型的字段,进行Hash计算之后长度可以缩短到几个字节。当字段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。
另外,InnoDB本身不支持 Hash索引,但是提供自适应Hash索引(Adaptive Hash Index)。什么情况下才会使用自适应Hash索引呢?如果某个数据经常被访问,当满足一定条件的时候,就会将这个数据页的地址存放到Hash表中。这样下次查询的时候,就可以直接找到这个页面的所在位置。这样让B+树也具备了Hash 索引的优点。
采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+ 树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。
我们可以通过 innodb_adaptive_hash_index 变量来查看是否开启了自适应 Hash,比如:
mysql> show variables like '%adaptive_hash_index';
如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。
二叉搜索树的特点
查找规则
我们先来看下最基础的二叉搜索树(Binary Search Tree),搜索某个节点和插入节点的规则一样,我们假设搜索插入的数值为key:
举个例子,我们对数列(34,22,89,5,23,77,91)创造出来的二分查找树如下图所示:
但是存在特殊情况,就是有时候二叉树的深度非常大,比如我们给出的数据顺序是(5,22,23,34,77,89,91)创造出来的二分查找树如图所示:
上面第二棵树也属于二分查找树,但是性能上已经退化成了一条链表,查找数据的时间复杂度变成o(n)。你能看出来第一个树的深度是3,也就是说最多只需3次比较,就可以找到节点,而第二个树的深度是7,最多需要7次比较才能找到节点。
为了提高查询效率,就需要减少磁盘IO数。为了减少磁盘lo的次数,就需要尽量降低树的高度,需要把原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好。
为了解决上面二叉树找树退化成链表的问题,人们提出了平衡二叉树(balanced Binary Tree),又称为AVL树(有别于AVL算法),它在二叉搜索树的基础上增加了约束,具有以下性质:
它是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树
这里说一下,常见的平衡二叉树有很多种,包括了平衡二叉搜索树、红黑树、数堆、伸展树。平衡二叉搜索树是最早提出来的自平衡二叉搜索树,当我们提到平衡二叉树时一般指的就是平衡二叉搜索树。事实上,第一棵树就属于平衡二叉搜索树,搜索时间复杂度就是0( log2n)。
数据查询的时间主要依赖于磁盘I/o的次数,如果我们采用二叉树的形式,即使通过平衡二叉搜索树进行了改进,树的深度也是 o(log2n),当n比较大时,深度也是比较高的,比如下图的情况:
每访问一次节点就需要进行一次磁盘工/0操作,对于上面的树来说,我们需要进行5次I/O操作。虽然平衡二叉树的效率高,但是树的深度也同样高,这就意味着磁盘I/o操作次数多,会影响整体数据查询的效率。
针对同样的数据,如果我们把二叉树改成M叉树―(M>2)呢?当M=3时,同样的31个节点可以由下面的三叉树来进行存储:
你能看到此时树的高度降低了,当数据量N大的时候,以及树的分叉数M大的时候,M叉树的高度会远小于二叉树的高度(M >2)。所以,我们需要把树从“瘦高"变“矮胖”。
B树的英文是Balance Tree,也就是多路平衡查找树。简写为B-Tree (注意横杠表示这两个单词连起来的意思,不是减号)。它的高度远小于平衡二叉树的高度。
B树作为多路平衡查找树,它的每一个节点最多可以包括M个子节点,M称为B树的阶。每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了×个关键字,那么指针数就是x+1。对于一个100阶的B树来说,如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说,采用B树的结构是非常适合的,因为树的高度要远小于二叉树的高度。
一个M阶的B树(M>2)有以下特性:
根节点的儿子数的范围是 [2,M]。
每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]。
叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为 [ceil(M/2), M]。
假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i]
所有叶子节点位于同一层。
上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2,里面的关键字为(8,12),它 有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15) 大于 12,刚好符合刚才我们给出的特征。
然后我们来看下如何用B树进行查找。假设我们想要查找的关键字是9,那么步骤可以分为以下几步:
1.我们与根节点的关键字(17,35)进行比较,9小于17那么得到指针P1;
2.按照指针P1找到磁盘块2,关键字为(8,12),因为9在8和12之间,所以我们得到指针P2;
3.按照指针P2找到磁盘块6,关键字为(9,10),然后我们找到了关键字9。
你能看出来在B树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行I/O操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素。B树相比于平衡二叉树来说磁盘工/0操作要少,在数据查询中比平衡二叉树效率要高。所以只要树的高度足够低,IO次数足够少,就可以提高查询性能。
小结:
B+树也是一种多路搜索树,基于B树做出了改进,主流的DBMS都支持B+树的索引方式,比如MySQL。相t于B-Tree,B+Tree适合文件索引系统。
B+树和B树的差异在于以下几点:
1.有k个孩子的节点就有k个关键字。也就是孩子数量=关键字数,而B树中,孩子数量=关键字数+1。
2.非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小
3非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而B树中,非叶子节点既保存索引,也保存数据记录。
4.所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
B+树中间节点并不直接存储数据,这样的好处有什么?
首先**,B+树查询效率更稳定**。因为B+树每次只有访问到叶子节点才能找到对应的数据,而在B树中,非叶子节点也会存储数据,这样就会造成查询效率不稳定的情况,有时候访问到了非叶子节点就可以找到关键字,而有时需要访问到叶子节点才能找到关字。
其次,B+树的查询效率更高。这是因为通常B+树比B树更矮胖(阶数更大,深度更低),查询所需要的磁盘I/o 也会更少。同样的磁盘页大小,B+树可以存储更多的节点关键字。
不仅是对单个关键字的查询上,在查询范围上,B+树的效率也比B树高。这是因为所有关键字都出现在B+树的叶子节点中,叶子节点之间会有指针,数据又是递增的,这使得我们范围查找可以通过指针连接查找。而在B树中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
B 树和 B+ 树都可以作为索引的数据结构,在 MySQL 中采用的是 B+ 树。
但B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。
思考题:为了减少IO,索引树会一次性加载吗
1、数据库索引是存储在磁盘上的,如果数据量很大,必然导致索引的大小也会很大,超过几个G。
2、当我们利用索引查询时候,是不可能将全部几个G的索引都加载进内存的,我们能做的只能是:逐一加载每一个磁盘页,因为磁盘页对应着索引树的节点。
思考题:B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO
InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree 中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为103。也就是说一个深度为3的B+Tree索引可以维护103*103*103=10亿条记录。(这里假定一个数据页也存储10^3条行记录数据了)实际情况中每个节点可能不能填充满,因此在数据库中,**B+Tree的高度一般都在24层**。MysQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要13次磁盘I/o操作。
思考:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
1、B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说Io读写次数也就降低了。
2、B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
Hash索引与B+树索引的区别
我们之前讲到过B+树索引的结构,Hash索引结构和B+树的不同,因此在索引使用上也会有差别
1、Hash索引不能进行范围查询,而B+树可以。这是因为Hash索引指向的数据是无序的,而B+树的叶子节点是个有序的链表。
2、Hash 索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),而B+树可以。对于联合索引来说,Hash索引在计算Hash值的时候是将索引键合并后再一起计算Hash值,所以不会针对每个索引单独计算Hash值。因此如果用到联合索引的一个或者几个索引时,联合索引无法被利用。
3、Hash索引不支持ORDER BY排序,因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,而B+树索引数据是有序的,可以起到对该字段ORDER BY排序优化的作用。同理,我们也无法用Hash索引进行模糊查询,而B树使用LIKE进行模糊查询的时候,LIKE后面后模糊查询(比如%结尾)的话就可以起到优化作用
4.InnoDB不支持哈希索引
思考:Hash索引与B+树索引是在键索引的时候手动指定的吗?
如果使用的是MySQL的话I我们需要了解MysQL的存储引擎都支持哪些索引结构,如下图所示(参考来源https:)/dev.mysql.com/doc/refman/8.0/en/create-index.html)。如果是其他的DBMS,可以参考相关的DBMS文档。
你能看到,针对InnoDB和MyIlSAM存储引擎,都会默认采用B+树索引,无法使用Hash索引。InnoDB提供的自适应Hash是不需要手动指定的。如果是Memory/Heap和NDB存储引擎,是可以进行选择Hash索引的。
R-Tree在MysQL很少使用,仅支持 geometry数据类型
,支持该类型的存储引擎只有myisam、bdb、innodb、ndb、archive几种。举个R树在现实领域中能够解决的例子:查找2o英里以内所有的餐厅。如果没有R树你会怎么解决?一般情况下我们会把餐厅的坐标(xy)分为两个字段存放在数据库中,一个字段记录经度,另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息,然后计算是否满足要求。如果一个地区有100家餐厅的话,我们就要进行10o次位置计算操作了,如果应用到谷歌、百度地图这种超大数据库中,这种方法便必定不可行了。R树就很好的解决了这种高维空间搜索问题。它把B树的思想很好的扩展到了多维空间,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。因此,R树就是一棵用来**存储高维数据的平衡树。**相对于B-Tree,R-Tree的优势在于范围查找。
使用索引可以帮助我们从海量的数据中快速定位想要查找的数据,不过索引也存在一些不足,比如占用存储空间、降低数据库写操作的性能等,如果有多个索引还会增加索引选择的时间。当我们使用索引时,需要平衡索引的利(提升查询效率)和弊(维护索引所需的代价)。
在实际工作中,我们还需要基于需求和数据本身的分布情况来确定是否使用索引,尽管索引不是万能的,但数据量大的时候不使用索引是不可想象的,毕竟索引的本质,是帮助我们提升数据检索的效率。
附录:算法的时间复杂度
同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。
1.普通索引
在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空要由字段本身的完整约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表student的字段name上建立一个普通索引,查询记录时就可以根据该索引进行查询
2.唯一性索引
使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的但允许有空值。在一张数据表里可以有多个唯一索引。
例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引,可以更快速地确定某条记录。
3.主键索引
主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOT NULL+UNIQUE,一张表里最多只有一个主键索引。
why? 这是由主键索引的物理实现方式决定的,因为数据存储在文件只能按照一种顺序进行存储。
4.单列索引
在表中的单个字段上创建索引,单列索引只根据该字段进行索引。单列索引可以是普通索引吗,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个可以有多个单列索引。
5.多列(组合、联合)索引
多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段idname和gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合。
6.全文索引
全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【**分词技术】**等多种算法智能分杉出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果全文索引非常适合大型数据集,对于小的数据集,它的用处比较小。
使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHAR 或TEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。例如,表student的字段information是TEXT类型该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。
全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引
MysQL数据库从3.23.23版开始支持全文索引,但MySQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支持,但是**官方版本不支持中文分词,**需要第三方分词插件。在5.7.6版本,MySQL内置了ngram全文解析器,用来支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MySQL版本、存储引擎和数据类型是否支持全文索引。
7.补充:空间索引
使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINESTRING和POLYGON等。目前只有MyISAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。
小结:不同的存储引擎支持的索引类型也不一样
InnoDB:支持B-tree、Full-text等索引,不支持Hash索引
MyISAM:支持B-tree、Full-text等索引,不支持Hash索引
Memory :支持B-tree、Hash 等索引,不支持 Full-text索引
NDB :支持Hash索引,不支持 B-tree、Full-text等索引;
Archive :不支持B-tree、Hash、Full-text等索引;
MySQL支持多种方法在单个或多个列上创建索引:在创建表的定义语句CREATE TABLE中指定索引列,使用ALTER TABLE语句在存在的表上创建索引,或者使用CREATE INDEX语句在已存在的表上添加索引
使用CREATE TABLE创建表时,除了可以定义列的数据类型外,还可以定义主键约束、外键约束或者唯一性约束,而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引。
但是,如果是显式创建表时创建索引的话,基本语法如下:
CRAETE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]
1.创建普通索引
在book表中的year_publication字段上建立普通索引,SQL语句如下:
使用EXPLAIN语句查看是否正在使用:
EXPLAIN语句输出结果主要关注两个字段:
(1)possible_keys行给出了MySQL在搜索数据记录时可选用的索引
(2)key行是M有SQL实际选用的索引
可以看到,possible_keys和key的值都为year_publication,查询时使用了索引
2.创建唯一索引
创建唯一索引的目的也是减少查询索引列操作的执行时间,尤其是对比较庞大的数据表。它与前面的普通索引类似,不同的是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
该语句执行完后,使用show create table查看表结构:
SHOW INDEX FROM test1 \G
其中各个主要参数的含义为:
(1) Table表示创建索引的表。
(2) Non_unique表示索引非唯一,1代表非唯一索引,o代表唯一索引。
(3)Key_name表示索引的名称。
(4) Seq_in_index表示该字段在索引中的位置,单列索引该值为1,组合索引为每个字段在索引定义中的顺序。
(5)Column_name表示定义索引的列字段。
(6) Sub_part表示索引的长度。
(7)Null表示该字段是否能为空值。(8 ) Index_type表示索引类型。
有结果可以看出,id字段上已经成功建立了一个名为uk_idx_id的唯一索引。
3.主键索引
设定为主键后数据库会自动建立索引,InnoDB为聚簇索引,语法:
4.创建单列索引
单列索引是再数据表中的某一字段上创建的索引,一个表中可以创建多个单列索引。前面例子中创建的索引都为单列索引
该语句执行完毕后,使用show create table查看表结构:
SHOW INDEX FROM test2 \G
有结果可以看出,id字段上已经成功建立一个名为single_idx_name的单列索引,索引长度为20
5.创建组合索引
组合索引是在多个字段上创建一个索引 。
举例:创建表test3,在表中的id、name和age字段上建立组合索引:
该语句执行完后,使用SHOW INDEX :
SHOW INDEX FROM test3 \G
由结果可以看到;id、name和age字段上已经成功建立了一个名为multi_idx的组合索引。
组合索引可起几个索引的作用,但是使用时并不是随便查询哪个字段都可以使用索引,而是遵从“最左前u"。例如,索引可以搜索的字段组合为:(id, name, age) . (id,name)或者id。而(age)或者 (name,age)组合不能便用索引查询。
在test3表中,查询id和name字段,使用EXPLAIN语句查看索引的使用情况:
EXPLAIN SELECT * FROM test3 WHERE id=1 AND nam=”zhangsan“ \G
可以看到,查询id和name字段时,使用了名称为Multildx的索引,如果查询(name,age)组合或者单独查询name和age字段,会发现结果中possible_keys和key值为NULL,并没有使用在t3表中创建的索引进行查询
6.创建全文索引
FULLTEXT全文索引可以用于全文搜索,并且只为CHAR、VARCHAR 和TEXT列创建索引。索引总是对整个列进行,不支持局部(前缀)索引。
举例1:创建表test4,在表中的info字段上建立全文索引,SQL语句如下:
在MySQL5.7及之后版本中可以指定最后的ENGINE,因为在此版本中InnoDB支持全文索引
语句执行完后,使用SHOW CREATE TABLE查看表结构:
SHOW INDEX FROM test4 \G
有结果可以看到,info字段上已经成功建立了一个名为futxt_idx_info的FULLTEXT索引。
举例2:
创建了一个个title和body字段添加全文索引的表
明显的提高查询效率
注意点:
1.使用全文索引前,搞清楚版本支持情况;
2.全文索引比like + %快N倍,但是可能存在精度问题;
3.如果需要全文索引的是大量数据,建议先添加数据I再创建索引。
7.创建空间索引
创建空间索引中,要求空间类型的字段必须非空
举例:创建test5,在空间类型为GEOMETRY的字段上创建空间索引:
该语句执行完后,使用SHOW CREATE TABLE查看:
SHOW INDEX FROM test5 \G
可以看到,test5表的geo字段上创建了名称为spa_idx_geo的空间索引。注意创建时指定空间类型字段值的非空约束,并且表的存储引擎为MyISAM。
在已经存在的表上创建索引可以使用ALTER TABLE…ADD…或者CREATE INDEX…ON…语句
基本语法
-- 1、创建索引 [UNIQUE]可以省略
-- 如果只写一个字段就是单值索引,写多个字段就是复合索引
CREATE [UNIQUE] INDEX indexName ON tabName(columnName(length));
ALTER TABLE tabName ADD [UNIQUE] INDEX indexName ON (columnName(length));
-- 2、删除索引
DROP INDEX [indexName] ON tabName;
ALTER TABLE table_name DROP INDEX index_name;
-- 3、查看索引
-- 加上\G就可以以列的形式查看了 不加\G就是以表的形式查看
SHOW INDEX FROM tabName \G;
SHOW CREATE TABLE tabname \G;
使用alter命令为数据表添加索引
-- 1、该语句添加一个主键,这意味着索引值必须是唯一的,并且不能为NULL
ALTER TABLE tabName ADD PRIMARY KEY(column_list);
-- 2、该语句创建索引的键值必须是唯一的(除了NULL之外,NULL可能会出现多次)
ALTER TABLE tabName ADD UNIQUE INDEX indexName(column_list);
-- 3、该语句创建普通索引,索引值可以出现多次
ALTER TABLE tabName ADD INDEX indexName(column_list);
-- 4、该语句指定了索引为FULLTEXT,用于全文检索
ALTER TABLE tabName ADD FULLTEXT indexName(column_list);
使用create添加索引
#添加普通索引
CREATE INDEX indexname ON tabname(column_list)
#添加唯一索引
CREATE UNIQUE INDEX indexname ON tabname(column_list)
#创建单列索引
CREATE INDEX indexname ON tabname(column_list(length))
#创建组合索引
CREATE IDNEX idx_aut_info ON tabname(authors(20),info(50))
#创建主键索引
CREATE PRIMARY KEY ON tabname(column_list)
提示:添加AUTO_INCRMENT约束字段的唯一索引不能删除
删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除。
降序索引以降序存储键值。虽然在语法上,从MySQL 4版本开始就已经支持降序索引的语法了,但实际上该DESC定义是被忽略的,直到MysQL 8.x版本才开始真正支持降序索引(仅限于InnoDB存储引擎)。
MySQL在8.0版本之前创建的仍然是升序索引,使用时进行反向扫描,这大大降低了数据库的效率。在某些场景下,降序索引意义重大。例如,如果一个查询,需要对多个列进行排序,且顺序要求不一致,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。
举例:分别在MySQL 5.7版本和MySQL 8.0版本中创建数据表ts1,结果如下:
CREATE TABLE ts1(a int,b int,index idx_a_b(a,b,desc));#a后不写asc默认是升序
从结果可以看出,索引仍然是默认的升序(使用时反向扫描)。
在MySQL 8.0版本中查看数据表ts1的结构,结果如下:
从结果可以看出,索引已经是降序。下面继续测试降序索引在执行计划中的表现。
分别在MySQL 5.7版本和MySQL 8.o版本的数据表ts1中插入800条随机数据,执行语句如下:
DELIMITER //
CREATE PROCEDURE ts_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i < 800
DO
INSERT INTO ts1 SELECT RAND()*80000,RAND()*80000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;
在MySQL5.7版本中查看数据表ts1的执行计划,结果如下:
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;
从结果可以看出,执行计划中扫描数为799,而且使用了Using filesort。
提示
Using filesort是MysQL中一种速度比较慢的外部排序,能避免是最好的。多数情况下,管理员可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。
在MySQL 8.0版本中查看数据表ts1的执行计划。从结果可以看出,执行计划中扫描数为5,而且没有使用Usingfilesort。
注意
降序索引只对查询中特定的排序顺序有效,如果使用不当,反而查询效率更低。例如,上述查询排序条件改为order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。
在MysQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MysQL 8.x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除。
同时,如果你想验证某个索引删除之后的查询性能影响,就可以暂时先隐藏该索引。
注意:
主键不能被设置为隐藏索引。当表中没有显示主键时,表中第一个唯一非空索引会称为隐式主键,也不能设置为隐藏索引
索引默认是可见的,在使用CREATETABLE,CREATE INDEX或者ALTERTABLE等语句时可以通过VISIBLE或者INVISIBLE关键词设置索引的可见性。
1.创建表时直接创建
在MySQL中创建隐藏索引通过SQL语句INVISIBLE来实现,其语法如下:
上述语句比普通索引多了一个关键字INVISIBLE,用来标记索引为不可见索引
练习:在创建班级表classes时,在字段cname上创建隐藏索引
向表中添加数据,并通过explain查看发现,优化器并没有使用索引,而是全表扫描
EXPLAIN SELECT * FROM classes HWERE cname = ‘高一’
2.在已存在的表上创建索引
举例:
3.通过ALTER TABLE语句创建
举例:
如果将index_cname索引切换成可见状态,通过explain查看执行计划,发现优化器选择了index_cname索引。
注意
当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。
通过设置隐藏索引的可见性可以查看索引对调优的帮助。
5.使隐藏索引对查询优化器可见
在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果use_invisible_indexes 设置为of(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计划时仍会考虑使用隐藏索引。
(1)在MySQL命令行执行如下命令查看查询优化器的开关设置。
mysql> select @@optimizer_switch \G
在输出信息中找到如下属性配置
sue_inviaible_indexes=off
此属性配置值为off,说明隐藏索引默认对查询优化器不可见.
(2)使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:
再次查看查询优化器的开关设置
说明,此时隐藏索引对查询优化器可见。
(3)使用EXPLAIN查看以字段invisible_column作为查询条件的索引使用情况
(4)如果需要使隐藏索引对查询优化器不可见,则只需要执行如下命令即可。
为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于邘获得良好的性能非常重要。设计索引时,应该考虑相应准则。
索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。
例如,学生表中学号是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名的话,可能存在同名现象,从而降低查询速度。
业务上具有唯一特性的字段,即使是组合字段,也必须创建唯一索引。
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略不计,但提高查找速度明显
某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者 使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多 个,那么可以在这些列上建立 组合索引 。
对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就 能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或 删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。
有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。
首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。
为什么类型必须一致:例:‘101’=101;
会进行隐式类型转换,调用函数,索引会失效
我们这里所说的类型大小指的就是该类型表示的数据范围的大小。
我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、
BLGINT等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增。如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT。这是因为:
这个类型对于表的主键来说更加适用,因为不仅是聚簇索引会使用主键值,其他所有的二级索引的节点都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O。
假设字符串很长,那存储一个字符串就需要占用很大的存储空间,在我们需要为这个字符串列建立索引时,那就意味着在对应的B+树中有这么个问题:
我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。这样在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。
例如TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面的若干字符,这样可以提高检索速度。
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引
create table shop(address varchar(120) not null); alter table shop add index(address(12));
问题是:截取多少呢?截取多了达不到节省索引存储空间的目的;截取多了,重复内容太多,字段散列度(选择性)会降低。
先看一下字段在全部数据中的选择度:
select count(distinct address) / count(*) from shop;
公式:
count(distinct left(列名, 索引长度))/count(*)
通过选择的当前索引的前缀查询出来的不重复的个数 / 总记录数
越大选择性越好
引申另一个问题:索引列前缀对排序的影响
如果使用了索引列前缀,比方说前边只把address列的前12个字符放到了二级索引中,下边这个查询:
SELECT * FROM shop
ORDER BY address
LIMIT 12;
因为二级索引中不包含完整的address列信息,所以无法对前12个相同,后面字符不同的列排序,也就是使用索引列前缀的方式无法支持使用索引排序,只能使用文件排序
拓展:Alibaba《Java开发手册》
【 强制 】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达 90% 以上 ,可以使用 count(distinct left(列名, 索引度))/count(*)的区分度来确定。
列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但该列的基数却是3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。
可以使用公式 select count(distinct a)/count(*) from t1计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。
拓展:联合索引把区分度高(散列性高)的列放在前面。
这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。
在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:
①每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
②索引会影响INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。
③优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MySQL优化器生成执行计划时间,降低查询性能。
WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举例:
因为我们是按照student_id来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。
如果表记录太少,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的“性别"字段上只有“男"与“女"两个不同值,因此无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度。
举例1:要在100万行数据中查找其中的50万行(比如性别为男的数据),一旦创建了索引,你需要先访问50万次索引,然后再访问50万次数据表,这样加起来的开销比不使用索引可能还要大。
结论:当数据重复度大,比如高于10%的时候,也不需要对这个字段使用索引
第一层含义︰频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。
第二层含义:避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。此时,虽然提高了查询速度,同时却会降低更新表的速度。
例如身份证、UUID(在索引比较时需要转为ASCII。并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
1.冗余索引
有时候有意或者无意的就对同一个列创建了多个索引,比如: index(a,b,c)相当于index(a)、index(a,b)、index(a,b,c)。
我们知道,通过idx_name_birthday_phone_number索引就可以对name列进行快速搜索,再创建一个专门针对name列的索引就算是一个冗余索引,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。
2.重复索引
我们看到,col1既是主键、又给他定义为唯一索引,还给定义了普通索引,主键本身就会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。
索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。
选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则.
在数据库调优中,我们的目标就是响应时间更快,吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮我们快速找到调优的思路和方式。
当我们遇到数据库调优问题的时候,该如何思考呢?这里把思考的流程整理成下面这张图。
整个流程划分成了**观察(Show status)和行动(Action) **两个部分。字母S的部分代表观察(会使用相应的分析工具),字母A代表的部分是行动(对应分析可以采取的行动)。
我们可以通过观察了解数据库整体的运行状态,通过性能分析工具可以让我们了解执行慢的SQL都有哪些,查看具体的sQL执行计划,甚至是SQL执行中的每一步的成本代价,这样才能定位问题所在,找到了问题,再采取相应的行动。
详细解释以下图:
首先在S1部分,我们需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,有可能是周期性节点的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存失效策略。
如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因。接下来进入S2这一步,我们需要开启慢查询。慢查询可以帮我们定位执行慢的SQL语句。我们可以通过设置long_query_time参数定义“慢”的阈值,如果SQL执行时间超过了long_query_time,则会认为是慢查询。当收集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。
在S3这一步骤中,我们就知道了执行慢的SQL,这样就可以针对性地用EXPLAIN查看对应SQL语句的执行计划,或者使用 show profile查看SQL中每一个步骤的时间成本。这样我们就可以了解sQL查询慢是因为执行时间长,还是等待时间长。
如果是SQL等待时间长,我们进入A2步骤。在这一步骤中,我们可以调优服务器的参数,比如适当增加数据库缓冲池等。如果是SQL执行时间长,就进入A3步骤,这一步中我们需要考虑是索引设计的问题?还是查询关联的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。
如果A2和A3都不能解决问题,我们需要考虑数据库自身的SQL查询性能是否已经达到了瓶颈,如果确认没有达到性能瓶颈,就需要重新检查,重复以上的步骤。如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务器,采用读写分离的架构,或者考虑对数据库进行分库分表,比如垂直分库、垂直分表和水平分表等。
以上就是数据库调优的流程思路。如果我们发现执行SQL时存在不规则延迟或卡顿的时候,就可以采用分析工具帮我们定位有问题的SQL,这三种分析工具你可以理解是sQL调优的三个步骤:慢查询、EXPLAIN和 SHOW PROFILING。
在MySQL中,可以是使用SHOW STATUS语法查询一些MySQL数据库服务器的性能参数、执行效率。
SHOW STATUS语句语法:
SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
一些常用的性能参数如下:
慢查询次数参数可以结合慢查询日志找出慢查询语句,然后针对慢查询语句进行表结构优化或者查询语句优化
再比如,如下的指令可以查看相关的指令情况:
SHOW STATUS LIKE 'Innodb_rows_%';
一条SQL查询语句在执行前需要确定查询执行计划,如果存在多种执行计划的话,MySQL 会计算每个执行计划所需要的成本,从中选择成本最小的一个作为最终执行的执行计划。
如果我们想要查看某条sQL语句的查询成本,可以在执行完这条sQL语句之后,通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。它通常也是我们评价一个查询的执行效率的一个常用指标。这个查询成本对应的是SQL语句所需要读取的页的数量。
如果想查询id=900001的记录,然后查看成本,可以直接在聚簇索引上进行查找:
运行结果(1条记录,运行时间为0.042秒)
运行结果(100条记录,运行时间为0.046s)
然后看下查询优化器的成本,大概需要进行20个页的查询
你能看到页的数量是刚才的20倍,但是查询的效率并没有明显的变化,实际上这两个sQL查询的时间基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。虽然页数量(last_query_cost)增加了不少,但是通过缓冲池的机制,并没有增加多少查询时间。
使用场景:它对于比较开销是非常有用的,特别是我们有好几种查询方式可选的时候。
sQL查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:
1.位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
2.批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms) ,而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。
MysQL的慢查询日志,用来记录在MysQL中响应时间超过阀值的语句,具体指运行时间超过long.query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上(不含10秒)的语句,认为是超出了我们的最大忍耐时间值。
它的主要作用是,帮助我们发现那些执行时间特别长的SQL查询,并且有针对性地进行优化,从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候,检查一下慢查询日志,找到那些慢查询,对解决问题很有帮助。比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合explain进行全面分析。
默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。
慢查询日志支持将日志记录写入文件。
在使用前,需要先看下慢查询是否已经开启,使用下面命令:
mysql > show variables like '&slow_query_log&';
可以看到slow_query_log=OFF,可以把慢查询日志打开,注意设置变量值的时候需要使用global,否则会报错:
mysql > set global slow_query_log='ON';
然后我们再来看下慢查询日志是否开启,以及慢查询日志文件位置
可以看到慢查询日志已经开启,同时文件位置保存在/var/ lib/mysql/atguigu02-slow . log文件中。
使用以下命令设置慢查询的时间阈值:
mysql > show variables like '%long_query_time%';
补充:配置文件中一并设置参数
如下的方式相较于前面的命令行方式,可以看作时永久设置的方式
修改my .cnf 文件,[mysqld]下增加或修改参数long_query_time 、 slow_query_log和slow_query_log_file后,然后重启MySQL服务器。
如果不指定存储路径,慢查询日志将默认存储到MySQL数据库的数据文件下。如果不指定文件名, 默认文件名为hostname-slow.log
查看当前系统中有多少条慢查询记录
SHOW GLOBAL STATUS LIKE ‘Slow_queries’;
步骤二:设置参数log_bin_trust_function_creators
创建函数,假如报错:
This function has none of DETERMINISTIC....
set global log_bin_trust_function_creators=1;
#不加global只是当前会话有效
步骤三:创建函数
从上面的结果可以看出来,查询学生编号为“3455655"的学生信息花费时间为2.09秒。查询学生姓名为“oQmLur的学生信息花费时间为2.39秒。已经达到了秒的数量级,说明目前查询效率是比较低的,下面的小节我们分析一下原因。
2.分析
show status like 'slow_queries';
补充说明:
除了long_query_time变量外,控制慢查询日志的还有一个系统变量:min_examined_row_limit。这个变量的意思是,查询扫描过的最少记录数。这个变量和查询执行时间,共同组成了判别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值,并且查询执行时间超过long_query_time的值,那么,这个查询就被记录到慢查询日志中;反之,则不被记录到慢查询日志中。
这个值默认是0。与long_query_time=10合在一起,表示只要查询的执行时间超过10秒钟,哪怕一个记录也没有扫描过,都要被记录到慢查询日志中。你也可以根据需要,通过修改“my.ini"文件,来修改查询时长,或者通过SET 指令,用SQL语句修改“min_examined_row_limit”的值。
在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MysQL提供了日志分析工具mysqldumpslow 。
查看mysqldumpslow的帮助信息
mysqldumpslow --help
mysqldumpslow命令的具体参数如下:
-a: 不将数字抽象成N,字符串抽象成S
-s: 是表示按照何种方式排序:
-t: 即为返回前面多少条的数据;
-g: 后边搭配一个正则匹配模式,大小写不敏感
举例:我们想要按照查询时间排序,查看前五条 SQL 语句,这样写即可:
mysqldumpslow -s t -t 5 /var/lib/mysql/atguigu01-slow.log
工作常用参考
#得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log
#得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log
#得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log
#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more
MySQL服务器停止慢查询日志有两种方法:
方式一:永久性方式
修改my.cnf或者my.ini文件,把[mysqld]组下的slow_query_log值设置为OFF,修改保存后,再重启MySQL服务,即可生效;
[mysqld]
slow_query_log=OFF
或者,把slow_query_log一项注释掉或删除
[mysqld]
#slow_query_log=OFF
重启MySQL服务,执行如下语句查询慢查询日志功能
SHOW VARIABLES LIKE '%slow%'; #查询慢查询日志所在目录
SHOW VARIABLES LIKE '%long_query_time%' #查询超时时长
可以看到,MySQL系统中的慢查询日志是关闭的。
方式二:临时性方式
使用SET语句来设置。
(1)停止MySQL慢查询日志功能,具体SQL语句如下。
SET GLOBAL slow_query_log=of;
(2)重启MySQL服务,使用SHOW语句查询慢查询日志功能信息,具体SQL:
SHOW VARIABLES LIKE '%slow%';
#以及
SHOW VARIABLES LIKE '%long_query_time%';
使用SHOW语句显示慢查询日志信息,具体SQL语句如下。
SHOW VARIABLES LIKE 'slow_query_log%';
从执行结果可以看出,慢查询日志的目录默认为MysQL的数据目录,在该目录下手动删除慢查询日志文件即可。
使用命令mysqladmin flush-logs 来重新生成查询日志文件,具体命令如下,执行完毕会在数据目录下重新生成慢查询日志文件。
mysqladmin -uroot -p flush-logs slow
提示
慢查询日志都是使用mysqladmin flush-logs命令来删除重建的。使用时一定要注意,一旦执行了这个命令,慢查询日志都只存在新的日志文件中,如果需要旧的查询日志,就必须事先备份。
Show Profile是MySQL提供的可以用来分析当前会话中SQL都做了什么、执行的资源消耗的情况工具,可用于sql调优的测量。默认情况下处于关闭状态,并保存最近的15次运行结果。
可以在会话级别开启这个功能
mysql > show variables like 'profiling';
通过设置profiling=’ON‘来开启slow profile;
mysql > set profiling = 'ON';
然后执行相关查询语句,接着看下当前会话都有哪些 profiles,使用下面这条命令:
mysql > show profiles;
能看到当前会话一共有 2 个查询。如果我们想要查看最近一次查询的开销,可以使用:
mysql > show profile;
我们也可以查看指定的Query ID的开销,比如show profile for query 2查询结果是一样的。在SHOW PROFILE 中可以查看不同部分的开销,比如cpu、block.io等
mysql > show profile cpu,block io for query 2;
通过上面的结果,就可以弄清楚每一步耗时,以及在不同部分,比如CPU、block.io的执行时间,这样我们可以判断出SQL到底慢在哪里
show profile的常用查询参数:
① ALL:显示所有的开销信息。 ② BLOCK IO:显示块IO开销。 ③ CONTEXT SWITCHES:上下文切换开销。 ④ CPU:显示CPU开销信息。 ⑤ IPC:显示发送和接收开销信息。 ⑥ MEMORY:显示内存开销信 息。 ⑦ PAGE FAULTS:显示页面错误开销信息。 ⑧ SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。 ⑨ SWAPS:显示交换次数开销信息。
日常开发需注意的结论:
converting HEAP to MyISAM:查询结果太大,内存不够,数据往磁盘上搬了。
Creating tmp table:创建临时表。先拷贝数据到临时表,用完后再删除临时表。
Copying to tmp table on disk:把内存中临时表复制到磁盘上,警惕!
blocked 。
如果在show profile诊断结果中出现了以上4条结果中的任何一条,则sql语句需要优化。
注意:
不过SHOW PROFILE 命令将被弃用,我们可以从information_schema 中的profiling数据表进行查看。
什么是explain?
即:sql的执行计划,使用explain关键字可以模拟优化器执行sql查询语句,从而知道mysql是如何处理sql语句的
定位了查询慢的SQL之后,就可以使用EXPLAIN或DESCRIBE工具做针对性的分析查询语句。DESCRIBE语句的使用方法与EXPLAIN语句一样,分析结果也是一样的
MysQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供它认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)。
这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL为我们提供了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,大家看懂EXPLAIN语句的各个输出项,可以有针对性的提升我们查询语句的性能。
1.能做什么?
2.官网介绍
https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
3.版本情况
MySQL 5.6.3以前只能EXPLAIN SELECT;MYSQL 5.6.3以后就可以EXPLAIN SELECT,UPDATE,DELETE
在5.7以前的版本中,想要显示partitions需要使用explain partitions命令;想要显示filtered需要使用explain extended命令。在5.7版本后,默认explain直接显示partitions和filtered中的信息。
EXPLAIN或DESCSRIBE语句的语法格式:
EXPLAIN SELECT select_options
#或
DESCRIBE SELECT select_options
如果想看看某个查询执行计划,可以在具体语句前加explain:
mysql > EXPLAIN SELECT 1;
输出的上述信息就是所谓的执行计划。在这个执行计划的辅助下,我们需要知道应该怎样改进自己的查询语句以使查询执行起来更高效。其实除了以SELECT开头的查询语句,其余的DELETE、INSERT、REPLACE 以及UPDATE语句等都可以加上EXPLAIN,用来查看这些语句的执行计划,只是平时我们对SELECT语句更感兴趣。
注意:执行EXPLAIN时并没有真正的执行该后面的语句,因此可以安全的查看执行计划。
EXPLAIN语句输出的各个列的作用如下:
列名 | 描述 |
---|---|
id | 在一个大的查询语句中每个SELECT关键字对应一个唯一id(表的读取顺序) |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表明 |
pertitions | 匹配的分区信息 |
type | 针对单表的访问方法(访问类型) |
possible_keys | 可能用到的索引 |
key | 实际使用的索引 |
key_len | 实际使用的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估需要读取的行数 |
filterd | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外信息 |
1.建表
2.设置参数log_bin_trust_function_creators
创建函数,加入报错,开启:
set global log_bin_trust_function_creators; #不加global只对当前窗口有效
3.创建函数
4.创建存储过程
5.调用存储过程
不论我们的查询语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)。
一条大的查询语句里边可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句,而每个SELECT关键字的FROM子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个SELECT关键字中的表来说,它们的id值是相同的。
MysQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,我们看一下select_type都能取哪些值,请看官方文档:
具体分析:
select_type:查询类型,用于区别普通查询、联合查询、子查询等复杂查询
SIMPLE:简单select查询,查询中不包含子查询或union
PRIMARY:查询中若包含任何复杂的子部分,最外层则被标记为primary
DERIUED:在from列表中包含的子查询被标记为此(衍生)mysql会递归执行这些子查询,把结果放在临时表
UNION:若第二个select出现在union后,则被标记为UNION,若union包含在from子句的子查询中,外层select被标记为derived,对于union或union all来说,除了最左边小查询外,其余的就是union
UNION RESULT:从union表获取结果的select(合并之后的查询就是这个选项)
type:访问类型,结果值从最好到最坏依次:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>All
常见指标主要有:system>const>eq_ref>ref>range>index>All,一般只要要达到range级别,最好到ref
system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyIASM,那么对该表访问方法就是sytem
const
当我们根据主键或唯一的二级索引(unique)与常数进行等值匹配时,对单表访问方法就是const
eq_ref
在连接查询时,如果被驱动表是通过主键或唯一二级索引列等值匹配的方式进行访问(如果该主键或唯一二级索引是联合索引,所有索引列都必须等值比较),则对该被驱动表的访问方法就是eq_ref
ref
当通过普通二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
range
如果使用索引获取某些范围区间的记录,就可能使用range
index
当我们可以使用索引覆盖,但需要扫描全部索引记录时,该表访问方法是index
all
全表扫描
在EXPLAIN语句输出的执行计划中, possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。比方说下边这个查询:
实际使用到的索引长度(即,字节数)
检查是否充分利用索引,值越大越好(针对于联合索引)
当使用索引列等值查询时,与索引列进行等值匹配的对象信息,比如只是一个常数或某个列
预估读取的记录行数,值越小越好
某个表经过搜索条件过滤后剩余记录条数中应用到结果的条数的百分比
对于单表查询来说,这个filtered列的值没什么意义,我们更关注连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动表要执行的次数(即:rows*filtered)
一些额外信息,可以通过额外信息来更准确的理解MySQL到底将如何执行给定的查询语句。
no table used
当查询语句没有from子句时会提示该信息
explain select 1
Impossible WHERE
查询语句的WHERE子句永远为FALSE时将会提示该信息
explain select * from s1 where 1!=1
using where
(1)当使用全表扫描(没使用索引)来执行对某个表的查询,并且该语句的where子句中有针对该表的搜索条件时,会提示该信息
explain select * from s1 where common_field=‘a’
(2)当使用索引执行来执行对某个表的查询,并且该语句的where子句中有除了该索引包含的列之外的其他搜索条件时,会提示该信息
explain select * from s1 where key1=‘a’ and common_field = ‘a’
no matching min/max row
当查询列表处有min或max聚合函数,但表中并没有where子句中的搜索条件的记录时,会提示该信息
explain select MIN(key1) from s1 where key1=‘abcdef’
表s1中没有abcdef这一条记录
using index
当查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在‘Extra’列将会提示该额外信息。
EXPLAIN SELECT key1 FROM s1 WHERE key1=‘a’;
using index condition
有些搜索条件中虽然出现了索引列,但却不能使用到索引
EXPLAIN SELECT * FROM s1 WHERE key>‘z’ AND key1 LIKE ‘%a’
using join buffer
在连接查询过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫“join buffer”的内存块来加快查询速度
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field
MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结
果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。
子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子
**查询的执行效率不高。**原因:
在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询 不需要建立临时表 ,其 速度比子查询
要快 ,如果查询中使用索引的话,性能就会更好。
结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
举例一:查询学生表中是班长的学生的信息。
#创建班级表中班长的索引
CREATE INDEX idx_monitor ON class(monitor);
#查询班长的信息(子查询)
EXPLAIN SELECT * FROM student stu1
WHERE stu1.`stuno` IN (
SELECT monitor
FROM class c
WHERE monitor IS NOT NULL
);
# 最好改为join的写法
EXPLAIN SELECT stu1.* FROM student stu1 JOIN class c
ON stu1.`stuno` = c.`monitor`
WHERE c.`monitor` IS NOT NULL;
#查询不为班长的学生信息
EXPLAIN SELECT SQL_NO_CACHE a.*
FROM student a
WHERE a.stuno NOT IN (
SELECT monitor FROM class b
WHERE monitor IS NOT NULL)
EXPLAIN SELECT SQL_NO_CACHE a.*
FROM student a LEFT OUTER JOIN class b
ON a.stuno =b.monitor
WHERE b.monitor IS NULL;
问题:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
回答:
在MySQL中,支持两种排序方式,分别是FileSort和Index排序。
优化建议:
避免全表扫 描
,在 ORDER BY 子句避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排#5. 排序优化
#删除student和class表中的非主键索引
CALL proc_drop_index('atguigudb2','student');
CALL proc_drop_index('atguigudb2','class');
SHOW INDEX FROM student;
SHOW INDEX FROM class;
#过程一:没有使用索引的时候
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
#过程二:order by时不limit,索引失效
#创建索引
CREATE INDEX idx_age_classid_name ON student (age,classid,NAME);
#不限制,索引失效(原因是数据过于多,而我们使用的是一个二级索引,需要进行回表,这时优化器发现回表的消耗比直
#接加载到内存还过分,于是乎就没有了进行优化)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
#下面这个语句则使用了索引,是因为他返回值刚好是索引,即不需要回表的操作(覆盖索引)
#EXPLAIN SELECT SQL_NO_CACHE age,classid,name,id FROM student ORDER BY age,classid;
#增加limit过滤条件,使用上索引了。是因为我们这里只要十条数据,数据量少,所以使用到了索引。
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
#过程三:order by时顺序错误,索引失效
#创建索引age,classid,stuno
CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
#以下哪些索引失效?
#失效
EXPLAIN SELECT * FROM student ORDER BY classid LIMIT 10;
#失效
EXPLAIN SELECT * FROM student ORDER BY classid,NAME LIMIT 10;
#可以匹配到
EXPLAIN SELECT * FROM student ORDER BY age,classid,stuno LIMIT 10;
#可以匹配到
EXPLAIN SELECT * FROM student ORDER BY age,classid LIMIT 10;
#可以匹配
EXPLAIN SELECT * FROM student ORDER BY age LIMIT 10;
#过程四:order by时规则不一致, 索引失效 (顺序错,不索引;方向反,不索引)
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid ASC LIMIT 10;
EXPLAIN SELECT * FROM student ORDER BY classid DESC, NAME DESC LIMIT 10;
EXPLAIN SELECT * FROM student ORDER BY age ASC,classid DESC LIMIT 10;
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid DESC LIMIT 10;
#过程五:无过滤,不索引
#确实是使用了索引了,但是也只是用了age字段,其原因是数据量少,加入内存的消耗比回表小
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid;
EXPLAIN SELECT * FROM student WHERE age=45 ORDER BY classid,NAME;
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age;
#是用到了索引
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age LIMIT 10;
CREATE INDEX idx_cid ON student(classid);
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age;
#总结一下:对于排序来说,如果我们排序的字段满足最左匹配原则的前提下,如果他们的排序都是升序或者降序就可以使用索引,否则不能使用索引
总结(代码)
NDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c 这里可以用是用的索引联合索引的部分值
不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序。
执行案例前先清除student上的索引,只留主键:
DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid_stuno ON student;
DROP INDEX idx_age_classid_name ON student;
#或者
call proc_drop_index('atguigudb2','student');
场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY
NAME ;
查询结果如下:
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY
NAME ;
+---------+--------+--------+------+---------+
| id | stuno | name | age | classId |
+---------+--------+--------+------+---------+
| 922 | 100923 | elTLXD | 30 | 249 |
| 3723263 | 100412 | hKcjLb | 30 | 59 |
| 3724152 | 100827 | iHLJmh | 30 | 387 |
| 3724030 | 100776 | LgxWoD | 30 | 253 |
| 30 | 100031 | LZMOIa | 30 | 97 |
| 3722887 | 100237 | QzbJdx | 30 | 440 |
| 609 | 100610 | vbRimN | 30 | 481 |
| 139 | 100140 | ZqFbuR | 30 | 351 |
+---------+--------+--------+------+---------+
8 rows in set, 1 warning (3.16 sec)
结论:type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。
优化思路:
方案一: 为了去掉filesort我们可以把索引建成
#创建新索引
CREATE INDEX idx_age_name ON student(age,NAME);
方案二: 尽量让where的过滤条件和排序使用上索引
建一个三个字段的组合索引:
DROP INDEX idx_age_name ON student;
CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY
NAME ;
mysql> SELECT SQL_NO_CACHE * FROM student
-> WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
+-----+--------+--------+------+---------+
| id | stuno | name | age | classId |
+-----+--------+--------+------+---------+
| 167 | 100168 | AClxEF | 30 | 319 |
| 323 | 100324 | bwbTpQ | 30 | 654 |
| 651 | 100652 | DRwIac | 30 | 997 |
| 517 | 100518 | HNSYqJ | 30 | 256 |
| 344 | 100345 | JuepiX | 30 | 329 |
| 905 | 100906 | JuWALd | 30 | 892 |
| 574 | 100575 | kbyqjX | 30 | 260 |
| 703 | 100704 | KJbprS | 30 | 594 |
| 723 | 100724 | OTdJkY | 30 | 236 |
| 656 | 100657 | Pfgqmj | 30 | 600 |
| 982 | 100983 | qywLqw | 30 | 837 |
| 468 | 100469 | sLEKQW | 30 | 346 |
| 988 | 100989 | UBYqJl | 30 | 457 |
| 173 | 100174 | UltkTN | 30 | 830 |
| 332 | 100333 | YjWiZw | 30 | 824 |
+-----+--------+--------+------+---------+
15 rows in set, 1 warning (0.00 sec)
结果竟然有 filesort的 sql 运行速度, 超过了已经优化掉 filesort的 sql ,而且快了很多,几乎一瞬间就出现了结果。
原因:
所有的排序都是条件消耗之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际上性能很有限。相对的stuno<101000这个条件,如果没有用到索引的话,要对几万条数据进行扫描,这是非常消耗内存的,所以缩影放在这个字段上性价比最高,是最优选
结论:
- 两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的 。
- 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
双路排序 (慢)
MySQL 4.1之前是使用双路排序
,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by
列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取磁盘取其他字段 。
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种
改进的算法,就是单路排序。
单路排序 (快)
从磁盘读取查询需要的所有列
,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输
出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空
间, 因为它把每一行都保存在内存中了。
结论及引申出的问题
多占用很多空间
,因为单路是把所有字段都取出,所以有可能取出的数据总大小超出了sort_buffer
的容量,导致每次只能去sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量的大小,再排。。从而多次I/O。优化策略
尝试提高 sort_buffer_size
尝试提高 max_length_for_sort_data
提高这个参数,会增加改进算法的概率。
SHOW VARIABLES LIKE '%max_length_for_sort_data%'; #默认1024字节
但是如果设置的太高,数据总容量超出sort_buffer_size的概率就会增大,明显症状是高的磁盘I/O活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort_data,使用双路算法,否则使用单路算法。1024-8192字节之间调整。
Order by 时select * 是一个大忌。最好只Query需要的字段。
max_length_for_sort_data
,而且排序不是TEXT|BLOB类型时,会用改进后的算法进行单路排序,否则用老算法多路排序。max_length_for_sort_data
的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size.一般分页查询时,通过创建覆盖索引能够较好的提高性能。一个常见又非常头疼的问题是limit 2000000,10
,此时需要MySQL排序前2000010记录,仅仅返回10条数据,其他记录丢弃,查询排序的代价非常的大。
EXPLAIN SELECT * FROM student LIMIT 2000000,10;
优化思路一
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
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子句用到的所有列
(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。
#6. 覆盖索引
#删除之前的索引
#举例1:
DROP INDEX idx_age_stuno ON student;
CREATE INDEX idx_age_name ON student (age,NAME);
EXPLAIN SELECT * FROM student WHERE age <> 20;
#可以用到索引,因为优化器发现用了索引后的消耗小
EXPLAIN SELECT age,NAME FROM student WHERE age <> 20;
#举例2:
EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc';
EXPLAIN SELECT id,age FROM student WHERE NAME LIKE '%abc';
###
SELECT CRC32('hello')
FROM DUAL;
好处:
弊端:
索引字段的维护
总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务
DBA,或者称为业务数据架构师的工作。
有一张教师表,表定义如下:
create table teacher(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
讲师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:
mysql> select col1, col2 from teacher where email='xxx';
如果email这个字段上没有索引,那么这个语句就只能做全表扫描
。
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
mysql> alter table teacher add index index1(email);
#或
mysql> alter table teacher add index index2(email(6));
这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。
以及
如果使用的是index1(即email整个字符串的索引结构),执行顺序是这样的:
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的是index2(即email(6)索引结构),执行顺序是这样的:
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。
结论:
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考
虑的一个因素。
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
通俗的讲,就是我们可以在索引里面进行操作完后就可以在索引里操作,从而减少随机I/O。
在不使用ICP索引扫描的过程:
storage层:只将满足index key条件的索引记录对应的整行记录取出,返回给server层
server 层:对返回的数据,使用后面的where条件过滤,直至返回最后一行。
使用ICP扫描的过程:
使用前后的成本差别
使用前,存储层多返回了需要被index filter过滤掉的整行记录
使用ICP后,直接就去掉了不满足index filter条件的记录,省去了他们回表和传递到server层的成本。
ICP的加速效果
取决于在存储引擎内通过ICP筛选
掉的数据的比例。
案例1
#举例2:
CREATE TABLE `people` (
`id` INT NOT NULL AUTO_INCREMENT,
`zipcode` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`firstname` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`lastname` VARCHAR(20) COLLATE utf8_bin DEFAULT NULL,
`address` VARCHAR(50) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `zip_last_first` (`zipcode`,`lastname`,`firstname`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
INSERT INTO `people` VALUES
('1', '000001', '三', '张', '北京市'),
('2', '000002', '四', '李', '南京市'),
('3', '000003', '五', '王', '上海市'),
('4', '000001', '六', '赵', '天津市');
EXPLAIN SELECT * FROM people
WHERE zipcode='000001'
AND lastname LIKE '%张%'
AND address LIKE '%北京市%';
EXPLAIN SELECT * FROM people
WHERE zipcode='000001'
AND lastname LIKE '张%'
AND firstname LIKE '三%';
#关闭ICP
SET optimizer_switch = 'index_condition_pushdown=off';
#打开ICP
SET optimizer_switch = 'index_condition_pushdown=on';
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引,假设字段 k 上的值都不重复。
这个表的建表语句是:
mysql> create table test(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6)。
问题:
不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?
回答:
问:在 MySQL 中统计数据表的行数,可以使用三种方式:SELECT COUNT(*)
、SELECT COUNT(1)
和
SELECT COUNT(具体字段)
,使用这三者之间的查询效率是怎样的?
答:
聊一个实际问题:淘宝的数据库,主键是如何设计的?
某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显的错误就是关于MySQL的主键设计。
大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错 !
这样的回答,只站在了数据库这一层,而没有 从业务的角度 思考主键。主键就是一个自增ID吗?站在2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到 。
我们推测业务的主键很有可能是(订单)下单时间的时间戳加上随机生成数亦或者别的方法,但绝对不是简单的自增。而且使用这种方式还避免了UUID的不一定自增的结果。
如何确定呢?一般情况下,有如下几种方式:
除了活动会话监控以外,我们也可以对事务 、 锁等待等进行监控,这些都可以帮助我们对数据库的运
行状态有更全面的认识。
我们需要调优的对象是整个数据库管理系统,它不仅包括 SQL 查询,还包括数据库的部署配置、架构等。从这个角度来说,我们思考的维度就不仅仅局限在 SQL 优化上了。通过如下的步骤我们进行梳理:
nnodb_buffer_pool_size
:这个参数是Mysql数据库最重要的参数之一,表示InnoDB类型的表和索引的最大缓存
。它不仅仅缓存索引数据
,还会缓存 表的数据
。这个值越大,查询的速度就会越快。但是这个值太大会影响操作系统的性能。
key_buffer_size
:表示索引缓冲区的大小 。索引缓冲区是所有的线程共享 。增加索引缓冲区可以得到更好处理的索引(对所有读和多重写)。当然,这个值不是越大越好,它的大小取决于内存的大小。如果这个值太大,就会导致操作系统频繁换页,也会降低系统性能。对于内存在 4GB 左右的服务器该参数可设置为 256M 或 384M 。
table_cache
:表示同时打开的表的个数 。这个值越大,能够同时打开的表的个数越多。物理内存越大,设置就越大。默认为2402,调到512-1024最佳。这个值不是越大越好,因为同时打开的表太多会影响操作系统的性能。
query_cache_size
:表示查询缓冲区的大小 。可以通过在MySQL控制台观察,如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,就要增加Query_cache_size的值;如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。MySQL8.0之后失效。该参数需要和query_cache_type配合使用。
query_cache_type
的值是0时,所有的查询都不使用查询缓存区。但是query_cache_type=0并不会导致MySQL释放query_cache_size所配置的缓存区内存。
sort_buffer_size
:表示每个需要进行排序的线程分配的缓冲区的大小。增加这个参数的值可以提高 ORDER BY 或 GROUP BY 操作的速度。默认数值是2 097 144字节(约2MB)。对于内存在4GB左右的服务器推荐设置为6-8M,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 × 6= 600MB。
join_buffer_size = 8M
:表示 联合查询操作所能使用的缓冲区大小 ,和sort_buffer_size一样,该参数对应的分配内存也是每个连接独享。
read_buffer_size
:表示 每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节) 。当线程从表中连续读取记录时需要用到这个缓冲区。SET SESSION read_buffer_size=n可以临时设置该参数的值。默认为64K,可以设置为4M。
innodb_flush_log_at_trx_commit
:表示何时将缓冲区的数据写入日志文件 ,并且将日志文件写入磁盘中。该参数对于innoDB引擎非常重要。该参数有3个值,分别为0、1和2。该参数的默认值为1。
innodb_log_buffer_size
:这是 InnoDB 存储引擎的 事务日志所使用的缓冲区 。为了提高性能,也是先将信息写入 Innodb Log Buffer 中,当满足 innodb_flush_log_trx_commit 参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件(或者同步到磁盘)中。
max_connections
:表示 允许连接到MySQL数据库的最大数量 ,默认值是 151 。如果状态变量connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。这个连接数 不是越大越好 ,因为这些连接会浪费内存的资源。过多的连接可能会导致MySQL服务器僵死。
back_log
:用于 控制MySQL监听TCP端口时设置的积压请求栈大小 。如果MySql的连接数达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 对于Linux系统推荐设置为小于512的整数,但最大不超过900。
如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大back_log 的值。
thread_cache_size
: 线程池缓存线程数量的大小 ,当客户端断开连接后将当前线程缓存起来,当在接到新的连接请求时快速响应无需创建新的线程 。这尤其对那些使用短连接的应用程序来说可以极大的提高创建连接的效率。那么为了提高性能可以增大该参数的值。默认为60,可以设置为120。
可以通过如下几个MySQL状态值来适当调整线程池的大小:
当 Threads_cached 越来越少,但 Threads_connected 始终不降,且 Threads_created 持续升高,可适当增加 thread_cache_size 的大小。
wait_timeout
:指定 一个请求的最大连接时间 ,对于4GB左右内存的服务器可以设置为5-10。
interactive_timeout
:表示服务器在关闭连接前等待行动的秒数。
这里给出一份my.cnf的参考配置:
[mysqld]
port = 3306serverid = 1
socket = /tmp/mysql.sock skip-locking #避免MySQL的外部锁定,减少
出错几率增强稳定性。 skip-name-resolve #禁止MySQL对外部连接进行DNS解析,使用这一选
项可以消除MySQL进行DNS解析的时间。但需要注意,如果开启该选项,则所有远程主机连接授权
都要使用IP地址方式,否则MySQL将无法正常处理连接请求!back_log = 384
key_buffer_size = 256Mmax_allowed_packet = 4M
thread_stack = 256K
table_cache = 128Ksort_buffer_size = 6M
read_buffer_size = 4M
read_rnd_buffer_size=16Mjoin_buffer_size = 8M
myisam_sort_buffer_size =64M
table_cache = 512
thread_cache_size = 64
query_cache_size = 64M
tmp_table_size = 256Mmax_connections = 768
max_connect_errors = 10000000
wait_timeout = 10thread_concurrency = 8 #该参数取值为服务器逻辑CPU数量2,在本
例中,服务器有2颗物理CPU,而每颗物理CPU又支持H.T超线程,所以实际取值为42=8 skip-
networking #开启该选项可以彻底关闭MySQL的TCP/IP连接方式,如果WEB服务器是以远程连接
的方式访问MySQL数据库服务器则不要开启该选项!否则将无法正常连接! table_cache=1024
innodb_additional_mem_pool_size=4M #默认为2Minnodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=2M #默认为1M innodb_thread_concurrency=8 #你的服务器CPU
有几个就设置为几。建议用默认一般为8 tmp_table_size=64M #默认为16M,调到64-256最挂
thread_cache_size=120query_cache_size=32M
举例1: 会员members表 存储会员登录认证信息,该表中有很多字段,如id、姓名、密码、地址、电话、个人描述字段。其中地址、电话、个人描述等字段并不常用,可以将这些不常用的字段分解出另一个表。将这个表取名叫members_detail,表中有member_id、address、telephone、description等字段。这样就把会员表分成了两个表,分别为 members表 和 members_detail表 。
创建这两个表的SQL语句如下:
CREATE TABLE members (
id int(11) NOT NULL AUTO_INCREMENT,
username varchar(50) DEFAULT NULL,
password varchar(50) DEFAULT NULL,
last_login_time datetime DEFAULT NULL,
last_login_ip varchar(100) DEFAULT NULL,
PRIMARY KEY(Id)
);
CREATE TABLE members_detail (
Member_id int(11) NOT NULL DEFAULT 0,
address varchar(255) DEFAULT NULL,
telephone varchar(255) DEFAULT NULL,
description text
);
如果需要查询会员的基本信息或详细信息,那么可以用会员的id来查询。如果需要将会员的基本信息和详细信息同时显示,那么可以将members表和members_detail表进行联合查询,查询语句如下:
SELECT * FROM members LEFT JOIN members_detail on members.id =members_detail.member_id;
通过这种分解可以提高表的查询效率。对于字段很多且有些字段使用不频繁的表,可以通过这种分解的
方式来优化数据库的性能。
举例1: 学生信息表 和 班级表 的SQL语句如下:
CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE
现在有一个模块需要经常查询带有学生名称(name)、学生所在班级名称(className)、学生班级班长(monitor)的学生信息。根据这种情况可以创建一个 temp_student 表。temp_student表中存储学生名称(stu_name)、学生所在班级名称(className)和学生班级班长(monitor)信息。创建表的语句如下:
CREATE TABLE `temp_student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stu_name` INT NOT NULL ,
`className` VARCHAR(20) DEFAULT NULL,
`monitor` INT(3) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
接下来,从学生信息表和班级表中查询相关信息存储到临时表中:
insert into temp_student(stu_name,className,monitor)
select s.name,c.className,c.monitor
from student as s,class as c
where s.classId = c.id
以后,可以直接从temp_student表中查询学生名称、班级名称和班级班长,而不用每次都进行联合查询。这样可以提高数据库的查询速度。
设计数据库表时应尽量遵循范式理论的规约,尽可能减少冗余字段,让数据库设计看起来精致、优雅。但是,合理地加入冗余字段可以提高查询速度。
表的规范化程度越高,表与表之间的关系就越多,需要连接查询的情况也就越多。尤其在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。
这部分内容在《第11章_数据库的设计规范》章节中 反范式化小节 中具体展开讲解了。这里省略。
当MySQL单表记录过大的时候,数据库的CRUD性能就会明显下降,一些常见的优化措施如下:
禁止不带任何限制数据范围条件的查询语句。比如:我们当用户再查询订单历史的时候,我们可以控制再一个月的范围内。
经典的数据库拆分方案,主库负责写,从库负责读。
当数据级达到千万级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器中,减少对单一数据服务器访问的压力。
垂直拆分的优点:可以是得数据变小,再查询的时候减少读取的Block数,减少I/0次数。此外,处置分区可以简化表的结构,易于维护。
垂直拆分的缺点:主键会出现冗余,需要管理冗余列,会引起JOIN操作。此外,垂直拆分会让事务变得更加复杂
下面补充一下数据库分片的两种常见方案:
在MySQL 8.0中可以设置服务器语句超时的限制 ,单位可以达到毫秒级别 。当中断的执行语句超过设置的毫秒数后,服务器将终止查询影响不大的事务或连接,然后将错误报给客户端。
设置服务器语句超时的限制,可以通过设置系统变量MAX_EXECUTION_TIME
来实现。默认情况下,MAX_EXECUTION_TIME的值为0,代表没有时间限制。 例如:
SET GLOBAL MAX_EXECUTION_TIME=2000;
SET SESSION MAX_EXECUTION_TIME=2000; #指定该会话中SELECT语句的超时时间
满足ACID特性的一组操作,要么全部成功,要么全部失败,可以通过Commit提交,或Rollback回滚
举个栗子
转账操作:小明要给小红转账1000,这个事物涉及两个操作:
而事物会把这两个操作再逻辑上看成一个整体,要么全部成功,要么全部失败
这样因为事物的存在就不会出现小明余额减少而小红余额没变的情况
数据库事物的作用
数据库的事物可以保证多个对数据库的操作(sql语句)构成一个逻辑上的整体,而这个逻辑上的整体需要遵循:要么全部成功、要么全部失败
# 开启一个事物
START TRANSACTION;
# 多余的sql语句
sql1,sql2...
# 提交事物
COMMIT;
原子性(Atomicity)
事物被视为不可分割的修小单位,事物的所有操作要么全部成功 ,要么全部失败回滚,而回滚可以通过回滚日志(undo log)来实现
一致性(Consistency)
事物在执行前后保持一致性状态,例如转账事物中,无论是否成功,转账人和收款人总额不变
隔离性(Lsolation)
一个事物在最终提交前,对其他事物不可见
持久性(Durability)
一旦事物提交,则所作修改时永久性的保留在数据库中 ,即使发生系统崩溃,事物执行过的结果也不会丢失。 系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。
事物的ACID并不是一种平级关系:
只有满足一致性,事物的执行结果才是正确的
在无并发的情况下,事物串行执行,隔离性一定能满足,只需要满足原子性,就一定满足一致性
在并发情况下,多个事物并行执行,事物不仅要满足原子性、还需要满足隔离性,才能保证一致性
事物满足持久化是为了能应对系统崩溃的情况
保证了事物的原子性、持久性、隔离性,才能保证一致性
事务是一个抽象概念,对应着一个或多个数据库操作,根据这些操作所执行的不同阶段把事务大致分为几个状态:
活动的(active)
事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。
部分提交的(partially comitted)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态
失败的(failed)
当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败的 状态。
中止的(aborted)
如果事务执行了一部分而变为 失败的 状态,那么就需要把已经修改的事务中的操作还原到事务执 行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。
提交的(committed)
当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了 提交的 状态。
事务分为显示事务和隐式事务
步骤一:START TRANSACTION 或 BEGIN,作用是显示开启一个事务
mysql> BEGIN;
#或者
mysql> START TRANSACTION;
START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
步骤二:一系列事务中的操作(主要是DML,不含DDL)
步骤三:提交事务或终止事务(即回滚事务)
# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]
当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:
显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
把系统变量 autocommit 的值设置为 OFF ,就像这样:
SET autocommit = OFF;
#或
SET autocommit = 0;
数据定义语言(DDL)
隐式使用或修改mysql数据库中的表
事务控制或关于锁定的语句
① 当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了
另一个事务时,会 隐式的提交 上一个事务。即:
② 当前的 autocommit 系统变量的值为 OFF ,我们手动把它调为 ON 时,也会 隐式的提交 前边语
句所属的事务。
③ 使用 LOCK TABLES 、 UNLOCK TABLES 等关于锁定的语句也会 隐式的提交 前边语句所属的事
务。
加载数据的语句
关于MySQL复制的一些语句
其他一些语句
当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事 务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。
当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。 不过这时,如果你采用 START TRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。
在并发环境下,事物的隔离性很难保证,因此会出现很多并发一致性问题
丢失修改
丢失修改是指一个事物的更新操作被另一个事物更新操作替换。例如:T1和T2两个事物对一个数据修改,T1先修改并提交生效,T2随后修改,T2的修改覆盖了T1的修改
读脏数据(脏写)
读脏数据指在不同事物下,当前事物读取到了另外事物未提交的数据。 例如:T1 修改一个数据但未提交,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
不可重复读
不可重复度指一个事物内多次读取同一数据集合,在这一事物还未结束前,另一事物也访问了同一数据集合并做了修改,从而导致第一个事物两次读取同一数据结果不一致。例如 :T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
幻影读
某次select的结果不足以支撑后续业务操作
幻读本质上也属于不可重复读, T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
不可重复读和幻读区别
不可重复读的重点是修改 比如多次读取一条记录发现其中某些列的值被修改 ,而幻读的重点在于新增或删除 比如多次查询同一条查询语句(DQL)时,记录发现记录增多或减少了。
举例:
//例一:事物1中A读取自己的工资1000的操作还没完成,事物2的B就修改了A的工资为2000,导致A再次读取自己的工资变为2000;这就是不可重复度
//例二:某工资单表中薪资大于3000的有4人,事物1读取了所有工资大于3000的人查到四条记录,这时事物2插入了一条大于3000的记录,事物1再次读取发现变为了5条,这就是幻读
产生并发不一致性问题的主要原因是破坏了事物的隔离级别,解决方法是通过并发控制来保证隔离性。并发控制通过封锁来实现,但封锁操作需要用户操作,数据库管理系统提供了事物的隔离性级别,以轻松方式处理并发一致性问题
事物中的修改,即使没有提交对其他事物也是可见。可能会发生脏读、幻读,不可重复度
一个事务只能读取已经提交的事物所作的修改,解决脏读,会发生不可重复读、幻读
保证在一个事物中多次读取统一数据的结果一样,解决脏读和不可重复读,会发生幻读
强制事物串行执行,多个事物互不干扰,不会出现并发一致性问题。
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事物执行,也就是保证事物串行执行。解决脏读、不可重复读、幻读
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL8.0默认隔离级别是可重复读
MySQL的InnoDB的可重复读,加上加锁读来保证避免幻读。而这个加锁读的机制是Next-Key Locks
通过一下语句修改事物的隔离级别
SET [GLOBAL|SESSION] TRANSACTION IOSLATION LEVEL 隔离级别;
其中隔离级别:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
或者
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION='隔离级别'
其中隔离级别:
READ-UNCOMMITTED
READ-COMMITTED
REPETABLE-READ
SERIALIZABLE
关于设置时使用GLOBAL或SESSION的影响:
使用GLOBAL关键字(全局范围):
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 或者
SET GLOBAL TRANSACTION_ISOLATION='SERIALIZABLE';
则:
使用SESSION关键字(在会话范围影响):
SET SESSION TRANSCATION IOSLATION LEVEL SERIALIZABLE;
#或者
SET SESSION TRANSACTION_ISOLATION='SERIALIZABLE';
则:
小结:数据库规定了多种事物隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,并发度越低
使用2个命令行,模拟多线程(多事物)对同一份数据的脏读
MySQL 命令行的默认配置中事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。如果要显式地开启一个事务需要使用命令:START TRANSACTION
。
我们可以通过下面的命令来设置隔离级别。
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
我们再来看一下我们在下面实际操作中使用到的一些并发控制语句:
START TRANSACTION
|BEGIN
:显式地开启一个事务。COMMIT
:提交事务,使得对数据库做的所有修改成为永久性。ROLLBACK
:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。解决幻读方法
解决幻读方法有很多,但核心思想就是一个事物在操作这张表数据时,另外一个事物不允许新增或删除这张表的数据,解决幻读方式:
1.将事物隔离级别调整为SERIALIZABLE
2.在可重复读隔离级别下,给事物操作的这张表添加表锁
3.在可重复读隔离级别下,给事物操作的这张表添加Next-key Locks
说明:Next-key Locks 相当于行锁+间歇锁
从事务理论角度:
事物有四种特性:原子性、隔离性、一致性、持久性
两种日志都可以视为恢复操作,但是:
InnoDB存储引擎是以页为单位来管理存储空间的,在真正访问页面之前,需要把磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘(checkPoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样来保证整体性能下降太快。
一方面,缓冲池的存在可以帮助我们消除CPU和磁盘间的鸿沟,checkpoint机制可以保证数据的最终落点,然而由于checkpoint并不是每次数据变更都触发,而是以一定频率去处理。所以最坏情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就会丢失。
另一方面,事务包含持久性特性,就是说一个已提交的事务所作的修改应该是永久的,即使发生宕机所作的更改也不应该丢失。
那么,应该如何保持这个持久性呢?一个简单的做法:在事务提交完成之前把该事务所作的修改的所有页面刷新到磁盘,但这个做法有些问题:
修改量与刷新磁盘工作量严重不成比例
有时候我们仅仅修改了某个数据页的一个字节,但在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在提交事务时不得不将一整个数据页从内存中刷新到磁盘,而一个数据页有16KB大小,只修改一个字节就要刷新16KB数据到磁盘显得小题大做
随机IO刷新较慢
一个事务可以包含很多语句,即使一条语句也可能修改很多页面,如果事务修改的页面并不相邻,这意味着在将事务修改的Bufer Pool中的页面刷新到磁盘时,会有很多随机IO,而随机IO要比顺序IO慢很多
另一个解决思路:我们只是想让事务所作的修改对数据库数据永久生效,即使系统崩溃,在重启后也能恢复过来,所以没有必要再每次事务提交后,将事务所作的再内存中修改的数据页刷新到磁盘,而是只需要将所作的修改记录下来即可,比如某个事务将系统表空间中第10号页面中的偏移量为100的哪个字节的值1修改为2,我们秩只需记录一下:将第10号表空间的偏移量100的值修改为2即可。
InnooDB引擎的事务采用了WAL技术(Write-Ahead Loging),这种技术的思想先写日志,再写磁盘,只有日志写成功,才算事务提交成功,这里的日志就是redo log,当系统发生宕机且数据未刷到磁盘的时候,可以通过redo log来恢复,保证ACID的D,这就是redo log的作用。
1.好处
存储表空间ID、也好、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘块
2.特点
redo日志是顺序写入磁盘的
在执行事务过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的
事务执行过程由redo log不断记录
redo log更bing log的区别redo log是存储引擎层产生的,而bing log是数据库层产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,一直不断的往redo log里顺序记录,我、而bin log 不会记录,直到事务提交,才会一次写入到bin log文件中。
redo log可以分为以下两个部分:
重做日志的缓冲(redo log buffer),保存在内存,易丢失
在服务器启动时向操作系统申请的一大片称之为redo log buffer的连续内存空间,翻译成中文是redo日志缓冲区。这片内存空间被划分为若干个连续的redo log block。一个redo log block占用512字节大小
参数设置:innodb_log_buffer_size;
重做日志文件(redo log file),保存在硬盘中,持久的
Write-Ahead Log(预先日志持久化),在持久化一个数据页之前,先将内存中相应的日志页持久化
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以一定频率刷入到真正的redo log file中。这里的频率就是要说的刷盘策略
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘时机是根据策略进行的。
每条redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成
//注意:redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘,只是刷入到文件系统缓存(page cache)中(现代系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己决定(比如page cache足够大了)。对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小)
针对这种情况,InnoDB给出innodb_flush_log参数,该参数控制commit提交事务时,如何将redo log bufer中的日志刷新到redo log file中,它支持三种策略:
另外,InnoDB存储引擎由一个后台线程,每隔1s,就会把redo log buffer中的内容写入到文件系统缓存(page cache),然后调用刷盘操作。
也就是说,一个没有提交事务的redo log记录,也可能刷盘。因为在事务执行过程中redo log记录是会写入redo log buffer中的,这些redo log记录会被后台线程刷盘
除了后台线程每秒1次的轮询操作,还有一种情况,当redo log buffer占用空间即将达到innodb_log_buffer_size(这个参数默认16M)的一半的时候,后台线程会主动刷盘
1.流程图
Innodb_flush_log_at_trx_commit=1
小结:Innodb_flush_log_at_trx_commit=1
为1时,只要事务提交成功,redo log记录就一定在硬盘里,不会有数据丢失。
如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但事务并没有提交,所以日志丢了也不会有损失,可以保证ACID的D,数据绝对不会丢失,但是效率最差。
建议使用默认值,虽然操作系统宕机概率理论小于数据库宕机概率,但一般既然使用了事务,那么数据的安全更重要些
Innodb_flush_log_at_trx_commit=2
小结:Innodb_flush_log_at_trx_commit=2
为2时,只要事务提交成功,redo log buffer中的内容只写入文件系统缓存(page cache)
如果仅仅只是MySQL挂了不会有数据丢失,但是操作系统宕机可能会有1秒数据的丢失,这种情况下无法满足ACID中的D,但是数值2效率更高
Innodb_flush_log_at_trx_commit=0
小结:Innodb_flush_log_at_trx_commit=0
为0时,master thread中每1秒进行一次重做日志的fsync操作,因此示例crash最多丢失1秒的事务(master thread负责将缓冲池中的数据异步刷新到磁盘,保证数据一致性)
数值0的话,是一种折中的做法,它的效率理论高于1,低于2的,这种策略也有丢失数据的风险,也无法保证D
MySQL把对底层页面中的一次原子访问的过程成为一个Mini-Transaction,简称mtr,比如向某个索引对应的B+树中插入一条记录的过程就是一个Mini-Transaction。一个所谓的mtr可以包含一组redo日志,在进行崩溃恢复时这一组redo日志作为一个不可分割的整体
一个事务可以包含若干条语句,每一条语句其实可以包含若干个mtr,每一个mtr又可以包含若干条redo日志,如图:
向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。当我们想往log buffer中写入日志时,第一个遇到的问题时往哪个block中的哪个偏移量处,所以InnoDB的设计者特意提供了一个称之为buf_free的全局变量,该变量指明后续redo日志应该写入到log buffer中的哪个位置,如图:
一个mtr执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,所以其实并不是每生成一条redo日志,就将其插入到log buffer中,而是每个mtr运行过程中产生的日志先暂时存到一个地方,当该mtr结束的时候,将过程中产生的一组redo日志再全部复制到log bufer。我们假设两个名为T1、T2的事务都包含2个mtr,我们给这几个mtr命名一下:
每个mtr都会产生一组redo日志,用示意图来描述一下这些mtr产生的日志情况
不同的事物可能是并发执行的,所以T1、T2之间的mtr可能是交替执行。每当一个mtr执行完成,伴随着该mtr生成的一组redo日志就需要被复制到log buffer中,也就是说不同事务的mtr可能是交替写入log buffer的,如图:
有的mtr产生的redo日志量非常大,比如mtr_t1_2产生的redo日志占用空间比较大,占用了3个block来存储
一个redo log block是由日志头、日志体、日志尾组成。日志头占用12字节,日志尾占用8字节,所以一个block真正能存储的数据就是512-12-8=492字节。
为什么一个block是512字节?
和磁盘的扇区有关,机械硬盘默认扇区就是512字节,如果要写入的数据大于512字节,那么要写入的扇区肯定不止一个,这是就涉及到盘片的转动,找到下一个扇区,假设现在需要写入两个扇区A和B,如果扇区A写入成功,但扇区B失败,那么会出现非原子性的写入 ,而如果每次只写入和扇区大小一样二点512字节,那么每次的写入都是原子性的
1.相关参数设置
mysql>show variables like 'innodb_log_files_in_group';
根据业务修改其大小,以便容纳较大的事务。编辑my.cnf文件并重启数据库生效,如下图所示:
在数据库实例更新比较频繁的情况下,可以适当加大redo log组数和大小。但也不推荐redo log设置过大,在mysql崩溃恢复时会重新执行redo日志中的记录
总共的redo日志文件大小其实就是: innodb_log_file_size × innodb_log_files_in_group 。
采用循环使用的方式向redo日志文件组里写数据的话,会导致后写入的redo日志覆盖掉前边写的redo日志?当然!所以InnoDB的设计者提出了checkpoint的概念。
3.checkpoint
在整个日志文件组有两个重要属性:
write pos:当前记录的位置,一边写一边后移
checkpoint:当前要擦除的位置,也是往后移
每次刷盘redo log记录到日志文件组中,write pos位置就会后移更新。每次mysql加载日志文件组恢复数据时,会清空加载过的redo log记录,并把checkpoint后移更新。write pos和checkpoint之间空着的部分可以用来写入新的redo log记录
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
InnoDB的更新操作采用的是Write Ahead Log(预先日志持久化)策略,即先写日志,再写入磁盘
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log 。
事务需要保证原子性;也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
以上情况出现,就需要把数据改回原来的样子,这个过程称之为回滚,这样就造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。
每当对一条记录进行改动时,(这里的改动是指insert、delete、update),都需要”留一手“–把回滚时所需的东西记录下来。比如:
MySQL把这些为了回滚而记录的这些内容称之为撤销日志或者回滚日志,查询操作并不会修改任何记录,所以在查询操作时,并不需要相应的undo日志。
此外,undo log会产生redo log,也就是undo log的产生伴随着redo log的产生,这是因为undo log也需要持久性保护
3.1回滚段与undo页
InnoDB对undo log的管理采用段的方式,也就是 回滚段(rollback segment) 。每个回滚段记录了1024 个 undo log segment ,而在每个undo log segment段中进行 undo页 的申请。
3.2回滚段与事务
1.每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数 据会被复制到回滚段。
在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘 区或者在回滚段允许的情况下扩展新的盘区来使用。
回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个undo表空间。
当事务提交时,InnoDB存储引擎会做以下两件事情:
3.3回滚段中的数据分类
未提交的回滚数据(uncommitted undo information)
已经提交但未过期的回滚数据(committed undo information)
事务已经提交并过期的数据(expired undo information)
在InnoDB存储引擎中,undo log分为:
5.1简要生成过程
5.2详细生成过程
当我们执行insert时:
begin;
INSERT INTO user (name) VALUES (“tom”);
UPDATE user SET id=2 WHERE id=1;
5.3undo log如何回滚
以上面的例子来说,假设执行rollback,那么对应的流程应该是这样:
通过undo no=3的日志把id=2的数据删除
通过undo no=2的日志把id=1的数据的deletemark还原成0
通过undo no=1的日志把id=1的数据的name还原成Tom
通过undo no=0的日志把id=1的数据删除
5.4undo log的删除
因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJRWiHwJ-1657680141227)(MySQL/1651158117353.png)]
undo log是逻辑日志,对事物回滚时,只是将数据库逻辑地恢复到原来的样子
redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程
对于线上数据库应用系统,突然遭遇数据库宕机怎么办?在这种情况下,定位宕机的原因就非常关键了。我们可以查看数据库的错误日志。因为日志中记录了数据运行中的诊断信息,包括了错误,警告和注解信息。比如:日志中发现了某个连接中的sql擦欧总发生了死循环。导致内存不足,被系统强行终止了。明确了原因,处理起来就很轻松了,系统就很快恢复。
除了发现错误,日志在数据复制,数据恢复,操作审计,以及确保数据的永久性和一致性等方面,都有着不可替代的作用。
千万不要小看日志。很多看似奇怪的问题,答案往往就藏在日志里。很多情况下,只有通过查看日志才
能发现问题的原因,真正解决问题。所以,一定要学会查看日志,养成检查日志的习惯,对提升你的数
据库应用开发能力至关重要。
MySQL8.0 官网日志地址:“ https://dev.mysql.com/doc/refman/8.0/en/server-logs.html ”
MySQL有不同类型的日志文件,用来存储不同类型的日志,分为 二进制日志 、 错误日志 、 通用查询日志和慢查询日志 ,这也是常用的4种。MySQL 8又新增两种支持的日志:中继日志和数据定义语句日志 。使用这些日志文件,可以查看MySQL内部发生的事情。
这6类日志分别为:
除二进制日志外,其他日志都是文本文件。默认情况下,所有日志创建于MySQL数据目录 中。
前面已经讲过了。
通用查询日志用来记录用户的所有操作 ,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等。当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。
mysql> SHOW VARIABLES LIKE '%general%';
+------------------+------------------------------+
| Variable_name | Value |
+------------------+------------------------------+
| general_log | OFF | #通用查询日志处于关闭状态
| general_log_file | /var/lib/mysql/atguigu01.log | #通用查询日志文件的名称是atguigu01.log
+------------------+------------------------------+
2 rows in set (0.03 sec)
方式1:永久性方式
修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启MySQL服务。格式如下:
[mysqld]
general_log=ON
general_log_file=[path[filename]] #日志文件所在目录路径,filename为日志文件名
如果不指定目录和文件名,通用查询日志将默认存储在MySQL数据目录中的hostname.log文件中,hostname表示主机名。
方式2:临时性方式
SET GLOBAL general_log=on; # 开启通用查询日志
SET GLOBAL general_log_file=’path/filename’; # 设置日志文件保存位置
对应的,关闭操作SQL命令如下
SET GLOBAL general_log=off; # 关闭通用查询日志
查看设置后情况:
SHOW VARIABLES LIKE 'general_log%';
通用查询日志是以文本文件的形式存储在文件系统中的,可以使用文本编辑器直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。
从 SHOW VARIABLES LIKE ‘general_log%’; 结果中可以看到通用查询日志的位置。
/usr/sbin/mysqld, Version: 8.0.26 (MySQL Community Server - GPL). started with:
Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock
Time Id Command Argument
2022-01-04T07:44:58.052890Z 10 Query SHOW VARIABLES LIKE '%general%'
2022-01-04T07:45:15.666672Z 10 Query SHOW VARIABLES LIKE 'general_log%'
2022-01-04T07:45:28.970765Z 10 Query select * from student
2022-01-04T07:47:38.706804Z 11 Connect root@localhost on using Socket
2022-01-04T07:47:38.707435Z 11 Query select @@version_comment limit 1
2022-01-04T07:48:21.384886Z 12 Connect [email protected] on using TCP/IP
2022-01-04T07:48:21.385253Z 12 Query SET NAMES utf8
2022-01-04T07:48:21.385640Z 12 Query USE `atguigu12`
2022-01-04T07:48:21.386179Z 12 Query SHOW FULL TABLES WHERE Table_Type !=
'VIEW'
2022-01-04T07:48:23.901778Z 13 Connect [email protected] on using TCP/IP
2022-01-04T07:48:23.902128Z 13 Query SET NAMES utf8
2022-01-04T07:48:23.905179Z 13 Query USE `atguigu`
2022-01-04T07:48:23.905825Z 13 Query SHOW FULL TABLES WHERE Table_Type !=
'VIEW'
2022-01-04T07:48:32.163833Z 14 Connect [email protected] on using TCP/IP
2022-01-04T07:48:32.164451Z 14 Query SET NAMES utf8
2022-01-04T07:48:32.164840Z 14 Query USE `atguigu`
2022-01-04T07:48:40.006687Z 14 Query select * from account
在通用查询日志里面,我们可以清楚地看到,什么时候开启了新的客户端登陆数据库,登录之后做了什么 SQL 操作,针对的是哪个数据表等信息。
方式1:永久性方式
修改 my.cnf 或者 my.ini 文件,把[mysqld]组下的 general_log 值设置为 OFF 或者把general_log一项注释掉。修改保存后,再 重启MySQL服务 ,即可生效。 举例1:
[mysqld]
general_log=OFF
举例2:
[mysqld]
#general_log=ON
方式2:临时性方式
使用SET语句停止MySQL通用查询日志功能:
SET GLOBAL general_log=off;
查询通用日志功能:
SHOW VARIABLES LIKE 'general_log%';
如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很长时间之前的查询日志,以保证MySQL服务器上的硬盘空间。
手动删除文件
SHOW VARIABLES LIKE 'general_log%';
可以看出,通用查询日志的目录默认为MySQL数据目录。在该目录下手动删除通用查询日志atguigu01.log。
使用如下命令重新生成查询日志文件,具体命令如下。刷新MySQL数据目录,发现创建了新的日志文件。前提一定要开启通用日志。
mysqladmin -uroot -p flush-logs
在MySQL数据库中,错误日志功能是默认开启的。而且,错误日志无法被禁止 。
默认情况下,错误日志存储在MySQL数据库的数据文件夹下,名称默认为 mysqld.log (Linux系统)或
hostname.err (mac系统)。如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:
[mysqld]
log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名
修改配置项后,需要重启MySQL服务以生效。
MySQL错误日志是以文本文件形式存储的,可以使用文本编辑器直接查看。
查询错误日志的存储路径:
mysql> SHOW VARIABLES LIKE 'log_err%';
+----------------------------+----------------------------------------+
| Variable_name | Value |
+----------------------------+----------------------------------------+
| log_error | /var/log/mysqld.log |
| log_error_services | log_filter_internal; log_sink_internal |
| log_error_suppression_list | |
| log_error_verbosity | 2 |
+----------------------------+----------------------------------------+
4 rows in set (0.01 sec)
在window中,一般都在C:\ProgramData\MySQL\MySQL Server 8.0\Data
中存放所有的错误日志。
执行结果中可以看到错误日志文件是mysqld.log,位于MySQL默认的数据目录下。
对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除,以保证MySQL服务器上的 硬盘空间 。MySQL的错误日志是以文本文件的形式存储在文件系统中的,可以直接删除 。
[root@atguigu01 log]# mysqladmin -uroot -p flush-logs
Enter password:
mysqladmin: refresh failed; error: 'Could not open file '/var/log/mysqld.log' for error logging.'
官网提示:
补充操作:
install -omysql -gmysql -m0644 /dev/null /var/log/mysqld.log
binlog可以说是MySQL中比较重要的日志了,在日常开发及运维过程中,经常会遇到。
binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDL 和 DML 等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。
它是以事务形式记录并保存在二进制文件中。通过这些信息,我们可以再现数据库更新操作的全过程。
如果想要记录所有的语句(例如,为了识别有问题的查询),需要使用通用查询日志。
binlog主要应用场景:
可以说MYSQL数据库的数据备份,主备,主主,主从都离不开binlog,需要依赖binlog来同步数据,保证数据的一致性。
查看记录二进制日志是否开启:在MySQL8中默认情况下,二进制文件是开启的。
mysql> show variables like '%log_bin%';
+---------------------------------+----------------------------------+
| Variable_name | Value |
+---------------------------------+----------------------------------+
| log_bin | ON |
| log_bin_basename | /var/lib/mysql/binlog |
| log_bin_index | /var/lib/mysql/binlog.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+----------------------------------+
6 rows in set (0.00 sec)
方式1:永久性方式
修改MySQL的 my.cnf 或my.ini文件可以设置二进制日志的相关参数:
mysqld]
#启用二进制日志
log-bin=atguigu-bin
binlog_expire_logs_seconds=600
max_binlog_size=100M
重新启动MySQL服务,查询二进制日志的信息,执行结果:
mysql> show variables like '%log_bin%';
+---------------------------------+----------------------------------+
| Variable_name | Value |
+---------------------------------+----------------------------------+
| log_bin | ON |
| log_bin_basename | /var/lib/mysql/atguigu-bin |
| log_bin_index | /var/lib/mysql/atguigu-bin.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+----------------------------------+
6 rows in set (0.00 sec)
设置带文件夹的bin-log日志存放目录
如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:
[mysqld]
log-bin="/var/lib/mysql/binlog/atguigu-bin"
注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。
chown -R -v mysql:mysql binlog
方式2:临时性方式
如果不希望通过修改配置文件并重启的方式设置二进制日志的话,还可以使用如下指令,需要注意的是在mysql8中只有 会话级别 的设置,没有了global级别的设置。
# global 级别
mysql> set global sql_log_bin=0;
ERROR 1228 (HY000): Variable 'sql_log_bin' is a SESSION variable and can`t be used
with SET GLOBAL
# session级别
mysql> SET sql_log_bin=0;
Query OK, 0 rows affected (0.01 秒)
当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。
MySQL服务 重新启动一次 ,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过了max_binlog_size
的上限(默认是1GB),就会创建一个新的日志文件。
查看当前的二进制日志文件列表及大小。指令如下:
mysql> SHOW BINARY LOGS;
+--------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+--------------------+-----------+-----------+
| atguigu-bin.000001 | 156 | No |
+--------------------+-----------+-----------+
1 行于数据集 (0.02 秒)
下面命令将行事件以伪SQL的形式
表现出来
mysqlbinlog -v "/var/lib/mysql/binlog/atguigu-bin.000002"
#220105 9:16:37 server id 1 end_log_pos 324 CRC32 0x6b31978b Query thread_id=10
exec_time=0 error_code=0
SET TIMESTAMP=1641345397/*!*/;
SET @@session.pseudo_thread_id=10/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0,
@@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb3 *//*!*/;
SET
@@session.character_set_client=33,@@session.collation_connection=33,@@session.collatio
n_server=255/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 324
#220105 9:16:37 server id 1 end_log_pos 391 CRC32 0x74f89890 Table_map:
`atguigu14`.`student` mapped to number 85
# at 391
#220105 9:16:37 server id 1 end_log_pos 470 CRC32 0xc9920491 Update_rows: table id
85 flags: STMT_END_F
BINLOG '
dfHUYRMBAAAAQwAAAIcBAAAAAFUAAAAAAAEACWF0Z3VpZ3UxNAAHc3R1ZGVudAADAw8PBDwAHgAG
AQEAAgEhkJj4dA==
dfHUYR8BAAAATwAAANYBAAAAAFUAAAAAAAEAAgAD//8AAQAAAAblvKDkuIkG5LiA54+tAAEAAAAL
5byg5LiJX2JhY2sG5LiA54+tkQSSyQ==
'/*!*/;
### UPDATE `atguigu`.`student`
### WHERE
### @1=1
### @2='张三'
### @3='一班'
### SET
### @1=1
### @2='张三_back'
### @3='一班'
# at 470
#220105 9:16:37 server id 1 end_log_pos 501 CRC32 0xca01d30f Xid = 15
COMMIT/*!*/;
前面的命令同时显示binlog格式的语句,使用如下命令不显示它
mysqlbinlog -v --base64-output=DECODE-ROWS "/var/lib/mysql/binlog/atguigu-bin.000002"
#220105 9:16:37 server id 1 end_log_pos 324 CRC32 0x6b31978b Query thread_id=10
exec_time=0 error_code=0
SET TIMESTAMP=1641345397/*!*/;
SET @@session.pseudo_thread_id=10/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0,
@@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb3 *//*!*/;
SET
@@session.character_set_client=33,@@session.collation_connection=33,@@session.collatio
n_server=255/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 324
#220105 9:16:37 server id 1 end_log_pos 391 CRC32 0x74f89890 Table_map:
`atguigu14`.`student` mapped to number 85
# at 391
#220105 9:16:37 server id 1 end_log_pos 470 CRC32 0xc9920491 Update_rows: table id
85 flags: STMT_END_F
### UPDATE `atguigu14`.`student`
### WHERE
### @1=1
### @2='张三'
### @3='一班'
### SET
### @1=1
### @2='张三_back'
### @3='一班'
# at 470
#220105 9:16:37 server id 1 end_log_pos 501 CRC32 0xca01d30f Xid = 15
关于mysqlbinlog工具的使用技巧还有很多,例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句,更多操作可以参考官方文档。
# 可查看参数帮助
mysqlbinlog --no-defaults --help
# 查看最后100行
mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |tail
-100
# 根据position查找
mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |grep -A
20 '4939002'
上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方
便的查询命令:
mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];
IN 'log_name
:指定要查询的binlog文件名(不指定就是第一个binlog文件)FROM pos
: 指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)LIMIT [offset]
: 偏移量(不指定就是0)ow_count
: 查询总条数(不指定就是所有行)mysql> show binlog events in 'atguigu-bin.000002';
+--------------------+-----+----------------+-----------+-------------+---------------
--------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info
|
+--------------------+-----+----------------+-----------+-------------+---------------
--------------------------------------------------------------+
| atguigu-bin.000002 | 4 | Format_desc | 1 | 125 | Server ver:
8.0.26, Binlog ver: 4 |
| atguigu-bin.000002 | 125 | Previous_gtids | 1 | 156 |
|
| atguigu-bin.000002 | 156 | Anonymous_Gtid | 1 | 235 | SET
@@SESSION.GTID_NEXT= 'ANONYMOUS' |
| atguigu-bin.000002 | 235 | Query | 1 | 324 | BEGIN
|
| atguigu-bin.000002 | 324 | Table_map | 1 | 391 | table_id: 85
(atguigu14.student) |
| atguigu-bin.000002 | 391 | Update_rows | 1 | 470 | table_id: 85
flags: STMT_END_F |
| atguigu-bin.000002 | 470 | Xid | 1 | 501 | COMMIT /*
xid=15 */ |
| atguigu-bin.000002 | 501 | Anonymous_Gtid | 1 | 578 | SET
@@SESSION.GTID_NEXT= 'ANONYMOUS' |
| atguigu-bin.000002 | 578 | Query | 1 | 721 | use
`atguigu14`; create table test(id int, title varchar(100)) /* xid=19 */ |
| atguigu-bin.000002 | 721 | Anonymous_Gtid | 1 | 800 | SET
@@SESSION.GTID_NEXT= 'ANONYMOUS' |
| atguigu-bin.000002 | 800 | Query | 1 | 880 | BEGIN
|
| atguigu-bin.000002 | 880 | Table_map | 1 | 943 | table_id: 89
(atguigu14.test) |
| atguigu-bin.000002 | 943 | Write_rows | 1 | 992 | table_id: 89
flags: STMT_END_F |
| atguigu-bin.000002 | 992 | Xid | 1 | 1023 | COMMIT /*
xid=21 */ |
+--------------------+-----+----------------+-----------+-------------+---------------
--------------------------------------------------------------+
14 行于数据集 (0.02 秒)
上面我们讲了这么多都是基于binlog的默认格式,binlog格式查看
mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
1 行于数据集 (0.02 秒)
除此之外,binlog还有2种格式,分别是Statement和Mixed
Statement
每一条会修改数据的sql都会记录在binlog中。
优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
Row
5.1.5版本的MySQL才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
Mixed
从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。
mysqlbinlog恢复数据的语法如下:
mysqlbinlog [option] filename|mysql –uuser -ppass;
这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容恢复到数据库中。
filename
: 是日志文件名。option
: 可选项,比较重要的两对option参数是–start-date、–stop-date 和 --start-position、–stop-position。
--start-date 和 --stop-date
: 可以指定恢复数据库的起始时间点和结束时间点。--start-position和--stop-position
: 可以指定恢复数据的开始位置和结束位置。注意:使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如atguigu-bin.000001必须在atguigu-bin.000002之前恢复。
具体说明
MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。PURGE MASTER LOGS
只删除指定部分的二进制日志文件,RESET MASTER
删除所有的二进制日志文件。具体如下:
1. PURGE MASTER LOGS:删除指定日志文件
PURGE MASTER LOGS语法如下:
PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’
PURGE {MASTER | BINARY} LOGS BEFORE ‘指定日期’
2. RESET MASTER:删除所有二进制日志文件
使用RESET MASTER
语句,清空所有的binlog日志。MySQL会重新创建二进制文件,新的日志文件扩展名重新从000001开始编号。慎用!!!
**举例:**使用RESET MASTER语句删除所有日志文件。
重启MySQL服务器若干次(目的是为了生成多份binlog),执行SHOW语句现实二进制文件列表。
SHOW BINARY LOGS;
执行RESET MASTER语句,删除所有日志文件。
RESET MASTER;
执行完后就删除完了。
二进制日志可以通过数据库的 全量备份 和二进制日志中保存的 增量信息 ,完成数据库的 无损失恢复 。但是,如果遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。
在这种情况下,一个有效的解决办法是 配置主从数据库服务器 ,甚至是 一主多从 的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。
binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
我们可以通过binlog_cache_size
参数控制单个线程binlog cache大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。binlog日志刷盘流程如下:
write和fsync的时机,可以由参数 sync_binlog 控制,默认是 0 。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。如图:
为了安全起见,可以设置为 1 ,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。
在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。
在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的 写入时机 不一样。
redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?
由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。
为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。
使用两阶段提交后,写入binlog时发生异常也不会有影响
另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?
并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。
中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。
文件名的格式是: 从服务器名 -relay-bin.序号 。中继日志还有一个索引文件: 从服务器名 -relay-
bin.index ,用来定位当前正在使用的中继日志。
中继日志与二进制日志的格式相同,可以用 mysqlbinlog 工具进行查看。下面是中继日志的一个片段:
SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map:
`atguigu`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id
91 flags: STMT_END_F -- server id 1 是主服务器,意思是主服务器删了一行数据
BINLOG '
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
'/*!*/;
# at 1040
这一段的意思是,主服务器(“server id 1”)对表 atguigu.test 进行了 2 步操作:
定位到表 atguigu.test 编号是 91 的记录,日志位置是 832;
删除编号是 91 的记录,日志位置是 872。
如果从服务器宕机,有的时候为了系统恢复,要重装操作系统,这样就可能会导致你的服务器名称与之前不同 。而中继日志里是包含从服务器名的。在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是名称不对了。
解决的方法也很简单,把从服务器的名称改回之前的名称。
事务的隔离性由锁来实现
锁的粒度
MySQL提供了两种锁:行级锁和表级锁。
选择时,应尽量只锁需要修改的那部分数据,而不是所有的资源,锁定的数量越少,并发度越高,发生锁的争用越少。
但是加锁需要消耗资源,所得各种操作(包括获取锁、释放锁以及检查锁的状体)都会增加系统开销,因此锁的粒度越小,系统开销越大。在选择时需要在锁开销和并发程度间做权衡 。
1.读写锁
规定:
2.意向锁
使用意向锁,可以更容易的支持多粒度封锁。
在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
意向锁在原来的X/S锁基础上引入了IS/IX锁,两者都是表锁,表示:
通过引入意向锁,事物T想要对表A加X锁,只需要检查是否有其他事物对表A加了X/IX/S/IS锁,如果加了就表示有其他事物正在使用表或表中的一行,因此事物T加锁失败
3.InnoDB存储引擎行锁的三种实现
record lock:单个行记录上的锁
gap lock:间歇锁,锁定一个范围,不包括记录本身
next-key lock:record lock+gap锁定一个范围包括记录本身
4.乐观锁和悲观锁
并不是真正意义的锁,而是一种思想
悲观锁:悲观并发控制,持一种消极态度,默认数据被访问时,会发生冲突,所以在处理数据整个过程加锁状态
乐观锁:乐观并发控制,并发环境中始终认为对数据的操作不会发生冲突,在对事物更新提交时,才判断是否发生冲突
5.行锁、表锁与页锁
三级锁协议
1.一级封锁协议
事物T想要修改数据A时必须加X锁,知道T结束才释放锁。
可以解决丢失修改的问题,因为不能有同时两个事物对同一个数据修改
2.二级封锁协议
在一级基础上,要求读取数据A时必须加S锁,读取完马上释放。
可以解决脏读问题,因为如果一个事物在对数据A修改,根据一级协议会加X锁,呢么就不能加S锁,也就不会读入数据。
3.三级封锁协议
在二级的基础上,要求一个事物读取数据A时加S锁,知道事物结束才释放锁。可以解决不可重复读的问题,因为读A时,其他事物不能修改数据A
两端锁协议
加锁和解锁分为两个阶段进行。
可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。串行执行的事务互不干扰,不会出现并发一致性问题。
事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
但不是必要条件,例如以下操作不满足两段锁协议,但它还是可串行化调度。
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)
MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
InnoDB 也可以使用特定的语句进行显示锁定:
SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;
锁是计算机协调多个进程或线程并发访问某一资源的机制,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据,需要保证这个数据在任何时刻最多只有一个线程在访问,保证数据的完整性和一致性。
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性,需要对 并发操作进行控制 ,因此产生了 锁 。同时 锁机制 也为实现MySQL 的各个隔离级别提供了保证。 锁冲突 也是影响数据库 并发访问性能 的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
读-读情况,即并发事务相继读取相同记录。读取操作本身不会对记录有任何影响,所以允许这种情况发生
即并发事务相继对相同记录做出改动。
在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。这个所谓 的锁其实是一个 内存中的结构 ,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进行关联的,如图所示:
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候 就会在内存中生成一个 锁结构 与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个 锁结构 与之关联:
小结几种说法:
不加锁
意思就是不需要在内存中生成对应的 锁结构 ,可以直接执行操作。
获取锁成功,或者加锁成功
意思就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务可以继续执行操作。
获取锁失败,或者加锁失败,或者没有获取到锁
意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务
需要等待,不可以继续执行操作。
读-写或写-读,即一个事务进行读取操作,另一个事务进行改动操作。这种情况下可能发生脏读,不可重复读,幻读的问题。
各个数据库厂商对 SQL标准 的支持都可能不一样。比如MySQL在 REPEATABLE READ 隔离级别上就已经解决了 幻读 问题。
如何解决脏读、不可重复读、幻读问题呢
方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
所谓MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能读到在ReadView前已提交事务的修改,在未提交或才开启的事务所作的修改是看不到的。而写操作是针对最新版本的记录,读记录的历史版本和改记录的最新版本并不冲突,也就是MVCC读-写不冲突
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
- 在READ COMMITED隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可读取到未提交的事务所作的修改,也就避免了脏读现象
- 在REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都会复用这个readview,这样就避免了不可重复读和幻读问题
mvcc能否解决幻读?
方案二:读写操作都采用加锁
如果一些业务场景不允许读取记录的旧版本,而是每次必须去读取记录的最新版本。比如,在银行存款的事务中,需要先把账户的余额读出来,然后将其加上本次存款的数额,最后写到数据库。在将账户余额读取出来后,就不想让其他事务访问该余额,直到本次存款事务执行成功,其他事务才可以访问到账户余额。这样在读取记录的时候就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作一样排队执行
脏读的产生是因为当前事务读取了另一个未提交事务写的记录,如果另一个事务在写记录的时候就给记录加锁,那么当前事务就无法读到该记录,也就就没有脏读问题
不可重复读的产生是因为当前事务先读取到某一记录,另一个事务对该类记录修改并提交后,当前事务再次读取会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读。
幻读的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新的记录,当前事务再次读取该范围的记录时发现了新插入的新纪录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务第一次读取记录时幻影记录并不存在,所以读取的时候加锁不知道给谁加
小结对比:
对于数据库中并发事务的读-读情况并不会引起什么问题。对于读-写、写-写或写-读这些情况可能会引起一些问题。需要使用MVCC或加锁方式来解决。在使用加锁方式解决时,由于既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞,所以MySQL实现了一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常称为共享锁、排他锁,也叫读锁和写锁
需要注意的是对于InnoDb引擎来说,读锁和写锁可以加在表上也可以加在行上
举例(行级读写锁):如果一个事务T1已经获得某个行r的读锁,此时另一个事务是可以去获得这个行r的读锁的,因为读操作并没有改变行r的数据,但是T3事务想获得行r的写锁,则必须等到1 2两个事务释放掉r上的读锁
1.锁定读
在采用加锁方式解决脏读、不可重复读、幻读问题时,读取一条记录时需要获得该行记录的S锁,其实不严谨,有时候需要在读取记录时获取记录的X锁。来禁止别的事务读读写该记录,为此MySQL提出了两种特殊的SELECT语句格式:
对读取记录加S锁:
SELECT ... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE;#(8.0新增)
在普通的SELECT语句后边加LOCK IN SHARE MODE,如果对读取记录加X锁:如果当前事务执行到了该语句,那么它会为读取到的记录加S锁,这样允许别的事物j继续获取这些记录的S锁,但不能获取记录的X锁。如果别的事物想获得记录的X锁,就会阻塞,直到当前事务提交后将这些记录上的S锁释放掉
对读取记录加X锁:
SELECT ... FOR UPDATE;
在普通的SELECT语句后加FOR UPDATE,如果当前事务执行到该语句,那么会为读取到的记录加X锁,这样及不允许别的事物获取这些记录的S锁,也不允许别的事物获取X锁
MySQL8.0新特性
在5.7及之前的版本,SELECT…FOR UPDATE,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,SELECT…FOR UPDATE,SELECT…FOR SHARE添加的NOWAIT SKIP LOCKED语法,跳过锁等待,或者跳过锁定。
2.写操作
平常锁用到的写操作无非是DELETE、UPDATE、INSERT三种:
DELETE:
对一条记录做DELETE操作的过程其实是现在B+树定位到这条记录的位置,然后获取这条记录的X锁,在执行delete mark操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成一个获取X锁的锁定读
UPDATE:在对一条记录做UPDATE操作时分为三种情况
INSERT:
一般情况下,新插入一条记录的操作并不加锁,通过一种叫隐式锁的结构来保护这条新插入的记录在本事物提交前不被别的事物访问。
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了锁粒度的概念。
对一条记录加锁影响的只是这条记录,我们就说这个锁的粒度比较细,其实一个事务也可以在表上加锁,称之为表锁,对一个表加锁影响的也是一个表的记录,我们就说这个锁的粒度比较粗,锁的粒度主要分为表级锁、页级锁、行锁
该锁会锁定整张表,它是MySQL最基本的锁策略,并不依赖存储引擎,并且表锁是开销最小的策略(因为粒度比较大),由于表级锁一次会将整张表锁住,所以可以很好的避免死锁问题。当然锁粒度的增大带来的负面影响就是出现锁资源竞争的概率也会增大,导致并发率降低
1.表级的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或X锁的。在对某个表执行一些诸如ALTER TABLE、DROP TABLE这类的DDL语句时,其他事物对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事物种对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁结构来实现的
一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下,比方说 崩 溃恢复 过程中用到。比如,在系统变量 autocommit=0,innodb_table_locks = 1 时, 手动 获取InnoDB存储引擎提供的表t 的 S锁 或者 X锁 可以这么写:
不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 行锁 ,关于InnoDB表级别的 S锁 和 X锁 大家了解一下就可以了。
MyISAM在执行查询语句前,会给涉及的所有表加读锁,在执行增删改之前,会给涉及的所有表加写锁。InnoDB存储引擎不会为这个表添加表级别的读锁或写锁
MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)
2.意向锁
InnoDB支持多粒度锁,允许行级锁和表级锁共享,而意向锁就是其中一种表锁。
意向锁分为两种:
意向共享锁:事物有意向对表中的某些行加共享锁(S锁)
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。 SELECT column FROM table ... LOCK IN SHARE MODE;
意向排他锁:事物有意向对表中某些行加排他锁(X锁)
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。 SELECT column FROM table ... FOR UPDATE;
即:意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前InooDB 会先获取该数据行 所在数据表的对应意向锁 。
意向锁解决的问题
现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排他锁,如果没有意向锁的存在,那么T2就需要去检查各个页或行是否存在锁,如果存在意向锁,那么此时就会享受到由T1控制的表级别意向锁的阻塞。T2锁定该表前不必检查各个页或行锁,只需要检查该表的意向锁。
在数据表场景中,如果我们给某一行数据加上了排他锁,数据库会自动 给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有其他人上过排他锁,这样当其他人想要获取数据表排他锁时,只需要了解是否有人已经获取了这个数据表的意向排他锁即可
意向锁的并发性
意向锁不会与行级锁的共享/排他锁互斥。正因如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性
从上面例子可以得到如下结论:
3.自增锁(AUTO-INC锁)
在使用MySQL过程中,我们可以分为表的某个列添加AUTO_INCREMENT属性。举例:
CREATE TABLE `teacher` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
由于这个表的id字段声明了AUTO_INCREMENT,意味着在书写插入语句时不需要为其赋值,SQL语句修改 如下所示。
INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi');
上边的插入语句并没有为id列显示赋值,所以系统会自动为它附上递增的值,结果如下:
现在我们看到的上面插入数据只是一种简单的插入模式,所有插入数据的方式总共分为三类,分别是
“ Simple inserts ”,“ Bulk inserts ”和“ Mixed-mode inserts ”。
1. “Simple inserts” (简单插入)
可以 预先确定要插入的行数 (当语句被初始处理时)的语句。包括没有嵌套子查询的单行和多行 INSERT…VALUES() 和 REPLACE 语句。比如我们上面举的例子就属于该类插入,已经确定要插入的行数。
2. “Bulk inserts” (批量插入)
事先不知道要插入的行数 (和所需自动递增值的数量)的语句。比如 INSERT … SELECT , REPLACE … SELECT 和 LOAD DATA 语句,但不包括纯INSERT。 InnoDB在每处理一行,为AUTO_INCREMENT列 分配一个新值。
3. “Mixed-mode inserts” (混合模式插入)
这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 INSERT INTO teacher (id,name) VALUES (1,‘a’), (NULL,‘b’), (5,‘c’), (NULL,‘d’); 只是指定了部分id的值。另一种类型的“混 合模式插入”是 INSERT … ON DUPLICATE KEY UPDATE 。
innodb_autoinc_lock_mode有三种取值,分别对应与不同锁定模式:
(1)innodb_autoinc_lock_mode = 0(“传统”锁定模式)
在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有 AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子,即每当执行insert的时候,都会得到一个 表级锁(AUTO-INC锁),使得语句中生成的auto_increment为顺序,且在binlog中重放的时候,可以保证 master与slave中数据的auto_increment是相同的。因为是表级锁,当在同一时间多个事务中执行insert的 时候,对于AUTO-INC锁的争夺会 限制并发 能力。
(2)innodb_autoinc_lock_mode = 1(“连续”锁定模式)
在 MySQL 8.0 之前,连续锁定模式是 默认 的。
在这个模式下,“bulk inserts”仍然使用AUTO-INC表级锁,并保持到语句结束。这适用于所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。
对于“Simple inserts”(要插入的行数事先已知),则通过在 mutex(轻量锁) 的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁, 它只在分配过程的持续时间内保持,而不是直到语句完成。不使用 表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则“Simple inserts”等待AUTO-INC锁,如同它是一个“bulk inserts”。
(3)innodb_autoinc_lock_mode = 2(“交错”锁定模式)
从 MySQL 8.0 开始,交错锁模式是 默认 设置。
在此锁定模式下,自动递增值 保证 在所有并发执行的所有类型的insert语句中是 唯一 且 单调递增 的。但 是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的。
④ 元数据锁(MDL锁) MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL 的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更 ,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,当对一个表做增删改查操作的时候,加 MDL****读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
行锁也称为记录锁,就是锁住某一行。mysql服务层并没有实现行锁机制,行级锁只在存储引擎层实现
优点:锁定粒度小,发生锁冲突概率低,可以实现的并发度高
缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁
1.记录锁(record lock)
记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
记录锁是有S锁和X锁之分,称之为S型记录锁和X型记录锁。
2.间歇锁(Gap Locks)
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读 取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为 Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁 。比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着不允许别的事物在id值为8的记录前边的间歇插入新纪录,其实就是idlie的值(3,8)这个区间的新记录时不允许立即插入。比如,有另外一个事物想再插入一条id值为4的新纪录,他定位到该条新纪录的下一条的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,知道拥有这个gap锁的事物提交之后,id列的值在区间(3,8)中的新纪录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的
3.临键锁(Next-Key Locks)
有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间歇插入新纪录,所以InnoDB就提出了一种称之为Next-Key Locks的锁。官方称之为LOCK_ORDINARY,简称为 Next-Key 锁。Next-Key Locks是存在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁,InnoDB默认就是Next-Key Locks。
begin; select * from student where id <=8 and id > 3 for update;
4.插入意向锁(insert Intention Locks)
我们说一个事务在 插入 一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 ( next-key锁也包含 gap锁 ),如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个 间隙 中 插入 新记录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks ,官方的类型名称为:LOCK_INSERT_INTENTION ,我们称为 插入意向锁 。插入意向锁是一种 Gap锁 ,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁 。
事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量 超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的 设计思想 。
悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
秒杀案例1:
商品秒杀过程中,库存数量的减少,避免出现超卖的情况。比如,商品表中有一个字段为quantity标识当前该商品的库存量。假设商品为华为mate40,id为1001, quantity为100个,如果不使用锁的情况下,操作方法如下:
#第一步:查出商品库存
select quantity from items where id = 1001;
#第二步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第三步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num where id = 1001;
这样写的话,在并发量小的公司没有大的问题,但如果在高并发环境下可能会出现以下问题
其中线程B此时已经下单并且减完库存,这个时候线程A依然执行step3,就造成了超卖。
我们使用悲观锁可以解决这个问题,商品信息从查询出来到修改,中间有一个生成订单过程,使用悲观锁的原理就是,当我们查询items信息后把当前数据锁定,知道修改完毕后再解锁。那么整个过程中,因为数据被锁定了,就不会出现第三者来对其修改。而这样做是需要将执行的sql语句放在同一个事务,否则达不到锁定数据行的目的。
修改如下:
#第一步:查询商品库存
select quantity from items where id = 1001 for update;
#第二步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第三步:修改商品库存,num表示购买数量
update items set quantity = quantity - num where id = 1001;
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者 CAS机制 实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。
1.乐观锁的版本号机制
在表中设计一个 版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE … SET version=version+1 WHERE version=version 。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
2.乐观锁的时间戳机制
时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。
秒杀案例2:
依然使用上面秒杀的案例,执行流程如下:
#第一步,查出商品库存
select quantity from items where id = 1001;
#第二步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第三步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num,version=version+1 where id = 1001 and version = #(version)
注意,如果数据表是读写分离的表,当masterr表中写入的数据没有及时同步到slave表中时,会造成更新一直失败的问题。此时需要强制读取master表中的数据(即将select语句放到事务中即可,这时候查询的就是master主库了)
如果我们对同一条数据进行频繁的修改的话,那么就会出现这么一种场景,每次修改都只有一个事务能更新成功,在业务感知上面就有大量的失败操作。把代码修改如下:
#第一步:查出商品库存
select quantity from items where id = 1001;
#第二步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第三步:修改商品的库存,num表示购买数量
update items set quantity = quantity - num where id = 100 and quantity-num>0;
这样就会使每次修改都能成功,而且我不会出现超卖的现象。
乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。
session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert INTO student VALUES(34,"周八","二班");
Query OK, 1 row affected (0.00 sec)
session2:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student lock in share mode; #执行完,当前事务被阻塞
隐式锁的逻辑过程如下:
A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。
B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将 隐式锁 转换为 显式锁 (就是为该事务添加一个锁)。
C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。
D. 等待加锁成功,被唤醒,或者超时。
E. 写数据,并将自己的trx_id写入trx_id字段。
通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
显示加共享锁:
select .... lock in share mode
显示排他锁:
select .... for update
全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。
全局锁的命令:
Flush tables with read lock
概念
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死
这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 :
第二种策略的成本分析
方法一:如果你确保这个业务一定不会出现死锁,可以把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务涉及的时候一般不会把死锁当作一个严重错误,毕竟出现了死锁,就回滚,然后通过业务重试一般就没问题,这是业务无损的。而关掉死锁检测意味着可能会出现大量超时,这是业务有损。
方法二:控制并发度。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低,就不会出现这个问题
产生死锁条件
死锁的关键在于两个(或以上)的session加锁顺序不一致
如何处理死锁
方式一等待,知道超时(innodb_lock_wait_timeout=50s).
即当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚,另外事务继续进行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeoput用来设置超时时间。
缺点:对于在线服务来说,这个等待时间往往是无法接受的
那就将此值修改短一些,比如1s,0.1s是否合适?不合适,容易误伤到普通锁的等待
方式二使用死锁检测进行死锁处理。
方式一检测死锁太过被动,innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法就会被出发。
这是一种较为主动的死锁检测机制,要求数据库保存锁的信息链表和事务等待链表两部分信息。
基于这两个信息,可以绘制wait-for graph(等待图)
死锁检测的原理是构建一个以事务为顶点,锁为边的有向图,判断有向图是否存在环,存在即有死锁
一旦检测到回路、有死锁,这时候InnoDB存储引擎会选择回滚undo量最小的事务,让其他事务继续执行(innodb_deadlock_detect=on表示开启这个逻辑)
缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致了死锁,这个操作时间复杂度是O(n)。如果100并发线程同时更新同一行,意味着要检测100*100=1万次,1万个线程就会有1千万次检测。
如何解决?
进一步的思考
可以考虑通过将一行改成逻辑上的多行来减少锁冲突。比如,连锁超市总额的记录,可以考虑放到多条记录上。账户总额等于这多个记录的值的总和。
如何避免死锁
结构分析:
不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个 事务的信息。
此 锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比 方说事务id等。
对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。
表锁结构 和 行锁结构 在这个位置的内容是不同的:
表锁
记载着是对哪个表加的锁,还有一些其他信息
行锁
记载了三个重要的信息:
Space ID:记录所在表空间
Page Number:记录所在页号
n_bits:对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行结构的末尾放置了一堆比特位,这个n_bits属性代表使用了多少比特位
n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构
4.type_mode:
这是一个32位的数,被分成lock_mode、lock_type、rec_lock_type三个部分,如图所示:
在InnoDB存储引擎中,LOCK_IS、LOCK_IX、LOCK_WUTO_INC都算是表级锁的模式,LOCK_S和LOCK_X既可以算是表级锁的模式,也可以是行级锁
5.其他信息:
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
6.一堆比特位:
如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits 属性 表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no 属性,伪记录 Infimum 的 heap_no 值为 0 , Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结 构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射 到页内的一条记录。
关于MySQL锁的监控,我们一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况
对各个状态量的说明如下:
对于这5个变量,比较重要的是上面三个(加粗)
其他监控方法:
MySQL把事务和锁的信息记录在了 information_schema 库中,涉及到的三张表分别是 INNODB_TRX 、 INNODB_LOCKS 和 INNODB_LOCK_WAITS 。
MySQL5.7及之前 ,可以通过information_schema.INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。
MySQL8.0删除了information_schema.INNODB_LOCKS,添加了 performance_schema.data_locks ,可以通过performance_schema.data_locks查看事务的锁情况,和MySQL5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。同时,information_schema.INNODB_LOCK_WAITS也被 performance_schema.data_lock_waits 所代替。
我们模拟一个锁等待的场景,以下是从这三张表收集的信息;锁等待场景,依然使用记录锁中的案例,当事务2进行等待时,查询情况如下:
(1)查询正在被锁阻塞的sql语句。
SELECT * FROM information_schema.INNODB_TRX\G;
(3)查询锁的情况
从锁的情况可以看出来,两个事务分别获取了IX锁,我们从意向锁章节可以知道,IX锁互相时兼容的。所以这里不会等待,但是事务1同样持有X锁,此时事务2也要去同一行记录获取X锁,他们之间不兼容,导致等待的情况发生。
间隙锁是在可重复读隔离级别下才会生效的:
next-key lock 实际上是由间隙锁加行锁实现的,如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。而在读提交隔离级别下间隙锁就没有了,为了解决可能出现的数据和日志不一致问题,需要把binlog 格式设置为 row 。也就是说,许多公司的配置为:读提交隔离级别加 binlog_format=row。业务不需要可重复读的保证,这样考虑到读提交下操作数据的锁范围更小(没有间隙锁),这个选择是合理的。
next-key lock的加锁规则,包含两个“原则”、两个“优化”和一个“bug”
1.原则1:加锁的基本单位是next-key lock。前开后闭区间
2.原则2:查找过程中访问到的对象才会加锁。任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁
3.优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock会退化为行锁。也就是说如果InnoDB扫描的是一个主键或者一个唯一索引的话,只会采用行锁方式加锁。
4.优化2:索引上(不一定是唯一索引)的等值查询,向右遍历的时候且最后一个值不满足等值条件时,next-key lock退化为间歇锁
5.一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
以表test为例,建表语句及初始化如下:id为主键索引
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`col1` int(11) DEFAULT NULL,
`col2` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into test values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
由于表test中没有id=7的记录,根据原则1,加锁单位是next-key lock,sessionA加锁范围就是(5,10];同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lcok退化成间歇锁,因此最终加锁范围是(5,10)
这里sessionA要给索引col1上col1=5的这一行加上读锁。
1.根据原则1,加锁单位是next-key lock,左开有闭,5是闭上的,因此会给(0,5]加上next-key lock
2.要注意c是普通索引,因此仅访问次这一条记录是不能停下的(可能有col1=5的其他记录),需要继续向右遍历,查到c=10才放弃。根据原则2,访问到的对象都要加锁,因此要给(5,10]加上next-key lock
3.但是同时这个符合优化2:等值判断,向右遍历,最后一个不满足col1=5这个等值条件,因此退化成间歇锁(5,10)。
4.根据原则2,只有访问到的对象才会加锁,这个查询使用这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引没有加任何锁,这就是为什么sessionB可以完成的原因
但sessionC要插入一个(7,7,7)的记录,就会被sessionA的间歇锁(5,10)锁住,这个例子说明锁加载索引上的。
执行for update时,系统会认为接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
如果要使用lock in share mode来给行加读锁避免数据被更新的话,就必须绕过覆盖索引的优化,因为覆盖索引的优化不会访问主键索引,不会给主键索引加锁
上面两个例子是等值查询的,这个例子是关于范围查询的,也就是说下面的语句
select * from test where id=10 for update
select * from test where id>=10 and id<11 for update;
1.开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10],但根据优化1,主键id上的等值条件,退化成行锁,只加了id=10这一条的行锁。
2.它是范围查询,范围查询就往后继续找,知道找到id=15这一行停下来,不满足条件,因此需要加next-key lcok(10,15].
sessionA这时候锁上的范围就是主键索引上,行锁id=10和next-key lcok(10,15]。首次sessionA定位查找id=10的行时,是当作等值查询来判断的,而向右扫描到id=15时,用的是范围查询判断
与案例三不同的是,案例四中查询语句的 where 部分用的是字段 c ,它是普通索引
在第一次用col1=10定位记录时,索引c上加了(5,10]这个next-key lock后,由于索引col1是非唯一索引,没有优化规则,也就是说不会蜕变为行锁。因此最终sessionA加的锁是,索引c上的(5,10]和(10,15]这两个next-key lock
这里需要扫描到col1=15才停止扫描,是合理的。因为InnoDB要扫描到col1=15,才知道不需要继续往后找了。
session A 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15] 这个 next-key lock ,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。
但是实现上, InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20 。而且由于这是个范围扫描,因此索引 id 上的 (15,20] 这个 next-key lock 也会被锁上。照理说,这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15 ,就可以确定不用往后再找了。
这里,我给表 t 插入一条新记录:insert into t values(30,10,30);也就是说,现在表里面有两个c=10的行
但是它们的主键值 id 是不同的(分别是 10 和 30 ),因此这两个****c=10 的记录之间,也是有间隙的。
这次我们用 delete 语句来验证。注意, delete 语句加锁的逻辑,其实跟 select … for update 是类似的,也就是我在文章开始总结的两个 “ 原则 ” 、两个 “ 优化 ” 和一个 “bug” 。
这时, session A 在遍历的时候,先访问第一个 col1=10 的记录。同样地,根据原则 1 ,这里加的是 (col1=5,id=5) 到 (col1=10,id=10) 这个 next-key lock 。
由于c是普通索引,所以继续向右查找,直到碰到 (col1=15,id=15) 这一行循环才结束。根据优化 2 ,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (col1=10,id=10) 到 (col1=15,id=15) 的间隙锁。
这个 delete 语句在索引 c 上的加锁范围,就是上面图中蓝色区域覆盖的部分。这个蓝色区域左右两边都是虚线,表示开区间,即 (col1=5,id=5) 和 (col1=15,id=15) 这两行上都没有锁
session A 的 delete 语句加了 limit 2 。你知道表 t 里 c=10 的记录其实只有两条,因此加不加 limit 2 ,删除的效果都是一样的。但是加锁效果却不一样
这是因为,案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (col1=10, id=30) 这一行之后,
满足条件的语句已经有两条,循环就结束了。因此,索引 col1 上的加锁范围就变成了从(col1=5,id=5)到(col1=10,id=30) 这个前开后闭区间,如下图所示:
这个例子对我们实践的指导意义就是,在删除数据的时候计量加limit
这样不仅可以控制删除数据的条数,让操作更方便,还可以缩小加锁范围
1.sessionA启动事务后执行查询语句加lock in share mode,在索引上加了next-key lock(5,10]和间歇锁(10,15)(索引向右遍历退化为间歇锁)
2.session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待; 实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 col1=10 的行锁,因为sessionA上已经给这行加上了读锁,此时申请死锁时会被阻塞
3.然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁, InnoDB 让session B 回滚
如下面一条语句
begin;
select * from test where id>9 and id<12 order by id desc for update;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e5FZnAfM-1657720788615)(MySQL/1651827877071.png)]
1.由于是order by col1 desc,第一个定位的是col1上最右边的col1=20的行。这是一个非唯一索引的等值查询:
左开右闭区间,首先加上 next-key lock (15,20] 。 向右遍历,col1=25不满足条件,退化为间隙锁 所以会加上间隙锁(20,25) 和 next-key lock (15,20] 。
2.在索引 col1 上向左遍历,要扫描到 col1=10 才停下来。同时又因为左开右闭区间,所以 next-keylock 会加到 (5,10] ,这正是阻塞session B 的 insert 语句的原因。
3.在扫描过程中, col1=20 、 col1=15 、 col1=10 这三行都存在值,由于是 select * ,所以会在主键id 上加三个行锁。 因此, session A 的 select 语句锁的范围就是:
索引 col1 上 (5, 25) ;
主键索引上 id=15 、 20 两个行锁。
注意:根据 col1>5 查到的第一个记录是 col1=10 ,因此不会加 (0,5] 这个 next-key lock 。
session A 的加锁范围是索引 col1 上的 (5,10] 、 (10,15] 、 (15,20] 、 (20,25] 和(25,supremum] 。
之后 session B 的第一个 update 语句,要把 col1=5 改成 col1=1 ,你可以理解为两步:
插入 (col1=1, id=5) 这个记录;
删除 (col1=5, id=5) 这个记录。
通过这个操作, session A 的加锁范围变成了图 7 所示的样子:
接下来 session B 要执行 update t set col1 = 5 where col1 = 1 这个语句了,一样地可以拆成两步:
插入 (col1=5, id=5) 这个记录;
删除 (col1=1, id=5) 这个记录。 第一步试图在已经加了间隙锁的 (1,10) 中插入数据,所以就被堵住了。
MVCC(Multi-Version Consurrency Controller),即多版本并发控制。顾名思义,MVCC是通过数据行的多个版本管理来实现数据库的并发控制这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以被更新之前的值,这样查询的时候就不用等待另一个事务释放锁。
MVCC在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的SELECT都属于快照读,即不加锁的非阻塞读;比如:
SELECT * FROM player WHERE ...
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,他在很多情况下,避免加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前历史版本。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化为当前读。
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前事务,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE;#共享锁
SELECT * FROM student FOR UPDATE;#排他锁
SELECT * INTO student VALUES...;#排他锁
SELECT * FROM srudent WHERE ...;#排他锁
UPDATE student SET ...;#排他锁
数据库并发场景分三种,分别为:
带来的好处:
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库快照。所以MVCC可以为数据库解决以下问题:
因此我们可以形成两个组合:
MVCC+悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC+乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_TRX_ID
6 byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR
7 byte,回滚指针,指向这条记录的上一个版本(存储在rollback segment里)
DB_ROW_ID
6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
实际上还有一个删除flag隐藏字段,既记录被更新或删除并不代表真的删除,而是删除flag变了。
如上图,DB_ROW_ID是数据库默认为该记录生成的唯一主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向当前记录上一个版本。
undo日志
undo log主要分为两种:
insert undo log
代表事务在insert新纪录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以立即被丢弃
update undo log
事务在进行update或delete时产生的undo log;不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
purge
- 从前面的分析可以看出,为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit ,并不真正将过时的记录删除。
- 为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录。为了不影响 MVCC 的正常工作,purge 线程自己也维护了一个read view(这个 read view 相当于系统中最老活跃事务的 read view );如果某个记录的 deleted_bit 为 true ,并且 DB_TRX_ID 相对于 purge 线程的 read view 可见,那么这条记录一定是可以被安全清除的。
对MVCC有帮助的实质是update undo log,undo log实际上存在rollback segment中旧纪录链,执行流程如下:
1.比如一个有个事务插入 persion 表插入了一条新记录,记录如下,name
为 Jerry , age
为 24 岁,隐式主键
是 1,事务 ID
和回滚指针
,我们假设为 NULL
2.现在来了一个事务 1
对该记录的 name
做出了修改,改为 Tom 。
3.又来了个事务2修改person表的同一个记录,将age修改为30岁
从上面,可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即链表,undo log的链表首部就是最新的旧纪录,链表尾部就是最早的旧纪录
MVCC的实现依赖于:隐藏字段、undo log、readview
readview就是事务进行快照读操作的时候,产生的读视图(ReadView),在该事务执行到快照读那一刻,会生成一个数据库当前的一个快照,记录并维护当前活跃的事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新的事务,ID值最大)
所以我们知道了ReadView是用来做可进行判断的,即当我们某个事务执行快照读时。为该记录创建一个系统的ReadView读视图,将其作为判断当前事务能够看到哪个版本的数据,即可能是最新的也可能是历史版本的数据。
使用READ UNCOMMITED(读未提交)隔离级别的事务,可以读到未提交事务修改过的记录,所以直接读取最新版本
使用SERIALIZABLE(串行化)隔离级别的事务,InnoDB规定使用加锁方式访问记录
使用READ COMMITED(读提交)和REPREATABLE READ(可重复读)隔离级别的事务,都必须保证读到已提交了的事务修改过的记录。假如另一个事务已经修改了记录但尚未提交,是不能读取到最新版本的记录的,核心问题在于需要判断一下版本链中哪个版本是当前事务可见的这是ReadView主要解决的问题
ReadView遵循一个可进行算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID对比(由ReadView维护),如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出undo log中的DB_TRX_ID再比较,既遍历链表的DB_TRX_ID(从链表首到链表尾,即从最近一次查起),直到找到满足特定条件的DB_TRX_ID,那么这个DB_TRX_ID所在记录就是当前事务能看见的最新老版本
ReadView中主要包含4个比较重要的内容:
creator_trx_id,创建这个Read View的事务ID。
trx_ids,表示在生成Read View时,当前系统中活跃的事务id列表
up_limit_id,活跃的事务中最小的事务ID
low_limit_id,表示生成ReadView时系统中下一个事务应该分配事务ID,也就是目前已出现过的事务ID的最大值+1。low_lmit_id是系统最大的事务ID值,需要注意的是系统中的事务ID,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids最大值,事务id是递增分配的比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了,那么一个新的事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4.
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见
当我们查询一条记录时,系统通过MVCC查找步骤:
举个例子
当事务 2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务 ID 为 2,此时还有事务1和事务3在活跃中,事务 4在事务 2快照读前一刻提交更新了,所以 Read View 记录了系统当前活跃事务 1,3 的 ID,维护在一个列表上,假设我们称为trx_ids
Read View不仅仅会通过一个列表trx_ids来维护事务2执行快照读那刻系统正在活跃的事务ID,还有两个属性up_limit_id(trx_ids中事务ID最小的ID),low_limit_id(快照读时刻系统下一个尚未分配的事务ID,也就是目前已出现过的事务ID的最大值+1)所以这里up_limit_id是1,low_limit_id是4+1=5,trx_ids的值是1,3,ReadView如图:
我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交事务。所以当前该行数据的undo log如下图所示:我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和**活跃事务ID列表(trx_ids)**进行比较,判断当前事务2能看到该记录的版本是哪个
所以先拿该记录 DB_TRX_ID 字段记录的事务 ID 4 去跟 Read View 的 up_limit_id 比较,看 4 是否小于 up_limit_id( 1 ),所以不符合条件,继续判断 4 是否大于等于 low_limit_id( 5 ),也不符合条件,最后判断 4 是否处于 trx_list 中的活跃事务, 最后发现事务 ID 为 4 的事务不在当前活跃事务列表中, 符合可见性条件,所以事务 4修改后提交的最新结果对事务 2 快照读时是可见的,所以事务 2 能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
正是由于ReadView生成的时机不同,从而造成RC,RR级别下快照读的结果不同
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次 Read View。
注意,此时同样的查询语句都会重新获取一次ReadView,这时如果ReadView不同,就可能产生不可重复度或幻读情况
当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会 获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:
总结
MVCC的实现逻辑就是一个事务如果要查看之前的版本,那么我就需要对于当前情况进行一个快照,从而转化为ReadView,然后去和Undo log中的过去版本进行比较。
READ COMMITED:每次读取数据前都生成一个ReadView
此刻,表student中id为1的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITED隔离级别的事务开始执行:
然后再到事务id为20的事务中更新一下表student中id为1的记录:
然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:
使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
比如,系统里有两个事务id分别为10、20的事务在执行:
此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:
假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:
之后,我们把 事务id 为 10 的事务提交一下,就像这样:
然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:
此刻,表student 中 id 为 1 的记录的版本链长这样:
然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:
假设现在表student中只有一条数据,数据内容中,主键id=1,隐藏的DB_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 的可重复读隔离级别下,不存在幻读问题。
当前读和快照读在RR级别下的区别
在上表的顺序下,事务B的在事务A提交修改后的快照读是旧版本数据,而当前读是实时新数据400
而在表2这里的顺序中,事务B在事务A提交后的快照读和当前读都是实时的新数据400,为什么?
表 1
的事务 B 在事务 A 修改金额前快照读
过一次金额数据,而表 2
的事务B在事务A修改金额前没有进行过快照读。所以我们知道事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
我们这里测试的是更新
,同时删除
和更新
也是一样的,如果事务B的快照读是在事务A操作之后进行的,事务B的快照读也是能读取到最新的数据的
正是 Read View
生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View
这里介绍了MVCC在READ COMMITTED、REPEATABLE READ这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事物的读-写、写-读操作并发执行,从而提升系统性能。
核心点在于ReadView的原理,READ COMMITED、REPEATABLE READ这两个隔离级别的一个很大不同就是ReadView的生成时机不同: