MySQL索引,Explain,事务,锁与MVCC

MySQL的索引为什么不能为二叉树

假如为二叉树,索引值插入顺序为1,2,3,4,5,6,那么形成的索引结构如图:
MySQL索引,Explain,事务,锁与MVCC_第1张图片

搜索效率并不高。此时可以优化为红黑树(二叉平衡树),如图:
MySQL索引,Explain,事务,锁与MVCC_第2张图片

但是红黑树也有问题,就是树的高度,如果数据过多,红黑树过高也会影响效率。为了控制高度,可以给每一个节点分配大一点的空间,例如上面的0002节点可以存储多组数据,树形结构将演变为B树,如图:
MySQL索引,Explain,事务,锁与MVCC_第3张图片

因为B树非叶子节点包含数据,所以占用空间大一点,假如用作MySQL的索引,MySQL规定节点大小为16K,当数据为千万级别,树的高度将远远高于3;换一种说法,使用B+树将能存储更多的索引。B+树结构如下图:
MySQL索引,Explain,事务,锁与MVCC_第4张图片

MySQL索引的数据结构为B+树,B+树相比于B树拥有一下不同:

  1. 非叶子节点不包含数据
  2. 叶子结点只包含冗余索引字段
  3. 叶子结点之间拥有双向指针(MySQL特有双向指针,普通B+单向)

MySQL查询数据只需要进行3次磁盘io,高版本MySQL将非叶子节点数据存储于内存,查询效率将会更快。

MySQL还有一种索引结构为hash,存在的问题是hash冲突并且只能满足“=”和“in”,无法满足范围查找。B+树之所以可以范围查找归功于

  1. 数据默认排序
  2. 叶子节点之间的双向指针

聚簇索引和非聚簇索引

聚簇索引表示叶节点包含数据,索引和数据存在一起(InnoDB的主键索引);非聚簇索引表示叶子结点包含地址,数据存在另外的地方(索引和数据分开存储,例MyISAM)。

InnoDB的非主键索引结构也是B+树但是叶子结点存储的是主键索引的id,查询时需要回表,一张表只有一个主键索引即一个聚簇索引,这样设计的目的是节约空间和保证一致性。

建议InnoDB每张表都有索引并且索引为整型自增。如果没有索引,MySQL会自动添加索引,添加方式为选取数据不重复的某一个字段列,如果都不满足,则会花费额外空间新建一隐藏列作为索引。索引查询会涉及大小比较,整型的比较会比字符串(uuid)快并且更节约空间。如果索引不是自增的,每次插入索引元素MySQL都会重新平衡B+树,影响效率。

最左前缀原则

联合索引的结构如下所示:
MySQL索引,Explain,事务,锁与MVCC_第5张图片

结构依旧是B+树,根据name,age,position依次排序。不带左边的字段,只看右边的字段并没有排好序,只能全表扫描。

explain语句

常用属性:

id:多个select的优先级,id越大越先执行

type:访问类型,最优到最差分别为 system>const>eq_ref>ref>range>index>all,一般来说至少要保证达到range,最好达到ref;const类似于查询常量,速度快,system是const的特例,速度更快。eq_ref代表使用了主键或唯一索引,通常来说返回一条数据,速度快,ref是使用普通索引的查询,通常可能返回多条数据,较慢。range为走索引的范围查找。index为扫描全索引能拿到数据,但一般为二级索引。all为全表扫描

possible_key:可能用到的索引

key:实际用到的索引

key_len:索引长度,可以根据长度来判断用到了联合索引里的几个

ref:索引关联字段,例如‘索引’ = ‘1’,ref就代表const常量

rows:扫描行数,扫描行数不会决定是否使用索引,决定因素可以使用trace工具查看cost成本

extra:额外信息,常见的有using index使用覆盖索引(不用回表);using where使用了where语句,但未用到索引;using temporary使用了临时表(优化可以使用覆盖索引,因为索引自动排序,可以轻易去重);using filesort使用外部排序而不是索引排序;select tables optimized away使用了某些聚合函数来访问索引

索引下推

对于联合索引(a,b,c),使用like的时候(a like ‘a%’ and b=‘b’ and c=‘c’),按理来说like截取以后的字段所对应的后续索引顺序(b,c)已经无法保证,但是MySQL会做出优化,查询到like不会立即回表,而是把后续的字段匹配再回表,减少了回表次数;索引下推是MySQL5.6以后的优化,以前的做法是根据a%查询到索引以后就停止,然后全部回表,再和b,c作比较

强制使用索引

select * from table force index(index_name),一般来说MySQL自己优化的方案要优于自己强制走索引

explain语句的extra

using filesort未走索引,using index走索引


using filesort文件排序方式:

  1. 单路排序:select * from table order by col,将order by以前的数据全部一次性取出,然后在缓存中sort_buffer排序
  2. 双路排序(回表排序):将排序字段和聚簇索引id取出并在内存sort_buffer排序,然后将排序结果根据id回表查询结果集,使用内存小于单路排序

sql调优

group by底层使用了order by排序,两者调优依据索引树和最左前缀,不想让group by默认排序可以加上order by null
where效率高于having,能不用就不用

trace函数

可以临时开启,会影响sql效率。根据trace函数可以知道sql准备阶段和运行阶段一些参数,根据cost可以知道sql具体开销。

索引设计

1.代码先行,索引后上
2.根据where,order by,group by条件建立联合索引,少用单值索引
3.不在小基数字段建立索引(如性别)
4.长字符串使用前缀索引(index(name(20))
5.where和order by冲突时优先where
6.尽量用一两个复杂联合索引抗下80%查询,然后用一两个辅助索引匹配特殊场景

limit底层原理与优化

假如使用limit(1000,,10)实际上是将1-1010查询出来再删除前1000条数据


limit优化实践:

  1. 使用自增连续id,然后where id > 1000 limit 10,使用到索引树(不实用)
  2. 加where条件走索引select * from table where id in (select id from table order by name limit 9000,5),order by可能不走索引,使用select id覆盖索引走索引,得到5个id再主键关联,效率高

join流程

join的时候,如果外键是索引,实行NLJ算法,流程为将小表数据取出,与大表的索引匹配(大表有索引所以匹配很快),如果小表数据100大表一万,将进行大小表各100次磁盘扫描


如果外键不是索引,实行BNL算法,流程为将小表放入join buffer内存,然后将大表依次匹配,如果小表数据100大表一万,大小表分别进行一万和100次磁盘,buffer中无序,所以buffer内存扫描100万次

join关联优化

  1. 被关联的大表尽量走索引
  2. 小表驱动大表,可以强制使用straight_join代替inner join,left/right join已经规定了驱动表方向,例如left代表左边为驱动表

大表小表指的是关联后的结果集,不是表的数据行

in和exists优化

in和exists用法差不多,优化方式为小表驱动大表
select * from A where id in (select id from B)先执行B再执行A,B应该为小表
select * from A where [conditions] and exists (select * from B) 先执行A再执行B,A应该为小表


exists : 外表先进行循环查询,将查询结果放入exists的子查询中进行条件验证,确定外层查询数据是否保留
in : 先查询内表,将内表的查询结果当做条件提供给外表查询语句进行比较

count优化

对于求和,count有多种写法,其实效率差不多
ps:count(name)不会统计null行
对于有索引name的情况:count(*)≈count(1)>count(name)>count(id)

对于无索引name的情况:count(*)≈count(1)>count(id)>count(name)


条数过多时优化:

  1. 如果是myisam结构,内存会维护条数,直接用count(*)
  2. innodb如果不追求绝对精确的条数可以使用show table status like ‘tableName’
  3. 使用redis,也存在不一致
  4. 增加一行字段代表条数

事务的ACID特性

原子性:当前事务同时成功同时失败,由undo log保证(undo:假如sql为insert,undo为delete,便于回滚使用)

一致性:事务的目的,有其他三个特性和业务代码保证

隔离性:事务并发互不干扰,由MVCC机制保证

持久性:一旦提交了事务,它对数据库的改变就应该是永久性的。持久性由redo log日志(顺序写)来实现


事务的隔离级别

  1. read uncommit(读未提交):存在脏读,读到其他事务未提交的数据,别人回滚就会出现问题

  2. read commit(读已提交): 存在不可重复读,事务内部不同时刻读出来的数据不同,底层使用了MVCC

  3. repeatable read(可重复读):存在幻读(事务A读取了事务B新增的数据,select,update 新行,select,新数据出现);可重复读在事物内部一直获取的第一次数据,数据和数据库不一致也不会发现,这个时候update就会出问题,相当于事务开启并且有第一条查询的时候数据库生成了快照,后续操作全是基于此快照,底层使用了MVCC

  4. serializable(串行):全部解决,对同一条数据的修改和查询全部串行,底层将select语句加读锁


    脏写:事务操作数据并update,但是数据库已经更新了,上述前三种隔离级别都有这个问题。解决办法是不要在后台操作数据再更新而是直接在数据库事务中使用update table set a = a + 500,此时数据库会加行锁获取到最新的数据。事务后续对该数据的查询是最新值,其他值还是快照。读已提交级别下,可以使用乐观锁来解决脏写。


读写锁

读锁:select …lock in share mode,读锁共享,多个事务可以同时读,不能修改

写锁:select …for update,阻塞其他读锁和写锁,update,delete,insert都会加写锁(行锁)

读锁和写锁互斥,写锁和写锁互斥

MVCC

多版本并发控制,可以做到读写不阻塞,主要通过undo log实现。

select操作是快照读(历史版本)

insert,update,delete是当前读(当前版本)

read commit(读已提交),语句级别快照

repeatable read(可重复读),事务级别快照


MVCC结构如下所示:

其中trx_id为事务id,roll_pointer指向undo日志,RR读取的是开始事务的快照,RC读取的是已提交的版本
MySQL索引,Explain,事务,锁与MVCC_第6张图片

事务优化

  1. RC隔离可将查询等数据准备操作放到事务外
  2. 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久
  3. 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理
  4. 更新等涉及加锁的操作尽可能放在事务靠后的位置
  5. 能异步处理的尽量异步处理
  6. 应用侧(业务代码)保证数据一致性,非事务执行(不太推荐)

锁分类

  1. 性能上:乐观锁,悲观锁
  2. 数据操作粒度:表锁,页锁,行锁
  3. 数据操作:读锁,写锁,意向锁

表锁不会自动生效,需要在数据迁移等场景下手动加lock table

页锁只存在于BDB引擎

行锁实际上是对某一行的索引加锁,例如存在索引id,update col = col + 500 where id = 1(或者 select * from table where id = 1 for update)就会对id为1的行加锁,如果使用where name = ‘’因为name不是索引,行锁无法定位到具体索引,在RR会升级为表锁,RC不会升级

意向锁是为了提高加表锁的效率,是mysql数据库自己加的。当有事务给表的数据行加了行锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁,而这个标识就是意向锁

间隙锁(RR生效):在间隙中锁定一条不存在的记录,则这个间隙里都会被锁住(不包含边界),这样做可以解决RR下的幻读。例如存在id为1,10的数据,select * from table where id = 2 for update,将会锁住(1,10)的开区间的数据,在里面插入数据会排队。因为是开区间,所以可以修改1和10的记录。(还是针对索引)

锁优化实践

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁(RR)
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
  • 尽可能用低的事务隔离级别

MVCC可见性算法

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,MySQL会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
MySQL索引,Explain,事务,锁与MVCC_第7张图片
MySQL索引,Explain,事务,锁与MVCC_第8张图片

一致性视图read-view表示当前时刻多事务对某一个值的提交与未提交状态,RR在事务结束之前永远都不会变化(RC级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果

ps:事务创建以后,事务id会自增,所以事务之间是有顺序的。

版本链比对规则:

  1. 如果 row 的 trx_id 落在绿色部分( trx_id可见的;
  2. 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
  3. 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
  • 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的 事务是可见的);
  • 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见

注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如 select…for update)的语句,事务才真正启动,才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

你可能感兴趣的:(MySQL学习,mysql,数据库)