从外部查询看数据库的内部实现机制

在上一章中,咱们简单的描述了组成一个小型数据库的核心组成部分,那么在本章,我会用一些常见的操做,将这些组件串联起来,让你们对这些东西如何被有机的组织起来完成了你们的功能的。但须要注意的是,这里面提到的顺序,可能在不一样的数据库内会有些许的变化,由于这些组件的执行顺序,没有明确的规范和约定要求某个数据库必定要这样,更多的只是由于数据库发展了这么多年而造成的约定俗成的执行模式sql

场景描述,咱们有个关系表叫T,有三个行组成pk,cash,col2。总共有”N行”的数据。pk是主键,sql路径过程当中,我将依照“谁[作了什么]”的模式进行解说数据库

好了,下面第一个须要解决的问题:数据结构

我须要尽量快的查找ide

select*fromTwherepk=100020。这个应该怎么作?性能

这是个很简单的主键查询,在上一篇文章中,咱们介绍过“映射”这个概念,在这里,让咱们将这个查询应用到一个映射上,来看看咱们如何依托映射这种数据结构,来快速的完成这个查询。优化

一个映射,必定是有个key,有个value的,主要组织方式有两类,一类是hash,一类是有序数据(后面咱们会常常碰到须要映射的场合:)。咱们在这里,为了简化起见,选择有序数据做为实现方式。这种方式的时间复杂度通常都是O(logN)spa

那么下一个最重要的问题是,咱们应该按什么方式来组织这个key-value,能作到最快的查询速度呢?日志

很容易的能够想到,既然我要查询pk,天然的把pk的值放在key的位置,cash,col2放在value的方式,明显是查询最快的方法。因而,咱们首先须要创建一个映射,这个映射的key是pk的值,value则是cash+col2的组合,这种组合在不一样数据库实现中是不同的,好比使用竖线分割,或者固定数据大小等,核心要保证的是尽量清晰,节省空间。索引

那么,select*fromTwherepk=100020这个查询就能够被转译成一个很是简单的针对映射的操做了,map.get(100020)事件

返回的结果就是用户须要的结果。

咱们来看看这条sql走的路径:

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[申请读锁(或使用MVCC)]=>映射[读取主数据]=>触发器[触发读取事件]=>锁[释放读锁]

select*fromTwherecash=100。应该怎么作?

首先最容易想到的就是:遍历这一百万行记录,把cash不等于100的记录都丢弃。剩下的就是符合要求的咯。

但速度太慢,必须加速,想到加速,理性的反应必定是想办法空间换时间,没错,这里的索引的核心目的,就是空间换时间。把数据进行重排。

简单分析一下,一个映射关系,只有按照key进行查询的时候才可以作到O(1)或者O(logN)。但对非key则只有O(N)的查询效率。

那么若是想加速,就让但愿加速的数据也。享受O(logN)的查询速度不就行了?因此咱们能够创建一个新的映射关系,key是cash,value则是pk,为了表述方便,咱们给他命名为idx_cash。由于这种映射是针对原有T表中部分数据的重排,为了表示方便,咱们通常把以pk做为key的数据,叫作一级索引或主索引,而把以其余列做为key的数据,叫作二级索引或辅索引。

这样,再进行cash等于100的查询的时候,就能够先查辅助idx_cash,以logN的复杂度找到一批pk数据,而后再去,主索引中按照pk去找到度和要求的记录了,这样作,速度就可以获得极大的提高

这条sql走的路径是:

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[申请读锁(或使用MVCC)]=>映射[读取二级索引]=>映射[读取主数据]=>触发器[触发读取事件]=>锁[释放读锁]

能够看到,这条sql由于没有写入,因此没有走到涉及写入的那些模块,在查询过程当中,主要是针对查询进行各类优化,让这条查询能够尽量的使用高效的索引来下降查询的延迟。这也是数据库的重要目的–在不大影响写入的前提下,提供尽量快的数据库查询。

而后咱们再来看另一个sql的例子

insertintoT(pk,cash,col2)values(100,10,20)

这是一次写入,但执行的过程,必定会与你们的预期略有不一样,咱们来看看:)

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[申请写锁,同时锁住主数据和辅助索引数据]=>映射[读取主索引,判断该值是否存在]=>预写式日志[写入数据日志]=>映射[写入数据,若是不存在]=>触发器[触发写入事件]=>映射[根据触发器,更新二级索引]=>触发器[触发二级索引写入事件]=>预写式日志[标记该条记录所有写入完成]=>锁[释放写锁]

能够看到,写入与读取,最明显的差异就在于须要申请写锁,以及须要写预写式日志(WAL)。

同时,这里还有个现象,须要让你们予以重视,那就是对于insert语义来讲,数据库须要额外的作一次“查询”操做,以判断该值是否存在,若是存在则丢主键冲突异常。

这种操做,就是关系数据库中一个很重要的概念:约束,的具体表现形式了。这种约束,在一些老的数据库更新模式中不会成为瓶颈,但对于新式的LSMTree实现的插入类操做来讲,就有多是个性能的瓶颈点了。为此,tokuDB里面也针对这个场景作过一些优化。

在后面介绍LSMTree系列映射的时候,会再次细致的针对这个问题进行原理性分析。这里,只须要你们有个印象,就是,每一种操做,都有其固有的代价。写软件,更多的时候是找到共性的东西,并把合适的功能放在合适的地方,更多的时候要多问问:这个功能,别的地方能不能作呢?若是不行,是否是真的有不少人在使用呢?若是都是确定的,那么这就应该是咱们的系统中应该拥有的功能。若是不是,那么不必为原本已经很复杂的系统增长过多的功能,让他独立出去就行了。

再来看一个更复杂的例子:

一天,李雷在英语课上把韩梅梅的钢笔弄坏了,要赔给她100元。

咱们来用数据库模拟一下这个过程:

假定李雷帐户是pk=1,韩梅梅的帐户是pk=2

begintransaction;

{查看李雷是否有一百元}

selectcashfromTwherepk=1;

{肯定有足够的钱,减小李雷的钱}

updateTsetcash=cach-100wherepk=1;

{给韩梅梅增长一百元}

updateTsetcach=cash+100wherepk=2;

commit;

这里,要完成一笔交易,在真实的世界里,可能就是李雷从钱包里拿出100元的纸钞交给韩梅梅而已。

可是,对于数据库来讲,他却没办法用一步操做来完成咱们所但愿的操做。因此,他只能使用“锁”来进行访问控制,来模拟减钱加钱的这个模型。想必各位在数据库原理的大部头上都看过这么个例子吧?不过我写这些东西的主要目标就是让你们快速的抓住主线,从而更容易的扩展旁支的内容,咱们会在后面更细致的讨论事务的问题。

begintransaction;

预写式日志[声明一个事务的惟一标记]

selectcashfromTwherepk=1;

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[申请读锁]=>映射[读取主数据]=>触发器[触发读取事件]

updateTsetcash=cach-100wherepk=1;

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[读锁升级为写锁]=>映射[读取主数据pk=1]=>预写式日志[写入数据日志,添加事务的惟一标记]=>映射[写入数据]=>触发器[触发写入事件]=>映射[根据触发器,更新二级索引]=>触发器[触发二级索引写入事件]

updateTsetcach=cash+100wherepk=2;

sql解析器[sql->sql解析->AST]=>执行优化器[AST->执行优化->executionplan执行计划]=>锁[读锁升级为写锁]=>映射[读取主数据pk=2]=>预写式日志[写入数据日志,添加事务的惟一标记]=>映射[写入数据]=>触发器[触发写入事件]=>映射[根据触发器,更新二级索引]=>触发器[触发二级索引写入事件]

commit;

预写式日志[标明该事务提交]

好了,以上是三种最多见的数据库操做使用咱们上面关键的组件的方法,里面可能有些地方的顺序在不一样数据库内的作法不一样,也有些时候,一些场景会可以使用MVCC来替换读写锁的操做从而可以进一步的提高并行度,不过那些不是咱们今天要关注的主题,若是你看完了这篇文章之后,可以对数据库的运转状态有一个粗浅的认识,那么我想个人目标就达到了:)

相关文章
相关标签/搜索