MySQL的万字总结(缓存,索引,Explain,事务,redo日志等)

hello,小伙伴们,很久不见,MySQL系列停更了差很少两个月了,也有小伙伴问我为啥不更了呢?其实我去看了MySQL的全集,准备憋个大招,更新篇长文(我不会告诉你是由于我懒的)。html

好了,话很少说,直接开始吧。这篇文章将从查询缓存,索引,优化器,explain,redo日志,undo日志,事务隔离级别,锁等方面来说,若是想了解某个方面,直接跳到指定目录。mysql

开局一张图

这张图是重点!!!咱要先对MySQL有一个宏观的了解,知道他的执行流程。面试

一条SQL语句过来的流程是什么样的?那就follow me。哈哈哈哈,皮一下很开心。sql

1.当客户端链接到MySQL服务器时,服务器对其进行认证。能够经过用户名与密码认证,也能够经过SSL证书进行认证。登陆认证后,服务器还会验证客户端是否有执行某个查询的操做权限。数据库

2.在正式查询以前,服务器会检查查询缓存,若是能找到对应的查询,则没必要进行查询解析,优化,执行等过程,直接返回缓存中的结果集。缓存

3.MySQL的解析器会根据查询语句,构造出一个解析树,主要用于根据语法规则来验证语句是否正确,好比SQL的关键字是否正确,关键字的顺序是否正确。bash

而预处理器主要是进一步校验,好比表名,字段名是否正确等服务器

4.查询优化器将解析树转化为查询计划,通常状况下,一条查询能够有不少种执行方式,最终返回相同的结果,优化器就是根据成本找到这其中最优的执行计划网络

5.执行计划调用查询执行引擎,而查询引擎经过一系列API接口查询到数据session

6.获得数据以后,在返回给客户端的同时,会将数据存在查询缓存中


查询缓存

咱们先经过show variables like '%query_cache%'来看一下默认的数据库配置,此为本地数据库的配置。


概念

have_query_cache:当前的MYSQL版本是否支持“查询缓存”功能。

query_cache_limit:MySQL可以缓存的最大查询结果,查询结果大于该值时不会被缓存。默认值是1048576(1MB)

query_cache_min_res_unit:查询缓存分配的最小块(字节)。默认值是4096(4KB)。当查询进行时,MySQL把查询结果保存在query cache,可是若是保存的结果比较大,超过了query_cache_min_res_unit的值,这时候MySQL将一边检索结果,一边进行保存结果。他保存结果也是按默认大小先分配一块空间,若是不够,又要申请新的空间给他。若是查询结果比较小,默认的query_cache_min_res_unit可能形成大量的内存碎片,若是查询结果比较大,默认的query_cache_min_res_unit又不够,致使一直分配块空间,因此能够根据实际需求,调节query_cache_min_res_unit的大小。

注:若是上面说的内容有点弯弯绕,那举个现实生活中的例子,好比咱如今要给运动员送水,默认的是500ml的瓶子,若是过来的是少年运动员,可能500ml太大了,他们喝不完,形成了浪费,那咱们就能够选择300ml的瓶子,若是过来的是成年运动员,可能500ml不够,那他们一瓶喝完了,又开一瓶,直接不渴为止。那么那样开瓶子也要时间,咱们就能够选择1000ml的瓶子。

query_cache_size:为缓存查询结果分配的总内存。

query_cache_type:默认为on,能够缓存除了以select sql_no_cache开头的全部查询结果。

query_cache_wlock_invalidate:若是该表被锁住,是否返回缓存中的数据,默认是关闭的。

原理

MYSQL的查询缓存实质上是缓存SQL的hash值和该SQL的查询结果,若是运行相同的SQL,服务器直接从缓存中去掉结果,而再也不去解析,优化,寻找最低成本的执行计划等一系列操做,大大提高了查询速度。

可是万事有利也有弊。

  • 第一个弊端就是若是表的数据有一条发生变化,那么缓存好的结果将所有再也不有效。这对于频繁更新的表,查询缓存是不适合的。
好比一张表里面只有两个字段,分别是id和name,数据有一条为1,张三。我使用select * from 表名 where name=“张三”来进行查询,MySQL发现查询缓存中没有此数据,会进行一系列的解析,优化等操做进行数据的查询,查询结束以后将该SQL的hash和查询结果缓存起来,并将查询结果返回给客户端。可是这个时候我有新增了一条数据2,张三。若是我还用相同的SQL来执行,他会根据该SQL的hash值去查询缓存中,那么结果就错了。因此MySQL对于数据有变化的表来讲,会直接清空关于该表的全部缓存。这样实际上是效率是不好的。
  • 第二个弊端就是缓存机制是经过对SQL的hash,得出的值为key,查询结果为value来存放的,那么就意味着SQL必须完彻底全如出一辙,不然就命不中缓存。
咱们都知道hash值的规则,就算很小的查询,哈希出来的结果差距是不少的,因此select * from 表名 where name=“张三”和SELECT * FROM 表名 WHERE NAME=“张三”和select * from 表名 where name = “张三”,三个SQL哈希出来的值是不同的,大小写和空格影响了他们,因此并不能命中缓存,但其实他们搜索结果是彻底同样的。

生产如何设置MySQL Query Cache

先来看线上参数:


咱们发现将query_cache_type设置为OFF,其实网上资料和各大云厂商提供的云服务器都是将这个功能关闭的,从上面的原理来看,在通常状况下,他的弊端大于优势

索引

例子

建立一个名为user的表,其包括id,name,age,sex等字段信息。此外,id为主键聚簇索引,idx_name为非聚簇索引。

CREATE TABLE `user` (
  `id` varchar(10) NOT NULL DEFAULT '',
  `name` varchar(10) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `sex` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;复制代码

咱们将其设置10条数据,便于下面的索引的理解。

INSERT INTO `user` VALUES ('1', 'andy', '20', '女');
INSERT INTO `user` VALUES ('10', 'baby', '12', '女');
INSERT INTO `user` VALUES ('2', 'kat', '12', '女');
INSERT INTO `user` VALUES ('3', 'lili', '20', '男');
INSERT INTO `user` VALUES ('4', 'lucy', '22', '女');
INSERT INTO `user` VALUES ('5', 'bill', '20', '男');
INSERT INTO `user` VALUES ('6', 'zoe', '20', '男');
INSERT INTO `user` VALUES ('7', 'hay', '20', '女');
INSERT INTO `user` VALUES ('8', 'tony', '20', '男');
INSERT INTO `user` VALUES ('9', 'rose', '21', '男');复制代码

聚簇索引(主键索引)

先来一张图镇楼,接下来就是看图说话。


他包含两个特色:

1.使用记录主键值的大小来进行记录和页的排序。

页内的记录是按照主键的大小顺序排成一个单项链表。

各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。

2.叶子节点存储的是完整的用户记录

注:聚簇索引不须要咱们显示的建立,他是由InnoDB存储引擎自动为咱们建立的。若是没有主键,其也会默认建立一个。复制代码

非聚簇索引(二级索引)

上面的聚簇索引只能在搜索条件是主键时才能发挥做用,由于聚簇索引能够根据主键进行排序的。若是搜索条件是name,在刚才的聚簇索引上,咱们可能遍历,挨个找到符合条件的记录,可是,这样真的是太蠢了,MySQL不会这样作的。

若是咱们想让搜索条件是name的时候,也能使用索引,那能够多建立一个基于name的二叉树。以下图。


他与聚簇索引的不一样:

1.叶子节点内部使用name字段排序,叶子节点之间也是使用name字段排序。

2.叶子节点再也不是完整的数据记录,而是name和主键值。

为何再也不是完整信息?

MySQL只让聚簇索引的叶子节点存放完整的记录信息,由于若是有好几个非聚簇索引,他们的叶子节点也存放完整的记录绩效,那就不浪费空间啦。

若是我搜索条件是基于name,须要查询全部字段的信息,那查询过程是啥?

1.根据查询条件,采用name的非聚簇索引,先定位到该非聚簇索引某些记录行。

2.根据记录行找到相应的id,再根据id到聚簇索引中找到相关记录。这个过程叫作

联合索引

图就不画了,简单来讲,若是name和age组成一个联合索引,那么先按name排序,若是name同样,就按age排序。

一些原则

1.最左前缀原则。一个联合索引(a,b,c),若是有一个查询条件有a,有b,那么他则走索引,若是有一个查询条件没有a,那么他则不走索引。

2.使用惟一索引。具备多个重复值的列,其索引效果最差。例如,存放姓名的列具备不一样值,很容易区分每行。而用来记录性别的列,只含有“男”,“女”,无论搜索哪一个值,都会得出大约一半的行,这样的索引对性能的提高不够高。

3.不要过分索引。每一个额外的索引都要占用额外的磁盘空间,并下降写操做的性能。在修改表的内容时,索引必须进行更新,有时可能须要重构,所以,索引越多,所花的时间越长。

四、索引列不能参与计算,保持列“干净”,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,缘由很简单,b+树中存的都是数据表中的字段值,但进行检索时,须要把全部元素都应用函数才能比较,显然成本太大。因此语句应该写成create_time = unix_timestamp(’2014-05-29’);

5.必定要设置一个主键。前面聚簇索引说到若是不指定主键,InnoDB会自动为其指定主键,这个咱们是看不见的。反正都要生成一个主键的,还不如咱们设置,之后在某些搜索条件时还能用到主键的聚簇索引。

6.主键推荐用自增id,而不是uuid。上面的聚簇索引说到每页数据都是排序的,而且页之间也是排序的,若是是uuid,那么其确定是随机的,其可能从中间插入,致使页的分裂,产生不少表碎片。若是是自增的,那么其有从小到大自增的,有顺序,那么在插入的时候就添加到当前索引的后续位置。当一页写满,就会自动开辟一个新的页。

注:若是自增id用完了,那将字段类型改成bigint,就算每秒1万条数据,跑100年,也没达到bigint的最大值。复制代码

万年面试题(为何索引用B+树)

一、 B+树的磁盘读写代价更低:B+树的内部节点并无指向关键字具体信息的指针,所以其内部节点相对B树更小,若是把全部同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的须要查找的关键字也就越多,相对IO读写次数就下降了。

二、因为B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只须要扫一遍叶子结点便可,可是B树由于其分支结点一样存储着数据,咱们要找到具体的数据,须要进行一次中序遍历按序来扫,因此B+树更加适合在区间查询的状况,因此一般B+树用于数据库索引。

优化器

在开篇的图里面,咱们知道了SQL语句从客户端经由网络协议到查询缓存,若是没有命中缓存,再通过解析工做,获得准确的SQL,如今就来到了咱们这模块说的优化器。

首先,咱们知道每一条SQL都有不一样的执行方法,要不经过索引,要不经过全表扫描的方式。

那么问题就来了,MySQL是如何选择时间最短,占用内存最小的执行方法呢?

什么是成本?

1.I/O成本。数据存储在硬盘上,咱们想要进行某个操做须要将其加载到内存中,这个过程的时间被称为I/O成本。默认是1。

2.CPU成本。在内存对结果集进行排序的时间被称为CPU成本。默认是0.2。

单表查询的成本

先来建一个用户表dev_user,里面包括主键id,用户名username,密码password,外键user_info_id,状态status,外键main_station_id,是否外网访问visit,这七个字段。索引有两个,一个是主键的聚簇索引,另外一个是显式添加的以username为字段的惟一索引uname_unique。


若是搜索条件是select * from dev_user where username='XXX',那么MySQL是如何选择相关索引呢?

1.使用全部可能用到的索引

咱们能够看到搜索条件username,因此可能走uname_unique索引。也能够作聚簇索引,也就是全表扫描。

2.计算全表扫描代价

咱们经过show table status like ‘dev_user’命令知道rowsdata_length字段,以下图。


rows:表示表中的记录条数,可是这个数据不许确,是个估计值。

data_length:表示表占用的存储空间字节数。

data_length=聚簇索引的页面数量X每一个页面的大小

反推出页面数量=1589248÷16÷1024=97

I/O成本:97X1=97

CPU成本:6141X0.2=1228

总成本:97+1228=1325

3.计算使用不一样索引执行查询的代价

由于要查询出知足条件的全部字段信息,因此要考虑回表成本。

I/O成本=1+1X1=2(范围区间的数量+预计二级记录索引条数)

CPU成本=1X0.2+1X0.2=0.4(读取二级索引的成本+回表聚簇索引的成本)

总成本=I/O成本+CPU成本=2.4

4.对比各类执行方案的代价,找出成本最低的那个

上面两个数字一对比,成本是采用uname_unique索引成本最低。

多表查询的成本

对于两表链接查询来讲,他的查询成本由下面两个部分构成:

  • 单次查询驱动表的成本
  • 屡次查询被驱动表的成本(具体查询屡次取决于对驱动表查询的结果集有多少个记录)

index dive

若是前面的搜索条件不是等值,而是区间,如select * from dev_user where username>'admin' and username<'test'这个时候咱们是没法看出须要回表的数量。

步骤1:先根据username>'admin'这个条件找到第一条记录,称为区间最左记录

步骤2:再根据username<'test'这个条件找到最后一条记录,称为区间最右记录

步骤3:若是区间最左记录和区间最右记录相差不是很远,能够准确统计出须要回表的数量。若是相差很远,就先计算10页有多少条记录,再乘以页面数量,最终模糊统计出来。

Explain

产品来索命

产品:为何这个页面出来这么慢?

开发:由于你查的数据多呗,他就是这么慢

产品:我无论,我要这个页面快点,你这样,客户怎么用啊

开发:。。。。。。。你行你来


哈哈哈哈,不瞎BB啦,若是有些SQL贼慢,咱们须要知道他有没有走索引,走了哪一个索引,这个时候我就须要经过explain关键字来深刻了解MySQL内部是如何执行的。


id

通常来讲一个select一个惟一id,若是是子查询,就有两个select,id是不同的,可是凡事有例外,有些子查询的,他们id是同样的。


这是为何呢?

那是由于MySQL在进行优化的时候已经将子查询改为了链接查询,而链接查询的id是同样的。

select_type

  • simple:不包括union和子查询的查询都算simple类型。
  • primary:包括union,union all,其中最左边的查询即为primary。
  • union:包括union,union all,除了最左边的查询,其余的查询类型都为union。

table

显示这一行是关于哪张表的。

type:访问方法

  • ref:普通二级索引与常量进行等值匹配
  • ref_or_null:普通二级索引与常量进行等值匹配,该索引多是null
  • const:主键或惟一二级索引列与常量进行等值匹配
  • range:范围区间的查询
  • all:全表扫描

possible_keys

对某表进行单表查询时可能用到的索引

key

通过查询优化器计算不一样索引的成本,最终选择成本最低的索引

rows

  • 若是使用全表扫描,那么rows就表明须要扫描的行数
  • 若是使用索引,那么rows就表明预计扫描的行数

filtered

  • 若是全表扫描,那么filtered就表明知足搜索条件的记录的满分比
  • 若是是索引,那么filtered就表明除去索引对应的搜索,其余搜索条件的百分比

redo日志(物理日志)

InnoDB存储引擎是以页为单位来管理存储空间的,咱们进行的增删改查操做都是将页的数据加载到内存中,而后进行操做,再将数据刷回到硬盘上。

那么问题就来了,若是我要给张三转帐100块钱,事务已经提交了,这个时候InnoDB把数据加载到内存中,这个时候还没来得及刷入硬盘,忽然停电了,数据库崩了。重启以后,发现个人钱没有转成功,这不是尴尬了吗?

解决方法很明显,咱们在硬盘加载到内存以后,进行一系列操做,一顿操做猛如虎,还未刷新到硬盘以前,先记录下,在XXX位置个人记录中金额减100,在XXX位置张三的记录中金额加100,而后再进行增删改查操做,最后刷入硬盘。若是未刷入硬盘,在重启以后,先加载以前的记录,那么数据就回来了。

这个记录就叫作重作日志,即redo日志。他的目的是想让已经提交的事务对数据的修改是永久的,就算他重启,数据也能恢复出来。

log buffer(日志缓冲区)

为了解决磁盘速度过慢的问题,redo日志不能直接写入磁盘,咱先整一大片连续的内存空间给他放数据。这一大片内存就叫作日志缓冲区,即log buffer。到了合适的时候,再刷入硬盘。至于何时是合适的,这个下一章节说。

咱们能够经过show VARIABLES like 'innodb_log_buffer_size'命令来查看当前的日志缓存大小,下图为线上的大小。


redo日志刷盘时机

因为redo日志一直都是增加的,且内存空间有限,数据也不能一直待在缓存中, 咱们须要将其刷新至硬盘上。 

那何时刷新到硬盘呢?

  •  log buffer空间不足。上面有指定缓冲区的内存大小,MySQL认为日志量已经占了 总容量的一半左右,就须要将这些日志刷新到磁盘上。 
  • 事务提交时。咱们使用redo日志的目的就是将他未刷新到磁盘的记录保存起来,防止 丢失,若是数据提交了,咱们是能够不把数据提交到磁盘的,但为了保证持久性,必须 把修改这些页面的redo日志刷新到磁盘。 
  • 后台线程不一样的刷新 后台有一个线程,大概每秒都会将log buffer里面的redo日志刷新到硬盘上。
  •  checkpoint 下下小节讲

redo日志文件组

咱们能够经过show variables like 'datadir'命令找到相关目录,底下有两个文件, 分别是ib_logfile0和ib_logfile1,以下图所示。


 


咱们将缓冲区log buffer里面的redo日志刷新到这个两个文件里面,他们写入的方式 是循环写入的,先写ib_logfile0,再写ib_logfile1,等ib_logfile1写满了,再写ib_logfile0。 那这样就会存在一个问题,若是ib_logfile1写满了,再写ib_logfile0,以前ib_logfile0的内容 不就被覆盖而丢失了吗? 这就是checkpoint的工做啦。

checkpoint

redo日志是为了系统崩溃后恢复脏页用的,若是这个脏页能够被刷新到磁盘上,那么 他就能够功成身退,被覆盖也就没事啦。

 冲突补习 

从系统运行开始,就不断的修改页面,会不断的生成redo日志。redo日志是不断 递增的,MySQL为其取了一个名字日志序列号Log Sequence Number,简称lsn。 他的初始化的值为8704,用来记录当前一共生成了多少redo日志。

 redo日志是先写入log buffer,以后才会被刷新到磁盘的redo日志文件。MySQL为其 取了一个名字flush_to_disk_lsn。用来讲明缓存区中有多少的脏页数据被刷新到磁盘上啦。 他的初始值和lsn同样,后面的差距就有了。

 作一次checkpoint分为两步 

  • 计算当前系统能够被覆盖的redo日志对应的lsn最大值是多少。redo日志能够被覆盖, 意味着他对应的脏页被刷新到磁盘上,只要咱们计算出当前系统中最先被修改的oldest_modification, 只要系统中lsn小于该节点的oldest_modification值磁盘的redo日志都是能够被覆盖的。
  •  将lsn过程当中的一些数据统计。

undo日志(这部分不是很明白,因此大概说了)

基本概念

undo log有两个做用:提供回滚和多个行版本控制(MVCC)。

undo log和redo log记录物理日志不同,它是逻辑日志。能够认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

举个例子:

insert into a(id) values(1);(redo)
这条记录是须要回滚的。
回滚的语句是delete from a where id = 1;(undo)

试想一想看。若是没有作insert into a(id) values(1);(redo)
那么delete from a where id = 1;(undo)这句话就没有意义了。

如今看下正确的恢复:
先insert into a(id) values(1);(redo)
而后delete from a where id = 1;(undo)
系统就回到了原先的状态,没有这条记录了

存储方式

是存在段之中。

事务

引言

事务中有一个隔离性特征,理论上在某个事务对某个数据进行访问时,其余事务应该排序,当该事务提交以后,其余事务才能继续访问这个数据。

可是这样子对性能影响太大,咱们既想保持事务的隔离性,又想让服务器在出来多个事务时性能尽可能高些,因此只能舍弃一部分隔离性而去性能。

事务并发执行的问题

  • 脏写(这个太严重了,任何隔离级别都不容许发生)
sessionA:修改了一条数据,回滚掉

sessionB:修改了同一条数据,提交掉

对于sessionB来讲,明明数据更新了也提交了事务,不能说本身啥都没干

  • 脏读:一个事务读到另外一个未提交事务修改的数据
session A:查询,获得某条数据

session B:修改某条数据,可是最后回滚掉啦

session A:在sessionB修改某条数据以后,在回滚以前,读取了该条记录

对于session A来讲,读到了session回滚以前的脏数据

  • 不可重复读:先后屡次读取,同一个数据内容不同
session A:查询某条记录
session B : 修改该条记录,并提交事务
session A : 再次查询该条记录,发现先后查询不一致
  • 幻读:先后屡次读取,数据总量不一致
session A:查询表内全部记录
session B : 新增一条记录,并查询表内全部记录
session A : 再次查询该条记录,发现先后查询不一致

四种隔离级别

数据库都有的四种隔离级别,MySQL事务默认的隔离级别是可重复读,并且MySQL能够解决了幻读的问题。

  • 未提交读:脏读,不可重复读,幻读都有可能发生
  • 已提交读:不可重复读,幻读可能发生
  • 可重复读:幻读可能发生
  • 可串行化:都不可能发生
但凡事没有百分百,emmmm,其实MySQL并无百分之百解决幻读的问题。


举个例子:

session A:查询某条不存在的记录。

session B:新增该条不存在的记录,并提交事务。

session A:再次查询该条不存在的记录,是查询不出来的,可是若是我尝试修改该条记录,并提交,其实他是能够修改为功的。

MVCC

版本链:对于该记录的每次更新,都会将值放在一条undo日志中,算是该记录的一个旧版本,随着更新次数的增多,全部版本都会被roll_pointer属性链接成一个链表,即为版本链。

readview:

  • 未提交读:由于能够读到未提交事务修改的记录,因此能够直接读取记录的最新版本就行
  • 已提交读:每次读取以前都生成一个readview
  • 可重复读:只有在第一次读取的时候才生成readview
  • 可串行化:InnoDB涉及了加锁的方式来访问记录

求个关注

小老弟不容易,忙了好几天,终于写好。


参考文献

【原创】面试官:讲讲mysql表设计要注意啥

【原创】杂谈自增主键用完了怎么办

MySQL 是怎样运行的:从根儿上理解 MySQL

详细分析MySQL事务日志(redo log和undo log)

相关文章
相关标签/搜索