关系型数据库,一方面它是数据库,能够存储数据,另外一方面,它是关系的,也就是基于关系模型的。在关系型数据库中,专门为关系模型设计了对应的"关系引擎",关系引擎中包含了语句分析器、优化器、查询执行器。语句分析器用于分析语句是否正确,优化器用于生成查询的执行计划,查询执行器按照优化器生成的执行计划去执行查询操做,并将相关操做指令交给存储引擎,由存储引擎跟底层的数据(磁盘/缓存)打交道。算法
这里咱们不谈数据存储,而是站在数据库的角度上谈关系模型的一个特性:基于集合理论。sql
高中数学里,咱们都学过集合有三个特征:肯定性、互异性和无序性。其中肯定性和互异性是成为集合的条件,无序性是集合的特性。数据库
肯定性指的是集合中的元素必须是明确的,不能在集合中存放一个可能大于2,也可能大于3这样的元素。在关系型数据库中,这一特征能够不用考虑,由于数据只要存到数据库中,数据就必定是肯定的。缓存
互异性是指集合中的元素不能重复。在关系型数据库中,记录是否重复的问题没有严格的规范。一方面,各类各样的业务逻辑不容许在数据库的角度上严格限制记录的重复性。另外一方面,能够经过设置主键或者惟一索引来保证记录之间不重复,也就是"惟一性"。在不少状况下,具备惟一性的表能优化查询,减小记录的检索次数。性能
无序性是指集合中的元素之间是无序的。在关系型数据库中,对集合的无序性实现的最完整。甚至能够说,无序性贯穿了整个关系型数据库,尤为是关系引擎中的优化器更是关注"序"这个概念。优化
所以,本文主要围绕集合的"序",来解释关系型数据库中和"序"有关的行为。spa
这里的"序",不是指数据按大小排过序,也不是指物理存储数据时排过序,而是站在集合的角度或关系引擎的角度(若不理解,就当是数据库角度)上看"数据是否有序"的概念,它是逻辑上的"序"。设计
举个例子就能理解。集合A(1,2,3,4)和集合B(2,1,4,3),看上去集合A中元素是有序的,集合B中元素是无序的。指针
若是要从集合A和集合B中取出小于2的元素,不管是集合A仍是集合B都要比较4次才能获得最终结果,由于集合并不知道元素2的后面是否还有比2小的值。并且对于集合来讲,根本就没有前面和后面的概念。code
也就是说,对于集合A来讲,2是第二个元素是错误的说法。集合中不该该有"第"这种说法(数据库也如此,只要检索的对象不是order by后的结果或游标对象,取第几行这种概念将老是按照物理存储顺序去访问的)。
若是咱们使用下图的模式去看待这两个集合,也用这种模式去看到数据库表中的记录,不少时候更有助于理解sql语法的本质和sql优化。
因此,这两个集合是等价的集合,只不过这里的集合A,在咱们人眼中碰巧有序而已。这也是数据库中索引的意义,咱们人为地将集合中的元素排序,人为地告诉优化器它们是有序的,即便关系引擎依旧认为它是无序的。
咱们能够站在集合总体的角度上看待"序"这个概念。集合不关注其内部的元素是什么,它只关注它自身是一个容器,包含了一堆知足集合条件的元素。当须要找出集合中某个或某些具体的元素时,须要扫描整个集合。
站在数据库层面来说,关系型数据库表中的数据是无序的,也就是咱们俗称的"堆heap"。咱们应该这样看待表中的每行记录:
你可能会疑惑,使用(B+树)索引不是能够将表中全部数据排序后存储吗?没错,但在关系引擎看来,这个表仍然是集合的,它是一个乱序的总体,其内每行数据也都认为是无序的。只不过在检索数据的时候,优化器在生成执行计划时能发现已经存在索引,它知道这些数据是根据索引排过序的,藉今生成成本更低的执行计划。
转换成上面集合的说法,索引的意义就是让集合B(2,1,4,3)变成集合A(1,2,3,4),让集合中的元素可以使用"第几"这种概念。
例如,在集合的层次上要找出值大于2的元素,须要扫描整个集合,即须要比较4次才能获得结果。同理,没有使用索引的表也同样会进行表扫描才能获得最终结果。当使用索引后,也就是人为地将集合B变成集合A,对于咱们人来讲,只要从前向后找,当找到第一个大于2的元素后(请注意,不是等于2,而是大于2,这两种行为是有区别的),就知道它后面的全部元素必定是大于2的。对于数据库来讲,索引就是咱们人为告诉优化器这个表排序过,优化器天然知道只要找到符合条件的记录。
题外话:从这里是否感觉到了关系引擎和优化器之间的关系?
- 对于关系引擎来讲,整个表都是无序的。若是没有优化器组件,关系引擎(查询执行器,后文将直接使用关系引擎来表示查询执行器的行为)会进行表扫描,若是没有索引,关系引擎也会进行表扫描。可是有了优化器,且通过"咱们的提醒"后,优化器就能决定关系引擎的执行计划:走索引。
- 另外,既然咱们能隐式"提醒"优化器表存在索引,那么咱们也能显式"提醒"优化器优化器有别的索引,甚至强制"提醒"优化器没有索引。这就是为何关系型数据库中都会有"hint"关键字的缘由。
- 绝大多数状况下,咱们应该彻底信任优化器,相信它能帮咱们选出成本最低的执行计划。但优化器有时候也会"聪明反被聪明误",选出一条不怎么好甚至性能极低的执行计划,这时咱们要hint强制干涉,由咱们本身告诉优化器应该怎么走索引、走哪条索引。
- 从这方面看,一个不支持hint功能的数据库系统是不合格的数据库系统,好比老版本的PostgreSQL。固然,在2012年它已经添加了hint的扩展功能。
再来看看无序表联接的问题。当无序表1和无序表2进行内联接、外联接时,咱们应该这样看待联接的本质:
这也是站在关系引擎角度上看联接的本质。因为无序,关系引擎只能对两个表都进行表扫描并逐一比对,这样就造成了咱们常说的"笛卡尔积"。无疑,这样的效率很低。为了提升联接时的效率,应该尽量多地减小记录的扫描次数,这是联接语句优化的本质。
虽然老是提到索引,感受索引能带来性能上的提升,但不管如何请记得,对于关系引擎来讲,表是集合的,是无序的,无论它是否有索引,是否人为排序过。至于索引,它是优化器才认识的东西,关系引擎不认识。
先简单说说数据库是如何存储数据的。
在数据库系统中,表中的全部数据都存储在数据库文件中,这是磁盘上的文件。但在数据库看来,表中的每一行数据都是存放在"页"上的。页是数据库操做的最小单位,例如想读取某一行数据时(假如走索引,不会表扫描),存储引擎会将这行数据所在的一整页地加载到内存中,并扫描这一页。
数据页中,使用槽位(slot)来记录每一行数据,每一个槽表示一行数据。例如,下图是一个页面的大体示意图(不一样数据库系统有所不一样,但不影响理解):
这个页面中每插入一行数据,就分配一个槽位,并在槽位图上标记这个槽的位置(好比距离页面顶端的偏移字节是多少),这样就能知道这一行数据在页中的位置。
而咱们所说的"物理存储顺序"就是槽位图标记的顺序。注意,不是页面空间上的先后位置。由于槽位的顺序和页面空间的位置多是不一致的,例以下图:
在页面的空间位置上,slot2对应的记录行在3的后面,可是扫描这个页面的时候,将先扫描slot2,再扫描slot3。也就是说,物理顺序是slot位图的顺序(或者说,将先返回slot2对应的行)。
在堆表中,slot位图的和页面的空间位置是彻底对应的。删除一行数据,这行数据的槽位会保留,只不过槽位图上的偏移会指向0。当插入新数据的时候,这个新数据可能会直接插入到这个槽位中(若是这个槽位装不下这行数据,则会寻找其余的槽位)。
而在有索引的状况下,slot的顺序和页面空间的位置顺序可能不同,这关乎到索引的类型。例如插入2,它是1和3中间的值,按理说应该插在slot1对应行的后面,但这样会使得slot3向后移动。而这样的设计,可让数据直接插在页面的尾部,只须要对slot号码从新编号便可。性能要提升很多。
上面说的是单个页内部的数据行顺序问题。除了页内顺序,还有页间的顺序。例如页面2紧跟在页面1的后面,咱们称之为"页面是连续的",但若是页面2在页面1的前面,或者页面1和页面2中间隔了不少其余的页,咱们称之为"页面不连续"。在页面不连续的时候,存储引擎须要不断地进行页面跳跃,反映到磁盘上就是须要不断的寻址。而咱们知道,机械硬盘花在寻址的时间上远远高于读取数据的时间。这也称之为"页面碎片",当碎片较多的时候,它会对性能形成极大的影响。
本文不会去详细介绍这些东西。在这里,惟一须要知道的就是"数据存储的物理顺序并非空间上的先后顺序"。
那么,集合的"序"和物理存储顺序之间有什么关系呢?
在关系引擎看来表中的数据是无序的,但即便无序,数据也已经持久化到磁盘上了。它总要找出一个能扫描全部数据的方案。在不走索引的状况下,优化器无其余路可选,它只能按照物理存储顺序进行表扫描。在这以后,若是没有排序算法对数据进行排序,那么以后全部的操做都按照这个顺序访问数据。
所以,★★★★★★物理存储顺序是无序的起点,是数据随机性的起点。★★★★★★虚拟表之因此无序,就是从这里物理存储顺序开始的,当表扫描(或加载部分页面)完成后,已经加载完成的数据已经固定在内存中,是有固定顺序的,这时候已经不适合称之为"无序",而应该称之为"随机"。
除了数据库中的实体表,在查询的时候,中途生成的虚拟表都是无序的,但order by和distinct后的结果除外。关于order by的结果,见后文的说明。
简单的几个例子。
首先建立示例表,并查看表结构和数据以下:
MariaDB [test]> create table Student1 (sid int ,name char(20),age int,class char(20));
MariaDB [test]> insert into Student1 values(3,'zhangsan',21,'Java');
(6,'zhaoliu',19,'Java'),
(2,'huanger',23,'Python'),
(1,'chenyi',22,'Java'),
(4,'lisi',20,'C#'),
(5,'wangwu',21,'Python'),
(7,'qianqi',22,'C'),
(8,'sunba',20,'C++'),
(9,'yangjiu',24,'Java');
MariaDB [test]> select * from Student1;
+------+----------+------+--------+
| sid | name | age | class | +------+----------+------+--------+ | 3 | zhangsan | 21 | Java | | 6 | zhaoliu | 19 | Java | | 2 | huanger | 23 | Python | | 1 | chenyi | 22 | Java | | 4 | lisi | 20 | C# | | 5 | wangwu | 21 | Python | | 7 | qianqi | 22 | C | | 8 | sunba | 20 | C++ | | 9 | yangjiu | 24 | Java | +------+----------+------+--------+
这里面没有任何索引,不管是关系引擎,仍是优化器,都认为这个表是无序的,所以只能执行表扫描。而表扫描的过程是按照数据的"物理存储顺序"进行访问的,sid=3的记录先存进数据库,就先访问这个记录(按照前文的物理存储方式,这个说法是错误的,但如今忽略这个问题)。在没有任何索引、没有任何优化"提醒"时,优化器就会生成"按照物理存储顺序"的执行计划去表扫描。
可是表扫描的结果对于咱们人类来讲是无序的结果,更准确的说,是随机的结果,是咱们没法去预料的结果。由于堆中的数据在进行物理存储时,可能会"见缝插针",而咱们根本不知道这根"针"插在哪一个"缝"里,也不知道它前面的数据是什么,后面的数据是什么。例如,堆表中的部分数据删除了,再插入一部分数据,新插入的数据有些可能会插入在表的尾部,有些也可能插在数据删除后留下的"槽"(slot)中。
再来讲明查询执行过程当中生成的虚拟表。虚拟表是逻辑的概念,是SQL语句执行过程当中每个阶段产生的数据集合,在咱们人的感官上,咱们会把这个集合当作虚拟表。但多数时候,它们并不真的是二维表结构的形式,只是内存中一段存储了数据的缓存空间。少数时候,因为算法或某些操做的须要,会实实在在地建立虚拟临时表,例如使用DISTINCT子句对结果去重时,就会先生成一张临时表用于排序并去重。
例如,在两表联接时咱们总说会产生"笛卡尔积",而后用一张二维表的形式去感觉这个笛卡尔积的虚拟表。
但在实际执行过程当中不会是这样的表结构,而应该是下面这种结构。
也就是说,虚拟结果集中的数据是无序的,随后对该虚拟结果集的操做也是随机而没法保证顺序的。例如上面的笛卡尔积结果集,若是使用了WHERE子句筛选某些行,则筛选的过程是对笛卡尔积进行"表"扫描,但笛卡尔积本就是不保证顺序的,因此当where筛选出多行时,这些行的顺序可能会和咱们预料的结果有所不一样。固然,优化器不会真的采用这样的方案,但站在逻辑角度上看,由于虚拟结果集无序,要从中检索数据只能进行"表"扫描。
再好比说TOP子句(MySQL、MariaDB中等价的是LIMIT子句),若是没有结合ORDER BY子句,那么TOP将从其前面的虚拟结果集中按某种顺序挑出知足数量的行出来,挑出的这些行是咱们人没法预料的,因此TOP的结果是随机的。这里的"某种顺序"并不是有序,例如从上图的笛卡尔积中选一行时,因为笛卡尔积是无序的,挑选的这一行将受表A的物理存储顺序、表B的物理存储顺序影响。
只有使用了ORDER BY子句,才能保证TOP的结果是可预料而非随机的,由于ORDER BY的虚拟结果集是有序的。事实上,ORDER BY的结果不该该叫"集",而应该叫"游标对象",由于排序后的结果中,每一行都按照咱们期待的顺序固定好了位置,以后TOP再去操做这样的结果就必定能获得咱们预料之中的数据。
经过前面的内容,咱们已经发现无序性的最大问题在于返回结果的没法预测性。返回结果没法预测,意味着数据增、删、改的时候存在危险,意味着咱们可能对检索的数据认知不足。
前面说了一大堆,总结一下就是:数据都是无序的,每一步的检索都有随机性,没法保证能达到咱们人的期待。可是,关系型数据库中的全部数据都是无序,都是随机的吗?换句话说,上面的总结对吗?答案是不对。
在关系型数据库中,咱们除了考虑从存储引擎到磁盘这段路程(受物理存储顺序影响),还要考虑语句执行过程当中内存中的虚拟表。在前面,咱们说虚拟表是无序的,这句话不许确。
一方面,数据加载成功后,它们在内存中已是有序的,但对咱们人来讲,咱们没法看到这样的"序",也就是说结果是随机的,是咱们没法预料的。
另外一方面,当有排序算法对虚拟表进行排序后,结果也是有序的,这样的结果是符合咱们人所预料的。对于排序后的结果,ANSI将其称之为"游标对象"。
常见的两个涉及到排序算法的子句是ORDER BY和DISTINCT。此处以外,还有游标自身,它的结果也是有序的。
对于ORDER BY子句,它会将它前面的虚拟结果集进行排序,排序的结果集中,每行记录都固定好了位置,咱们能够预料到任何一行数据处于哪一个位置,也知道它前面是什么数据,后面是什么数据。也就是说,排序后的数据是固定而非随机的。
对于DISTINCT子句,在对其前面的虚拟结果集进行去重操做时,DISTINCT总会带有排序操做(即便没有指定order by,内部也自带排序),排序以后再对结果集进行去重。例以下面的表。
id name
---- -----
5 e
2 b
4 d
1 a
2 x
3 c
对id列去重时,它将对id列进行排序,获得的结果将是:
id
----
1
2
3
4
5
结果是有序的。但对于id=2的记录来讲,在去重时应该保留name=b的仍是name=x的记录呢?没法保证,由于DISTINCT只对id列排序,不对id以外的列排序。所以对于有重复值的记录,DISTINCT只能返回一个随机记录,咱们没法预料这个记录是否是咱们想要的结果。
在SQL Server和Oracle中这没什么问题,由于使用DISTINCT后,它后面的过程不容许使用非DISTINCT列(DISTINCT后面还有ORDER BY和TOP,但涉及到列的只有ORDER BY子句),所以最终获得的结果只有指定的去重列(id列)。可是在MySQL/MariaDB中,ORDER BY容许使用非DISTINCT列,例如select distinct id from t1 order by name
,它将先按name排序,再按id排序,最后对id去重。也许你发现了,这种状况下DISTINCT是在ORDER BY以后才执行的,没错,事实就是如此。
不管是MySQL仍是SQL Server(Oracle的知识都忘光了,因此不说它了),均可以建立"汇集索引"和"非汇集索引"(MySQL中没有这种称呼,但它的主键索引就是汇集索引,非主键索引就是非汇集索引,非汇集索引有时候也称之为secondary index)。它们都是B+树的组织结构。
对于汇集索引,B+树的叶级页包含了全部数据行以及全部列(有些时候还包括一个或两个额外的列),它们是排过序的。可是这种排序是经过双链表和指针的方式实现的。
例如,表中有数据行1,2,3,4,5,6,假设这几行数据都较大,每两行占用一个数据页面,那么存储数据的数据页总共须要3页(假设分别称为A、B、C页)。
这3页之间经过双链表的方式组织起来,例如B页的page header中记录了它前一页是A,后一页是C,C页的page header中记录了它的前一页是B,没有后一页(用0表示)。
而对于页面内的数据,则是经过slot槽位偏移指针来组织的。在前面的"物理存储顺序"一节中已经说明了这个叶级页是如何每行数据的。
不管是汇集索引仍是非汇集索引,都必需要有一列或多列能惟一识别每一行记录。能够经过同时建立惟一性索引的方式实现,在没有建立惟一性索引时,系统内部会自动添加一个列,帮助索引列惟一识别每一行记录,只不过当没有重复值的时候,这一列占用的空间未0。
总之,有了索引,不管是汇集仍是非汇集索引,都必定能保证每一行数据都惟一,每一行数据在排序时都有彻底肯定的位置。也就是说,当咱们走索引去检索数据的时候,数据再也不无序,返回的结果也总能预料到。