索引的基本原理
索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。
索引的原理:就是把⽆序的数据变成有序的查询
-
把创建了索引的列的内容进⾏排序
-
对排序结果⽣成倒排表
-
在倒排表内容上拼上数据地址链
-
在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据
索引设计的原则
查询更快、占⽤空间更⼩
-
适合索引的列是出现在where⼦句中的列,或者连接⼦句中指定的列。
-
基数较⼩的表,索引效果较差,没有必要在此列建⽴索引。
-
使⽤短索引,如果对⻓字符串列进⾏索引,应该指定⼀个前缀⻓度,这样能够节省⼤量索引空间,如果搜索词超过索引前缀⻓度,则使⽤索引排除不匹配的⾏,然后检查其余⾏是否可能匹配。
-
不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进⾏更新甚⾄重构,索引列越多,这个时间就会越⻓。所以只保持需要的索引有利于查询即可。
-
定义有外键的数据列⼀定要建⽴索引。
-
更新频繁字段不适合创建索引。
-
若是不能有效区分数据的列不适合做索引列(如性别,男⼥未知,最多也就三种,区分度实在太低)。
-
尽量的扩展索引,不要新建索引。⽐如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
-
对于那些查询中很少涉及的列,重复值⽐较多的列不要建⽴索引。
-
对于定义为text、image和bit的数据类型的列不要建⽴索引。
索引覆盖和索引下推
索引覆盖(Index Covering)和索引下推(Index Pushdown)是MySQL中的两个优化技术,用于提高查询性能。
-
索引覆盖(Index Covering):
索引覆盖是指查询所需的数据可以完全通过索引来获取,而无需访问实际的数据行。当一个查询只需要从索引中获取所需的列数据时,MySQL可以直接从索引中读取数据,而不需要额外的IO操作去读取实际的数据行(回表)。这样可以减少磁盘IO和内存开销,提高查询性能。 -
索引下推(Index Pushdown):
索引下推是指在查询过程中,MySQL将过滤条件下推到存储引擎层级执行。通常情况下,MySQL会先根据索引定位到满足条件的数据行,然后再应用查询条件进行过滤。而索引下推可以将部分查询条件下推到存储引擎层级执行,减少了从存储引擎返回的数据量,提高了查询效率。
索引下推可以减少从存储引擎返回的数据量,减少了网络传输和内存开销。同时,索引下推还可以减少存储引擎层级的数据访问次数,提高查询性能。
需要注意的是,索引覆盖和索引下推都依赖于合适的索引设计和查询语句的优化。正确地选择和使用索引,以及编写高效的查询语句是提高查询性能的关键。
mysql索引失效场景
- 未满足最左匹配原则的,index(a,b,c),但是查询条件是where b= ? and c= ? 这样会导致索引失效
- 函数、计算、类型转换会导致索引失效
- 左模糊会导致索引失效
- or前后不管存不存在非索引的列,索引都会失效
- 范围查询时右边的列会索引失效,比如索引树值为 1, 2,3,4,5,6,7,8,9 ,where > 1 妥妥的索引失效了
- !=、 <> 都会导致索引失效
- is null 可以走索引,is not null不能走索引
- 查询时数据类型不一致,可能会导致索引失效,比如 varchar类型用 数字类型比对时
MySQL慢查询该如何优化
- 检查是否走了索引,如果没有则优化SQL利用索引(同时注意索引失效)。
- 如果走了索引,检查所利用的索引是否是最优索引。
- 检查所查询字段是否都是必须的,是否查询了过多字段,查出了多余数据。
- 检查表中数据是否过多,是否应该进行分库分表了。
- 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源。
事务的基本特性和隔离级别
事务基本特性ACID分别是:
**原⼦性(undo_log)**指的是⼀个事务中的操作要么全部成功,要么全部失败。
⼀致性指的是数据库总是从⼀个⼀致性的状态转换到另外⼀个⼀致性的状态。⽐如A转账给B100块钱,假设A只有90块,⽀付之前我们数据库⾥的数据都是符合约束的,但是如果事务执⾏成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这⾥我们说事务提供了⼀致性的保证。
**隔离性(锁、MVCC)**指的是⼀个事务的修改在最终提交前,对其他事务是不可⻅的。
**持久性(redo_log)**指的是⼀旦事务提交,所做的修改就会永久保存到数据库中。
隔离性有4个隔离级别,分别是:
● read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
⽤户本来应该读取到id=1的⽤户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
● read commit 读已提交,两次读取结果不⼀致,叫做不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。⽤户开启事务读取id=1⽤户,查询到age=10,再次读取发现结果=20,在同⼀个事务⾥同⼀个查询
读取到不同的结果叫做不可重复读。
● repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都⼀样,但是有可能产⽣幻读。
● serializable 串⾏,⼀般是不会使⽤的,他会给每⼀⾏读取的数据加锁,会导致⼤量超时和锁竞争的问题。
MVCC(Multi-Version Concurrency Control)
一、什么是MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制,是为了在读取数据时不加锁来提高读取效率和并发性的一种手段。
数据库并发有以下几种场景:
-
读-读:不存在任何问题。
-
读-写:有线程安全问题,可能出现脏读、幻读、不可重复读。
-
写-写:有线程安全问题,可能存在更新丢失等。
MVCC解决的就是读写时的线程安全问题,线程不用去争抢读写锁。
当前读和快照读
MVCC所提到的读是快照读,也就是普通的select语句。快照读在读写时不用加锁,不过可能会读到历史数据。
还有一种读取数据的方式是当前读,是一种悲观锁的操作。它会对当前读取的数据进行加锁,所以读到的数据都是最新的。主要包括以下几种操作:
- select lock in share mode(共享锁)
- select for update(排他锁)
- update(排他锁)
- insert(排他锁)
- delete(排他锁)
二、MVCC实现
-
回顾事务的特性
- 原子性:通过undo_log实现。
- 持久性:通过redo_log实现。
- 隔离性:通过加锁(当前读)和 MVCC(快照读)实现。
- 一致性:通过undo_log、redo_log、隔离性共同实现。
-
回顾事务的隔离级别
- 读未提交:允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读。
- 读已提交:允许读取已经提交的数据。可能会导致幻读和不可重复读。
- 可重复读:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。可能会导致幻读。
- 可串行化:最高隔离级别。
在读已提交(RC)和可重复读(RR)隔离级别下的快照读,都是基于MVCC实现的!
RC隔离级别下的事务,在每次执行查询sql语句时都会生成一个ReadView视图(注意:不是在开启事务的时候,而是执行select的时候生成),RR隔离级别在第一次读的时候会生成一个ReadView视图,之后读都会利用这个视图。
-
MVCC实现原理
MVCC的实现,基于undo_log版本链和ReadView视图实现。
undo_log版本链
在mysql存储的数据中,除了我们显式定义的字段,mysql会隐含的帮我们定义几个字段。
- trx_id:事务id,每进行一次事务操作,就会自增1。
- roll_pointer:回滚指针,用于找到上一个版本的数据,结合undo_log进行回滚。
ReadView视图
当我们用select读取数据时,这一时刻的数据会有很多个版本(例如上图有四个版本),但我们并不知道读取哪个版本,这时就靠readview来对我们进行读取版本的限制,通过readview我们才知道自己能够读取哪个版本。
在一个readview快照中主要包括以下这些字段:
字段 | 解释 |
---|---|
m_ids | 当前活越的事务集合,统计所有没有提交的事务 |
min_trx_id | 这个集合中的最小事务,链尾事务id |
max_trx_id | 这个集合中的最大事务+1,下一个将被分配的事务 |
creator_trx_id | 创建这个ReadView的事务 |
查找唯一可用版本
以上图为例子:
当前活越事务集合为:m_ids:[2,3,4]
链尾事务id:min_trx_id:2
链头事务id:max_trx_id:5
创建事务id:creator_trx_id:5(此处假设是通过其他select查询语句创建的ReadView,如果是通过trx=4这个事务先执行修改而后查询,则creator_trx_id = 4)
查找规则:
① 判断该版本是否为当前事务创建,如果链头事务id(4) = 当前事务id(5),可访问。
② 如果trx_id < min_trx_id,表示事务在生成ReadView之前就已经提交,可以直接访问。
③ 如果trx_id > max_trx_id,表示该事务在ReadView之后开启,不可访问,遍历下一个版本。
④ 如果min_trx_id <= trx_id <= max_trx_id,如果trx_id在m_ids里面,则不可访问;trx_id不在m_ids里面,可以直接访问。
mvcc如何实现RC和RR的隔离级别
(1)RC的隔离级别下,每个快照读都会生成并获取最新的ReadView。
(2)RR的隔离级别下,只有在同一个事务的第一个快照读才会创建ReadView,之后的每次快照读都使用的同一个ReadView,所以每次的查询结果都是一样的。
MyISAM和InnoDB的区别
MyISAM | InnoDB | |
---|---|---|
事务 | 不支持事务,但是每次查询都是原子的 | 支持ACID的事务,支持事务的四种隔离级别 |
锁 | 支持表级锁,即每次操作是对整个表加锁 | 支持行级锁及外键约束:因此可以支持写并发 |
表总行数 | 储存表的总行数 | 不储存总行数 |
存储 | 一个MyISAM表有三个文件:索引文件、表结构文件、数据文件 | 一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空间,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制 |
索引 | 采用非聚簇索引,索引文件的数据域指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性 | 主键索引采用聚簇索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好采用自增主键,防止插入数据时,为维持B+数结构,文件的大调整 |
Explain
列名 | 描述 |
---|---|
id | 查询语句种每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值,某些子查询会被优化为join查询,那么出现的id会一样 |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的查询方式(全表扫描、索引)(type = const , 通过主键id 或者唯一索引进行查询,type = ref | eq_ref,通过普通索引查询) |
possible_keys | 可能用到的索引 |
key | 实际上使用的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外的信息,比如排序等 |
Innodb是如何实现事务的
Innodb通过Buffer Pool,LogBuffer,Redo Log(保证持久性),Undo Log(保证原子性)来实现事务,以⼀个update语句为例:
-
Innodb在收到⼀个update语句后,会先根据条件找到数据所在的⻚,并将该⻚缓存在Buffer Pool中。
-
执⾏update语句,修改Buffer Pool中的数据,也就是内存中的数据。
-
针对update语句⽣成⼀个RedoLog对象,并存⼊LogBuffer中。
-
针对update语句⽣成undolog⽇志,⽤于事务回滚。
-
如果事务提交,那么则把RedoLog对象进⾏持久化,后续还有其他机制将Buffer Pool中所修改的数据⻚持久化到磁盘中。
-
如果事务回滚,则利⽤undolog⽇志进⾏回滚。
B树和B+树的区别,为什么Mysql使⽤B+树
B树的特点:
-
节点排序
-
⼀个节点了可以存多个元素,多个元素也排序了
B+树的特点:
-
拥有B树的特点
-
叶⼦节点之间有指针
-
⾮叶⼦节点上的元素在叶⼦节点上都冗余了,也就是叶⼦节点中存储了所有的元素,并且排好顺序
Mysql索引使⽤的是B+树,因为索引是⽤来加快查询的,⽽B+树通过对数据进⾏排序所以是可以提⾼查询速度的,然后通过⼀个节点中可以存储多个元素,从⽽可以使得B+树的⾼度不会太⾼,在Mysql中⼀个Innodb⻚就是⼀个B+树节点,⼀个Innodb⻚默认16kb,所以⼀般情况下⼀颗两层的B+树可以存**( 16 * 1024 ÷ (8 + 6)) * ( 16K ÷ 1K ) = 18720**条左右的数据(此处假设一条数据1K,主键是bigint类型,总记录数 = 根节点指针数 * 每页记录数),然后通过利⽤B+树叶⼦节点存储了所有数据并且进⾏了排序,并且叶⼦节点之间有指针,可以很好的⽀持全表扫描,范围查找等SQL语句。
B+树能存多少数据
B+树是一种在非叶子节点存放排序好的索引而在叶子节点存放数据的数据结构,值得注意的是,在叶子节点中,存储的并非只是一行表数据,而是以页为单位存储,一个页可以包含多行表记录。非叶子节点存放的是索引键值和页指针。
那么,在MySql数据库里,一个页的大小可以通过查询语句进行查看:show variables like 'innodb_page_size;
可以看出MySQL数据库默认页大小为16KB,假设一条记录最大有1KB(实际上宽表一行记录才会有这么大),那么一页可以存储的数据量为16KB / 1KB = 16 条记录;
而根节点可以存储的指针数为16 * 1024 / ( 8 + 6 )= 1170 个指针(bigint占8个字节,指针占6个字节),从而得出两层的B+树可以存储的数据量为1170 * 16 = 18720 条记录。
三层的B+树可以存储多少条记录呢?答案显而易见了
根节点可以储存16 * 1024 / (8 + 6)= 1170个指针,第二层每层可以存储16 * 1024 / (8 + 6)= 1170个指针,第三层每一页可以存16条数据,
那么就可以得出三层的B+树可以存储 1170 * 1170 * 16 = 21902400 条记录了。
MySQL有哪些锁,举例
按锁的粒度分类:
- 行锁:锁某行数据,锁粒度最小,并发度高
- 表锁:锁整张表,锁粒度最大,并发最低
- 间隙锁:锁的是一个区间
还可以分为:
- 共享锁(select … lock in share mode):也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写。
- 排他锁(select … for update):也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
还可以分为:
- 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的。(如:update … where id = #{id} and version = #{version} )
- 悲观锁:上面的行锁、表锁等都是悲观锁。
在事务的隔离级别实现中,就需要利用锁来解决幻读(RR级别(可重复读)会出现的问题,只会在第一个sql语句执行时生成ReadView,即使其他事务修改数据后,也不会生成新的ReadView导致读取的数据一直是一样的)。