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

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

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

好了,下面第一个需要解决的问题:

我需要尽可能快的查找

select * from T where pk= 100020。这个应该怎么做?

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

 

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

 

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

 

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

 

那么,select * from T where pk= 100020这个查询就可以被转译成一个非常简单的针对映射的操作了,map.get(100020) 

 

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

 

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

 

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

 

 

 

 

 

 

select * from T where cash= 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->执行优化->execution plan执行计划] => 锁[申请读锁(或使用MVCC)] => 映射[读取二级索引] => 映射[读取主数据] =>触发器[触发读取事件] =>锁[释放读锁]

 

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

 

 

 

然后我们再来看另外一个sql的例子

 

insert into T (pk,cash,col2) values (100,10,20)

 

这是一次写入,但执行的过程,一定会与大家的预期略有不同,我们来看看:)

 

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

 

 

 

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

 

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

 

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

 

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

 

 

 

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

 

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

 

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

 

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

 

 

 

begin transaction;

 

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

 

select cash from T where pk = 1;

 

{确定有足够的钱,减少李雷的钱}

 

update T set cash = cach-100 where pk = 1;

 

{给韩梅梅增加一百元}

 

update T set cach = cash+100 where pk =2;

 

commit;

 

 

 

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

 

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

 

 

begin transaction ;

 

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

 

select cash from T where pk = 1;

 

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

 

update T set cash = cach-100 where pk = 1;

 

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

 

update T set cach = cash+100 where pk =2;

 

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

 

commit;

 

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

 

 

 

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

上一篇:解决cef加载flash时弹出黑框的问题


下一篇:【转】DataTable分组求和