[译]数据库是如何工做(五)查询管理器

这部分是数据库的强大之处。 在这部分中,一个写的不太好的查询被转换成一个快速的可执行代码。而后执行代码,并将结果返回给客户端管理器。这是一个多步骤操做:html

  • 首先对查询语句进行解析(parser),看看它是否有效的
  • 而后重写(rewritten)去移除无用的操做并添加一些预优化
  • 而后对其进行优化(optimized),以此来提升性能,并生成一个执行和数据访问的计划
  • 而后计划被编译(compiled)
  • 最后,执行

在这部分,我不会讨论最后两点由于他们不那么重要。 在阅读这部分以后,若是你想了解更多我推荐你阅读:
算法

  • 最初的基于成本(cost)优化研究论文(1979):关系数据库管理系统访问途径的选择。这篇文章只有12页,计算机科学的平均水平就能理解了
  • 一个又好又有深度的演讲,介绍 DB2 9.X 是如何优化查询这里
  • 很是好地介绍了PostgreSQL如何优化查询这里。这是一份最友好的文档,由于它更可能是介绍“让咱们看看 PostgreSQL 在这些状况下的查询计划” ,而不是“让咱们看看 PostgreSQL 使用的算法”
  • SQLite讲优化器的官方文档。这是很容易阅读的,由于 SQLite 使用的是很简单的规则。并且,只是惟一的官方文件解释它是怎样工做的
  • 一个很好的演讲关于 SQL Server 2005 的优化器这里
  • 关于Oracle 12C优化的白皮书。这里
  • 从《数据库系统概念》一书的做者那里,查询优化的2个理论课程。这里这里 这一个很好的,重点是磁盘I/O成本,但必须有计算机科学的良好水平。
  • 另一个理论课程,这更容易读,但只关系 join 操做符和 I/O 读写。

查询解析(parser)

每条SQL语言都会被送到解析器,在那里检查语法是否正确。若是你的查询语句有错,解析器会拒绝这条语句。例如:若是你写“SLECT ... ” 而不是 “SELECT ... ” ,就会在立刻结束。sql

解析不只仅是检查单词拼写,还会更深刻。它还检查关键字是否按正确的顺序使用。例如,WHERE 在 SELECT 以前也会拒绝。数据库

而后,对查询中的表和字段进行分析。解析器会使用数据库的元数据(metadata)来检查:编程

  • 表是否存在
  • 表中字段是否存在
  • 那些操做是否能够对那些类型字段使用(好比:你不能够用整数与字符串进行比较,就不能对整数使用substring()函数)

接着,它会检查你是否用读表(或写表)的受权。一样的,这些表的访问权限由你的DBA设置的。 在解析的时候,SQL查询语句会被转换成内部的表示(一般是树)数组

若是一切正常,就会把内部表示发送给查询重写器缓存

查询重写器

在这步,我有一个查询语句的内部表示。重写的目标是架构

  • 对查询预优化
  • 避免没必要要的操做
  • 帮助优化器寻找最佳的解决方案

这重写器执行 这重写器会对查询(query,这里应该是说内部表示)执行一系列已知的规则。若是这查询匹配到规则,就会用这个规则重写。一下是一些(可选)的非详尽的规则:oracle

  • 视图合并: 若是你在查询中用到视图,这视图会转成视图的SQL代码
  • 子查询的展开:子查询是很是难优化的,因此重写器会尝试用修改查询的方式来删除子查询

举个例子app

SELECT PERSON.*
    FROM PERSON
    WHERE PERSON.person_key IN
    (SELECT MAILS.person_key
    FROM MAILS
    WHERE MAILS.mail LIKE 'christophe%');

或被替换成

SELECT PERSON.*
    FROM PERSON
    WHERE PERSON.person_key IN
    (SELECT MAILS.person_key
    FROM MAILS
    WHERE MAILS.mail LIKE 'christophe%');
  • 去除没必要要的操做:举个例子,若是你用了 DISTINCT 而你又有一个 UNIQUE 的惟一性约束来阻止数据的不惟一,那就就会删除 DISTINCT 的关键字
  • 消除冗余关联:若是由于有一个 JOIN 被视图隐藏,而存在两个JOIN条件或者经过关联的传递性就能够获得等价,有链接是没用的,就会将其删除。
  • 常量的计算: 你若是你写了一些东西是须要运算的,那么他会在重写阶段就算出来的。好比, WHERE AGE > 10 + 2会被转换成WHERE AGE > 12 ,仍是 TODATE("some date") 会被转成日期时间(datetime)的格式
  • (高级) 分区裁决:若是使用分区表,重写器能够找到要使用的分区。
  • (高级)物化视图重写:若是你有个物化视图在查询中匹配到谓词子集,则重写器会检查视图是不是最新的,并修改查询以使用物化视图而不是原始表。
  • (高级) 多维分析转化:分析/窗口函数,星形链接,汇总...也被转换(但我不肯定它是由重写器仍是优化器完成的,由于两个进程都很是接近,它必须依赖于数据库)

而后会将这个重写的查询发送到查询优化器,开始有趣起来了!

统计

在咱们看到数据库是若是优化查询以前,咱们须要讨论一下统计由于没有了他,数据库是一个愚蠢的 。若是你没有告诉数据库要分析它本身的数据,它是不用去作的,并且它会作出很是糟糕的假设 可是什么样的信息是数据库须要的呢? 我不得不(简要)地谈论数据库和操做系统是如何存储数据的。他们都使用一个最小的单元叫页(page) 或者块(block)(默认是4或者8KB。这意味着若是你须要 1KB不管如何起码都会用掉你一页了。若是你一页是 8KB,就会浪费掉 7KB 回到统计!当你要求数据库要收集统计信息时,他会计算这些值:

  • 表有多少行/页
  • 表中的每一列(column)
    • 全部数据的惟一值(distinct data value)
    • 数据值的长度(最大,最小,平均)
    • 数据值的范围(最大,最小,平均)
  • 有关表索引的信息

这些统计会帮助优化器预估查询的磁盘 I/O、CPU 和内存使用状况 每列的统计信息都很重要。举个栗子,若是有一个 PERSON 的表格就要关联(JOIN)两个列: LAST_NAME , FIRST_NAME 。经过统计,数据库知道只有 FIRST_NAME 只有 1,000 不一样值,而 LAST_NAME 有 1,000,000 个不一样值(每一个人都有FIRST_NAME和LAST_NAME,量是同样)。因此数据库会按 LAST_NAME,FIRST_NAME 的顺序连到数据上,而不是按 FIRST_NAME,LAST_NAME 的顺序。这能减小不少对比由于 LAST_NAME 大多不相同,由于不少是时候只要对比前面 2(或者3) 个 LAST_NAME 的字符就够了。 但这些是基础统计。你能够要求数据库计算很高级的数据叫柱状图(histograms)。柱状图能够统计列中的(column)值的分布的信息。例如:

  • 最频繁使用的值
  • 位数
  • ...

这额外的统计会帮助数据库讯号一个很好的查询计划。特别是对于等式谓词(如:WHERE AGE = 18)或者是范围谓词(如:where AGE > 10 AND AGE < 40)由于这数据库能更好地知道这些谓词涉及的行数 这些统计会存储在数据库的元数据(metadata)中。例如你能够看到表(非分区表)的这些统计:

  • Oracle 中的 USER/ALL/DBA_TABLES 和 USER/ALL/DBA_TAB_COLUMNS
  • 在 DB2 中的 SYSCAT.TABLES 和 SYSCAT.COLUMNS

这些统计必须是最新的。没有什么比数据库认为表只有 500 行实际上有 1,000,000行更糟糕了。统计的惟一缺点是须要时间去计算。 这就是大部分数据库默认不是自动计算的缘由。数百万的数据让计算变得很困难。在这种状况下,你能够选择只计算基本统计信息或者按事例数据那样统计信息

举个栗子,当我处理每一个表都有百万行的数据的项目时,我选择只计算10%的统计信息,这让我耗费巨大的时间。事实证实是个糟糕的决定。由于有时候在ORACLE 10G 给特定的表的特定的列选择统计10%和统计全部的100%是很是不一样的(对于一百万个行之内的表不太可能发生)。这个错误的统计致使有时一个查询要用8个小时而不是30秒,并且寻找根源也是个噩梦。这个例子让咱们看到统计是多么的重要。

注意:固然,每一个数据库都有有不少高级的统计特性。若是你想知道更多,要看下数据可的文档。正如我所言,我会尝试明白如何使用统计而我发现一个最官方的文档是 PostgreSQL。

查询优化器

全部现代数据库都用基于成本的优化(CBO)去优化查询。这思想是每一个操做都放一个成本值,经过用最少成本的操做获取结果的方式,来寻找下降查询成本最好的方式。

为了明白一个基于成本的优化器是如何工做的,用一个去例子“感觉”下这个任务背后的复杂度,我想应该是不错的。在这部分,我将为你介绍链接2个表的3种普通方法,而且咱们将能很看到,一个简单的链接查询优化也是个噩梦。在此以后,咱们将看到真正的优化器上是如何工做的。

关于这些关联,我将会把重点放在他们的时间复杂度,但数据库的优化器会计算它们的CPU成本、磁盘 I/O 和内存需求。时间复杂度和CPU的成本是很是接近的(对于像我同样懒的人来说)。对于 CPU 的成本,我应该计算每一个操做箱加法,“if语句”,一个乘法,迭代。。。 更多:

  • 每一个高级代码操做有特定数量的低级CPU操做符
  • 每种CPU的操做符的成本是不一样的(在 CPU周期),不管你使用 Inter Core I7,Inter 奔腾4,仍是 AMD 的 opton , 换句话说,(CPU操做符的成本)取决于CPU的架构

使用时间复杂度更容易(至少对于我来说),而使用它咱们仍然能获得 CBO 的概念。我有时会讲磁盘 I/O,由于这也是个重要的概念。值得注意的是,大多数的瓶颈是磁盘I/O而不是CPU的使用率

索引

当看到 B+ 树的时候,咱们会谈及 B+ 树。要记得,索引都是已排序的

仅供参考,还有不少其余类型的索引,好比位图索引(bitmap indexs)。与B +树索引相比,它们在CPU,磁盘I / O和内存方面的成本不一样。

此外,不少现代的数据库中 ,若是动态建立临时索引能改善执行计划的成本,就会对当前的查询使用。

访问方式

在使用你的关联操做符(JOIN)以前,你首先要获取你的数据。下面是如何获取数据的方式

注意:由于访问途径的实际的问题是磁盘 I/O,因此我不会讨论太多时间复杂度

全局扫描

若是你读过执行计划,你会确定曾经看过这个单词全局扫描full scan 或只是扫描)。全局扫描是数据库读表或者完整的索引的简单方式。对磁盘I/O来说,一个表的全局扫描的成本明显是比一个索引扫描贵得多。

范围扫描

有不少其余类型的扫描,如索引范围扫描。例如:当你使用谓词如“WHERE AGE > 20 AND AGE < 40” 固然若是你须要有一条 AGE 字段的索引你才能使用。 咱们已经在第一部分中看到,范围查询的时候复杂度大概是 log(N)+M, 其中 N 是索引中数据的数量,而 M 大概是这个范围内的行数。感谢统计信息,M 和 N 都是已知的(注意:对于谓词 AGE > 20 AND AGE < 40 来说,M是可选择性的)。此外,对于范围扫描来说,你无需读全局索引 ,它在磁盘I/O上比全局扫描的成本更低

惟一扫描

若是你只须要索引中的一个值,则可使用惟一扫描。

经过行ID访问

大多数状况下,若是数据库使用索引,则必须查找与索引关联的行。为此,它将使用行ID访问。 例如,若是你作像这样的事

SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28

若是你 PERSON 表中有字段 AGE 的索引,优化器将使用索引来查找年龄是 28 的全部人,并询问表中的关联行,由于索引只有关于年龄的信息,而你想知道姓氏和名字。 但若是你如今作事是

SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON
    WHERE PERSON.AGE = TYPE_PERSON.AGE

PERSON上的索引将用于与TYPE_PERSON关联,但因为你没有询问表 PERSON 的信息,所以不会经过行ID访问表PERSON。 虽然它在少许访问的时候很是好用,但此操做的真正问题是磁盘I/O。若是你按行ID进行过多访问,则数据库可能会选择完整扫描。

其余访问方式

我没有介绍全部访问路径。若是您想了解更多信息,能够阅读 Oracle文档。其余数据库的 名称可能不一样,但背后的概念是相同的。

JOIN 操做符

因此,咱们知道如何获取咱们的数据,让咱们来 JOIN 下他们吧! 我将介绍3个常见的关联运算符:合并关联(Merge Join)哈希关联(Hash Join)嵌套循环关联(Nested Loop Join) 但在此以前,我须要引入新的词汇:内部联系(inner relation)外部联系(outer relation)。联系能够是:

  • 一个表
  • 一个索引
  • 来自上次操做的中间结果(如:前一个关联操做的结果)

当你正在关联两个联系时,关联算法会有两种不一样方式管理这两种联系。在本文的剩余部分,我会假设:

  • 外部联系是左侧的数据集
  • 内在联系是右侧的数据集

举个栗子,A JOIN B 就是 A 和 B 之间的关联 ,而 A 就是外部联系,B就是内部联系。 大多数时候,A JOIN B 的成本和 B JOIN A 的成本是不同的 在这部分,我也会假设外部联系有 N 个元素,内部关系有 M 个元素。请记得,真正的优化器会经过信息统计知道 N 和 M 的值。 注意:N 和 M 的关系是基数(cardinalities)

嵌套循环关联

嵌套循环关联是最简单的

这是是它的思想:

  • 首先遍历外部联系的每一行
  • 而后查看在内部关系中的全部看,看一下是否存在能匹配的行

下面是伪代码:

nested_loop_join(array outer, array inner)
    for each row a in outer
        for each row b in inner
            if (match_join_condition(a,b))
                write_result_in_output(a,b)
            end if
        end for
    end for

因为它是双重迭代,时间复杂度为O(N * M)

就磁盘 I/O 而言, 外部关系中(N行)要找每行的(对应关系),都须要内部关系中循环 M 遍。因此这算法须要从磁盘中读取 N + N*M 次。可是,若是内部关系是足够小的,你能够把内部关系放到内存中,那么你只须要读取磁盘 N+M 次了(M的数据写到内存)。这种修改,内部联系必定是最小的,由于这才能更好放进内存

对于时间复杂度而言,没有区别,但对磁盘I/O来说,上面那种方式更好。两条数据创建联系都只需读一次磁盘。

固然,内部关联能够被索引代替,这会对磁盘 I/O 更好

因为这个算法是很简单的, 若是内部关联太大以至于不易放进内存,这是另外一个对磁盘更友好的版本。构思以下:

  • 不要逐行读取两个联系
  • 一块一块地读数据,并保持有两块数据(来自每一个联系)在内存中
  • 比较两块数据,保留匹配到的数据,
  • 而后再从磁盘中建立新的块,并对比
  • 直到没有块能够加载

下载是可能的算法

//改进版本以减小磁盘I / O.
nested_loop_join_v2(file outer, file inner)
    for each bunch ba in outer
    // ba 如今在内存中了
        for each bunch bb in inner 
        // bb 如今在内存中了
           for each row a in ba
               for each row b in bb
                   if (match_join_condition(a,b))
                       write_result_in_output(a,b)
                   end if
               end for
           end for
        end for
    end for

使用此版本时,时间复杂度保持不变,但磁盘访问次数减小:

  •  先前版本,算法须要N + N * M个访问(每一个访问得到一行)。
  • 使用此新版本,磁盘访问次数变为 外部块的数量 + 外部块的数量 * 内部块的数量
  • 若是增长每一个块的大小,则减小磁盘访问次数。

注意:每一个磁盘访问都会采集比之前算法更多的数据,但这并不重要,由于它们是顺序访问(机械磁盘的真正问题是获取第一个数据的时间)。

哈希关联

哈希关联的思想是:

1) 从内部关联中获取全部元素
2) 建立内存哈希表
3) 逐一获取外部关系中的全部元素
4) 计算哈希表的每一个元素的哈希码(经过哈希表的哈希函数),用于寻找对应的内部联系桶(bucket)
5) 查看桶中的元素是否和外部表的元素匹配 对于时间复杂度来说,我须要 假设 一些东西去简化问题:

  • 内部联系被分红 X 个桶
  • 哈希函数几乎均匀地分布哈希值,换句话来说,每一个桶的大小是相同的
  • 外部联系一个元素和桶中全部元素的匹配的时间复杂度是桶中元素数量(M/X)

全部总共时间复杂度:(M/X) * N + 建立哈希表的成本(M) + 哈希函数的成本 * N 。 若是哈希函数建立了哈希桶的大小足够小(size小,桶数更多,X越大),那么复杂度就是 O(M+N)? 这是哈希链接的另外一个版本,更节省内存但对磁盘 I/O不友好,此次:
1) 内部和外部联系都计算哈希表
2) 而后将他们放进磁盘
3) 而后逐个桶比较二者的关系(一个用加载到内存,另外一个逐行读文件) [原文的意思是外部联系的全部元素哈希值存在一个文件中,逐行读取。经过哈希值能够知道是几号桶,就把桶加载到内存进行对比]

合并关联

合并链接是惟一一种关联能够生成排序结果

注意:在这个简化的合并关联中,不区份内部或外部表;二者都扮演了同样的角色。但实际的实现起来是有不一样的,例如,在处理重复项时。

排序

咱们已经讲过了合并排序,在这种状况下合并排序是个好的算法(但若是内存足够,就不是最好的) 就是数据集是已排序的,举个栗子:

  • 若是表内部原本就有序的,好比关联条件中的索引组织表(oracle的,表中的数据按主键存储和排序)
  • 若是关联条件的联系用的是索引
  • 若是关联的数据是在查询过程当中已排序的中间结果

合并关联

这部分很是相似于咱们看到的合并排序的合并操做。
但这一次,咱们只选择两个关系中相等的元素,而不是从两个关系中挑选每一个元素。

这是它的构思:
1) 对比两个关系中当前项(初始的时候两个当前项都是第一个)
2) 若是他们是相等的,就把两个元素放到结果,再比较两个关系的下一个元素
3) 若是不相等,就去对比值最小的那个关系的下个元素(由于下个元素可能能匹配)
4) 重复 1,2,3 直到有个关系到达最后一个元素

这是有效的,由于两个关系都是已排序的,因此你不须要在这些关系中返回。

这算法一个简化版,由于它没有处理数组中有多个相同值的状况(换句话说,多重匹配)。针对这种状况,真实的版本过重复了。这就是我选择简化版的缘由。

若是两个关系是已排序的,那么事件负责会是 O(N+M)

若是两个关系都须要排序,那么会加上排序的成本,时间复杂度会是 O(N*Log(N) + M*Log(M))

对计算机科学的Geeks 来说,这里有一个可能的算法来处理多个匹配(注意:我不是100%确定个人算法):

mergeJoin(relation a, relation b)
    relation output
    integer a_key:=0;
    integer b_key:=0;

    while (a[a_key]!=null or b[b_key]!=null)
        if ( a[a_key] < b[b_key])
            a_key++;
        else if (a[a_key] > b[b_key])
            b_key++;
        else //Join predicate satisfied
        //i.e. a[a_key] == b[b_key]

            //计算与a相关的重复数量
            integer nb_dup_in_a = 1:
            while (a[a_key]==a[a_key+nb_dup_in_a])
                nb_dup_in_a++;

            //计算与b相关的重复数量
            integer dup_in_b = 1:
            while (b[b_key]==b[b_key+nb_dup_in_b])
                nb_dup_in_b++;

           //在输出中写下重复项
           for (int i = 0 ; i< nb_dup_in_a ; i++)
               for (int j = 0 ; i< nb_dup_in_b ; i++)
                   write_result_in_output(a[a_key+i],b[b_key+j])

          a_key=a_key + nb_dup_in_a-1;
          b_key=b_key + nb_dup_in_b-1;

        end if
    end while

哪个是最好的?

若是存在最佳类型的关联,就不会有那么多种类型。这个问题很是困难,由于有不少因素发挥做用:

1) 有大量的空闲内存:没有足够的内存,你能够和强大的哈希关联说再见了(至少和全在内存中进行散列链接的方式说再见)
2) 2个数据集的大小:举个栗子,若是你有一个很是大的表要关联个小表,嵌套循环关联会比哈希关联快,那是由于哈希关联的建立成本较高。若是你有两个很大的表,那么嵌套查询就比较耗CPU了
3) 索引的存在:若是是两个B+树的索引,最机智的选择固然是合并关联了
4) 若是结果须要排序:即便你正在处理的数据集是没排序的,你可能会想使用成本昂贵的合并关联(用来排序),由于合并关联后结果是有序的,你也能够把它和其余的合并关联连起来用(或者由于使用 ORDER BY/GROUP BY/DISTINCT 等操做符隐式或显式地要求一个排序结果)
5) 若是关系已经排序:在这种状况下,合并链接是最佳候选
6) 关联的类型: 它是等值链接(即:tableA.col1 = tableB.col2)?它是内关联外关联笛卡尔积仍是自关联?某些关联在某些状况下没法工做。
7) 数据的分布: 若是数据在关联条件下是有偏向的(好比根据姓氏来关联人,可是不少人同姓),使用哈希关联将是一场灾难,由于哈希函数将建立分布不均匀的桶。
8) 若是但愿链接由 多个线程/进程 执行 有关更多信息,您能够阅读DB2ORACLESQL SERVER文档。

简单的例子

咱们刚刚看到了3种类型的关联操做。 如今,咱们须要关联5个表来表示一我的的信息。一我的要可能有:

  • 多个电话
  • 多个电子邮箱
  • 多个地址(多是个土豪)
  • 多个银行帐号

换个话说,咱们须要用下面的查询快速获得答案

SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS
    WHERE
        PERSON.PERSON_ID = MOBILES.PERSON_ID
        AND PERSON.PERSON_ID = MAILS.PERSON_ID
        AND PERSON.PERSON_ID = ADRESSES.PERSON_ID
        AND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID

做为查询优化器,我必须找处处理数据的最佳方法。可是有两个问题:

每次关联应该使用什么样类型?

我有3个可能的关联(哈希关联,合并关联,嵌套关联),有可能使用 0,1 或者 2个索引(更不用说有不一样类型的索引)

我应该选择什么顺序来计算关联?

例如,下图显示了4个表上3个关联的可能不一样状况

因此这是我以为的可能性:
1) 我用暴力遍历的方式 使用数据库的统计信息,我计算每一个方案的成本并获得最好的方案,但这也太多中方案了吧。对于给定的关联顺序,每一个关联有3个可能性: 哈希关联、合并关联、嵌套关联。因此会有 3^4 中可能性。肯定关联的顺序是个二叉树排列问题,有会有 (2*4)!/(4+1)! 种可能的顺序。而本例这这个至关简单的问题,我最后会获得 3^4*(2*4)!/(4+1)! 种可能。 抛开专业术语,那至关于 27,216 种可能性。若是给合并联接加上使用 0,1 或 2 个 B+树索引,可能性就变成了 210,000种。我忘了提到这个查询很是简单吗?
2) 我哭了,并退出这个任务 这很诱人,但你也不获得你想要结果,毕竟我须要钱来支付帐单。
3) 我只尝试几种计划并采起成本最低的计划。 因为我不是超人,我没法计算每一个计划的成本。 相反,我能够任意选择全部可能计划的子集,计算其成本并为你提供该子集的最佳计划。
4)我使用智能规则来减小可能的计划数量 下面有两种的规则: 1. 我可使用“逻辑”规则来消除无用的可能性,但它们不会过滤不少方案。好比:内部关系要用循环嵌套关联必定要是最好的数据集 2. 我能够接受不寻找最好的方案并用更积极的规则减小大量的可能性。好比:如何关系不多,使用循环嵌套关联并永远不使用合并关联或者哈希关联 在这个简单的例子中,我最终获得不少的可能性。但  现实中的查询还会有其余关系运算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 这意味着更多的可能性。 那么,数据库是如何作到的呢?

动态规划、贪心算法和启发式算法

关系数据库尝试过我刚才说过的多种方法。 大多数状况下,优化器找不到最佳解决方案,而是“好”解决方案 对于小型查询,能够采用暴力遍历的方法。可是有一种方法能够避免没必要要的计算,所以即便是中等查询也可使用暴力方法。这叫为动态规划编程。

动态规划

它们用了相同的(A JOIN B)子树。因此,咱们能够只计算这棵树一次,保存这树的城下,当看到这棵树的时候再次使用,而不是每次看到这棵树都从新计算一次。更正规地说,咱们面对的是重复计算的问题。为了不额外计算结果的部分,咱们使用了记忆术。

使用了这个技术,再也不是 (2*N)!/(N+1)! 的时间复杂度了,咱们只有 3^N 。在咱们以前的例子中有4个链接,这意味着 336个关联顺序会降到 81 个。若是你有一个大的查询有 8 个关联(也不是很大),这意味着会从 57,657,600 降到 6561

对于计算机科学的 GEEKS。这里有个算法,是在我曾经介绍给你正式课程找到的。我不会去解释这个算法,因此只有你已经明白动态规划编程或者你算法很好(你已经被警告过了)时才去读它:

procedure findbestplan(S)
if (bestplan[S].cost infinite)
   return bestplan[S]
// else bestplan[S] has not been computed earlier, compute it now
if (S contains only 1 relation)
         set bestplan[S].plan and bestplan[S].cost based on the best way
         of accessing S  /* Using selections on S and indices on S */
     else for each non-empty subset S1 of S such that S1 != S
   P1= findbestplan(S1)
   P2= findbestplan(S - S1)
   A = best algorithm for joining results of P1 and P2
   cost = P1.cost + P2.cost + cost of A
   if cost < bestplan[S].cost
       bestplan[S].cost = cost
      bestplan[S].plan = “execute P1.plan; execute P2.plan;
                 join results of P1 and P2 using A”
return bestplan[S]

对于大型查询你仍然能够用动态规划处理,可是还得要用额外的规则(或启发式算法)来消除可能性

  • 若是咱们只分析特定类型的方案。(例如:左深树 left-deep-tree),咱们会获得 n*2^n 而不是 3^n

  • 如何咱们在添加逻辑规则时避免一些模式(如:一个表有给定谓词的索引,就不是尝试用合并关联表而要只关联索引) 这会减小不少可能性,对最佳方案也不会形成很大的伤害。
  • 若是给流程添加规制(像是全部其余关系操做前执行关联操做) 这会减小不少可能性。

贪婪算法

但对一个大型的查询或者要快速获得答案(但查询速度不太快),就会有使用另外一种类型的算法,叫贪婪算法。

想法是经过一个规则(或者启发)以增量的方式构建查询计划。使用这个规则。贪婪算法能每次每步地找到问题的最好解决方案。算法从一个关联开始查询计划,而后每一步,算法会使用经过的规制把新的关联添加到新的查询计划

咱们举个简单的例子吧。假设咱们有个 5张表(A,B,C,D和E) 和 4次关联。为了简化问题,咱们用可能会用到嵌套关联。如今使用规则是 “用最小成本关联”

  • 咱们首先在5张表中任意选择一个(选A吧)
  • 咱们计算每种与A关联的成本(A多是内部联系或外部联系)
  • 咱们发现 A 和 B 关联的成本最低
  • 而后对比每种与 A JOIN B的结果 的成本(A JOIN B多是内部联系或者外部联系)
  • 咱们发现(A JOIN B)再关联C有最低的成本
  • 再每种与 (A JOIN B) JOIN C 的成本
  • 。。。
  • 最后,咱们会获得一个查询计划 (((A JOIN B) JOIN C) JOIN D) JOIN E)

咱们是从A开始的,咱们用一样的算法应用到 B 上 ,而后 C ,而后 D,而后 E。咱们能够保持这个计算是最少成本的。

顺便说一下,这个算法的名字叫最近邻居法

我不会详细介绍,但上面的问题(可能性不少的问题) 经过良好的建模和在 复杂度是 N*log(N) 的排序就能很容易地解决。 而这个算法的成本在 O(N*log(N))。而彻底动态规划的版本要用 O(3^N) 。若是是有20个链接的大查询,则意味着26 VS 3,486,784,401,这是一个巨大的差别!

这算法的问题是,咱们假设了:找到2个表的最佳关联,保存这个关联,下个新关联加进来,也会是最佳的成本,但:

  • 即便在 A, B, C 之间,A JOIN B 可得最低成本
  • (A JOIN C) JOIN B 也许比 (A JOIN B) JOIN C 更好。

为了改善这状况,您能够基于不一样的规则运行多个贪婪算法并保持最佳计划。、

其余算法

[若是你已经厌倦了算法,请跳到下一部分,这里说的对于文章的其他部分并不重要]

寻找最好的可能性计划对于计算机科学的研究者来说是一个活跃的话题。他们常常为特定的问题/模式尝试寻找更好的的解决方案。例如:

  • 若是查询是星型关联(这是某种屡次关联查询),某些数据库使用一种特定的算法。
  • 若是查询是并行查询,则某些数据库将使用特定算法

还研究了其余算法来替换大型查询的动态规划。贪婪算法属于称为启发式算法的你们族。贪心算法遵循规则(或启发式),保留在上一步找到的解决方案并将它追加到当前步骤的解决方案中。一些算法遵循规则并逐步应用(apply),蛋不用老是保留上一步的最佳解决方案。他们统称启发式算法。

好比,遗传算法遵循规则,但最后一步的最佳解决方案一般不会保留:

  • 解决方案表示可能的完整查询计划
  • 每一步保留了P个方案(即计划),而不是一个
  • 0) P个计划随机建立
  • 1) 成本最低的计划才会保留
  • 2) 这些最佳计划合起来产生P个新计划
  • 3) 一些新的计划被随机改写
  • 4) 1,2,3步重复 T 次
  • 5) 而后在最后一次循环,从P个计划里获得最佳计划。

循环次数越多,计划就越好。 这是魔术?

不,这是天然法则:适者生存!

实现了遗传算法,但我并无知道它会不会默认使用这种算法的。

数据库中还使用了其它启发式算法,像『模拟退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『双阶段优化算法(Two-Phase Optimization)』…..不过,我不知道这些算法如今是否在企业级数据库应用了,仍是只是用于研究型数据库。

若是想了解更多,你能够读这篇文章,它还介绍更多可能用到的算法《数据库查询优化中关联排序问题的算法综述》

现实中的优化器

[ 能够到下一部分,这里不过重要 ]

但,全部的叽里呱啦都是很是理论化的。由于我是开发者而不是研究员,我喜欢具体的例子。

咱们来看看SQLite 优化器是如何工做的。这是一个很轻型的数据库,因此他使用优化器基于贪心算法+额外规则来限制可能性数量

  • SQLite 在有交叉关联(CROSS JOIN 如: SELECT * FROM A,B)操做符的时候从不会给表从新排序
  • 关联都用嵌套链接实现
  • 外联(outer JOIN)始终按照顺序评估
  • ...
  • 3.8.0版本以前,SQLite 使用“最近邻居”算法来搜索最佳查询计划

等一下,咱们已经看过这个算法了。多么地巧合 ,从3.8.0版本(发布于2015年)开始,SQLite使用『N最近邻居』贪婪算法来搜寻最佳查询计划

接下来,让我看看其余的优化器是如何工做的。IBM 的 DB2 看起来和其余企业级别的数据库同样,我会关注 DB2 是由于我切换到大数据以前,最后使用的数据库。

若是咱们看它的官方文档,咱们了解到 DB2 的优化器容许你使用7中不一样级别的优化:

  • 对关联使用贪婪算法
    • 0 最小优化,使用索引扫描和嵌套循环关联并避免一些查询的重写
    • 1 低级优化
    • 2 全局优化
  • 对关联使用动态规划
    • 3 适度优化并粗略的近似估计
    • 5 全局优化并使用启发式的全部技术
    • 7 全局优化和5类似,但不用启发式
    • 9 最大程度的优化,不节省计算力/成本,考虑关联的全部顺序的可能性,包括笛卡尔乘积

咱们可能看到 DB2 使用贪心算法和动态规划 。固然,因为查询优化是数据库的主要功能,它们不会分享他们用的启发算法。

仅供参考, 默认的级别是 5 。默认的优化器使用下面的特性:

  • 使用全部可用的统计信息 ,包括频率的值和分数位的统计
  • 使用全部查询的重写规则 (包括 物化查询表路由materialized query table routing),除了再很是罕见的状况要用计算密集的规则外。
  • 使用 动态规划枚举关联 ,用于
    • 有限使用组合的内在联系
    • 涉及查找表的星型模式,有限使用笛卡尔乘积
  • 考虑各类的访问方式,含列表预取(list prefetch,注:咱们将会看这是什么),index ANDing(注:一种对索引的特殊操做),和物化查询表路由。

默认状况下, DB2 对关联顺序使用受限制启发的动态规划算法

查询计划缓存

因为建立查询计划会花不少时间,因此绝大部分数据库会存储 *查询计划的缓存* 避免没有必要的重复计算。这是个大话题啊,由于数据须要知道什么时间要更新过期的计划。解决这问题的想法是设置阈值,当统计表的改动超过某个阈值,就会从将这个表从缓存中清除

查询执行器

在这阶段,咱们有优化器的执行计划了。这计划会被编译成一个可执行代码。而后,若是有足够的资源(内存,CPU)就会经过查询执行器执行。在这计划中的操做(JOIN,SORT BY...)可能会串行或者并行执行;这取决于执行器。为了获取和写入数据,查询执行器会和数据管理器互相配合,这就是这篇文章下一部分要讲的

相关文章
相关标签/搜索