Mysql 实战 (二) 实践

1、普通索引和惟一索引

查询过程

对于普通索引来讲,查找到知足条件的第一个记录后,须要查找下一个记录,直到碰到第一个不知足 条件的记录。
对于惟一索引来讲,因为索引定义了惟一性,查找到第一个知足条件的记录后,就会中止继续检索。
InnoDB 的数据是按数据页为单位来读写的。在InnoDB 中,每一个数据页的大小默认是 16KB。对于整型字段,一个数据页能够放近千个 key。mysql

更新过程

当须要更新一个数据页时,若是数据页在内存中就直接更新, 而若是这个数据页尚未在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操做缓存在 changebuffer 中, 这样就不须要从磁盘中读入这个数据页了。在下次查询须要访问这个数据页的时候,将数据页读入内存,而后执行 change buffer 中与这个页有关的操做。

将 change buffer 中的操做应用到原数据页,获得最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会按期 merge。在数据库正常关闭(shutdown)的过程当中,也会执行 merge 操做。
惟一索引的更新操做都要先判断这个操做是否违反惟一性约束。而这必需要将数据页读入内存才能判断。因此惟一索引的更新不能使用 change buffer,只有普通索引可使用。

change buffer 用的是 buffer pool 里的内存,所以不能无限增大。change buffer 的大小,能够经过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

若是插入一个新记录 (4,400) 的话:
sql

  1. 这个记录要更新的目标页在内存中
    对于惟一索引来讲,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;对于普通索引来讲,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
  2. 这个记录要更新的目标页不在内存中
    对于惟一索引来讲,须要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;对于普通索引来讲,则是将更新记录在 change buffer,语句执行就结束了。

change buffer 的使用场景

对于写多读少的业务来讲,页面在写完之后立刻被访问到的几率比较小,此时change buffer 的使用效果最好。这种业务模型常见的就是帐单类、日志类的系统。
反过来,假设一个业务的更新模式是写入以后立刻会作查询,那么即便知足了条件,将更新先记录在 change buffer,但以后因为立刻要访问这个数据页,会当即触发 merge 过程。这样随机访问 IO 的次数不会减小,反而增长了 change buffer 的维护代价。因此,对于这种业务模式来讲,change buffer 反而起到了反作用。数据库

索引选择和实践

普通索引和惟一索引在查询能力上是没差异的,主要考虑的是对更新性能的影响。

若是全部的更新后面,都立刻伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其余状况下,change buffer 都能提高更新性能。缓存

change buffer 和 redo log

insert into t(id,k) values(id1,k1),(id2,k2)
k1 所在的数据页在内存 (InnoDBbuffer pool) 中,k2 所在的数据页不在内存中。下图是是带 change buffer 的更新状态图。 图片名称
分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)
(系统表空间就是用来放系统信息的,好比数据字典什么的,对应的磁盘文件是ibdata1, 数据表空间就是一个个的表数据文件,对应的磁盘文件就是 表名.ibd)

这条更新语句作了以下的操做(按照图中的数字顺序):
工具

  1. Page 1 在内存中,直接更新内存;
  2. Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息
  3. 将上述两个动做记入 redo log 中(图中 3 和 4)。作完上面这些,事务就能够完成了。因此,你会看到,执行这条更新语句的成本很低,就是写了两处内存,而后写了一处磁盘(两次操做合在一块儿写了一次磁盘),并且仍是顺序写的。同时,图中的两个虚线箭头,是后台操做,不影响更新的响应时间。

redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗

若是某次写入使用了 change buffer 机制,以后主机异常重启,是否会丢失 change buffer 和数据?
不会丢失,虽然是只更新内存,可是在事务提交的时候,咱们把 change buffer 的操做也记录到 redo log 里了,因此崩溃恢复的时候,change buffer 也能找回来。

merge 的过程是否会把数据直接写回磁盘?
merge 的执行流程是这样的:性能

  1. 从磁盘读入数据页到内存(老版本的数据页);
  2. 从 change buffer 里找出这个数据页的 change buffer 记录 (可能有多个),依次应用,获得新版数据页;
  3. 写 redo log。这个 redo log 包含了数据的变动和 change buffer 的变动。

到这里 merge 过程就结束了。这时候,数据页和内存中 change buffer 对应的磁盘位置都尚未修改,属于脏页,以后各自刷回本身的物理数据。测试

2、MySQL为何有时候会选错索引?

优化器的逻辑

化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。固然,扫描行数并非惟一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。

扫描行数是怎么判断的?
MySQL 在真正开始执行语句以前,并不能精确地知道知足这个条件的记录有多少条,而只能根据统计信息来估算记录数。这个统计信息就是索引的“区分度”。显然,一个索引上不一样的值越多,这个索引的区分度就越好。 而一个索引上不一样的值的个数,咱们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
咱们可使用 show index 方法,看到一个索引的基数。虽然这个表的每一行的三个字段值都是同样的,可是在统计信息中,这三个索引的基数值并不一样,并且其实都不许确。 优化

MySQL 是怎样获得索引的基数的呢? MySQL 采样统计,若是把整张表取出来一行行统计,虽然能够获得精确的结果,可是代价过高了,因此只能选择“采样统计”。InnoDB 默认会选择 N 个数据页,统计这些页面上的不一样值,获得一个平均值,而后乘以这个索引的页面数,就获得了这个索引的基数。

其实索引统计只是一个输入,对于一个具体的语句来讲,优化器还要判断,执行这个语句自己要扫描多少行。接下来,咱们再一块儿看看优化器预估的,这两个语句的扫描行数是多少。线程

优化器为何放着扫描 37000 行的执行计划不用,却选择了扫描行数是 100000 的执行计划呢?这是由于,若是使用索引 a,每次从索引 a 上拿到一个值, 都要回到主键索引上查出整行数据,这个代价优化器也要算进去的。

MySQL 选错索引,是没能准确地判断出扫描行数。既然是统计信息不对,那就修正。 analyze table t 命令,能够用来从新统计索引信息。咱们来看一下执行效果。

索引选择异常和处理

一种方法是,采用 force index 强行选择一个索引。
第二种方法就是,能够考虑修改语句(语义的逻辑是相同的),引导 MySQL 使用咱们指望的索引。
第三种方法是,在有些场景下,咱们能够新建一个更合适的索引,来提供给优化器作选择,或删掉误用的索引。日志

3、怎么给字符串字段加索引?

使用前缀索引后,可能会致使查询语句读数据的次数变多。

使用前缀索引,定义好长度,就能够作到既节省空间,又不用额外增长太多的查询成本。

当要给字符串建立前缀索引时,有什么方法可以肯定我应该使用多长的前缀呢?实际上,咱们在创建索引时关注的是区分度,区分度越高越好。由于区分度越高,意味着重复的键值越少。 所以,咱们能够经过统计索引上有多少个不一样的值来判断要使用多长的前缀。
首先,使用下面这个语句,算出这个列上有多少个不一样的值:
select count(distinct email) as L from SUser;
而后,依次选取不一样长度的前缀来看这个值,好比咱们要看一下 4~7 个字节的前缀索引,能够用这个语句:
select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7,from SUser;

前缀索引对覆盖索引的影响

使用前缀索引就用不上覆盖索引对查询性能的优化了

区分度很差的字符串怎么建索引?

如身份证号码
第一种方式是使用倒序存储。存储身份证号的时候把它倒过来存。
第二种方式是使用 hash 字段。能够在表上再建立一个整数字段,来保存身份证的校验码,同时在这个字段上建立索引。
它们的相同点是,都不支持范围查询。

4、为何MySQL会“抖”一下?

一条 SQL 语句,正常执行的时候特别快,可是有时也不知道怎么回事,它就会变得特别慢,而且这样的场景很难复现,它不仅随机,并且持续时间还很短。
InnoDB 在处理更新语句的时候,只作了写日志( redo log)这一个磁盘操做。而后把内存里的数据写入磁盘的过程,术语就是flush。在这个 flush 操做执行以前,内存和硬盘数据是不一致的。
当内存数据页跟磁盘数据页内容不一致的时候,咱们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
平时执行很快的更新操做,其实就是在写内存和日志,而 MySQL 偶尔“抖”一下的那个瞬间,可能就是在刷脏页(flush)。

什么状况会引起数据库的 flush 过程呢?

  1. InnoDB 的 redo log 写满了。这时候系统会中止全部更新操做。 若是你从监控上看,这时候更新数会跌为 0。
  2. 系统内存不足。当须要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。若是淘汰的是“脏页”,就要先将脏页写到磁盘。
    InnoDB 用缓冲池(buffer pool)管理内存, 缓冲池中的内存页有三种状态:尚未使用的;使用了而且是干净页;使用了而且是脏页。。
    而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:若是要淘汰的是一个干净页,就直接释放出来复用;但若是是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。
    一个查询要淘汰的脏页个数太多,会致使查询的响应时间明显变长。
  3. MySQL 认为系统“空闲”的时候。
  4. MySQL 正常关闭的状况。这时候,MySQL 会把内存的脏页都flush 到磁盘上,这样下次 MySQL 启动的时候,就能够直接从磁盘上读数据,启动速度会很快。

InnoDB 刷脏页的控制策略

首先,你要正确地告诉 InnoDB 所在主机的 IO 能力,这样 InnoDB 才能知道须要全力刷脏页的时候,能够刷多快。这就要用到 innodb_io_capacity 这个参数了,它会告诉 InnoDB 你的磁盘能力。这个值我建议你设置成磁盘的 IOPS。磁盘的 IOPS 能够经过 fio 这个工具来测试。

想要尽可能避免“一下”这种状况,**就要合理地设置 innodb_io_capacity 的值,而且平时要多关注脏页比例,不要让它常常接近 75%。**其中,脏页比例是经过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 获得的。

补充:mysql在准备刷一个脏页的时候,若是这个数据页旁边的数据页恰好是脏页,就会把这个“邻居”也带着一块儿刷掉。
在 InnoDB 中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,本身刷本身的。 找“邻居”这个优化在机械硬盘时代是颇有意义的,能够减小不少随机 IO。而若是使用的是 SSD 这类 IOPS 比较高的设备的话,由于这时候 IOPS 每每不是瓶颈,因此能够设置为0。

相关文章
相关标签/搜索