一.索引
1.索引简介
当在数据库中搜索一条数据时,若对整张表进行查询效率会很低,为了提高搜索速度,可以为数据表中的一个或多个字段添加索引。所谓索引就是将数据表中的记录按某个顺序进行排序,以便可以快速找到需要查找的数据。每个索引都是依赖数据表建立的,一个数据表在磁盘上的存储文件由两部分组成:一部分是用来存储数据的数据页,而另一部分是用来存放索引的索引页(会占磁盘空间)。当查询数据时,会先搜索索引页,从中找到所需数据的指针,再通过指针从数据页中读取数据(即索引中存储了指向数据的指针)。索引的底层结构是通过B-Tree实现的,将在后面的章节讨论。
2.索引分类
常见的索引类型有:聚集索引,非聚集索引,主键索引、唯一索引、普通索引、全文索引、组合索引。
1)聚集索引与非聚集索引
索引可以分为两类:聚集索引和非聚集索引。在设置聚集索引时,数据表中的数据也会按找索引的顺序来存放,比如:某个数据表将“编号”字段设置为了聚集索引,那么该数据表里的数据将会按”编号“字段的内容进行排序,如插入一个编号”123“,那么会放在 “122” 和 “124”之间。而非聚集索引不会对数据表中的数据进行排序,即不会影响数据在物理内存上的存储顺序,只是将索引建立在索引页上,在查数据时,通过索引页中的指针查找。
【注】:综上所述,即聚集索引会影响数据的实际物理排序,而非聚集索引则不会。
2)主键索引
即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值
3)唯一索引
用来建立索引的列的值必须是唯一的,允许空值(但只能有一条记录为空值)
3.B-Tree与B+Tree
1)B-Tree
一颗m阶B-树或为空树,或满足一下性质:
- 树中每个节点至多有m棵树,即m-1个关键字
- 若根节点不是叶子节点,则至少有两棵子树。
- 除根之外的所有非终端节点至少有m/2向上取整棵字数(即至少有一个关键字)
- 所有非终端节点的信息数据如下:(n, A0, Key1, A1, Key2, A2, ..., Keyn, An),即用n个关键字分出了n+1个区间,每个区间是一个指针。n为关键字数量。
- 所有的叶子节点都出现在同一层次上,且不带有任何信息,即NULL
而每个key一般都为一个【key, data】二元组,其中key为关键字,data为指向数据的指针。
至于B-树的插入和删除涉及到的分裂与合并,在此处不做说明。
2)B+Tree
B+树是对B-树的变型,其区别在于:(一个m阶B+树)
- 有n个子树的节点拥有n个关键字。
- 所有的叶子节点中包含了全部关键则的信息,及指向这些关键字记录的指针,且叶子节点本身依关键字大小自小而大的顺序连接。
- 所有非终端节点可以看作是索引部分,节点中仅含有其子树的最大(或最小)关键字,即非叶子节点不存储数据。
下图便是一个3阶B+树:
3)区别
1)m阶B+树每个节点可以比B-树多存储一个关键字。2)B-树的每个节点都存储数据,而B+树只有叶子节点中存储数据,3)B+树的页节点彼此顺序相连。
4)B+树地优点
优点1:B+树做为索引结构可以减少磁盘IO次数
原因:B+树除了叶子节点外的其它节点都不存储数据,因此可以以此载入更多的索引到内存中,从而减少了磁盘IO的次数。
优点2:B+树更利于遍历
原因:因为其叶子节点是是顺序相连的,因此当找到某个Key后可直接访问其后的其它数据,而无需每个key都搜索一遍树。
优点3:B+更稳定
原因:因为B+树的数据都存在叶子节点上,因此搜索任意节点所需的时间相同。至于搜索的时间复杂度大致值是一个log(m/2, n),其中m为B+数的阶树,n为节点个数,推到过程:第1层B+树的节点数为1, 第二层最少为m/2个, 第三层为(m/2)^2,第四层为(m/2)^3,...,第h层为(m/2)^(h - 1),B+数保存的数据数量都在叶子节点中,即最后一层,因此要保存n条数据,需要层数h可由:n <= (m/2)^(h-1),可得h>=log(m/2, n)。因此时间复杂度大致为log(m/2, n)
二.数据库事务
1.事务的四个特性
事务是做为单个逻辑单元(所谓单个逻辑单元是不可再分割的)执行的一系列工作。一个逻辑工作单元必须有以下四个性质:
- 原子性:对于事务里的操作要么全都执行,要么全都不执行。(Reids这点做的不够好)
- 一致性:原子性只保证了一个事物内的所有操作同一性,大家同生死,不会出现你死了,我还活着。但是,原子性并没有保证大家同一时刻一起生,一起死。计算机指令是有先后顺序的,这样就决定了一个事物的提交,会经历一个时间过程,那么如果事物提交进行到了一半,我读取了数据库,会不会读到中间结果?为了防止这样的情况,数据库事物的一致性就规定了事物提交前后,永远只可能存在事物提交前的状态和事物提交后的状态,从一个一致性的状态到另一个一致性状态,而不可能出现中间的过程态。也就是说事物的执行结果是量子化状态,而不是线性状态。
- 隔离性:由并发事务所作的修改与其它并发事务所作的修改隔离。即某个事务正在对某数据进行修改时,其它事务不应该去修改它。由于多个事物可能操作同一个资源,不同的事物为了保证隔离性,会有很多锁方案,当要保证事务见互不干扰。事务的隔离级别如下表所示:
事务的隔离级别 Read Uncommited (未提交读) 不隔离数据库,即使有事务正在使用数据,其它事务也能同时修改和删除该数据,一个事务可以读到另一个事务未提交的结果。 Read Committed (已提交读) 不允许读取没有提交的数据,一个事务只能读取其他事务已经提交的更新结果,否则等待。 Repeatable Read (可重复读) 可重复读就是一个事务只能读到另一个事务修改的已提交了事务的数据,但是第一次读取的数据,即使别的事务修改的这个值,这个事务再读取这条数据的时候还是和第一次获取的一样,不会随着别的事务的修改而改变。这和已提交读的区别就在于,它重复读取的值是不变的。所以取了个贴切的名字叫可重复读。
Snap
(快照隔离)
可以为读取数据的事务提供所需数据的一个已提交版本,因此写入数据的事务不会阻塞读取数据的事务。 Serializable 将事务所要用到的数据表全部锁定,牺牲了系统的并发性。可以解决并发事务的所有问题 - 持久性:在事务完成后,其操作结果对系统的影响是永久的,即事务成功提交后,就不能再回滚到提交前的状态了。
2.脏读
事务A读到了事务B未提交的数据,当事务的隔离性处于Read Uncommited时会出现此情况。
3.不可重复读与可重复读
不可重复读与可重复读都只能读取其它事务已提交后的数据,但其区别在于在第一次读取数据后,数据被其它事务修改,若此时本事务再次读取该数据是否与第一次保持一致,若一致则称为可重复度,若不一致则称为不可重复读。由上所述可知可重复读又可防止“幻读”的出现。
4.幻读
幻读是指在同一个事务中,前后两个相同的对同一个范围的查询,后一次查询看到了前一次查询没有看到的行。这是由于在两次查询之间其它事务又添加了新的数据。在Repeatable Read隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的,因为在同一个事务里,连续相同的read读到相同的结果集。
三.数据库的三大范式
1.第一范式(1NF)
如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库满足第一范式。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成为一个数据库表的字段就行,但是如果系统经常访问“地址”属性中的“城市”部分,那么一定要把“地址”这个属性重新拆分为省份、城市、详细地址等多个部分来进行存储,这样设计才算满足数据库的第一范式。
2.第二范式(2NF)
第二范式在第一范式的基础上更进一层,第二范式需要确保数据库表中每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。比如:有一张成绩表,其主键为学号+课程号,此时课程名若出现在成绩表中,则违反了第二范式,因为课程名只与主键的部分(课程号)相关。
3.第三范式(3NF)
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。比如:有一张学校的院系表,若每个学院都有只一个院长,那么不因该将院长的信息直接放在该表中,因为这只是一种间接相关,而非直接相关,应该通过外键进行连接。
四.数据库锁
1.乐观并发性控制(乐观锁)
该方法假设数据在读取和写入时发生冲突的机会很小,因此不必在事务中长时间地锁定数据,只有在更新数据时采取锁定数据,并检查是否发生冲突。第二节中所述地Read Uncommited和Read commited隔离级别都是使用乐观锁实现的。
【注】:Read commited隔离级别下只会在修改数据时加锁,因此可能在事务执行过程中看到其它事务对数据已提交的修改。
2.悲观并发控制(悲观锁)
该方法假设数据在读取和写入时发生冲突的机会很大,或者对数据的正确性要求很高,因此在事务中会持续锁定使用的数据,直至事务结束。Repeatable Read和Serializable都是悲观锁定的。
【注】:数据库实际上也是一个程序,可以认为每个事务可能执行在不同的线程中,