这部分是数据库的强大之处。 在这部分中,一个写的不太好的查询被转换成一个快速的可执行代码。而后执行代码,并将结果返回给客户端管理器。这是一个多步骤操做:html
在这部分,我不会讨论最后两点由于他们不那么重要。 在阅读这部分以后,若是你想了解更多我推荐你阅读:
算法
每条SQL语言都会被送到解析器,在那里检查语法是否正确。若是你的查询语句有错,解析器会拒绝这条语句。例如:若是你写“SLECT ... ” 而不是 “SELECT ... ” ,就会在立刻结束。sql
解析不只仅是检查单词拼写,还会更深刻。它还检查关键字是否按正确的顺序使用。例如,WHERE 在 SELECT 以前也会拒绝。数据库
而后,对查询中的表和字段进行分析。解析器会使用数据库的元数据(metadata)来检查:编程
接着,它会检查你是否用读表(或写表)的受权。一样的,这些表的访问权限由你的DBA设置的。 在解析的时候,SQL查询语句会被转换成内部的表示(一般是树)数组
若是一切正常,就会把内部表示发送给查询重写器缓存
在这步,我有一个查询语句的内部表示。重写的目标是架构
这重写器执行 这重写器会对查询(query,这里应该是说内部表示)执行一系列已知的规则。若是这查询匹配到规则,就会用这个规则重写。一下是一些(可选)的非详尽的规则:oracle
举个例子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%');
WHERE AGE > 10 + 2
会被转换成WHERE AGE > 12
,仍是 TODATE("some date")
会被转成日期时间(datetime)的格式而后会将这个重写的查询发送到查询优化器,开始有趣起来了!
在咱们看到数据库是若是优化查询以前,咱们须要讨论一下统计由于没有了他,数据库是一个愚蠢的 。若是你没有告诉数据库要分析它本身的数据,它是不用去作的,并且它会作出很是糟糕的假设 可是什么样的信息是数据库须要的呢? 我不得不(简要)地谈论数据库和操做系统是如何存储数据的。他们都使用一个最小的单元叫页(page) 或者块(block)(默认是4或者8KB。这意味着若是你须要 1KB不管如何起码都会用掉你一页了。若是你一页是 8KB,就会浪费掉 7KB 回到统计!当你要求数据库要收集统计信息时,他会计算这些值:
这些统计会帮助优化器预估查询的磁盘 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)中。例如你能够看到表(非分区表)的这些统计:
这些统计必须是最新的。没有什么比数据库认为表只有 500 行实际上有 1,000,000行更糟糕了。统计的惟一缺点是须要时间去计算。 这就是大部分数据库默认不是自动计算的缘由。数百万的数据让计算变得很困难。在这种状况下,你能够选择只计算基本统计信息或者按事例数据那样统计信息
举个栗子,当我处理每一个表都有百万行的数据的项目时,我选择只计算10%的统计信息,这让我耗费巨大的时间。事实证实是个糟糕的决定。由于有时候在ORACLE 10G 给特定的表的特定的列选择统计10%和统计全部的100%是很是不一样的(对于一百万个行之内的表不太可能发生)。这个错误的统计致使有时一个查询要用8个小时而不是30秒,并且寻找根源也是个噩梦。这个例子让咱们看到统计是多么的重要。
注意:固然,每一个数据库都有有不少高级的统计特性。若是你想知道更多,要看下数据可的文档。正如我所言,我会尝试明白如何使用统计而我发现一个最官方的文档是 PostgreSQL。
全部现代数据库都用基于成本的优化(CBO)去优化查询。这思想是每一个操做都放一个成本值,经过用最少成本的操做获取结果的方式,来寻找下降查询成本最好的方式。
为了明白一个基于成本的优化器是如何工做的,用一个去例子“感觉”下这个任务背后的复杂度,我想应该是不错的。在这部分,我将为你介绍链接2个表的3种普通方法,而且咱们将能很看到,一个简单的链接查询优化也是个噩梦。在此以后,咱们将看到真正的优化器上是如何工做的。
关于这些关联,我将会把重点放在他们的时间复杂度,但数据库的优化器会计算它们的CPU成本、磁盘 I/O 和内存需求。时间复杂度和CPU的成本是很是接近的(对于像我同样懒的人来说)。对于 CPU 的成本,我应该计算每一个操做箱加法,“if语句”,一个乘法,迭代。。。 更多:
使用时间复杂度更容易(至少对于我来说),而使用它咱们仍然能获得 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访问。 例如,若是你作像这样的事
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 下他们吧! 我将介绍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
使用此版本时,时间复杂度保持不变,但磁盘访问次数减小:
注意:每一个磁盘访问都会采集比之前算法更多的数据,但这并不重要,由于它们是顺序访问(机械磁盘的真正问题是获取第一个数据的时间)。
哈希关联的思想是:
1) 从内部关联中获取全部元素
2) 建立内存哈希表
3) 逐一获取外部关系中的全部元素
4) 计算哈希表的每一个元素的哈希码(经过哈希表的哈希函数),用于寻找对应的内部联系桶(bucket)
5) 查看桶中的元素是否和外部表的元素匹配 对于时间复杂度来说,我须要 假设 一些东西去简化问题:
全部总共时间复杂度:(M/X) * N + 建立哈希表的成本(M) + 哈希函数的成本 * N 。 若是哈希函数建立了哈希桶的大小足够小(size小,桶数更多,X越大),那么复杂度就是 O(M+N)? 这是哈希链接的另外一个版本,更节省内存但对磁盘 I/O不友好,此次:
1) 内部和外部联系都计算哈希表
2) 而后将他们放进磁盘
3) 而后逐个桶比较二者的关系(一个用加载到内存,另外一个逐行读文件) [原文的意思是外部联系的全部元素哈希值存在一个文件中,逐行读取。经过哈希值能够知道是几号桶,就把桶加载到内存进行对比]
合并链接是惟一一种关联能够生成排序结果
注意:在这个简化的合并关联中,不区份内部或外部表;二者都扮演了同样的角色。但实际的实现起来是有不一样的,例如,在处理重复项时。
咱们已经讲过了合并排序,在这种状况下合并排序是个好的算法(但若是内存足够,就不是最好的) 就是数据集是已排序的,举个栗子:
这部分很是相似于咱们看到的合并排序的合并操做。
但这一次,咱们只选择两个关系中相等的元素,而不是从两个关系中挑选每一个元素。
这是它的构思:
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) 若是但愿链接由 多个线程/进程 执行 有关更多信息,您能够阅读DB2,ORACLE 或 SQL 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]
对于大型查询你仍然能够用动态规划处理,可是还得要用额外的规则(或启发式算法)来消除可能性
但对一个大型的查询或者要快速获得答案(但查询速度不太快),就会有使用另外一种类型的算法,叫贪婪算法。
想法是经过一个规则(或者启发)以增量的方式构建查询计划。使用这个规则。贪婪算法能每次每步地找到问题的最好解决方案。算法从一个关联开始查询计划,而后每一步,算法会使用经过的规制把新的关联添加到新的查询计划
咱们举个简单的例子吧。假设咱们有个 5张表(A,B,C,D和E) 和 4次关联。为了简化问题,咱们用可能会用到嵌套关联。如今使用规则是 “用最小成本关联”
咱们是从A开始的,咱们用一样的算法应用到 B 上 ,而后 C ,而后 D,而后 E。咱们能够保持这个计算是最少成本的。
顺便说一下,这个算法的名字叫最近邻居法
我不会详细介绍,但上面的问题(可能性不少的问题) 经过良好的建模和在 复杂度是 N*log(N) 的排序就能很容易地解决。 而这个算法的成本在 O(N*log(N))。而彻底动态规划的版本要用 O(3^N) 。若是是有20个链接的大查询,则意味着26 VS 3,486,784,401,这是一个巨大的差别!
这算法的问题是,咱们假设了:找到2个表的最佳关联,保存这个关联,下个新关联加进来,也会是最佳的成本,但:
为了改善这状况,您能够基于不一样的规则运行多个贪婪算法并保持最佳计划。、
[若是你已经厌倦了算法,请跳到下一部分,这里说的对于文章的其他部分并不重要]
寻找最好的可能性计划对于计算机科学的研究者来说是一个活跃的话题。他们常常为特定的问题/模式尝试寻找更好的的解决方案。例如:
还研究了其余算法来替换大型查询的动态规划。贪婪算法属于称为启发式算法的你们族。贪心算法遵循规则(或启发式),保留在上一步找到的解决方案并将它追加到当前步骤的解决方案中。一些算法遵循规则并逐步应用(apply),蛋不用老是保留上一步的最佳解决方案。他们统称启发式算法。
好比,遗传算法遵循规则,但最后一步的最佳解决方案一般不会保留:
循环次数越多,计划就越好。 这是魔术?
不,这是天然法则:适者生存!
实现了遗传算法,但我并无知道它会不会默认使用这种算法的。
数据库中还使用了其它启发式算法,像『模拟退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『双阶段优化算法(Two-Phase Optimization)』…..不过,我不知道这些算法如今是否在企业级数据库应用了,仍是只是用于研究型数据库。
若是想了解更多,你能够读这篇文章,它还介绍更多可能用到的算法《数据库查询优化中关联排序问题的算法综述》
[ 能够到下一部分,这里不过重要 ]
但,全部的叽里呱啦都是很是理论化的。由于我是开发者而不是研究员,我喜欢具体的例子。
咱们来看看SQLite 优化器是如何工做的。这是一个很轻型的数据库,因此他使用优化器基于贪心算法+额外规则来限制可能性数量
等一下,咱们已经看过这个算法了。多么地巧合 ,从3.8.0版本(发布于2015年)开始,SQLite使用『N最近邻居』贪婪算法来搜寻最佳查询计划
接下来,让我看看其余的优化器是如何工做的。IBM 的 DB2 看起来和其余企业级别的数据库同样,我会关注 DB2 是由于我切换到大数据以前,最后使用的数据库。
若是咱们看它的官方文档,咱们了解到 DB2 的优化器容许你使用7中不一样级别的优化:
咱们可能看到 DB2 使用贪心算法和动态规划 。固然,因为查询优化是数据库的主要功能,它们不会分享他们用的启发算法。
仅供参考, 默认的级别是 5 。默认的优化器使用下面的特性:
默认状况下, DB2 对关联顺序使用受限制启发的动态规划算法
因为建立查询计划会花不少时间,因此绝大部分数据库会存储 *查询计划的缓存* 避免没有必要的重复计算。这是个大话题啊,由于数据须要知道什么时间要更新过期的计划。解决这问题的想法是设置阈值,当统计表的改动超过某个阈值,就会从将这个表从缓存中清除
在这阶段,咱们有优化器的执行计划了。这计划会被编译成一个可执行代码。而后,若是有足够的资源(内存,CPU)就会经过查询执行器执行。在这计划中的操做(JOIN,SORT BY...)可能会串行或者并行执行;这取决于执行器。为了获取和写入数据,查询执行器会和数据管理器互相配合,这就是这篇文章下一部分要讲的