[译]数据库是如何工做(二)回到原点 算法基础

好久以前(在一个遥远的银河系中。。。),开发者不得不彻底地知道他们编码时全部的细节。他们对算法和数据结构必需要十分理解,由于他们接受不了浪费慢速计算机的CPU和内存的时间。java

在这部分,我会提醒你一些概念,由于他们对理解数据库必不可少。我也会介绍数据库索引的概念。node

O(1) vs O(n2)

如今,不少开发者不关系时间复杂度。。。 他们是对的! 但当你处理茫茫大的数据时(我不是在说数千),或者若是你再和毫秒在战斗时,理解这个这个概念即为重要。你知道吗,数据库是要处理以上二者状况!我不会耽误你不少时间,只是过个概念。这会帮助咱们理解基于成本优化(Cost-Based Optimization) 的概念。算法

概念

时间复杂度是用来观察算法在给定数量的数据的状况下会耗费多长时间。为了描述这个复杂度,计算机科学们使用数学的大O表示法。这个符号与一个函数一块儿使用,这个函数用于描述一个算法在给定数量的输入数据下须要执行多少次操做。数据库

举个栗子,当我讲这算法在“O(some_function())”时,这意味着在必定数量的数据中,算法须要执行 some_funtion(a_certain_amount_of_data) 次操做。数组

更重要的不是数据量,而是当数量的量增大时操做次数增长的方式。而时间复杂的虽然没有给出准确的操做数,可是它依然是一个很好的想法。

在这图中,你能够看到不一样类型的复杂度的走向。我用对数坐标去绘制的。换句话说,这些数据是从 1 到 10亿 的快速增加的。咱们能够看到:服务器

  • O(1) 或者说是常数的复杂度保持不变(不然它也不会称为常数复杂度)
  • O(log(n)) 即使 10亿的数据,也能保持较低的操做数
  • 最可怕的复杂度是 O(n²) 它操做数迅速爆炸
  • 其余的两个复杂度类型也迅速增加

一些例子

在数据量较少的时候,O(1) 和 O(n²) 的差别能够基本忽略不算。举个例子*1,假设你有一个算法,须要处理 2000 个元素数据结构

  • O(1) 的算法须要 1 次操做
  • O(log(n)) 的算法须要 7 次操做
  • O(n) 的算法须要 2,000 次操做
  • O(n*log(n))的算法须要 14,000 次操做
  • O(n²) 的算法须要 4,000,000 次操做

O(1) 和 O(n²) 看起来相差不少(4百万),可是你最多失去的时候更多只有 2ms,知识一眨眼的时间。实际上,当前处理器能够处理每秒数亿次操做。这就是不少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²) 的算法须要 1,000,000,000,000 次操做

我不用算也知道 O(n²) 的算法可让你有时间喝杯咖啡(甚至是第二杯),若是在数据量上再加多个0,就能够有时间小睡一下了。框架

更深刻地看

再给你一些概念:分布式

  • 在一个好的哈希表中搜索得出一个元素,复杂度是 O(1)
  • 在一个好的平衡树中搜索得出一个结果,复杂度是 O(log(n))
  • 在一个数组中搜索得出结果,复杂度是 O(n)
  • 最好的排序算法,复杂度是 O(n*log(n))
  • 一个差的排序算法,复杂度是 O(n²)

注意:在下一个部分,咱们将看到这些算法和数据结构 有多种的复杂度类型:

  • 平均状况下
  • 最好的状况
  • 最坏的状况下

时间复杂度一般使用最快的状况 我只会谈及时间复杂度,可是复杂度也能用于:

  • 算法的内存消耗
  • 算法的磁盘的I/O消耗

固然,还有比 n² 更可怕的时间复杂度,如:

  • N^4: 太糟糕了。我将会提到有这种复杂度的一些算法
  • 3^n: 这也很糟糕。在这篇文章的中间部分咱们将会看到有一个算法有这种复杂度(而且在不少数据库中也使用这种算法)。
  • factorial n : 即便数据量不多,你也永远不会获得你的结果
  • n^n : 若是你的算法最终有这种复杂度,你该问问本身是否适合作IT

注意:我没有给你大O符号的真正定义,只是个概念。你能够去维基百科阅读这篇文章关于大O的真正定义。

合并排序

当你须要对一个集合进行排序的时候你要作什么?什么?你调用 sort() 函数 。。。 ok,好的答案。。。可是想了解数据库,你必须明白 sort() 函数是如何工做的。 有几个很好的排序算法,可是我将专一于最重要的一个:合并排序。你如今可能不明白为何排序数据如何有用,也要在查询优化部分才去作。此外,明白合并排序将会帮助咱们在以后理解一个普通数据库的操做叫合并关联(merge join)

合并

像不少有用的算法同样,合并排序是基本一个技巧的:合并两个长度是 N/2 的已排序的数组到一个长度为 N 的数组中,只须要 N 次操做。这个操做叫合并。 咱们用一个简单的例子来看看这是什么意思

从上面的图中能够看到,最想最终能构造出这长度为8的有序数组,你只需在那2个长度是4的有序数组中遍历一次。而因为那两个数组已经排序了,因此能够这样作:

1) 比较两个数组中的当前元素 (开始的时候,当前元素就是第一个元素了)
2) 把两个元素中数字最小的放到 最终数组(长度为8的) 中
3) 已被提取最小数字的数组访问下一个元素
4) 重复 1,2,3 直到有个数组访问到的最后一个元素
5) 而后你把另一个数组的剩余的元素都放在最终数组中去。 这样作是可行的,由于两个长度是4的数组都是已排序的,所以你不须要从这些数组中来回进行访问。 如今咱们已经理解了这个技巧,这是个人合并排序的伪代码。

array mergeSort(array a)
    if(length(a)==1)
       return a[0];
    end if
 
    //递归调用
    [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);
 
    //将两个有序的数组合并成一个大数组
    array result := merge(new_left_array,new_right_array);
    return result;

合并排序将一个问题分解成较小的问题,而后找到较小问题的结果再去获取最初的问题的结果(注意:这种算法叫分而治之)。若是你不明白这种算法,不要惧怕;我第一次看到它的时候也不名表。我对这类的算法会把它分红2个部分去看,这可能会帮助到你。

  • 切分阶段会把数组切分红更小的数组
  • 排序阶段会把小数组放在一块儿(使用合并),以造成更大的数组。

切分阶段

在切分的阶段,会用3个步将数组会被切分到单个元素的数组。正式步骤数应该是 log(N)(由于 N=8 ,log(N) = 3) 我怎样知道的? 我是天才 一句话:数字。想一下,每一个步骤都将初始数组的大小除以 2。步数是能够将初始数组除以2的次数。这是对数的精肯定义(在以 2 为底的对数中)。

排序阶段


在排序阶段,你能够从单个元素开始排序。在每一步中,你能够执行屡次的合并,总成本(每次合并的成本)是 N=8 次操做

  • 第一步,有4次合并,每次合并要用 2 个操做。
  • 第一步,有2次合并,每次合并要用 4 个操做。
  • 第三步,有1次合并,每次合并要用 8 个操做。

由于有 log(N) 步,因此总共要 N*log(N) 个操做。

合并排序的力量

为何这算法恐怖如斯? 由于:

  • 你能够对算法进行修改,以便减小内容占用。这方法是不会建立新数组的但你能够直接修改输入数组。

注意:这种算法叫原地排序(我国亦有书称为内排序)

  • 你能够对这算法进行修改,以便用磁盘空间来减小内容占用同时也不会有巨大的磁盘 I/O 损失。想法就是只对加载到内存的数据进行处理。这很重要,特别是当你的内存缓冲区仅有100MB而要对几GB的数据进行排序。 注意:这种算法叫外排序

  • 你能够对这算法进行修改,可让他在多线程/线程/服务器中使用

例如:分布式合并排序就是 Hadoop(大数据框架)的一个关键组件

  • 这算法能够铜化金(笑傲江湖的一我的名梗吧)原文是铅变成黄金。(!真实的故事)

这排序算法是绝大多数(可能不是全部)数据库会使用的,但不是为一种算法。 若是你想知道更多,你能够到看这篇论文,这论文说的数据库中常见排序算法的优缺点。

数组、树、哈希表

如今咱们了解了时间复杂度和排序的概念,我也必须在告诉你3种数据结构。这挺重要的,由于他们也是现代数据库的支柱,我还会介绍数据库索引的概念。

数组

二维数组是最简单的数据结构。表能够看做是一个数组。
例如:

这个二维数组是一个包含行和列的表:

  • 每一行就是一个对象
  • 每一列描述这些对象的特征
  • 每一列存储某种同一类型的状态(整数、字符串、日期...)

虽然这很容易存储和可视化数据,但当你须要寻找一个特种的值时,它就显得很糟糕。

例如,若是你想找到全部在英国工做的人,你不得不查看每行看看这我的是否是属于英国的。这会耗费 n 个的操做(N 就是行数)这不算太差,但有更快的方式吗?这就轮到树的发挥了。

注意:大多数现代数据库会提供高级数组来高效存储表格,好比 堆组织表(heap-organized tables) 或者是索引组织表(index-organized tables)。但它不能改变在特定条件下 的按列进行快速搜索的问题。

树和数据库索引

二叉搜索树是具备特殊属性的二叉树,每一个节点的键(key) 都必须是知足

  • 大于左子树的全部键
  • 小于右子树全部的键

下面让咱们来看看二叉树可视化后是什么一回事

概念

这棵树有 N=15 个元素。假设我要找键值为208的结点:

  • 我会从 (键值是136的)根结点开始找,由于 136 < 208, 因此我会去找该结点的右子树
  • 由于 398 > 208 ,因此我去找该结点的左子树
  • 由于 250 > 208 , 因此我去找该结点的左子树
  • 由于 200 < 208 ,因此我去找该结点的右子树。 但键值为200的结点没有右子树了,因此是该树不存在键值为208的结点(由于若是它确实存在,它确定就在200的右子树中)

如今,假设我要找键值为40的结点

  • 我会从 (键值是136的)根结点开始找,由于 136 > 40, 因此我会去找该结点的左子树
  • 由于 80 > 40 ,因此我去找该结点的左子树
  • 40 = 40 , 因此结点是存在的。我能够从这个结点中提取行ID(这属性不在图中),而后经过这个ID去找到表中对应的行。
  • 知晓了行ID 让咱们能精确地知道数据放在表的哪一个位置,所以咱们能当即获取到。

最后,这两次搜索都用了 树的层数 次,若是你仔细阅读了合并排序那部分,你应该会知道这是 log(N) 级别的时间复杂度。搜索的成本的 log(n),还不错

回到问题

但这东西是挺抽象的,仍是回到咱们原来的问题吧。不用那些愚蠢的整数,想象下用字符串去表示上面那个表的人的国家。假设你有一个表有一个“国家(country)”的列(column):

  • 若是你想知道有谁在英国工做
  • 你查找树去得到英国的结点
  • 在英国的结点里,你会找到一些英国工人的行的位置

这种搜索只花费你 log(N) 次操做,而若是直接用数组搜索就要用 O(N) 此操做了。你刚才想到的东西就是 数据库索引。 你能够为任何一组列(1个字符串,1个整数,2个字符串,1个整数和1个字符串,日期) 构建索引,只要你有一个函数去对比它们的键(keys)来创建键与键之间的顺序(数据库任何基本类型都能这样)

B+ 树索引


就像你看到那些,这里多了不少结点(以前的两倍以上)。其实,有额外的结点叫“决策结点”(decision nodes? 应该是蓝色的部分)这会帮你找到正确的结点(存储了相关表中的行的位置)。但搜索的复杂度仍然是 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+树中去索引):

  • 你不得不在B+树中保持两个节点间的顺序,不然你没法在混乱中找到节点
  • 你必需要底部的结点保持在尽量的层数,要否则 log(N) 的复杂度可能会变成 O(N) (好比所有都在右子树)

总之,B+树须要自排序和自平衡。值得庆幸的是,能够经过智能删除和智能插入的操做是实现。但这也带来一个成本,在B+中插入和删除都会是 log(N) 。这就是为什么你会听到,使用太多索引不是好主意。其实,索引会减慢在表中插入/更新/删除行的速度,这是由于每条索引数据库都须要耗费 log(N) 的操做为进行更新维护。还有,添加索引意味着事务管理器有更多的负载(咱们在文件的最后能看到这个管理器)

更多细节,你能够看维基百科的 B+树的B+树的文章。若是你想要在一个数据库中实现B+s树的例子你能够看这篇文章这篇文章,这两篇文章都是 MySQL 的核心开发者写的。这两篇文章都关注 innoDB(MySQL引擎) 怎样处理索引

注意:读者告诉我,由于要底层化,因此 B+ 树须要彻底平衡

哈希表

咱们最好一个重要的数据结果就是哈希表。这是很是有用的当你想快速寻找值。此外,明白哈希表会帮主咱们再以后理解数据库一个基本链接操做叫哈希链接(hash join)。这数据结构也被数据库用来存储一些内部数据(像是锁表缓冲池,咱们会在后面的内容中看到这两个概念) 哈希表是能用键(key)快速寻找到元素的数据结构。要建立哈希表你须要定义:

  • 元素的键
  • 键的哈希函数。这函数会计算出哈希值从而元素的一堆位置(叫桶 buckets)
  • 对比键的函数。一旦你找到正确的桶,你就必须用这个函数比对从而找到正确的元素

一个简单的例子

这哈希表有10个桶。我很懒,因此只画了5个桶,但我知道大家很聪明,全部我让你想象其余5个桶。我使用的哈希函数是将键 模10(即key % 10)。换句话说,要找到桶我只要用元素的键(key)的最后一位数字

  • 若是最后一个数字是0,搜索元素会桶0中介素
  • 若是最后一个数字是1,搜索元素会桶1中结束
  • 若是最后一个数字是2,搜索元素会桶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位),上面的第二次搜索也只花费1次操做,由于 000059 中没有元素。因此真正的挑战是寻找一个好的哈希函数来建立只包含不多元素的桶。 在个人例子中,找到一个好的哈希函数很容易。但那只是一个简单的例子,找一个好的哈希函数是很困难的,尤为是遇到(键)key是:

  • 字符串(如:人的姓氏)
  • 2个字符串(如:人的姓和名)
  • 2个字符串和一个日期(如:人的姓名+生日)
  • 。。。

好的散列函数,会让哈希表搜索在 O(1)

数组 vs 哈希表

为何不用数组 恩,你问了个好的问题

  • 哈希表能在内存中半加载,其余的桶能够放在磁盘上
  • 一个数组你必需要在内存中开辟一片连续的空间。若是你加载一个很大的表,这是很难有足够多的连续空间的
  • 哈希表你能够选择你想要的键(如:国家和人的姓氏)

更多的信息,你能够读个人文章,java HashMap 一个高效的哈希表的实现;在这篇文章中你不须要理解 java 的概念

相关文章
相关标签/搜索