索引(Index)是帮助MySQL高效获取数据的数据结构。MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。
通常不加索引的情况,最基本的查询算法当然是顺序查找(linear search),其复杂度为O(n),一些优秀的查找算法,例如二分查找(binary search)、二叉树查找(binary tree search)只能应用于特定的数据结构之上。
为了满足快速查找的需求,数据库系统维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
索引的优点:
1.提高数据的检索速度
2.加快表和表之间的连接速度
索引的缺点:
1.数据库建立过程中需要花费一定时间建立和维护索引
2.索引需要占一定存储空间
3.对数据库进行修改时,索引需要动态维护
按字段个数划分
按逻辑划分
按物理实现划分
聚簇索引:数据存储和索引都是按照一定的顺序组织,找到索引也找到了数据,数据的物理存储顺序和索引顺序是一致的,即:只要索引相连,那么对应的数据一定也是相邻存储在磁盘上的。聚簇索引数据直接存放在B+树的叶子节点上,索引和数据一一对应,一个表中只能有一个聚集索引。(主键一定是聚簇索引)
非聚簇索引:也叫二级索引。B+树的叶子节点上不直接存放数据,而是存储数据行地址。查找时,先找到数据所在行地址,再去磁盘查找数据。
比较:聚簇索引查找快,增删慢;非聚簇索引查找慢,增删快
使用student表作为示例进行说明:
CREATE TABLE STUDENT(
SNO VARCHAR(3) PRIMARY KEY,
SNAME VARCHAR(10) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME DEFAULT NULL,
CCLASS VARCHAR(5) DEFAULT NULL
)CHARSET = utf8;
1.在新建表中隐式添加索引
① 普通索引
CREATE TABLE STUDENT(
SNO VARCHAR(3) PRIMARY KEY, #在创建时添加主键索引
SNAME VARCHAR(10) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME DEFAULT NULL,
CCLASS VARCHAR(5) DEFAULT NULL,
index index_SNO(SNO) #在创建表时添加普通索引
)CHARSET = utf8;
② 唯一索引
CREATE TABLE STUDENT(
SNO VARCHAR(3) PRIMARY KEY,
SNAME VARCHAR(10) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME DEFAULT NULL,
CCLASS VARCHAR(5) DEFAULT NULL,
unique index index_SNO(SNO) #在创建表时添加唯一索引
)CHARSET = utf8;
③ 全文索引
CREATE TABLE STUDENT(
SNO VARCHAR(3) PRIMARY KEY,
SNAME VARCHAR(10) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME DEFAULT NULL,
CCLASS VARCHAR(5) DEFAULT NULL,
fulltext index index_SNO(SNO) #在创建表时添加全文索引
)CHARSET = utf8;
④ 多列索引
CREATE TABLE STUDENT(
SNO VARCHAR(3) PRIMARY KEY,
SNAME VARCHAR(10) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME DEFAULT NULL,
CCLASS VARCHAR(5) DEFAULT NULL,
KEY index_sno_sname(SNO,SNAME) #添加多列索引
)CHARSET = utf8;
2.在已建表中添加索引
① 普通索引
create INDEX index_no on student(SNO);
ALTER TABLE student ADD INDEX index_no(SNO);
② 唯一索引
create UNIQUE INDEX index_no on student(SNO);
ALTER TABLE student ADD UNIQUE INDEX index_no(SNO);
③ 全文索引
create FULLTEXT INDEX index_no on student(SNO); #InnoDB引擎不支持fulltext类型索引
ALTER TABLE student ADD FULLTEXT INDEX index_no(SNO); #InnoDB引擎不支持fulltext类型索引
④ 多列索引
create INDEX index_name_no on student(SNO,SNAME);
ALTER TABLE student ADD INDEX index_name_no(SNO,SNAME);
3.删除索引
ALTER TABLE student DROP INDEX index_name_no;
DROP INDEX index_name_no ON student;
4.查看索引
SHOW INDEX FROM student;
1. 经常作为查询条件的列(where)
2. 经常用于表连接的列,例如外键(join)
3. 经常用于排序的列(order by)、用于分组的列(group by)
4. distinct 修饰的字段
5. 区分度高(散列性高)的列适合作为索引
1. 数据量少的时候不宜加索引(少于1000行)
2. 重复度较高的列不适合加索引
3. 经常dml(增、删、改)操作的表不需要创建创建索引
1. 对索引字段进行函数或表达式运算操作,索引会失效
2. like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效
3. or语句前后没有同时使用索引;只有当or左右查询字段均为索引时,才会生效
4. 组合索引,不是使用第一列索引(不符合最左前缀原则),索引失效
5. 如果列类型是字符串,在条件中没有使用引号引起来,数据类型会隐式转化,使索引无效,产生全表扫描
6. 在索引字段上使用not,<>,!=,因为对他的处理只会产生全表扫描,因此索引无效。
为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为非主键数据值。
一棵m阶B-tree有以下特点:
1. 每个节点最多有m个孩子。
2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
3. 若根节点不是叶子节点,则至少有2个孩子。
4. 所有叶子节点都在同一层,且不包含其它关键字信息。
5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)为关键字,且关键字升序排序。
8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)。
如下图所示为一个3阶的B-Tree:

模拟查找关键字29的过程:
1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
6. 在磁盘块8中的关键字列表中找到关键字29。
结论:
分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。
由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。
而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。
B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
B+Tree是在B-Tree基础上的一种优化,从B-Tree结构图中可以看到每个节点同时包含数据的key值和data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,较多的数据会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。
在B+Tree中,所有data都是按照key值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。另外,B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,所有叶子节点(即数据节点)之间是一种链式环结构。所以B+数有两种查找方式:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
上述B树优化的B+树结构示意图入下:

红黑树是在二叉平衡树(AVL)的基础上经过优化的数据结构。由于AVL左右子高度差不能超过1,每次进行插入/删除操作时几乎都要通过旋转操作保存平衡,在频繁的插入/删除场景中,使得AVL树的性能大打折扣,红黑树通过牺牲严格的平衡,换取少量的旋转操作,性能优于AVL树。

红黑树的规则:
1.节点不是黑色,就是红色(非黑即红)
2.根节点为黑色
3.叶节点为黑色(叶节点是指末梢的空节点 Nil或Null)
4.一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
5.每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
相比于红黑树:
1.平衡树查找的时间复杂度与树高h相关,红黑树出度为2,而B+数出度一般都很大,这就决定了B+树的高度要低,检索次数少。
2.数据库系统可以将索引的一个节点大小设置为页的大小,这样每次I/O都能完全载入一个节点,利用预读性,相邻的节点能够被预先载入,减少了磁盘巡道的时间。B+树可以通过链式结构顺序访问,从而利用预读性从底层加快I/O速度。
相比于B树:
1.B+树同时支持随机检索和顺序检索,B树只支持随机检索
2.B+树空间利用率更高,降低树的高度,从而减少I/O次数
3.B+树叶子节点使用指针顺序连接在一起,只要遍历叶子节点就能实现整棵树的遍历。
摘要:MySQL有多种存储引擎,每种存储引擎有各自的优缺点,可以择优选择使用:MyISAM、InnoDB、MERGE、MEMORY(HEAP)、BDB(BerkeleyDB)、EXAMPLE、FEDERATED、ARCHIVE、CSV、BLACKHOLE。其中使用最多和必须掌握的是:MylSAM和InnoDB这两种引擎。
select count(*) from table时需要全表扫描。而MyISAM执行上述语句时只需要读出该变量即可,速度很快(注意不能加有任何WHERE条件)InnoDB引擎中,定义了四种隔离级别,级别越高事务隔离性越好,但性能越差,隔离性是由MySQL的各种锁以及MVCC机制来实现的。
脏读问题:读到另外一个事务没有提交的数据,另外一个事务一旦回滚该数据就不存在了。(即读到可能不存在的数据)
不可重复读问题:多个事务并发过程中,在一个事务中,多次查询一个数据,可能每次结果不一样
幻读:一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。和不可重复读的区别在于,某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
select ...lock in share modeselect ...for updateupdate,delete,insert都会加写锁串行化的底层逻辑就是,在所有的读操作后面+读锁,这把读锁是在事务提交了之后才释放。这样在串行化的隔离级别下,读和写不能同时发生。
read commit和repeatable read则是使用MVCC机制实现。
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC主要的功能在于以不加锁的方式去处理读-写冲突,避免了读-写的相互阻塞,从而提高了数据库的并发性能。(原理类似于CopyOnWriteArrayList集合中解决读写冲突的方法,后面详细讲解)
事务版本号:事务每次开启的时候都会获得一个自增长的事务ID(trx_id),用于判断事务开始执行的先后顺序。这个ID就是事务版本号。
隐藏字段:对于InnoDB存储引擎,每一行记录都有两个隐藏的列(trx_id,roll_pointer),即记录操作该行数据的事务版本号以及回滚指针。此外,由于InnoDB是聚簇索引,如果没有定义主键约束以及非空的unique约束,还会自动生成一个单调递增的隐藏主键row_id。
undo log:回滚日志,用于存储旧版本数据。如果某个事务要修改某数据,首先会将原始数据复制一份到undo log中,这样如果回滚的话,可以通过当前的回滚指针找到原始数据对其进行恢复。另外,如果当前读不可见,可以顺着undo log链找到满足可见性的记录版本。
当前读:读取的是记录的最新版本,显示加锁的都是当前读;如果当前数据不可见,会阻塞等待其他事务提交后可见了才能继续查询。
select *from user where id=1 for update; #写锁
select *from user where id=1 lock in share mode; #读锁
快照读:读取的是undo log中可见版本的数据。普通的select语句都是快照读。
select *from user where id=1;
ReadView:是事务在进行快照读时生成的快照记录,用于解决可见性问题。它主要有以下几个属性:
trx_ids:一个list表,包含活跃的读写事务ID(未提交事务)。up_limit_id:trx_ids中事务最小的ID,如果trx_ids为空,则up_limit_id=low_limit_id。low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。creator_trx_id:表示生成该ReadView的事务IDReadView可见性定义规则为:
- 如果被访问版本数据的事务ID
trx_id == creator_trx_id,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见;- 如果被访问版本数据的事务ID
trx_id < up_limit_id,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。- 如果被访问版本数据的事务ID
trx_id > low_limit_id值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。- 如果被访问版本数据的事务ID在
up_limit_id之间,那就需要判断一下版本的事务ID是不是在 trx_ids列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。总结:
查询一条记录
1.获取事务自己的ID,即trx_id。(是在事务开启的时候获取的,即begin执行时)
2.获取readview(在select时候生成,RR隔离级别时只生成一次,RC级别每次select时都会生成一个新的readview)
3.数据库表中如果查询到了数据,就通过readview中的事务版本号进行比较
4.如果不符合可见性规则,就通过undo log链查找可见版本的记录。
总结: InnoDB实现MVCC,是通过readview+undo log来实现的,undo log保存了历史快照,readview用于找到符合可见性要求的版本记录。
MVCC实现读已提交和可重复读的过程:

总结: 读已提交(RC)和可重复读(RR)唯一的区别在于:在RC级别下,每个select都创建一个新的readview;而在RR级别下,则是第一个select才创建readview。
从准备更新一条数据到事务的提交的流程描述:

InnoDB存在两种表锁,一种是手动添加的表读锁&& 表写锁,一种是自动添加的意向锁:
表读锁(S锁,共享锁):当一个事务对一个表加读锁,则其他事务对该表可以获取读锁,但是不能获取写锁。即读-读不互斥。使用该语句为表添加读锁:lock table student read;
表写锁(X锁,排它锁):当一个事务对表加写锁,则其他事务对该表无法获取读锁也无法获取写锁,即读-写互斥,写-写互斥。使用该语句为表添加读锁:lock table student write;
InnoDB具有行锁,在对行进行加锁时,为了表明有行进行了加锁,会对表加意向锁(自动添加):
意向共享锁(IS锁,意向读锁):当InnoDB中有行加了读锁,数据库会自动为表添加意向读锁。其他事务如果想要对某些数据加行S锁,必须要先获得表的IS锁。
意向排他锁(IX锁,意向写锁):当InnoDB中有行加了写锁,数据库会自动为表添加意向写锁。其他事务如果想要对某些数据加行X锁,必须要先获得表的IX锁。
意向锁的作用: 假设某个事务对表某一行进行了加锁,数据库会自动加上意向锁,当另外一个事务要对整个表添加S锁/X锁时,不必去每一行遍历看是否有锁,只需看意向锁即可。此外,意向锁不会和行级X/S锁互斥,也就是说一个事务对某一行数据加锁后,另外一个事务可以获取到意向锁并在另一行数据上加行锁。
此外,InnoDB和MyISAM中的锁有一些不同:
InnoDB和MyISAM有两个本质的区别:InnoDB支持行锁、InnoDB支持事务。
InnoDB实现了以下两种类型的行锁:
共享锁(S锁、读锁): 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。即多个客户可以同时读取同一个资源,但不允许其他客户修改。在语句后面主动加:for share
排他锁(X锁、写锁): 允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的读锁和写锁。写锁是排他的,写锁会阻塞其他的写锁和读锁。在语句后面主动加:for update
注:行锁是加在索引上的而非物理行上,所以如果索引失效,行锁就退化成表锁。
行锁主要有三种:
乐观锁和悲观锁不是具体的锁,而是两种思想。
乐观锁:认为数据在读取的时候没有其他的事务对其进行修改,于是就不加锁,而是通过CAS机制实现。具体来说就是让数据和原来数据进行比较,如果不一致(说明被修改),进行重试或者读取失败;如果一致,则直接读取该数据。使用乐观锁的机制,避免了加锁阻塞,提高了查询效率。适合于修改少的场景。
一般我们在数据库表中添加一个版本字段version来实现,例如操作1和操作2在更新User表的时,执行语句如下:
# 这样,当有事务修改了数据时,这边按照版本号查询会失败
update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
悲观锁:认为数据在读的时候一定会有事务对其进行修改,于是在读的时候对其进行加锁。适合于修改多的场景。