假如为二叉树,索引值插入顺序为1,2,3,4,5,6,那么形成的索引结构如图:
但是红黑树也有问题,就是树的高度,如果数据过多,红黑树过高也会影响效率。为了控制高度,可以给每一个节点分配大一点的空间,例如上面的0002节点可以存储多组数据,树形结构将演变为B树,如图:
因为B树非叶子节点包含数据,所以占用空间大一点,假如用作MySQL的索引,MySQL规定节点大小为16K,当数据为千万级别,树的高度将远远高于3;换一种说法,使用B+树将能存储更多的索引。B+树结构如下图:
MySQL索引的数据结构为B+树,B+树相比于B树拥有一下不同:
MySQL查询数据只需要进行3次磁盘io,高版本MySQL将非叶子节点数据存储于内存,查询效率将会更快。
MySQL还有一种索引结构为hash,存在的问题是hash冲突并且只能满足“=”和“in”,无法满足范围查找。B+树之所以可以范围查找归功于
聚簇索引表示叶节点包含数据,索引和数据存在一起(InnoDB的主键索引);非聚簇索引表示叶子结点包含地址,数据存在另外的地方(索引和数据分开存储,例MyISAM)。
InnoDB的非主键索引结构也是B+树但是叶子结点存储的是主键索引的id,查询时需要回表,一张表只有一个主键索引即一个聚簇索引,这样设计的目的是节约空间和保证一致性。
建议InnoDB每张表都有索引并且索引为整型自增。如果没有索引,MySQL会自动添加索引,添加方式为选取数据不重复的某一个字段列,如果都不满足,则会花费额外空间新建一隐藏列作为索引。索引查询会涉及大小比较,整型的比较会比字符串(uuid)快并且更节约空间。如果索引不是自增的,每次插入索引元素MySQL都会重新平衡B+树,影响效率。
结构依旧是B+树,根据name,age,position依次排序。不带左边的字段,只看右边的字段并没有排好序,只能全表扫描。
常用属性:
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自己优化的方案要优于自己强制走索引
using filesort未走索引,using index走索引
using filesort文件排序方式:
group by底层使用了order by排序,两者调优依据索引树和最左前缀,不想让group by默认排序可以加上order by null
where效率高于having,能不用就不用
可以临时开启,会影响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(1000,,10)实际上是将1-1010查询出来再删除前1000条数据
limit优化实践:
join的时候,如果外键是索引,实行NLJ算法,流程为将小表数据取出,与大表的索引匹配(大表有索引所以匹配很快),如果小表数据100大表一万,将进行大小表各100次磁盘扫描
如果外键不是索引,实行BNL算法,流程为将小表放入join buffer内存,然后将大表依次匹配,如果小表数据100大表一万,大小表分别进行一万和100次磁盘,buffer中无序,所以buffer内存扫描100万次
大表小表指的是关联后的结果集,不是表的数据行
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有多种写法,其实效率差不多
ps:count(name)不会统计null行
对于有索引name的情况:count(*)≈count(1)>count(name)>count(id)
对于无索引name的情况:count(*)≈count(1)>count(id)>count(name)
条数过多时优化:
原子性:当前事务同时成功同时失败,由undo log保证(undo:假如sql为insert,undo为delete,便于回滚使用)
一致性:事务的目的,有其他三个特性和业务代码保证
隔离性:事务并发互不干扰,由锁和MVCC机制保证
持久性:一旦提交了事务,它对数据库的改变就应该是永久性的。持久性由redo log日志(顺序写)来实现
read uncommit(读未提交):存在脏读,读到其他事务未提交的数据,别人回滚就会出现问题
read commit(读已提交): 存在不可重复读,事务内部不同时刻读出来的数据不同,底层使用了MVCC
repeatable read(可重复读):存在幻读(事务A读取了事务B新增的数据,select,update 新行,select,新数据出现);可重复读在事物内部一直获取的第一次数据,数据和数据库不一致也不会发现,这个时候update就会出问题,相当于事务开启并且有第一条查询的时候数据库生成了快照,后续操作全是基于此快照,底层使用了MVCC
serializable(串行):全部解决,对同一条数据的修改和查询全部串行,底层将select语句加读锁
脏写:事务操作数据并update,但是数据库已经更新了,上述前三种隔离级别都有这个问题。解决办法是不要在后台操作数据再更新而是直接在数据库事务中使用update table set a = a + 500,此时数据库会加行锁获取到最新的数据。事务后续对该数据的查询是最新值,其他值还是快照。读已提交级别下,可以使用乐观锁来解决脏写。
读锁:select …lock in share mode,读锁共享,多个事务可以同时读,不能修改
写锁:select …for update,阻塞其他读锁和写锁,update,delete,insert都会加写锁(行锁)
读锁和写锁互斥,写锁和写锁互斥
多版本并发控制,可以做到读写不阻塞,主要通过undo log实现。
select操作是快照读(历史版本)
insert,update,delete是当前读(当前版本)
read commit(读已提交),语句级别快照
repeatable read(可重复读),事务级别快照
MVCC结构如下所示:
其中trx_id为事务id,roll_pointer指向undo日志,RR读取的是开始事务的快照,RC读取的是已提交的版本
表锁不会自动生效,需要在数据迁移等场景下手动加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的记录。(还是针对索引)
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,MySQL会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
一致性视图read-view表示当前时刻多事务对某一个值的提交与未提交状态,RR在事务结束之前永远都不会变化(RC级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果
ps:事务创建以后,事务id会自增,所以事务之间是有顺序的。
版本链比对规则:
注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如 select…for update)的语句,事务才真正启动,才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。