MySQL总结[缓存,索引,Explain,事务,redo日志等]

MySQL总结[缓存,索引,Explain,事务,redo日志等]

    • MySQL的执行流程
    • 查询缓存
    • 缓存关键词介绍
    • 缓存原理
    • 缓存优劣
    • 生成环境如何配置查询缓存
    • 索引
    • 聚簇索引(主键索引)
    • 非聚簇索引(二级索引)
    • 联合索引
    • 索引为什么用B+树
    • 优化器
    • 什么是成本?
    • 单表查询的成本
    • 使用所有可能用到的索引
    • 计算全表扫描代价
    • 计算使用不同索引执行查询的代价
    • 对比各种执行方案的代价,找出成本最低的那个
    • 多表查询的成本
    • Explain
    • redo日志(物理日志)
    • redo日志刷盘时机
    • redo日志文件组
    • undo日志
    • 事务
    • 事务并发执行的问题
    • 四种隔离级别

MySQL的执行流程

  1. 客户端连接MySQL服务器
  2. 账号密码验证或SSL证书认证
  3. 服务器验证客户端执行权限
  4. 查询前检查是否有查询缓存(可避免查询解析)
  5. 如果没有循环,MySQL解析器根结查询语句构造解析树(语法验证)
  6. 之后预处理器验证表名字段等
  7. 下面查询优化器将解析树转为查询计划(找到最优的执行计划)
  8. 执行计划调用查询执行引擎,引擎通过API查到数据
  9. 数据返回客户端及缓存

查询缓存

通过==show variables like ‘%query_cache%’==查看数据库配置。MySQL总结[缓存,索引,Explain,事务,redo日志等]_第1张图片

缓存关键词介绍

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

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

query_cache_min_res_unit:查询缓存分配的最小块(字节)。默认值是4096(4KB)。如果查询结果比较小,默认的query_cache_min_res_unit可能造成大量的内存碎片,如果查询结果比较大,默认的query_cache_min_res_unit又不够,导致一直分配块空间,所以可以根据实际需求,调节query_cache_min_res_unit的大小。

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

query_cache_type:默认为on,可以缓存除了以select sql_no_cache开头的所有查询结果。

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

缓存原理

缓存SQL的hash值和该SQL的查询结果,如果运行相同的SQL,服务器直接从缓存中去掉结果,而不再去解析

缓存优劣

优势: 提高查询速度
劣势: 频繁更新的表,缓存会出现反复清空现象. 对于sql的微小调整都会使得缓存无法命中(比如大小写或者空格)

生成环境如何配置查询缓存

关闭query_cache_type , 将其设置为OFF,他的弊端大于优点。

索引

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

聚簇索引(主键索引)

他包含两个特点:

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. 不要过度索引。每个额外的索引都要占用额外的磁盘空间,并降低写操作的性能。在修改表的内容时,索引必须进行更新,有时可能需要重构,因此,索引越多,所花的时间越长。

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

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

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

索引为什么用B+树

  1. B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,相对IO读写次数降低。

  2. 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可。

优化器

每一条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是如何选择相关索引呢?

使用所有可能用到的索引

我们可以看到搜索条件username,所以可能走uname_unique索引。也可以做聚簇索引,也就是全表扫描。

计算全表扫描代价

我们通过show table status like ‘dev_user’命令知道rows和data_length字段.

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

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

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

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

I/O成本:97X1=97

CPU成本:6141X0.2=1228

总成本:97+1228=1325

计算使用不同索引执行查询的代价

因为要查询出满足条件的所有字段信息,所以要考虑回表成本。

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

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

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

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

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

多表查询的成本

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

单次查询驱动表的成本

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

  1. index dive
    如果前面的搜索条件不是等值,而是区间,如select * from dev_user where username>‘admin’ and username<'test’这个时候我们是无法看出需要回表的数量。

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

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

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

Explain

  1. id
    一般来说一个select一个唯一id,如果是子查询,就有两个select,id是不一样的,但是凡事有例外,有些子查询的,他们id是一样的。那是因为MySQL在进行优化的时候已经将子查询改成了连接查询,而连接查询的id是一样的。

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

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

  4. type:访问方法
    ref:普通二级索引与常量进行等值匹配
    ref_or_null:普通二级索引与常量进行等值匹配,该索引可能是null
    const:主键或唯一二级索引列与常量进行等值匹配
    range:范围区间的查询
    all:全表扫描

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

  6. key
    经过查询优化器计算不同索引的成本,最终选择成本最低的索引

  7. rows
    如果使用全表扫描,那么rows就代表需要扫描的行数
    如果使用索引,那么rows就代表预计扫描的行数

  8. filtered
    如果全表扫描,那么filtered就代表满足搜索条件的记录的满分比
    如果是索引,那么filtered就代表除去索引对应的搜索,其他搜索条件的百分比

redo日志(物理日志)

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

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

  1. 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,如下图所示。

MySQL总结[缓存,索引,Explain,事务,redo日志等]_第2张图片
redo日志是为了系统崩溃后恢复脏页用的,将缓冲区log buffer里面的redo日志刷新到这个两个文件里面,他们写入的方式 是循环写入的,先写ib_logfile0,再写ib_logfile1,等ib_logfile1写满了,再写ib_logfile0。

从系统运行开始,就不断的修改页面,会不断的生成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有两个作用:提供回滚和多个行版本控制。

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

事务

事务中有一个隔离性特征,理论上在某个事务对某个数据进行访问时,其他事务应该排序,当该事务提交之后,其他事务才能继续访问这个数据。这对性能会有影响,所以选择舍弃一部分隔离性。

事务并发执行的问题

脏写(这个太严重了,任何隔离级别都不允许发生)
A:修改了一条数据,回滚掉
B:修改了同一条数据,提交掉

脏读:一个事务读到另一个未提交事务修改的数据
A:查询,得到某条数据
B:修改某条数据,但是最后回滚掉啦
A:在B修改某条数据之后,在回滚之前,读取了该条记录

不可重复读:前后多次读取,同一个数据内容不一样
A:查询某条记录
B : 修改该条记录,并提交事务
A : 再次查询该条记录,发现前后查询不一致

幻读:前后多次读取,数据总量不一致
A:查询表内所有记录
B : 新增一条记录,并查询表内所有记录
A : 再次查询该条记录,发现前后查询不一致

四种隔离级别

数据库都有的四种隔离级别,MySQL事务默认的隔离级别是可重复读。

未提交读:脏读,不可重复读,幻读都有可能发生
已提交读:不可重复读,幻读可能发生
可重复读:幻读可能发生
可串行化:都不可能发生

你可能感兴趣的:(MySQL总结[缓存,索引,Explain,事务,redo日志等])