MySQL索引

索引的所用:提高数据查询效率。
常见索引模型:

  • 哈希表:存储键值对;把值放入数组中,用一个哈希函数把key换算成一个确定的位置,然后将value放入数组的这个位置;使用链表处理冲突的问题;适用于等值查询的场景。
  • 有序数组:按照顺序存储;查询用二分法可以快速查询,时间复杂度为O(logN);查询效率高,更新效率低;适用于静态存储引擎。
  • 二叉索引树:每个节点的左儿子小于父节点,父节点小于右儿子;查询时间复杂度为(O(logN)),更新时间复杂度为O(logN);数据库存储大多不适用于二叉树,因为树高过高,会适用N叉树。

InnoDB中的索引模型:B+ 树。

索引类型:

  • 主键索引:叶子节点存的都是整行的数据(聚集索引)
  • 非主键索引:非主键索引的叶子节点内容是主键的值(二级索引)

主键索引与非主键索引的区别:主键索引主要搜索主键的这个B+ 树即可拿到数据。普通索引先搜索索引拿到主键值,再到主键索引树搜索一次(回表)。

一个数据页满了,按照B+树算法,新增加一个数据页,叫做页分裂,会导致性能下降。空间利用率大概降低50%。当相邻的两个数据页利用很低的时候,会做
数据页合并,合并的过程是分类过程的逆过程。

从性能和存储空间方面考量,自增主键往往是更合理的选择。

如何避免长事务对业务的影响?
从应用开发端来看:

  • 确认是否使用了set autocommit = 0。打开MySQL的general_log打开。
  • 确认是否有不必要的只读事务。在读事务中去掉begin/commit 去掉。
  • 业务链接数据库的时候,根据业务本身的预估,通过set max_execution_time命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

从数据库端来看:

  • 监控information_schema.innodb_trx表,设置事务阈值,超过就报警/或者kill
  • 功能测试时输出general_log,分析日志行为提前发现问题。
  • MySQL 5.6或者更新版本,把innodb_undo_tablesapes设置为2或者更大的值。

由于覆盖索引可减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

B+ 树这种索引结构,可以利用索引的最左前缀,拉起定位记录。

在建立联合索引的时候,如何安排索引内的字段。
第一原则是如果通过调整顺序,可以少维护一个索引,那么这个顺序往往是需要优先考虑采用的。

MySQL5.6引入的索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

普通索引和唯一索引,如何选择

InnoDB的数据是按数据页为单位来读写的。每个数据页的大小默认是16KB。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,
在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。
在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。
通过这种方式就能保证这个数据逻辑的正确性。
change buffer中的数据也可以进行持久化,将change buffer中的操作应用到原数据页,得到最新结果的过程成为merge。
在数据库正常关闭的过程中,也会执行merge操作。

什么条件下可以使用change buffer(不能保证唯一性)?
唯一索引的更新不能使用change buffer,只有普通索引可以使用。

change buffer用的是buffer pool里的内存,因此不能无限增大。可以通过innodb_change_buffer_max_size来动态设置change buffer。

目标数据页在内存中:

  • 唯一索引,判断数据是否存在,不存在插入
  • 普通索引,找到位置直接插入

目标数据页不在内存中:

  • 唯一索引,将数据页读入内存中,判断是否有冲突,没有冲突,直接插入
  • 普通索引,将更新记录在change buffer

change buffer因为减少了随机磁盘访问,所以对更新性能的提升会很明显的。

change buffer的使用场景:写锁读少。页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好。
这种模式常见的就是账单类、日志类的系统。

WAL提升性能的核心机制,核心是尽量减少随机读写。

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

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

merge的过程是否会把数据直接写回磁盘:

  • 从磁盘读入数据页到内存;
  • 从change buffer里面找到这个数据页的change buffer记录,一次应用,得到新版数据页;
  • 写redo log。这个redo log包含了数据的变更和change buffer的变更。

MySQL为什么有时候会选错索引?

优化器选择索引的目的,就是找到一个最优的执行方案,并且最小的代价去执行语句。
在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。

扫描行数是怎么判断的?
一个索引上不同的值的个数越多,索引的区分度越好。可以通过show index from t来看。

MySQL是怎样得到索引的基数的?通过采样统计,取平均值。

在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent的值来选择:

  • 设置为on,统计信息会持久化存储。N(取样时,选择数据页的个数)默认是20,M(变换行数的分母)默认是10。
  • 设置为off,统计信息值存储在内存中。N的默认值为8。M的默认值为16。

使用analyze table t命令,可以用来重新统计索引信息。

索引选择异常和处理

  • 使用force index强行选择一个索引。
  • 通过修改sql语句来正确使用索引。
  • 新建一个更合适的索引,或者删掉误用的索引。
SessionA Session B
start transaction with consistent snapshot;
delete from t;call idata();
explain select * from t where a between 100000 and 200000;
commit;

经过上面的顺序,explain的结果不同啦?
delete语句删除了所有的数据,然后通过call idata()插入10w行数据,看上去是覆盖了原来的10w行数据。
SessionA开启了事务但没有提交,之前插入的10W行数据是不能删除的。拥有两个版本的数据:delete之前的数据(旧数据);标记位deleted的数据(新数据)。
索引A上就有两份数据。不过这个是主键,主键是按照表的行数来估计的。表的行数,优化器直接用的是show table status的值。

怎么给字符串字段加索引

MySQL支持前缀索引。前缀索引可以指定建立索引的位数,这样占用的空间会更小,这就是使用前缀索引的优势。会增加额外的记录扫描次数。
使用前缀索引的时候,需要定义好长度,就可以做到既节省空间又不用额外增加太多的查询成本。

为字符串创建索引的时候,需要考虑区分度,区分度越高越好。因为区分度越高,意味着重复的健值越少。可以通过查询字符串的长度,只要符合95%即可。

前缀索引对覆盖索引的影响:
前缀索引可能会增加扫描行数,这回影响到性能。
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是在选择是否使用前缀索引时考虑的一个因素。

身份证号这种使用什么索引:

  • 使用倒序存储。将身份证号反转存储。
  • 使用hash字段。创建一个整数字段,用来保存身份证的校验码,同时在这个字段上创建索引。

使用倒序存储和使用hash字段这种方法的异同点:

  • 相同点:不支持范围查询。hash只能支持等值查询。
  • 不同点:倒序存储方式在主键索引上,不会消耗额外的存储空间,hash字段方法需要增加一个字段;倒序方式每次都需要调用reverse函数,
    hash字段需要调用crc32()函数;查询效率,hash字段方式的查询性能相对稳定一些,倒序存储增加扫描行数。

为字符串创建索引:

  • 直接创建完整索引,这样可能比较占用空间;
  • 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
  • 倒序存储,在创建前缀索引时,用于绕过字符串本身前缀的区分度不够的问题;
  • 创建hash字段索引,查询性能稳定,有额外的存储和计算消耗,不支持范围查询。

为什么我的MySQL会抖一下

当内存数据页根磁盘数据页内容不一致时,称这个内存页为脏页。内存数据写入到磁盘后,内存和磁盘上的数据页内容就一致了,成为干净页。

什么情况会引发数据库的flush过程:

  • redo log写满了:这时系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。尽量避免这种情况。
  • 系统内存不足:就淘汰一部分数据页,写到磁盘上。脏页一定回写盘,就保证了每个数据页有两个状态:
    • 内存里存在,内存里就肯定是正确的结果,直接返回。
    • 内存里没有数据,可以肯定数据文件上是正确的结果,读入内存后返回。
  • 系统空闲的时候,将数据写入到磁盘上。
  • mysql正常关闭:mysql会把内存的脏页都flush到磁盘上,下次MySQL启动的时候,就可以直接从磁盘上读取数据,启动速度会很快。

InnoDB用缓存池管理内存,缓存池中的内存页有三种状态:

  • 还没有使用的
  • 使用了并且是干净页
  • 使用了并且是脏页

影响刷脏页的情况:

  • 淘汰的脏页个数太多,会导致查询的相应时间明显变长;
  • 日志写满,更新全部堵住,写性能跌为0。

InnoDB刷脏页的控制策略

使用innodb_io_capacity来表示磁盘能力。
InnoDB的刷盘速度就是要考虑这两个因素:一个是脏页比例,一个是redo log写盘速度。

innodb_max_dirty_pages_pact是脏页比例上线,默认值为75%,

innodb_flush_neighbors参数来控制是否刷新邻居,如果值为1,是有连坐机制,值为0表示各自刷各自的(MySQL 8.0的时候,已经默认值为0)。

如果将redo log值设置的小,就会将redo log快速写满,磁盘的压力也很小,但是数据库会出现间歇性的性能下降。

为什么表数据删除一半,表文件大小不变

一个InnoDB表包含两部分:表结构定义和数据。MySQL8.0之前,表结构是存在以.frm为后缀的文件里。
MySQL8.0,已经允许把表结构定义放在系统数据表中。

参数innodb_file_per_table

表数据既可以存在共享表空间里,也可以是单独的文件。设置为OFF时,表的数据放在系统共享表空间,也是根数据字典放在一起。
设置为ON时,每个InnoDB表数据存储在一个以.idb为后缀的文件里。MySQL 5.6版本开始,默认值就是ON。

一个表单独存储为一个文件,便于管理,可以直接删除这个文件;如果放在共享空间里,即使表删除了,空间也是不会回收的。

drop table可以收回表空间,删除部分行,不能回收表空间。

在删除数据时,会将数据添加上删除标签,不会造成文件的缩小,如果有其他的数据可以直接复用这个位置。如果删除一个数据页上的所有记录,
整个数据页就可以被复用了。

记录的复用只限于符合条件的数据,当整个数据页从B+树里面摘掉以后,可以复用到任何位置。

如果相邻的两个数据页利用率很小,系统就会把这两个页上的数据喝到其中一个页上,另一个数据页就被标记为可复用。

delete命令删除整个表的数据,所有数据页标记为可服用,磁盘上,文件不可变小。不能回收表空间。

经过大量增删改的表,都是可能是存在空洞的。所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。可以通过重建表,达到目的。

重建方法都会扫描原表数据和构建临时文件,这些操作都收消耗CPU和IO资源的。

DDL如果是Online的,一定是inplace的。反过来就不一定,MySQL 8.0的全文索引和空间索引就是inplace。

从MySQL 5.6开始,alter table t engine=InnoDB(也即是recreate),analyze table t 不是重建表,
只是对表的索引信息做重新统计,没有修改数据,这个过程中加了DML读锁。
optimize table t 等于 recreate+analyze。

Count(*) 这么慢,我该怎么办?

count(*) 的实现方式:

  • MyISAM引擎把一个表的总行数存在磁盘上,因此执行count(*)的时候会直接返回这个数,效率高。
  • InnoDB的时候,需要把数据一行一行第从引擎里面读出来,然后累计计数。

为什么InnoDB不跟MyISAM一样,把数字存起来:因为即使是在同一时刻的多个查询,
由于多版本控制的原因,InnoDb表应该返回多少行也是不确定的。

MyISAM表虽然count()很快,但是不支持事务;show table status命令返回很快,但是不准确;
InnoDB表直接count(
)会遍历全表,虽然结果准确,但会导致性能问题。

count(*)、count(主键id)和count(1)都表示返回满足条件的结果集的总行数;count(字段),
表示返回满足条件的数据行里面,参数字段不为NULL的总个数。

执行效率来看,count(字段)<count(主键id)<count(1)=count(),建议使用count()。

崩溃恢复的判断规则:

  • 如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
  • 如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
    • a,如果是,则提交事务;
    • b,否则,回滚事务。

MySQL如何知道binlog是完整的:
一个事务的binlog是有完整格式的:statement格式的binlog,最后会有commit;row格式的binlog,最后会有一个xid event;
MySQL5.6.2版本以后,还引入binlog-checksum参数,用来验证binlog内容的正确性.

redo log和binlog是怎么关联起来的?
因为它们有一个共同的数据字段,叫XID,崩溃回复的时候,会按顺序扫描redo log:

  • 如果碰到既有prepare、又有commit的redo log,就直接提交;
  • 如果碰到只有prepare、而没有commit的redo log,就拿着xid去binlog里面找对应的事务。

处于prepare阶段的redo log 加上完整binlog,重启就能恢复,MySQL为什么要这么设计?
binlog写完以后MySQL发生崩溃,这时候binlog已经写入了,之后就会被从库使用。

Order By 是怎么工作的

在进行sql优化的时候,需要使用explain命令,如果extra这个字段中的"Using filesort"表示需要排序,
MySQL会给每个线程分配一块内存用于排序,成为sort_buffer。

sort_buffer_size就是MySQL为排序开辟的内存的大小。如果数据太大,超过内存,需要使用磁盘辅助临时文件辅助排序。

需要打开optimizer_trace,查看number_of_tmp_files查看是否使用了临时文件。

sort_buffer_size越小,需要分成的分数越多,number_of_tmp_files的值越大。

sort_mode里面的packed_additional_fields的意思是排序过程对字符串做了紧凑处理。

max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的一个参数。如果超过这个值,需要换一个算法。

覆盖索引是指索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。

如何正确地现实随机消息

select word from words order by rand() limit 3;

上面语句可以使用explain,看到extra字段记录显示temporary。using temporary,表示需要使用临时表;using filesort,表示需要执行排序操作的。

对于InnoDB表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。

对于内存表,回表的过程只是简单第根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。

如果没有创建主键,InnoDB会自己生成一个长度为6自己的row_id来作为主键。

order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法。

不是所有的临时表都是内存表。内存临时表tmp_table_size默认值为16M。如果超过这个限制,就会使用磁盘临时表。

磁盘临时表使用的引擎默认是InnoDB,是有参数internal_tmp_disk_storage_engine控制的。

为什么sql语句逻辑相同,性能差异巨大

对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。

在MySQL中,字符串和数字做比较的话,是将字符串转换成数字。

表之间的编码格式不同,进行表连接查询操作不使用索引(连接过程中要求在被驱动表的索引上加函数操作)。

为什么只查一行的语句,也执行这么慢?

如果MySQL数据库本身就有很大的压力,导致数据库服务器CPU占用率很高或ioutil很高,这种情况下所有语句的执行都有可能会变慢。

查询长时间不返回

大概率是表被锁住啦,使用show processlist命令,查看当前语句处于什么状态。

等DML锁

使用show processlist命令查看Waiting for table metadata lock.表示一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了。
通过查询sys.schema_table_lock_waits这张表,可以直接找出造成阻塞的process id。

等flush

可以通过select * from information_schema.processlist where id = 1;查看这个process的状态,如果出现waiting for table flush。
出现flush table with read lock(只关闭表t);flush tables with read lock(关闭所有的打开的表);

等行锁

执行更新操作将整个表的大部分数据行锁住,导致其他操作阻塞。

查询慢

使用show log来查询慢log,可以设置查询的长度,set long_query_time = 0,
判断这个查询是否使用了索引,如果使用了索引,是否是当前读的原因,如果不是当前读,为了保证一致,需要会回退指定步骤之前的值,所以慢。

幻读

在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事物插入的数据的。因此,幻读在当前读下才会出现。当前读的规则,就是要能读到所有已经提交的记录的最新值。
锁的设计是为了保证数据的一致性。这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
产生幻读的原因:行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的间隙。为了解决幻读,引入间隔锁。
行锁之间的冲突关系

读锁 写锁
读锁 兼容 冲突
写锁 冲突 冲突

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是为了影响并发度。

为什么我只改一行的语句,锁这么多?

加锁规则前提:

MySQL 版本:5.x 系列  <= 5.7.24, 8.0系列 <= 8.0.13

加锁原则:

1.加锁的基本单位是next-key lock。next-key lock是前开后闭区间。
2.查找过程中访问到的对象才会加锁。
3.索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
4.索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
5.唯一索引上的范围查询会访问到不满足条件的第一个值为止。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342