关系型数据库进阶(一)数据库基础

       做为一个开发人员,我花了不少时间来使用关系型数据库,虽然对其中的查询、链接、调优有了必定经验,可是若是要我真的说出具体一个查询执行的流程以及细节,仍是有不少点是我不肯定的。既然有了这个盲区,这个系列,咱们就一块儿去看看关系型数据库背后的运行原理。本文重点参考了Christophe Kalenzaga的博文和一些关系型数据库的文档。html

这篇文章分为4个部分:算法

1、数据库基础:数学基础及数据结构数据库

2、全局概览:数据库各组件及关系apache

3、查询过程及实例api

4、事务和缓冲池管理服务器

 

数据库基础

  好久好久之前(在一个遥远而又遥远的星系……),开发者必须确切地知道他们的代码须要多少次运算。他们把算法和数据结构牢记于心,由于他们的计算机运行缓慢,没法承受对CPU和内存的浪费。数据结构

  在这一部分,我将提醒你们一些这类的概念,由于它们对理解数据库相当重要。我还会介绍数据库索引的概念。多线程

O(1) vs O(n^2)

概念框架

  时间复杂度用来检验某个算法处理必定量的数据要花多长时间。为了描述这个复杂度,计算机科学家使用数学上的『简明解释算法中的大O符号』。这个表示法用一个函数来描述算法处理给定的数据须要多少次运算。分布式

  好比,当我说『这个算法是适用 O(某函数())』,个人意思是对于某些数据,这个算法须要 某函数(数据量) 次运算来完成。

  重要的不是数据量,而是当数据量增长时运算如何增长。时间复杂度不会给出确切的运算次数,可是给出的是一种理念。

图中能够看到不一样类型的复杂度的演变过程,我用了对数尺来建这个图。具体点儿说,数据量以很快的速度从1条增加到10亿条。咱们可获得以下结论:

绿:O(1)或者叫常数阶复杂度,保持为常数(要不人家就不会叫常数阶复杂度了)。

红:O(log(n))对数阶复杂度,即便在十亿级数据量时也很低。

粉:最糟糕的复杂度是 O(n^2),平方阶复杂度,运算数快速膨胀。

黑和蓝:另外两种复杂度(的运算数也是)快速增加。

例子

数据量低时,O(1) 和 O(n^2)的区别能够忽略不计。好比,你有个算法要处理2000条元素。

O(1) 算法会消耗 1 次运算

O(log(n)) 算法会消耗 7 次运算

O(n) 算法会消耗 2000 次运算

O(n*log(n)) 算法会消耗 14,000 次运算

O(n^2) 算法会消耗 4,000,000 次运算

O(1) 和 O(n^2) 的区别彷佛很大(4百万),但你最多损失 2 毫秒,只是一眨眼的功夫。确实,当今处理器每秒可处理上亿次的运算。这就是为何性能和优化在不少IT项目中不是问题。

我说过,面临海量数据的时候,了解这个概念依然很重要。若是这一次算法须要处理 1,000,000 条元素(这对数据库来讲也不算大)。

O(1) 算法会消耗 1 次运算

O(log(n)) 算法会消耗 14 次运算

O(n) 算法会消耗 1,000,000 次运算

O(n*log(n)) 算法会消耗 14,000,000 次运算

O(n^2) 算法会消耗 1,000,000,000,000 次运算

我没有具体算过,但我要说,用O(n^2) 算法的话你有时间喝杯咖啡(甚至再续一杯!)。若是在数据量后面加个0,那你就能够去睡大觉了。

继续深刻

搜索一个好的哈希表会获得 O(1) 复杂度

搜索一个均衡的树会获得 O(log(n)) 复杂度

搜索一个阵列会获得 O(n) 复杂度

最好的排序算法具备 O(n*log(n)) 复杂度

糟糕的排序算法具备 O(n^2) 复杂度

注:在接下来的部分,咱们将会研究这些算法和数据结构。

有多种类型的时间复杂度

1  通常状况场景

2  最佳状况场景

3  最差状况场景

时间复杂度常常处于最差状况场景。

这里我只探讨时间复杂度,但复杂度还包括:

1  算法的内存消耗

2  算法的磁盘 I/O 消耗

固然还有比 n^2 更糟糕的复杂度,好比:

n^4:差劲!我将要提到的一些算法具有这种复杂度。

3^n:更差劲!本文中间部分研究的一些算法中有一个具有这种复杂度(并且在不少数据库中还真的使用了)。

阶乘 n:你永远得不到结果,即使在少许数据的状况下。

n^n:若是你发展到这种复杂度了,那你应该问问本身IT是否是你的菜。

注:我并无给出『大O表示法』的真正定义,只是利用这个概念。能够看看维基百科上的这篇文章

合并排序

当你要对一个集合排序时你怎么作?什么?调用 sort() 函数……好吧,算你对了……可是对于数据库,你须要理解这个 sort() 函数的工做原理。

优秀的排序算法有好几个,我侧重于最重要的一种:合并排序。你如今可能还不了解数据排序有什么用,但看完查询优化部分后你就会知道了。再者,合并排序有助于咱们之后理解数据库常见的联接操做,即合并联接。

合并

与不少有用的算法相似,合并排序基于这样一个技巧:将 2 个大小为 N/2 的已排序序列合并为一个 N 元素已排序序列仅须要 N 次操做。这个方法叫作合并。

咱们用个简单的例子来看看这是什么意思:

 

 

经过此图你能够看到,在 2 个 4元素序列里你只须要迭代一次,就能构建最终的8元素已排序序列,由于两个4元素序列已经排好序了:

1) 在两个序列中,比较当前元素(当前=头一次出现的第一个)

2) 而后取出最小的元素放进8元素序列中

3) 找到(两个)序列的下一个元素,(比较后)取出最小的

重复一、二、3步骤,直到其中一个序列中的最后一个元素

而后取出另外一个序列剩余的元素放入8元素序列中。

这个方法之因此有效,是由于两个4元素序列都已经排好序,你不须要再『回到』序列中查找比较。

【合并排序详细原理】

 

既然咱们明白了这个技巧,下面就是个人合并排序伪代码。

C

array mergeSort(array a)

   if(length(a)==1)

      return a[0];

   end if

   //recursive calls

   [left_array right_array] := split_into_2_equally_sized_arrays(a);

   array new_left_array := mergeSort(left_array);

   array new_right_array := mergeSort(right_array);

   //merging the 2 small ordered arrays into a big one

   array result := merge(new_left_array,new_right_array);

   return result;

合并排序是把问题拆分为小问题,经过解决小问题来解决最初的问题(注:这种算法叫分治法,即『分而治之、各个击破』)。我认为这个算法是个两步算法:

拆分阶段,将序列分为更小的序列

排序阶段,把小的序列合在一块儿(使用合并算法)来构成更大的序列

拆分阶段

 

在拆分阶段过程当中,使用3个步骤将序列分为一元序列。步骤数量的值是 log(N) (由于 N=8, log(N)=3)。【底数为2】

我怎么知道这个的?

一句话:数学。道理是每一步都把原序列的长度除以2,步骤数就是你能把原序列长度除以2的次数。这正好是对数的定义(在底数为2时)。

排序阶段

 

在排序阶段,你从一元序列开始。在每个步骤中,你应用屡次合并操做,成本一共是 N=8 次运算。

第一步,4 次合并,每次成本是 2 次运算。

第二步,2 次合并,每次成本是 4 次运算。

第三步,1 次合并,成本是 8 次运算。

由于有 log(N) 个步骤,总体成本是 N*log(N) 次运算。

【这个完整的动图演示了拆分和排序的全过程】

 

合并排序的强大之处

为何这个算法如此强大?

由于:

你能够更改算法,以便于节省内存空间,方法是不建立新的序列而是直接修改输入序列。

注:这种算法叫『原地算法』(in-place algorithm)

你能够更改算法,以便于同时使用磁盘空间和少许内存而避免巨量磁盘 I/O。方法是只向内存中加载当前处理的部分。在仅仅100MB的内存缓冲区内排序一个几个GB的表时,这是个很重要的技巧。

注:这种算法叫『外部排序』(external sorting)。

你能够更改算法,以便于在 多处理器/多线程/多服务器 上运行。

好比,分布式合并排序是Hadoop(那个著名的大数据框架)的关键组件之一。

这个算法能够点石成金(事实如此!)

这个排序算法在大多数(若是不是所有的话)数据库中使用,可是它并非惟一算法。若是你想多了解一些,你能够看看这篇论文,探讨的是数据库中经常使用排序算法的优点和劣势。

阵列,树和哈希表

既然咱们已经了解了时间复杂度和排序背后的理念,我必需要向你介绍3种数据结构了。这个很重要,由于它们是现代数据库的支柱。我还会介绍数据库索引的概念。

阵列

二维阵列是最简单的数据结构。一个表能够看做是个阵列,好比:

 

 

这个二维阵列是带有行与列的表:

1 每一个行表明一个主体

2 列用来描述主体的特征

3 每一个列保存某一种类型对数据(整数、字符串、日期……)

虽然用这个方法保存和视觉化数据很棒,可是当你要查找特定的值它就很糟糕了。 举个例子,若是你要找到全部在 UK 工做的人,你必须查看每一行以判断该行是否属于 UK 。这会形成 N 次运算的成本(N 等于行数),还不赖嘛,可是有没有更快的方法呢?这时候树就能够登场了(或开始起做用了)。

树和数据库索引

二叉查找树

二叉查找树是带有特殊属性的二叉树,每一个节点的关键字必须:

比保存在左子树的任何键值都要大

比保存在右子树的任何键值都要小

【binary search tree,二叉查找树/二叉搜索树,或称 Binary Sort Tree 二叉排序树。】

概念

 

这个树有 N=15 个元素。比方说我要找208:

我从键值为 136 的根开始,由于 136<208,我去找节点136的右子树。

398>208,因此我去找节点398的左子树

250>208,因此我去找节点250的左子树

200<208,因此我去找节点200的右子树。可是 200 没有右子树,值不存在(由于若是存在,它会在 200 的右子树)

如今比方说我要找40

我从键值为136的根开始,由于 136>40,因此我去找节点136的左子树。

80>40,因此我去找节点 80 的左子树

40=40,节点存在。我抽取出节点内部行的ID(图中没有画)再去表中查找对应的 ROW ID。

知道 ROW ID我就知道了数据在表中对精确位置,就能够当即获取数据。

最后,两次查询的成本就是树内部的层数。若是你仔细阅读了合并排序的部分,你就应该明白一共有 log(N)层。因此这个查询的成本是 log(N),不错啊!

回到咱们的问题

上文说的很抽象,咱们回来看看咱们的问题。此次不用傻傻的数字了,想象一下前表中表明某人的国家的字符串。假设你有个树包含表中的列『country』:

若是你想知道谁在 UK 工做

你在树中查找表明 UK 的节点

在『UK 节点』你会找到 UK 员工那些行的位置

此次搜索只需 log(N) 次运算,而若是你直接使用阵列则须要 N 次运算。你刚刚想象的就是一个数据库索引。

B+树索引

查找一个特定值这个树挺好用,可是当你须要查找两个值之间的多个元素时,就会有大麻烦了。你的成本将是 O(N),由于你必须查找树的每个节点,以判断它是否处于那 2 个值之间(例如,对树使用中序遍历)。并且这个操做不是磁盘I/O有利的,由于你必须读取整个树。咱们须要找到高效的范围查询方法。为了解决这个问题,现代数据库使用了一种修订版的树,叫作B+树。在一个B+树里:

只有最底层的节点(叶子节点)才保存信息(相关表的行位置)

其它节点只是在搜索中用来指引到正确节点的。

 

 

你能够看到,节点更多了(多了两倍)。确实,你有了额外的节点,它们就是帮助你找到正确节点的『决策节点』(正确节点保存着相关表中行的位置)。可是搜索复杂度仍是在 O(log(N))(只多了一层)。一个重要的不一样点是,最底层的节点是跟后续节点相链接的。

用这个 B+树,假设你要找40到100间的值:

你只须要找 40(若40不存在则找40以后最贴近的值),就像你在上一个树中所作的那样。

而后用那些链接来收集40的后续节点,直到找到100。

比方说你找到了 M 个后续节点,树总共有 N 个节点。对指定节点的搜索成本是 log(N),跟上一个树相同。可是当你找到这个节点,你得经过后续节点的链接获得 M 个后续节点,这须要 M 次运算。那么此次搜索只消耗了 M+log(N) 次运算,区别于上一个树所用的 N 次运算。此外,你不须要读取整个树(仅须要读 M+log(N) 个节点),这意味着更少的磁盘访问。若是 M 很小(好比 200 行)而且 N 很大(1,000,000),那结果就是天壤之别了。

然而还有新的问题(又来了!)。若是你在数据库中增长或删除一行(从而在相关的 B+树索引里):

1  你必须在B+树中的节点之间保持顺序,不然节点会变得一团糟,你没法从中找到想要的节点。

2  你必须尽量下降B+树的层数,不然 O(log(N)) 复杂度会变成 O(N)。

换句话说,B+树须要自我整理和自我平衡。谢天谢地,咱们有智能删除和插入。可是这样也带来了成本:在B+树中,插入和删除操做是 O(log(N)) 复杂度。因此有些人听到过使用太多索引不是个好主意这类说法。没错,你减慢了快速插入/更新/删除表中的一个行的操做,由于数据库须要以代价高昂的每索引 O(log(N)) 运算来更新表的索引。再者,增长索引意味着给事务管理器带来更多的工做负荷(在本文结尾咱们会探讨这个管理器)。

哈希表

咱们最后一个重要的数据结构是哈希表。当你想快速查找值时,哈希表是很是有用的。并且,理解哈希表会帮助咱们接下来理解一个数据库常见的联接操做,叫作『哈希联接』。这个数据结构也被数据库用来保存一些内部的东西(好比锁表或者缓冲池,咱们在下文会研究这两个概念)。

哈希表这种数据结构能够用关键字来快速找到一个元素。为了构建一个哈希表,你须要定义:

1 元素的关键字

2 关键字的哈希函数。关键字计算出来的哈希值给出了元素的位置(叫作哈希桶)。

3 关键字比较函数。一旦你找到正确的哈希桶,你必须用比较函数在桶内找到你要的元素。

一个简单的例子

咱们来看一个形象化的例子:

 

这个哈希表有10个哈希桶。由于我懒,我只给出5个桶,可是我知道你很聪明,因此我让你想象其它的5个桶。我用的哈希函数是关键字对10取模,也就是我只保留元素关键字的最后一位,用来查找它的哈希桶:

若是元素最后一位是 0,则进入哈希桶0,

若是元素最后一位是 1,则进入哈希桶1,

若是元素最后一位是 2,则进入哈希桶2,

用的比较函数只是判断两个整数是否相等。

取模运算

比方说你要找元素 78:

哈希表计算 78 的哈希码,等于 8。

查找哈希桶 8,找到的第一个元素是 78。

返回元素 78。

查询仅耗费了 2 次运算(1次计算哈希值,另外一次在哈希桶中查找元素)。

如今,比方说你要找元素 59:

哈希表计算 59 的哈希码,等于9。

查找哈希桶 9,第一个找到的元素是 99。由于 99 不等于 59, 那么 99 不是正确的元素。

用一样的逻辑,查找第二个元素(9),第三个(79),……,最后一个(29)。

元素不存在。

搜索耗费了 7 次运算。

一个好的哈希函数

你能够看到,根据你查找的值,成本并不相同。

若是我把哈希函数改成关键字对 1,000,000 取模(就是说取后6位数字),第二次搜索只消耗一次运算,由于哈希桶 00059 里面没有元素。真正的挑战是找到好的哈希函数,让哈希桶里包含很是少的元素。

在个人例子里,找到一个好的哈希函数很容易,但这是个简单的例子。当关键字是下列形式时,好的哈希函数就更难找了:

1 个字符串(好比一我的的姓)

2 个字符串(好比一我的的姓和名)

2 个字符串和一个日期(好比一我的的姓、名和出生年月日)

若是有了好的哈希函数,在哈希表里搜索的时间复杂度是 O(1)。

阵列 vs 哈希表

为何不用阵列呢?

嗯,你问得好。

1  一个哈希表能够只装载一半到内存,剩下的哈希桶能够留在硬盘上。

2  用阵列的话,你须要一个连续内存空间。若是你加载一个大表,很难分配足够的连续内存空间。

3  用哈希表的话,你能够选择你要的关键字(好比,一我的的国家和姓氏)。

相关文章
相关标签/搜索