MySQL-专家级教程-六-

209 阅读1小时+

MySQL 专家级教程(六)

原文:Expert MySQL, 2nd Edition

协议:CC BY-NC-SA 4.0

十一、数据库系统内部原理

本章介绍了一些数据库系统内部的概念,为更深入地研究数据库系统内部做准备。我将更深入地介绍查询在服务器内部是如何表示的,以及查询是如何执行的。我从一个更一般的角度来探讨这些主题,然后以讨论如何使用 MySQL 系统来进行自己的 MySQL 系统内部实验来结束这一章。最后,我将介绍数据库系统的内部实验项目。

查询执行

大多数数据库系统使用迭代或解释执行策略。迭代方法 提供了产生可用于处理离散操作(例如,连接、项目等)的调用序列的方法。),但其设计并不包含内部表示的特征。将查询转换成迭代方法使用了函数式编程和程序转换技术。一些算法从基于关系代数的查询规范生成迭代程序。

查询执行机制的实现创建了一组使用高级语言形成的已定义的已编译功能原语,然后通过调用堆栈或过程调用序列将它们链接在一起。当创建并选择执行查询执行计划时,编译器(通常与创建数据库系统的编译器相同)用于将过程调用编译成二进制可执行文件。由于迭代方法的高成本,编译后的执行计划通常会被存储起来,以供类似或相同的查询重用。

解释性方法 另一方面,使用基本操作的现有编译抽象来执行查询执行。所选择的查询执行计划被重新构造为一个方法调用队列,每个方法调用都从队列中取出并进行处理;然后将结果存储在内存中,供下一次或后续调用使用。这种策略的实现通常被称为惰性评估、,因为可用的编译方法没有针对最佳性能进行优化;相反,它们是为通用性而优化的。

MySQL 查询执行 再探

MySQL 中的查询处理和执行是解释性的。它是使用线程架构实现的,每个查询都有自己的执行线程。图 11-1 描绘了一个描述 MySQL 查询处理方法的框图。

9781430246596_Fig11-01.jpg

图 11-1。 MySQL 查询执行

当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。正如您在前一章中看到的,MySQL 解析器是使用一个用 Bison 编译的大型莱克斯-YACC 脚本实现的。解析器构造了一个用来保存查询的数据结构。这种数据结构或查询结构可用于执行查询。一旦创建了查询结构,控制就传递给查询处理器,查询处理器执行诸如验证表完整性和安全访问之类的检查。一旦授予了所需的访问权限并打开了表(如果查询是更新,则表被锁定),控制权就传递给执行基本查询操作的各个方法,如 select、restrict 和 project。通过对表和操作的列表进行排序来对数据结构进行优化,以形成基于常见实践的更有效的查询。这种形式的优化被称为选择项目连接查询处理器。使用已建立的通信协议和访问方法将查询操作的结果返回给客户端。

什么是编译查询 ?

理解“编译”的意思时,一个经常混淆的领域。编译查询是迭代查询执行计划的实际编译,但一些研究人员(如 C. J. Date)认为编译查询是经过优化并存储以供将来执行的查询。因此,在考虑使用编译后的查询时,您必须小心。在这项工作中,避免使用单词 compiled ,因为查询优化器和执行引擎不存储查询执行计划供以后重用,查询执行也不需要任何编译或汇编工作。

image 注意存储过程的概念是一个保存的计划——它被编译或优化,以便以后执行,并且可以对满足其输入参数的数据运行多次。

探索 MySQL 内部

你怎样才能教一个人优化器是如何工作的,而不允许他参与项目呢?此外,在没有实际看到数据库系统的内部情况下,您怎么能期望了解它们呢?在本节中,我将通过讨论如何将 MySQL 用作专业人士和学者的实验平台来回答这些问题。

开始使用 MySQL 进行实验

使用 MySQL 进行实验有几种方法。例如,您可以使用交互式调试器来研究内部组件,或者您可以使用 MySQL 系统作为您自己的内部数据库技术实现的宿主。查看服务器内部行为的另一个有用的方法是打开跟踪,并读取使用调试打开时编译的服务器生成的跟踪。关于调试跟踪的更多细节,参见第 5 章。

如果您要进行实验,请使用专用服务器进行实验。如果您计划使用 MySQL 开发自己的扩展,这一点很重要。你不想冒实验污染你的开发服务器的风险。

试用 MySQL 源代码

实验 MySQL 最具侵入性的方法是修改源代码。这包括在系统运行时观察它,然后通过用另一个算法或代码段替换另一个算法或代码段来设计改变部分的实验,然后观察行为的变化。虽然这种方法将使您能够研究 MySQL 的内部工作方式,但是以这种方式对源代码进行更改可能会导致服务器变得太不稳定而无法使用——特别是如果您突破了算法和数据结构的极限,或者更糟的是,违反了内存引用。然而,学习源代码最好的方法就是观察它的运行。以这种方式进行的测试可以用来为其他形式的实验收集数据。

使用 MySQL 作为实验技术的主机

使用 MySQL 进行实验的一种侵入性较小的方法是使用 MySQL 作为自己实验代码的宿主。这使您可以专注于优化器和执行引擎,而不用担心系统的其他部分。数据库系统有许多部分。仅举几个例子,MySQL 中有用于网络通信、数据输入和访问控制的子组件,甚至还有用于使用和管理文件和内存的实用程序。您可以在自己的代码中使用 MySQL 的资源,而不是创建自己的子组件。

我已经用这种方法实现了本书中描述的实验项目。我将向您展示如何连接到 MySQL 解析器,并使用 MySQL 解析器来读取、测试和接受有效命令,并将代码重定向到实验项目优化器和执行例程。

解析器和词法分析器 识别已经在解析器或词法散列中定义的字母数字字符串(也称为记号)。解析器用位置信息(在流中出现的顺序)标记所有的标记,并使用识别非标记字符串的特定模式的逻辑来识别文字和数字。一旦解析器完成,控制返回到词法分析器。MySQL 中的词法分析器被设计用来识别标记和非标记的特定模式。一旦有效命令被识别,控制就被传递给每个命令的执行代码。MySQL 解析器和词法分析器可以进行修改,以包括新的令牌或关键字,用于实验。关于如何修改解析器和词法分析器的更多细节,参见第 7 章。这些命令可以设计成模拟 SQL 的命令,代表典型的数据操作命令,如 select、update、insert 和 delete,以及典型的数据定义命令,如 create 和 drop。

一旦控制传递给实验优化/执行引擎,就可以使用 MySQL 内部查询表示结构运行实验,或者转换成另一种结构。从那里,可以运行查询优化和执行的实验实现,并使用 MySQL 系统将结果返回给客户机。这允许您在实现自己的内部数据库组件时使用网络通信和解析子组件。

运行多个 MySQL 实例

关于 MySQL 服务器的一个鲜为人知的事实是,可以在一台机器上运行服务器的多个实例。这允许您在与开发安装相同的机器上运行修改后的 MySQL 系统。如果您的资源有限,或者为了进行比较,您希望将修改后的服务器作为另一个安装在同一台计算机上运行,那么您可能需要这样做。运行多个 MySQL 实例需要在命令行或配置文件中指定某些参数。

虚拟机拯救世界

您还可以利用虚拟机来进一步隔离您的实验。为了充分利用这个选项,您可以用一个基本的 MySQL 服务器安装配置一个虚拟机 ,然后克隆这个虚拟机。这允许您运行 MySQL 的一个实例,就像它在一台单独的机器上一样。一些虚拟机环境还允许您在机器运行时拍摄快照,这允许您在拍摄快照时重新启动会话。这可以节省您设置测试环境的大量时间。当面对复杂或冗长的测试环境设置时,我经常选择这种方法。

有许多虚拟环境可供选择。开源的 Oracle VirtualBox 是学者和希望省钱的人的绝佳选择。VirtualBox 可以在大多数平台上运行,并提供设置虚拟 MySQL 服务器主机所需的所有功能。VMWare 的软件很贵,但是它提供了更多的功能。如果您运行 Mac OS X,VMWare 的 Fusion 和 Parallel Desktop 都是出色的低成本替代产品。

至少,您需要为服务器通信指定不同的 TCP 端口或套接字,并为数据库文件指定不同的目录。在 Windows 上启动第二个 MySQL 实例的一个例子是:

mysqld-debug.exe --port=3307 --datadir="c:/mysql/test_data" --console

在这个例子中,我告诉服务器使用 TCP 端口 3307 (默认为 3306),并使用不同的数据目录,作为控制台应用运行。为了连接到服务器的第二个实例,我必须告诉客户机使用与服务器相同的端口。例如,为了连接到我的第二个实例,我将启动 MySQL 客户端实用程序,它具有:

mysql.exe -uroot --port=3307

--port参数也可以与mysqladmin实用程序一起使用。例如,要关闭在端口 3307 上运行的第二个实例,请发出以下命令:

mysqladmin.exe -uroot --port=3307 shutdown

这项技术存在潜在的问题。很容易忘记您连接的是哪台服务器。防止混淆或避免向错误的服务器发出查询(如DELETEDROP)的一种方法是更改 MySQL 客户端实用程序上的提示,以指示您所连接的服务器。

例如,发出命令prompt DBXP->为连接到实验服务器的 MySQL 客户机设置提示符,为连接到开发服务器的 MySQL 客户机设置提示符prompt Development->。这种技术使您一眼就能看出您将要向哪个服务器发出命令。在 MySQL 客户端使用prompt命令的例子如清单 11-1 所示。

清单 11-1。 实验服务器改变 MySQL 客户端提示的例子

mysql> prompt DBXP->
PROMPT set to 'DBXP->'
DBXP->show databases;
+−−------------------+
| Database           |
+−−------------------+
| information_schema |
| mysql              |
| test               |
+−−------------------+
3 rows in set (0.01 sec)

DBXP->

image 提示您也可以使用\d选项在提示中设置当前数据库。例如,要在连接到实验服务器的客户机中设置提示符,发出命令prompt DBXP:\d->。这将设置提示,指示您连接到实验服务器和当前数据库(由最后一个use命令指定),用冒号(例如DBXP:TEST->)隔开。

您可以使用这种技术来限制对修改后的服务器的访问。如果更改端口号或套接字,只有知道正确参数的人才能连接到服务器。这将使您能够将修改暴露给用户群的风险降至最低。如果您的开发环境是多样化的,有许多共享相同资源的实验和研究项目(这在学术界是常见的),您可能还想采取这些步骤来保护您自己的实验免受其他项目的污染。这通常不是问题,但它有助于采取预防措施。

image 注意如果使用二进制、查询或慢速查询日志,还必须为 MySQL 服务器的每个实例指定日志文件的备用位置。否则可能会导致日志文件和/或数据损坏。

局限性和关注点

也许使用 MySQL 进行实验最具挑战性的方面是修改解析器来识别 SQL 命令的新关键字(见第 7 章)。虽然不完全是一种复杂或新的实现语言,但修改 YACC 文件需要仔细关注最初开发者的意图。解决方案包括在每个解析器命令定义的顶部放置新命令的 SQL 语法定义的副本。这允许您拦截解析器的流,以便重定向查询执行。

最常见也是最重要的挑战是跟上 MySQL 代码库的不断变化。不幸的是,升级的频率是不可预测的。如果您想跟上特性变化的步伐,实验性技术的集成需要在每次发布源代码时重新插入对 MySQL 源文件的修改。对于任何想尝试 MySQL 的人来说,这可能不是一个问题。如果您发现自己因为正在编写的扩展而想要跟上变化,您可能应该使用源代码管理工具或构建第二个服务器进行实验,并在原始服务器上进行开发。

image 提示您最有可能遇到的挑战是检查 MySQL 代码库,并发现各种内部数据表示的含义、布局和用途。克服这一点的唯一方法是通过熟悉。我鼓励您访问和阅读文档(在线 MySQL 参考手册)以及 MySQL 网站、博客和消息论坛上的文章。它们是丰富的信息。虽然有些很难理解,但是每读一遍,这些概念就会变得更加清晰。抵制对文档感到沮丧的诱惑。休息一会儿,然后回头再看一遍。每当我(重读)阅读技术资料时,我都能找到有用的信息。

数据库系统内部实验

我构建了数据库实验项目(DBXP ) 来让您探索 MySQL 的内部,并让您探索一些替代数据库系统的内部实现。您可以通过这个实验了解更多关于数据库系统是如何构建的,以及它们是如何工作的。

为什么是实验?

DBXP 是一个实验而不是一个解决方案,因为它是不完整的。也就是说,这些技术以最小的错误处理、有限的特征集和低鲁棒性来实现。

这并不意味着 DBXP 技术不能被修改来替代 MySQL 系统的内部组件;更确切地说,DBXP 是为探索而设计的,而不是为生产而设计的。

实验项目概述

DBXP 项目是一系列实现内部查询表示、查询优化、查询执行和文件访问的替代算法和机制的类。这不仅让您有机会探索查询优化理论 的高级实现,而且还使 DBXP 技术的核心能够在不修改 MySQL 内部操作的情况下执行。这提供了额外的安全性,本地 MySQL 核心可执行代码不会受到 DBXP 技术的影响。这个额外的好处可以帮助减轻修改现有系统的一些风险。

MySQL 解析器(见sql_parse.cc)的实现通过调用为每个 SQL 命令实现的函数,将控制指向执行子进程的特定实例。例如,SHOW命令被重定向到在sql_show.cc文件中实现的函数。需要修改sql_parse.cc中的 MySQL 解析器代码,将处理重定向到 DBXP 查询处理器。

DBXP 查询处理器的第一步是将 MySQL 内部查询表示转换成实验性的内部表示。选择的内部表示被称为查询树,其中每个节点包含一个原子关系操作(选择、投影、连接等)。)和链接代表数据流。图 11-2 显示了一个查询树的概念示例。在示例中,我使用了符号:投影/选择(π)、限制(σ)和连接(φ)。箭头表示数据如何从表流向根。一个连接操作被表示为一个有两个子节点的节点。当数据从每个子节点呈现时,join 操作能够处理该数据并将结果传递给树中的下一个节点(其父节点)。每个节点可以有零个、一个或两个子节点,并且只有一个父节点。

9781430246596_Fig11-02.jpg

图 11-2。查询树概念

选择查询树是因为它允许 DBXP 查询优化器使用树操作算法。也就是说,优化使用树结构和树操作算法来以更有效的执行顺序排列树中的节点。此外,优化查询的执行是通过将树遍历到叶节点,执行由节点指定的操作,并将信息传递回链接来完成的。这种技术也使得以流水线方式执行成为可能,数据从叶节点传递到根节点,一次一个数据项。

将树向下遍历到一个数据项的叶子,并将其返回到树上(这个过程称为脉冲)允许每个节点处理一个数据项,在结果集中一次返回一行。树的这种脉动或轮询允许流水线的执行。结果是查询结果的更快的初始返回和感觉到的查询结果到客户端的更快的传输时间。见证查询结果更快地返回——尽管不是一下子全部返回——给用户一种查询更快的感觉。

使用 MySQL 托管 DBXP 实现始于 MySQL 解析器,其中 DBXP 代码接管查询的优化和执行,然后使用 MySQL 网络通信实用程序将结果一次一行地返回给客户机。

实验项目的组成部分

实验项目旨在向您介绍数据库系统内部实现的替代方案,并允许您通过向项目添加自己的修改来探索实现。DBXP 是使用一组简单的 C++类实现的,这些类表示数据库系统中的对象。

有用于元组、关系、索引和查询树的类。添加了额外的类来管理多用户对表的访问。图 11-3 显示了一个 DBXP 的高层架构的例子。

项目主要类别的完整列表见表 11-1 。这些类以与类相同的名称存储在源文件中(例如,Attribute类分别在名为attribute.hattribute.cc的文件中定义和实现)。

表 11-1。数据库内部实验项目类

描述
查询 _ 树提供查询的内部表示形式。还包含查询优化器。
Expression提供了一个表达式评估机制。
Attribute存储和操作元组(行)的属性(列)的操作。

这些类代表了数据库系统的基本构件。第 12 章到第 14 章包含了对查询树、启发式优化器和流水线执行算法的完整解释。这些章节还包括该实用程序的概述。我将向您展示 DBXP 实现的某些部分(最复杂的部分)的实现细节,剩下的部分留给您作为练习来实现。

9781430246596_Fig11-03.jpg

图 11-3。实验项目的高层图

本书的引言部分介绍了如何在课堂环境中使用实验项目的建议。

进行实验

运行实验需要为新项目修改 cmake 文件,并用 MySQL 服务器编译它们。没有一个项目文件需要任何特殊的编译或库。对 MySQL 配置和 cmake 文件的修改细节将在下一章中讨论,在这一章中,我将向您展示如何在实验中删除 SQL 命令。

如果您没有尝试过前几章的示例程序,我在下面包含了构建和运行 DBXP 实验项目的基本过程。

  1. 修改/sql 文件夹中的CMakeLists.txt file
  2. 运行'cmake.’ from the root of the source tree.
  3. 运行'make’ from the root of the source tree
  4. 停止服务器,将可执行文件复制到二进制目录中。
  5. 重新启动服务器并通过 MySQL 客户端连接以运行 DBXP SQL 命令。

摘要

在这一章中,我介绍了一些更复杂的数据库内部技术。您了解了查询在服务器内部是如何表示的以及如何执行的。更重要的是,您发现了如何使用 MySQL 来进行自己的数据库内部实验。这些技术的知识应该让您更好地理解 MySQL 系统是如何构建的,以及它是如何执行的。

在下一章中,我将通过一个查询树结构的示例实现向您展示更多关于内部查询表示的内容。下一章是一系列章节的开始,旨在为您实现自己的查询优化器和执行引擎提供基础。如果你想知道构建一个数据库系统需要什么,接下来的章节将向你展示如何开始使用你自己的查询引擎。

十二、内部查询表示

本章介绍了数据库实验项目(DBXP)的高级数据库技术的第一部分。我首先介绍查询树结构的概念,它用于在内存中存储查询。接下来,我将展示这个项目使用的查询树结构,以及实现 DBXP 代码的一系列简短项目中的第一个。本章以一组练习结束,您可以使用这些练习来学习更多关于 MySQL 和查询树的知识。

查询树

查询树是对应于查询的树结构,其中树的叶节点包含访问关系的节点和具有零个、一个或多个子节点的内部节点。内部节点包含关系运算符。这些操作符包括 project(描述为π)、restrict(描述为σ)和 join(描述为θ或image)。 1 树的边表示自下而上的数据流——即从对应于读取数据库中数据的树叶到生成查询结果的最终操作符根。图 12-1 描述了一个查询树的例子。

9781430246596_Fig12-01.jpg

图 12-1。示例查询树 2

对查询树的评估包括在操作数可用时评估内部节点操作,并将评估操作的结果沿树向上传递给父节点。当根节点被评估并被形成查询结果的元组替换时,评估终止。以下部分给出了用于在存储器中存储查询表示的查询树结构的变体。使用这种机制相对于关系演算内部表示的优势在表 12-1 中显示。

表 12-1 。使用查询树相对于关系演算的优势

作战需求查询树关系演算
可以减吗?是的。可以在评估查询计划之前修剪查询树。只有通过应用代数运算。
它能支持执行吗?是的。该树可用于通过沿树向上传递数据来执行查询。否。需要翻译成另一种形式。
它能支持关系代数表达式吗?是的。这棵树非常适合关系代数。否。需要转换。
它能在数据库系统中实现吗?是的。树形结构是一种常见的数据结构。只有通过模拟微积分的设计。
它能包含数据吗?是的。树节点可以包含数据、操作和表达式。没有。只有构成表达式的文字和变量。

显然,查询树内部表示优于现代数据库系统中采用的更传统的机制。例如,MySQL 中的内部表示是一组类和结构的表示,这些类和结构旨在包含查询及其元素,以便于(快速)遍历。它为优化和执行组织数据。 3

查询树内部表示有一些缺点。大多数优化器不是为在树形结构中工作而设计的。如果您想将查询树与优化器一起使用,就必须改变优化器。类似地,查询执行将与大多数查询处理实现非常不同。在这种情况下,查询执行引擎将从树中运行,而不是作为一个单独的步骤运行。这些缺点将在后面的章节中讨论,我将探索另一种优化器和执行引擎。

DBXP 查询树是使用节点结构的树数据结构,该节点结构包含表示这些操作所需的所有参数 :

  • 限制:允许您包含匹配属性表达式的结果。
  • Projection :提供选择包含在结果集中的属性的能力。
  • Join :允许您组合两个或多个关系,在结果集中形成一组复合属性。
  • Sort (order by) :允许您对结果集进行排序。
  • Distinct :提供将结果集缩减为唯一元组的能力。

image 注意 Distinct 是一个被添加来完成关系操作的操作,它不被大多数 SQL 实现所支持,也不是关系代数的固有属性。

投影、限制和连接是基本操作。Sort 和 distinct 作为附加的实用操作提供,有助于形成完整的查询树(所有可能的操作都表示为节点)。连接操作可以有连接条件(θ连接)或没有条件(等连接)。连接操作细分为以下操作:

  • Inner :返回元组的两个关系在匹配处的连接。
  • Outer (left,right,full) :返回FROM子句中提到的至少一个表或视图中的所有行,只要这些行满足任何WHERE搜索条件。所有行都是从用左外连接引用的左表中检索的,而右表中的所有行都在右外连接中引用。两个表中的所有行都在完全外部联接中返回。不匹配行的属性值作为空值返回。
  • Leftouter :两个关系的连接,返回匹配的元组,加上左边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Rightouter :两个关系的连接,返回匹配的元组,加上右边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Fullouter :两个关系的连接从两个关系中返回所有元组,将另一个关系中指定的不匹配属性留空(null)。
  • 交叉积:两个关系的连接,将第一个关系中的每个元组映射到另一个关系中的所有元组。

查询树还支持一些集合操作。支持的集合操作包括:

  • Intersect :集合运算,只返回模式相同的两个关系的匹配。
  • Union :集合运算,其中只返回具有相同模式的两个关系的不匹配。

什么是θ连接?

你可能想知道为什么有些连接被称为等连接,而有些被称为θ连接 。相等联接是联接条件为相等(=)的联接。θ连接是一种连接条件是不等式的连接(>、<、> =、< =、< >)。从技术上讲,所有的连接都是θ连接。θ连接很少使用,而相等连接很常见。

虽然 DBXP 查询树提供了 union 和 intersect 操作,但大多数数据库系统都支持结果集串联形式的 union。尽管 MySQL 解析器目前不支持交集操作,但是它支持联合。需要进一步修改 MySQL 解析器来实现 intersect 操作。以下部分描述了将 MySQL 查询表示转换为 DBXP 查询树的主要代码实现和类。

查询转换

必须修改 MySQL 解析器来识别和解析 SQL 命令。然而,我们需要一种方法来告诉解析器我们想要使用 DBXP 实现,而不是现有的查询引擎。为了简化更改,我简单地在 SQL 命令中添加了一个关键字(例如 DBXP ),将解析重定向到将 MySQL 内部表示转换为 DBXP 内部表示的代码。尽管这个过程增加了一些执行时间,并且需要少量的额外计算工作,但是这个实现简化了对解析器的修改,并且提供了一个通用的机制来比较 DBXP 数据结构和 MySQL 数据结构。我将带有 DBXP 关键字的 SQL 命令简称为 DBXP SQL 命令。

转换 4 的过程始于 MySQL 解析器,它将命令识别为 DBXP 命令。然后,系统将控制权交给一个名为sql_dbxp_parse.cc的类,该类管理解析后的查询从 MySQL 形式到 DBXP 内部表示(查询树)的转换。这是通过名为buid_query_tree的方法完成的。这个方法只对SELECTEXPLAIN SELECT语句调用。

DBXP 查询树

DBXP 查询优化器的核心是 DBXP 内部表示数据结构。它用于在 SQL 命令被解析和转换后表示查询。

这个结构被实现为一个树形结构(因此命名为查询树,其中每个节点有零个、一个或两个孩子。没有孩子的节点是树的叶子,有一个孩子的节点表示对数据执行一元操作的内部节点,有两个孩子的节点是连接或集合操作。源代码中的实际节点结构如清单 12-1 所示。

清单 12-1 DBXP 查询树节点

/*
  STRUCTURE query_node

  DESCRIPTION
    This this structure contains all of the data for a query node:

    NodeId -- the internal id number for a node
    ParentNodeId -- the internal id for the parent node (used for insert)
    SubQuery -- is this the start of a subquery?
    Child -- is this a Left or Right child of the parent?
    NodeType -- synonymous with operation type
    JoinType -- if a join, this is the join operation
    join_con_type -- if this is a join, this is the "on" condition
    Expressions -- the expressions from the "where" clause for this node
    Join Expressions -- the join expressions from the "join" clause(s)
    Relations[] -- the relations for this operation (at most 4)
    PreemptPipeline -- does the pipeline need to be halted for a sort?
    Fields -- the attributes for the result set of this operation
    Left -- a pointer to the left child node
    Right -- a pointer to the right child node
*/
struct query_node
{
  query_node();
  ∼query_node();
  int                 nodeid;
  int                 parent_nodeid;
  bool                sub_query;
  bool                child;
  query_node_type     node_type;
  type_join           join_type;
  join_con_type       join_cond;
  Item                *where_expr;
  Item                *join_expr;
  TABLE_LIST          *relations[4];
  bool                preempt_pipeline;
  List<Item>          *fields;
  query_node          *left;
  query_node          *right;
};

其中一些变量用于管理节点组织和形成树本身。最有趣的两个是nodeidparent_nodeid。这些用于建立树中节点的父子关系。这是必要的,因为作为优化过程的一部分,节点可以在树中上下移动。使用parent_nodeid变量避免了在树中维护反向指针的需要。 5

sub_query变量用于指示子查询的开始节点。因此,数据结构可以支持嵌套查询(子查询),而无需对结构进行额外的修改。唯一需要注意的是,优化算法被设计成使用子查询指示符作为树遍历的停止条件。也就是说,当检测到子查询节点时,优化会将子查询视为一个单独的实体。一旦检测到,就使用子查询节点作为下一次优化的开始,重新运行查询优化例程。因此,可以支持任意数量的子查询,并将其表示为树结构中的子树。这是查询树的一个重要特性,它克服了许多内部表示的局限性。

where_expr变量是一个指向 MySQL Item树的指针,它管理一个典型的通用表达式树。稍后我们将把它改为一个封装表达式的特殊类。详见第十三章

relations数组用于包含指向关系类的指针,这些关系类表示 MySQL 存储引擎中的内部记录结构的抽象。关系类通过存储引擎处理程序类提供对存储在磁盘上的数据的访问层。数组大小当前设置为 4。前两个位置(0 和 1)分别对应于左边和右边的孩子。接下来的两个位置(2 和 3)代表临时关系,比如重新排序(排序)和索引的应用。

image 注意relations数组大小设置为 4,这意味着您可以处理多达四个表的查询。如果您需要处理超过四个表的查询,您将需要更改本章后面显示的转换代码以接受超过四个表。

fields属性是指向 MySQL Item类的指针,该类包含一个表的字段列表。它在投影操作和维护关系操作所需的属性(例如,满足表达式但不是结果集一部分的属性的传播)中很有用。

最后一个感兴趣的变量是preempt_pipeline变量,DBXP Execute类使用它在处理来自子节点的数据时实现一个循环。每当操作需要遍历整个数据集(行)时,循环都是必要的。例如,在缺少允许排序的索引的情况下,联接一个公共属性上的两个关系的联接可能需要遍历一个或两个子节点,以便实现正确的映射(联接)操作。

这个类也负责查询优化(在第 13 章中描述)。由于查询树提供了操纵树的所有操作,并且由于查询优化也是一组树操作,因此优化是使用包装查询树结构的类(称为查询树类)中的方法来完成的。

优化器方法实现了一种启发式算法(在第 13 章的中描述,在第 14 章中有更详细的描述)。这些方法的执行导致将树重组为更优的树,并将一些节点分成两个或更多的其他节点,这些节点也可以被重新定位以形成更优的树。最佳树允许更有效地执行查询。

该类还支持成本优化,使用遍历树的算法,将可用索引应用于每个叶节点(直接访问关系存储的节点)的访问方法。

这种结构可以支持各种各样的操作,包括限制、投影、连接、设置和排序(排序)。查询节点结构被设计为将这些操作中的每一个表示为单个节点,并且可以存储所有相关的和必需的信息以就地执行操作。此外,EXPLAIN命令被实现为树的后序遍历,从叶子开始打印出每个节点的内容(参见本章后面的show_plan方法)。这个操作的 MySQL 等价物需要更多的计算时间,并且是用一组复杂的方法实现的。

因此,查询树是一种内部表示,它可以表示任何查询,并提供一种通过操纵树来优化查询的机制。事实上,树结构本身简化了优化,并通过提供将查询操作关联为树中的节点的方式来实现启发式优化器。因此,该查询树是在任何关系数据库系统中使用的可行机制,并且可以推广到生产系统中使用。

在 MySQL 中实现 DBXP 查询树

本节展示了在 MySQL 源代码中添加的 DBXP 查询树结构。创建关系数据库研究工具的第一步旨在向您展示查询树是如何工作的,以及如何将 MySQL 查询结构转换为基本查询树(未优化)。后面的章节将描述优化器和执行引擎。

我们不会尝试重用现有的SELECT命令,而是在解析器中创建新的条目来实现带有字符串DBXP_SELECT的 SELECT 命令的特殊版本。这将允许修改被隔离,并且不会与解析器中现有的SELECT子组件混淆。接下来的部分向您展示了如何添加查询树和添加存根来执行DBXP_SELECTEXPLAIN DBXP_SELECT命令。

image 注意Apress 网站上关于本章和后续章节的源代码示例包含一个差异文件,您可以用它来应用于 MySQL 源代码树。根据您所使用的服务器版本,如果它是基于 5.6 版的,那么修补操作只需做最小的修改就可以应用。差异文件的使用使示例代码更小,并允许您查看上下文中的变化。

添加和更改的文件

按照本章中的示例,您将创建几个文件并修改一些 MySQL 源代码文件。表 12-2 列出了将要添加和更改的文件。

表 12-2 。添加和更改的文件摘要

文件描述
mysqld.cc在 MySQL 版本号上添加了 DBXP 版本号标签
莱克斯·h将 DBXP 标记添加到词法哈希中
查询 _ 树. hDBXP 查询树头文件(新文件)
query_tree.ccDBXP 查询树类文件(新文件)
sql_cmd.h将 DBXP_SELECT 添加到 enum_sql_command 列表
sql_yacc.yy向解析器添加了 SQL 命令解析
sql_parse.cc添加代码以处理“大交换机”的新命令

创建测试

以下部分解释了清除DBXP_SELECT命令的过程、查询树类以及EXPLAIN DBXP_SELECTDBXP_SELECT命令。目标是允许用户输入任何有效的SELECT命令,处理查询,并返回结果。

image 注意由于 DBXP 引擎是一个实验性的引擎,它仅限于表示检索数据的基本操作的查询。将这些章节的长度保持在可管理的大小和复杂性要求 DBXP 引擎不处理带有聚集的查询——那些包含HAVINGGROUP BYORDER BY子句的查询。(没有任何东西禁止这样做,所以你可以自由地自己实现这些操作。)

下面几节详细介绍了创建 DBXP 代码的这三个方面所需的步骤。我将创建一个单独的测试文件并使用它来测试功能,而不是创建三个小的测试。对于那些未实现的操作,您可以通过在命令的开头添加井号(#)来注释掉查询语句,或者如图所示运行测试并忽略尚未实现的命令的不可避免的错误(从而遵守测试优先开发的原则)。清单 12-2 显示了Ch12.test文件。

清单 12-2 章节测试(Ch12.test)

#
# Sample test to test the DBXP_SELECT and EXPLAIN DBXP_SELECT commands
#

# Test 1: Test stubbed DBXP_SELECT command.
DBXP_SELECT * FROM no_such_table;

# Test 2: Test stubbed Query Tree implementation.
DBXP_SELECT * FROM customer;

# Test 3: Test stubbed EXPLAIN DBXP_SELECT command.
EXPLAIN DBXP_SELECT * FROM customer;

当然,您可以使用这个测试作为指南,并添加您自己的命令来探索新代码。有关如何使用 MySQL 测试套件创建和运行该测试的更多详细信息,请参考第 4 章。

清除 DBXP_SELECT 命令

在这一节中,您将学习如何向 MySQL 解析器添加一个定制的SELECT命令。您将看到如何修改解析器以适应一个新命令,该命令模拟 MySQL 中传统的SELECT命令。

识别修改

您应该首先通过在 MySQL 版本号上添加一个标签来识别一个拥有 DBXP 技术的 MySQL 服务器,以确保您总是能够分辨出您连接到的是经过修改的服务器。

image 提示你可以随时使用SELECT VERSION()命令来检索服务器的版本。如果您使用的是 MySQL 命令行客户端,您可以更改命令提示符,以指示您所连接的服务器是带有 DBXP 代码的服务器。

要添加版本标签,打开mysqld.cc文件并找到set_server_version方法。添加一条语句,将标签附加到 MySQL 版本号字符串上。在这种情况下,我们将使用“-DBXP 2.0”来代表这本书的印刷。清单 12-3 显示了修改后的set_server_version方法。

清单 12-3。 修改 mysqld.cc 文件

static void set_server_version(void)
{
  char *end= strxmov(server_version, MYSQL_SERVER_VERSION,
                     MYSQL_SERVER_SUFFIX_STR, NullS);
#ifdef EMBEDDED_LIBRARY
  end= strmov(end, "-embedded");
#endif
#ifndef DBUG_OFF
  if (!strstr(MYSQL_SERVER_SUFFIX_STR, "-debug"))
    end= strmov(end, "-debug");
#endif
  if (opt_log || opt_slow_log || opt_bin_log)
    strmov(end, "-log");                        // This may slow down system
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the DBXP version number to the MySQL version number. */
  strmov(end, "-DBXP 2.0");
/* END DBXP MODIFICATION */
}

修改词汇结构

现在,让我们添加标识DBXP_SELECT命令所需的标记。打开lex.h文件,将清单 12-4 中粗体显示的代码添加到上下文中的symbols数组中。

清单 12-4 对 lex.h 文件的修改

static SYMBOL symbols[] = {
...
  { "DAY_MINUTE",       SYM(DAY_MINUTE_SYM)},
  { "DAY_SECOND",       SYM(DAY_SECOND_SYM)},
/* BEGIN DBXP MODIFICATION */
/* Reason for MODIFICATION */
/* This section identifies the symbols and values for the DBXP token */
  { "DBXP_SELECT",       SYM(DBXP_SELECT_SYM)},
/* END DBXP MODIFICATION */
  { "DEALLOCATE",       SYM(DEALLOCATE_SYM)},
  { "DEC", SYM(DECIMAL_SYM)},

。。。

添加命令枚举

本节解释如何添加新的DBXP_SELECT命令枚举。修改从在sql_parse.cc文件中向解析器命令开关添加一个新的 case 语句开始。开关使用事例的枚举值。

要添加新案例,您必须添加新的枚举值。这些值在解析器代码中被识别,并存储在lex->sql_command成员变量中。要向词法分析器添加新的枚举值,打开sql_cmd.h文件,并将清单 12-5 中的代码添加到enum_sql_command枚举中。

清单 12-5 添加 DBXP_SELECT 命令枚举

enum enum_sql_command {
...
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures the enumerations for the DBXP command tokens */
  SQLCOM_DBXP_SELECT,
  SQLCOM_DBXP_EXPLAIN_SELECT,
/* END DBXP MODIFICATION */
...

将 DBXP_SELECT 命令添加到 MySQL 解析器

一旦为 case 语句添加了新的枚举值,您还必须向解析器代码(sql_yacc.yy)添加代码,以标识新的DBXP_SELECT语句。这分几个部分完成。您将向解析器添加一个新的令牌,需要对三个地方进行更新。

新的令牌一旦被激活,将允许解析器区分普通的 MySQL SELECT语句和您想要用 DBXP 代码处理的语句。我们对解析器进行编程,以便当令牌存在时,它指示解析器应该将sql_command变量设置为SQLCOM_DBXP_SELECT值,而不是普通的 MySQL select 枚举值(SQLCOM_SELECT)。这种技术允许您向普通的 MySQL 代码和 DBXP 代码发出相同的基本SELECT语句。例如,下面的SELECT语句都完成相同的任务;它们只是被不同地优化了。第一个将指向SQLCOM_SELECT case 语句,而第二个将指向SQLCOM_DBXP_SELECT case 语句。

SELECT * FROM customer;
DBXP_SELECT * FROM customer;

添加新令牌的代码如清单 12-6 所示。在sql_yacc.yy文件中找到令牌列表并添加代码。(名单大致按字母顺序排列)。

清单 12-6 给解析器添加命令符号

%token  DAY_SECOND_SYM
%token  DAY_SYM                       /* SQL-2003-R */
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section defines the tokens for the DBXP commands */
%token  DBXP_SELECT_SYM
/* END DBXP MODIFICATION */
%token  DEALLOCATE_SYM                /* SQL-2003-R */
%token  DECIMAL_NUM

我们还需要将新命令添加到类型定义中。清单 12-7 展示了这一修改。

清单 12-7 向解析器添加命令语法操作

%type <NONE>
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* Add the dbxp_select statement to the NONE type definition. */
        query verb_clause create change select dbxp_select do drop insert replace insert2
/* END DBXP MODIFICATION */

添加令牌的最后一个区域是将以下代码添加到语句部分。清单 12-8 显示了上下文中的修改。

清单 12-8 向选择部分添加命令

statement:
...
        | select
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* Add the dbxp_select statement to the list of statements(commands). */
        | dbxp_select
/* END DBXP MODIFICATION */
        | set
...

我们现在可以添加语句来解析 DBXP_SELECT 命令。清单 12-9 显示了识别DBXP_SELECT命令和处理 select 命令的普通部分所需的解析器代码。注意,解析器识别 select 和 DBXP 符号,然后提供 select 选项、字段列表和FROM子句的其他解析。紧跟在那一行之后的是设置sql_command的代码。请注意,该代码还在原始 select-command 解析器代码前放置了一个竖线(|)。这是解析器语法用来处理命令变体的“or”运算符。要将这一更改添加到解析器中,打开sql_yacc.yy文件并定位select:标签,然后添加代码,如清单 12-9 所示。

清单 12-9 向解析器添加命令语法操作

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the SELECT DBXP statement */

dbxp_select:
         DBXP_SELECT_SYM DBXP_select_options DBXP_select_item_list
                 DBXP_select_from
         {
             LEX *lex= Lex;
           lex->sql_command = SQLCOM_DBXP_SELECT;
         }
         ;

/* END DBXP MODIFICATION */

select:
         select_init
          {
            LEX *lex= Lex;
            lex->sql_command= SQLCOM_SELECT;
          }
        ;

还要注意,代码引用了其他几个标签。清单 12-10 包含了这些操作的代码。第一个是DBXP_select_options,它标识了SELECT命令的有效选项。虽然这与 MySQL select 选项非常相似,但它只提供了两个选项:DISTINCTCOUNT(*)。下一个操作是识别FROM子句中的表的DBXP_select_from代码。它还调用DBXP_where_clause操作来识别WHERE子句。下一个操作是DBXP_select_item_list,它类似于 MySQL 代码。最后,DBXP_where_clause操作标识了WHERE子句中的参数。花些时间浏览这段代码,并按照操作找到它们相关的标签,看看每个标签都做了什么。要将这段代码添加到解析器中,找到select_from:标签并在它上面添加代码。尽管将代码放在哪里并不重要,但是这个位置似乎更符合逻辑,因为它与 MySQL select 操作位于同一区域。清单 12-10 显示了DBXP_SELECT解析器代码的完整源代码。

清单 12-10??【附加操作】为 DBXP_SELECT 命令

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the sub parts of the SELECT DBXP statement */

DBXP_select_options:
   /* empty */
   | DISTINCT
   {
     Select->options|= SELECT_DISTINCT;
   }
  ;

DBXP_select_from:
  FROM join_table_list DBXP_where_clause {};

DBXP_select_item_list:
  /* empty */
  | DBXP_select_item_list ',' select_item
  | select_item
  | '*'
    {
      THD *thd= YYTHD;
      Item *item= new (thd->mem_root)
                    Item_field(&thd->lex->current_select->context,
                               NULL, NULL, "*");
      if (item == NULL)
        MYSQL_YYABORT;
      if (add_item_to_list(thd, item))
        MYSQL_YYABORT;
      (thd->lex->current_select->with_wild)++;
    };

DBXP_where_clause:
  /* empty */  { Select->where= 0; }
   | WHERE expr
    {
      SELECT_LEX *select= Select;
      select->where= $2;
      if ($2)
        $2->top_level_item();
    }
    ;

/* END DBXP MODIFICATION */

...

image 注意一个精明的 yacc 开发者可能会发现代码中的一些地方,可以从原始 SELECT 语句的规则中减少或重用。对于那些对优化这段代码感兴趣的人来说,我把它作为一个练习。

既然您已经对词法分析器进行了更改,那么您必须生成等价的 C 源代码。幸运的是,正常的 cmake/make 步骤会解决这个问题。只需从源代码树的根执行这些命令。

cmake .
make

如果您想在不等待 make 文件处理所有源文件的情况下检查您的代码,您可以使用 Bison 来生成这些文件。打开一个命令窗口,导航到源代码树根下的/sql目录。运行命令:

bison -y -d sql_yacc.yy

这会生成两个新文件:y.tab.cy.tab.h。这些文件分别替换了sql_yacc.ccsql_yacc.h文件。在复制它们之前,请备份原始文件。完成后,将y.tab.c复制到sql_yacc.cc,将y.taqb.h复制到sql_yacc.h

莱克斯和 YACC 是什么,拜辛是谁?

Lex 代表“词法分析器生成器”,用作解析器来识别标记和文字,以及语言的语法。YACC 代表“又一个编译器”,用于识别和处理语言的语义定义。这些工具与 Bison(一个 YACC 兼容的解析器生成器,它从 Lex/YACC 代码生成 C 源代码)一起使用,为创建能够解析和处理语言命令的子系统提供了丰富的机制。事实上,这正是 MySQL 使用这些技术的方式。

如果您现在编译服务器,您可以发出DBXP_SELECT命令,但是什么也不会发生。这是因为您需要将 case 语句添加到sql_parse.cc中的解析器开关中。因为我们还没有一个完整的 DBXP 引擎,所以让我们通过删除 case 语句来使这个练习更有趣一些。清单 12-11 显示了一套完整的脚手架代码,你可以用它来实现DBXP_SELECT命令。在这段代码中,我使用 MySQL 实用程序类来建立一个记录集。代码的第一部分为虚拟表设置字段列表。接下来是将数据值写入网络流的代码行,最后,向客户端发送一个文件结束标记。将数据写入输出流需要调用protocol->prepare_for_resend(),使用protocol->store()存储要发送的数据,然后使用protocol->write()将缓冲区写入流。

清单 12-11 修改解析器命令开关

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    List<Item> field_list;
    /* The protocol class is used to write data to the client. */
    Protocol *protocol= thd->protocol;
    /* Build the field list and send the fields to the client */
    field_list.push_back(new Item_int("Id",(longlong) 1,21));
    field_list.push_back(new Item_empty_string("LastName",40));
    field_list.push_back(new Item_empty_string("FirstName",20));
    field_list.push_back(new Item_empty_string("Gender",2));
    if (protocol->send_ result_set_metadata (&field_list,
                              Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
      DBUG_RETURN(TRUE);
    protocol->prepare_for_resend();
    /* Write some sample data to the buffer and send it with write() */
    protocol->store((longlong)3);
    protocol->store("Flintstone", system_charset_info);
    protocol->store("Fred", system_charset_info);
    protocol->store("M", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);
    protocol->prepare_for_resend();
    protocol->store((longlong)5);
    protocol->store("Rubble", system_charset_info);
    protocol->store("Barnie", system_charset_info);
    protocol->store("M", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);

    protocol->prepare_for_resend();
    protocol->store((longlong)7);
    protocol->store("Flintstone", system_charset_info);
    protocol->store("Wilma", system_charset_info);
    protocol->store("F", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);

    /*
      send_eof() tells the communication mechanism that we're finished
      sending data (end of file).
    */
    my_eof(thd);
    break;
  }
/* END DBXP MODIFICATION */
  case SQLCOM_PREPARE:
...

每当检测到一个DBXP_SELECT命令时,这个存根代码就向客户机返回一个模拟记录集。继续输入这段代码,然后编译并运行测试。

测试 DBXP_SELECT 命令

我们想要运行的测试是发出一个DBXP_SELECT命令,并验证该语句是否被新的存根 case 语句解析和处理。您可以运行之前创建的测试,或者只需在 MySQL 命令行客户端中输入如下 SQL 语句(确保键入 DBXP 部分):

DBXP_SELECT * from no_such_table;

只要是有效的 SQL SELECT语句,在 DBXP 之后键入什么并不重要。清单 12-12 展示了一个你应该期待的输出的例子。

清单 12-12 存根测试结果

mysql> DBXP_SELECT * from no_such_table;
+----+------------+-----------+--------+
| Id | LastName   | FirstName | Gender |
+----+------------+-----------+--------+
| 3  | Flintstone | Fred      | M      |
| 5  | Rubble     | Barnie    | M      |
| 7  | Flintstone | Wilma     | F      |
+----+------------+-----------+--------+
3 rows in set (0.23 sec)

mysql>

添加查询树类

现在您已经有了一个存根DBXP_SELECT命令,您可以开始实现特定于 DBXP 的代码来执行一个SELECT命令。在这一节中,我将向您展示如何添加基本的查询树类并将 MySQL 内部结构转换为查询树。在下一章之前,我不会深入到查询树代码的内部。

添加查询树头文件

添加查询树类需要创建查询树头文件,并在 MySQL 代码中引用它。查询树头文件如清单 12-13 所示。请注意,我将该类命名为Query_tree。这遵循了 MySQL 编码准则,用首字母大写来命名类。花点时间浏览一下标题代码。您会看到这里没有太多代码——只有查询树节点结构和枚举的基础。注意,节点类型、连接条件类型、连接和聚集类型都有枚举。这些枚举允许查询树节点在查询的执行中承担独特的角色。我将在下一章详细解释如何使用它们。

您可以选择任何方式创建文件(或下载)。将其命名为query_tree.h,并将其放在 MySQL 源代码树的/sql目录中。不要担心如何将其添加到项目中;我将在后面的部分向您展示如何做到这一点。

清单 12-13 查询树头文件

/*
  query_tree.h

  DESCRIPTION
    This file contains the Query_tree class declaration. It is responsible for containing the
    internal representation of the query to be executed. It provides methods for
    optimizing and forming and inspecting the query tree. This class is the very
    heart of the DBXP query capability! It also provides the ability to store
    a binary "compiled" form of the query.

  NOTES
    The data structure is a binary tree that can have 0, 1, or 2 children. Only
    Join operations can have 2 children. All other operations have 0 or 1
    children. Each node in the tree is an operation and the links to children
    are the pipeline.

  SEE ALSO
    query_tree.cc
*/
#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include "records.h"

class Query_tree
{
public:
  enum query_node_type          //this enumeration lists the available
  {                              //query node (operations)
    qntUndefined = 0,
    qntRestrict = 1,
    qntProject = 2,
    qntJoin = 3,
    qntSort = 4,
    qntDistinct = 5
  };

  enum join_con_type            //this enumeration lists the available
  {                              //join operations supported
    jcUN = 0,
    jcNA = 1,
    jcON = 2,
    jcUS = 3
  };

  enum type_join                //this enumeration lists the available
  {                              //join types supported.
    jnUNKNOWN      = 0,          //undefined
    jnINNER        = 1,
    jnLEFTOUTER    = 2,
    jnRIGHTOUTER   = 3,
    jnFULLOUTER    = 4,
    jnCROSSPRODUCT = 5,
    jnUNION        = 6,
    jnINTERSECT    = 7
  };

    enum AggregateType          //used to add aggregate functions
    {
        atNONE      = 0,
        atCOUNT     = 1
    };

  /*
    STRUCTURE query_node

    DESCRIPTION
      This this structure contains all of the data for a query node:

      NodeId -- the internal id number for a node
      ParentNodeId -- the internal id for the parent node (used for insert)
      SubQuery -- is this the start of a subquery?
      Child -- is this a Left or Right child of the parent?
      NodeType -- synonymous with operation type
      JoinType -- if a join, this is the join operation
      join_con_type -- if this is a join, this is the "on" condition
      Expressions -- the expressions from the "where" clause for this node
      Join Expressions -- the join expressions from the "join" clause(s)
      Relations[] -- the relations for this operation (at most 2)
      PreemptPipeline -- does the pipeline need to be halted for a sort?
      Fields -- the attributes for the result set of this operation
      Left -- a pointer to the left child node
      Right -- a pointer to the right child node
*/
  struct query_node
  {
    query_node();
    ∼query_node();
    int                 nodeid;
    int                 parent_nodeid;
    bool                sub_query;
    bool                child;
    query_node_type     node_type;
    type_join           join_type;
    join_con_type       join_cond;
    Item                *where_expr;
    Item                *join_expr;
    TABLE_LIST          *relations[4];
    bool                preempt_pipeline;
    List<Item>          *fields;
    query_node          *left;
    query_node          *right;
  };

  query_node *root;              //The ROOT node of the tree

  ∼Query_tree(void);
  void ShowPlan(query_node *QN, bool PrintOnRight);

};

有了查询树头文件,您还需要查询树源文件。源文件必须提供查询树类的构造函数和析构函数方法的代码。清单 12-14 展示了完整的构造函数和析构函数方法。创建query_tree.cc文件,输入这个代码(或者下载)。将这个文件放在 MySQL 源代码树的/sql目录中。在后面的小节中,我将向您展示如何将它添加到项目中。

清单 12-14 查询树类

/*
  query_tree.cc

  DESCRIPTION
    This file contains the Query_tree class. It is responsible for containing the
    internal representation of the query to be executed. It provides methods for
    optimizing and forming and inspecting the query tree. This class is the very
    heart of the DBXP query capability! It also provides the ability to store
    a binary "compiled" form of the query.

  NOTES
    The data structure is a binary tree that can have 0, 1, or 2 children. Only
    Join operations can have 2 children. All other operations have 0 or 1
    children. Each node in the tree is an operation and the links to children
    are the pipeline.

  SEE ALSO
    query_tree.h
*/
#include "query_tree.h"

Query_tree::query_node::query_node()
{
  where_expr = NULL;
  join_expr = NULL;
  child = false;
  join_cond = Query_tree::jcUN;
  join_type = Query_tree::jnUNKNOWN;
  left = NULL;
  right = NULL;
  nodeid = -1;
  node_type = Query_tree::qntUndefined;
  sub_query = false;
  parent_nodeid = -1;
}

Query_tree::query_node::∼query_node()
{
  if(left)
    delete left;
  if(right)
    delete right;
}

Query_tree::∼Query_tree(void)
{
  if(root)
    delete root;
}

从 MySQL 结构构建查询树

接下来我们需要的是执行从 MySQL 内部结构到查询树的转换的代码。让我们使用一个助手源文件,而不是将代码添加到sql_parse.cc文件中。事实上,case 语句(在sql_parse.cc文件中)表示的许多命令都是这样完成的。创建一个名为sql_dbxp_parse.cc的新文件。在名为build_query_tree的文件中创建一个新函数,如清单 12-15 所示。代码是一个基本的转换方法。输入代码时,花点时间浏览一下代码(或者下载并复制粘贴到文件中)。

***清单 12-15 。***DBXP 解析器辅助文件

/*
  sql_dbxp_parse.cc

  DESCRIPTION
    This file contains methods to execute the DBXP_SELECT query statements.

  SEE ALSO
    query_tree.cc
*/
#include "query_tree.h"

/*
  Build Query Tree

  SYNOPSIS
    build_query_tree()
    THD *thd            IN the current thread
    LEX *lex            IN the pointer to the current parsed structure
    TABLE_LIST *tables  IN the list of tables identified in the query

  DESCRIPTION
    This method returns a converted MySQL internal representation (IR) of a
    query as a query_tree.

  RETURN VALUE
    Success = Query_tree * -- the root of the new query tree.
    Failed = NULL
*/
Query_tree *build_query_tree(THD *thd, LEX *lex, TABLE_LIST *tables)
{
  DBUG_ENTER("build_query_tree");
  Query_tree *qt = new Query_tree();
  Query_tree::query_node *qn = new Query_tree::query_node();
  TABLE_LIST *table;
  int i = 0;
  int num_tables = 0;

  /* Create a new restrict node. */
  qn->parent_nodeid = -1;
  qn->child = false;
  qn->join_type = (Query_tree::type_join) 0;
  qn->nodeid = 0;
  qn->node_type = (Query_tree::query_node_type) 2;
  qn->left = 0;
  qn->right = 0;

  /* Get the tables (relations) */
  i = 0;
  for(table = tables; table; table = table->next_local)
  {
    num_tables++;
    qn->relations[i] = table;
    i++;
  }

  /* Populate attributes */
  qn->fields = &lex->select_lex.item_list;
  /* Process joins */
  if (num_tables > 0)  //indicates more than 1 table processed
    for(table = tables; table; table = table->next_local)
      if (((Item *)table->join_cond() != 0) && (qn->join_expr == 0))
        qn->join_expr = (Item *)table->join_cond();
  qn->where_expr = lex->select_lex.where;
  qt->root = qn;
  DBUG_RETURN(qt);
}

注意,build_query_tree代码从创建一个新的查询节点开始,标识查询中使用的表,填充字段列表,并捕获joinwhere表达式。这些都是执行最基本的查询所需的基本项目。

阻止查询树执行

现在让我们考虑用代码创建一个查询树需要什么。创建一个名为DBXP_select_command的新函数,并复制清单 12-16 中的代码。将这个函数放在sql_dbxp_parse.cc文件中。该函数将从sql_parse.cc中的 case 语句中调用。

清单 12-16?? 处理 DBXP_SELECT 命令

/*
  Perform Select Command

  SYNOPSIS
    DBXP_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the SELECT command using the query tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_select_command");
  Query_tree *qt = build_query_tree(thd, thd->lex,
                                    (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  field_list.push_back(new Item_empty_string("Database Experiment Project (DBXP)",40));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();
  protocol->store("Query tree was built.", system_charset_info);
  if (protocol->write())
    DBUG_RETURN(TRUE);
  my_eof(thd);
  DBUG_RETURN(0);
}

这段代码首先调用转换函数(build_query_tree),然后创建一个存根结果集。这一次,我创建了一个只有一列一行的记录集,用于向客户机传递查询树转换完成的消息。虽然这段代码不是很有趣,但它是一个占位符,让您可以在查询树上进行更多的实验(参见本章末尾的练习)。将sql_dbxp_parse.cc文件放在 MySQL 源代码树的/sql目录中。

重新访问 DBXP_SELECT 命令

打开sql_parse.cc文件,为DBXP_select_command函数添加一个函数声明,将声明放在短语mysql_execute_command附近。清单 12-17 显示了DBXP_select_command函数的完整函数头。在注释块上方输入此代码,如下所示。

清单 12-17 修改解析器命令代码

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SELECT DBXP command. */
int DBXP_select_command(THD *thd);
int DBXP_explain_select_command(THD *thd);
/* END DBXP MODIFICATION */

您现在可以更改 case 语句中的代码(也称为解析器命令开关)来调用新的DBXP_select_command函数。清单 12-18 显示了调用这个函数的完整代码。注意,我们需要传入的唯一参数是当前线程(thd)。MySQL 内部查询结构和查询的所有其他元数据都是通过线程指针引用的。如您所见,这种技术大大简化了 case 语句。这也有助于模块化 DBXP 代码,使其更容易维护和修改您的实验。

清单 12-18 修改解析命令开关(sql_parse.cc)

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    res = DBXP_select_command(thd);
    if (res)
      goto error;
    break;
  }
/* END DBXP MODIFICATION */
  case SQLCOM_PREPARE:
  {
...

在编译服务器之前,您需要用将新的源代码文件(query_tree.hquery_tree.ccsql_dbxp_parse.cc)添加到项目(make)文件中。

将文件添加到 CMakeLists.txt 文件中

添加项目文件需要从源代码树的根目录修改/sql目录中的CMakeLists.txt文件。打开文件并找到 SQL_SHARED_SOURCES _标签。将源代码文件添加到服务器编译的源代码列表中(mysqld)。清单 12-19 显示了定义的开始和添加的项目文件。

清单 12-19?? 对 CMakeLists.txt 文件的修改

SET(SQL_SHARED_SOURCES
  abstract_query_plan.cc
  datadict.cc
...
  sql_dbxp_parse.cc
  query_tree.cc
...

image 注意修改 cmake 文件时,格式化列表时一定要使用空格。

测试查询树

一旦服务器编译无误,就可以使用 SQL 语句对其进行测试。与上一个测试不同,您应该输入一个引用现有对象的有效 SQL 命令。您可以如前一节所述运行测试(参见清单 12-20 ),或者在 MySQL 命令行客户端中输入以下命令:

DBXP_SELECT * from customer;

***清单 12-20 。***DBXP _ SELECT 测试的结果

mysql> DBXP_SELECT * FROM customer;

+--------------------------------------------------+
| Database Experiment Project (DBXP)               |
+--------------------------------------------------+
| Query tree was built.                            |
+--------------------------------------------------+
1 row in set (0.00 sec)

mysql>

您已经取消了DBXP_SELECT操作并构建了一个查询树,但是这并不有趣。如果我们能看到查询的样子会怎么样呢?我们将创建一个类似于EXPLAIN命令的函数,只是我们将创建一个树形查询的图形表示 6 ,而不是查询的信息列表。

显示查询树的详细信息

添加新命令需要在sql_parse.cc的解析器开关中为新的 case 语句添加新的枚举,并添加解析器代码来标识新命令。您还必须向sql_DBXP_parse.cc文件添加执行新命令的代码。虽然创建并向解析器添加解释查询树的EXPLAIN命令听起来很复杂,但是EXPLAIN SELECT命令在 MySQL 中是可用的,因此我们可以复制大量代码并重用其中的大部分。

向 MySQL 解析器添加 EXPLAIN DBXP_SELECT 命令

要向解析器添加新的枚举,打开sql_lex.h文件,在SQLCOM_DBXP_SELECT枚举的代码后面添加一个名为SQLCOM_DBXP_EXPLAIN_SELECT的枚举。清单 12-21 显示了完整的代码变更。一旦添加了代码,就可以像前面描述的那样重新生成词法哈希。

**清单 12-21添加解释枚举

/* A Oracle compatible synonym for show */
describe:
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the EXPLAIN (DESCRIBE) DBXP statements */

  describe_command DBXP_SELECT_SYM DBXP_select_options DBXP_select_item_list
                 DBXP_select_from
  {
          LEX *lex= Lex;
          lex->sql_command = SQLCOM_DBXP_EXPLAIN_SELECT;
          lex->select_lex.db= 0;
          lex->verbose= 0;
  }

/* END DBXP MODIFICATION */
...

注意,在这段代码中,解析器识别了一个EXPLAIN DBXP_SELECT命令。事实上,它调用了许多与DBXP_SELECT解析器代码相同的操作。唯一的区别是这段代码将sql_command设置为新的枚举(SQLCOM_DBXP_EXPLAIN_SELECT)。

sql_parse.cc中解析器开关语句的修改需要为sql_DBXP_parse.cc中执行EXPLAIN命令的代码添加函数声明。打开sql_parse.cc文件,为EXPLAIN函数添加函数声明。说出函数的名字DBXP_explain_select_command(你开始看到一个模式了吗?).将它添加到与DBXP_select_command函数声明相同的位置。清单 12-22 显示了两个 DBXP 命令的完整代码。

**清单 12-22修改解析器命令代码

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
int DBXP_select_command(THD *thd);
int DBXP_explain_select_command(THD *thd);
/* END DBXP MODIFICATION */

还需要为 DBXP explain 命令添加新的 case 语句。这些语句类似于DBXP_SELECT命令的 case 语句。清单 12-23 显示了添加的新 case 语句。

**清单 12-23对解析器切换语句的修改

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    res = DBXP_select_command(thd);
    if (res)
      goto error;
    break;
  }
  case SQLCOM_DBXP_EXPLAIN_SELECT:
  {
    res = DBXP_explain_select_command(thd);
    if (res)
      goto error;
    break;
  }
/* END DBXP MODIFICATION */

创建 show_plan 函数

EXPLAIN DBXP_SELECT命令将查询路径显示为在字符文本范围内打印出来的树。在sql_DBXP_parse.cc文件中名为show_plan的函数中执行EXPLAIN代码。一个名为write_printf的辅助函数用于使show_plan代码更容易阅读。清单 12-2412-25 显示了这两种方法的完整代码。

清单 12-24 增加一个函数来捕获协议存储和写语句

/*
  Write to vio with printf.

  SYNOPSIS
    write_printf()
    Protocol *p     IN the Protocol class
    char *first     IN the first string to write
    char *last      IN the last string to write

  DESCRIPTION
    This method writes to the vio routines printing the strings passed.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int write_printf(Protocol *p, char *first, const char *last)
{
  char *str = new char[1024];

  DBUG_ENTER("write_printf");
  strcpy(str, first);
  strcat(str, last);
  p->prepare_for_resend();
  p->store(str, system_charset_info);
  p->write();
  delete str;
  DBUG_RETURN(0);
}

请注意,write_printf代码调用了protocol->storeprotocol->write函数向客户端写入一行绘图。我将让您探索清单 12-25 中的源代码,看看它是如何工作的。在下一节中,我将向您展示一个代码执行的示例。该代码使用后序遍历从查询树的根开始生成查询计划。将这些方法添加到sql_DBXP_parse.cc文件中。

清单 12-25?? 展示 _ 计划源代码

/*
  Show Query Plan

  SYNOPSIS
    show_plan()
    Protocol *p         IN the MySQL protocol class
    query_node *Root    IN the root node of the query tree
    query_node *qn      IN the starting node to be operated on.
    bool print_on_right IN indicates the printing should tab to the right
                           of the display.

  DESCRIPTION
    This method prints the execute plan to the client via the protocol class

  WARNING
    This is a RECURSIVE method!
    Uses postorder traversal to draw the quey plan

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int show_plan(Protocol *p, Query_tree::query_node *root,
              Query_tree::query_node *qn, bool print_on_right)
{
  DBUG_ENTER("show_plan");

  /* spacer is used to fill white space in the output */
  char *spacer = (char *)my_malloc(80, MYF(MY_ZEROFILL | MY_WME));
  char *tblname = (char *)my_malloc(256, MYF(MY_ZEROFILL | MY_WME));
  int i = 0;

  if(qn != 0)
  {
    show_plan(p, root, qn->left, print_on_right);
    show_plan(p, root, qn->right, true);
    /* draw incoming arrows */
    if(print_on_right)
      strcpy(spacer, "          |               ");
    else
      strcpy(spacer, "     ");

    /* Write out the name of the database and table */
    if((qn->left == NULL) && (qn->right == NULL))
    {
      /*
         If this is a join, it has 2 children, so we need to write
         the children nodes feeding the join node. Spaces are used
         to place the tables side-by-side.
      */
      if(qn->node_type == Query_tree::qntJoin)
      {
        strcpy(tblname, spacer);
        strcat(tblname, qn->relations[0]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[0]->table_name);
        if(strlen(tblname) < 15)
          strcat(tblname, "               ");
        else
          strcat(tblname, "          ");
        strcat(tblname, qn->relations[1]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[1]->table_name);
        write_printf(p, tblname, "");
        write_printf(p, spacer, "     |                              |");
        write_printf(p, spacer, "     |   ----------------------------");
        write_printf(p, spacer, "     |   |");
        write_printf(p, spacer, "     V   V");
      }
      else
              strcpy(tblname, spacer);
        strcat(tblname, qn->relations[0]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[0]->table_name);
        write_printf(p, tblname, "");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     V");
      }
    }
    else if((qn->left != 0) && (qn->right != 0))
    {
      write_printf(p, spacer, "     |                              |");
      write_printf(p, spacer, "     |   ----------------------------");
      write_printf(p, spacer, "     |   |");
      write_printf(p, spacer, "     V   V");
    }
    else if((qn->left != 0) && (qn->right == 0))
    {
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     V");
    }
    else if(qn->right != 0)
    {
    }
    write_printf(p, spacer, "-------------------");

    /* Write out the node type */
    switch(qn->node_type)
    {
    case Query_tree::qntProject:
      {
        write_printf(p, spacer, "|     PROJECT     |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntRestrict:
      {
        write_printf(p, spacer, "|    RESTRICT     |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntJoin:
      {
        write_printf(p, spacer, "|      JOIN       |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntDistinct:
      {
        write_printf(p, spacer, "|     DISTINCT    |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    default:
      {        write_printf(p, spacer, "|      UNDEF      |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    }
    write_printf(p, spacer, "| Access Method:  |");
    write_printf(p, spacer, "|    iterator     |");
    write_printf(p, spacer, "-------------------");
    if(qn == root)
    {
      write_printf(p, spacer, "        |");
      write_printf(p, spacer, "        |");
      write_printf(p, spacer, "        V");
      write_printf(p, spacer, "    Result Set");
    }
  }
  my_free(spacer);
  my_free(tblname);
  DBUG_RETURN(0);
}

您需要做的最后一件事是添加代码来执行 DBXP EXPLAIN命令,调用show_plan()方法,并将结果返回给客户端。清单 12-26 显示了这个函数的完整代码。注意,在这个函数中,我构建了查询树,然后使用名为“执行路径”的单字符字符串列创建了一个字段列表,然后调用show_plan将计划写入客户端。

***清单 12-26 。***DBXP 解释命令源代码

/*
  Perform EXPLAIN command.

  SYNOPSIS
    DBXP_explain_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the EXPLAIN SELECT command.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_explain_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_explain_select_command");
  Query_tree *qt = build_query_tree(thd, thd->lex,
                                    (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  field_list.push_back(new Item_empty_string("Execution Path",NAME_LEN));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();
  show_plan(protocol, qt->root, qt->root, false);
  my_eof(thd);
  DBUG_RETURN(0);
}

现在,让我们编译服务器并用测试文件试一试。

测试 DBXP 解释命令

与前面的测试一样,您可以使用前面章节中描述的测试,或者在 MySQL 命令行客户端中输入一个有效的 SQL 命令。清单 12-27 展示了一个查询执行路径的例子。在这一点上应该说明的是,查询没有经过优化,将显示为单个节点。一旦你添加了优化器(见第 11 章),查询执行路径将反映输入的查询语句的适当执行。

***清单 12-27 。***DBXP 解释测试的结果

mysql> EXPLAIN DBXP_SELECT * FROM customer;

+--------------------------+
| Execution Path           |
+--------------------------+
|      test.customer       |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |     PROJECT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|              |           |
|              |           |
|              V           |
|          Result Set      |
+--------------------------+
15 rows in set (0.00 sec)

mysql>

这比枯燥的罗列事实有趣得多。在 DBXP 项目的这个阶段添加EXPLAIN命令允许您观察和诊断优化器是如何形成查询树的。当你开始自己的实验时,你会发现这很有帮助。

如果到目前为止您还没有这样做,那么您应该运行完整的测试来测试本章中介绍的代码的所有三个部分。

摘要

我在本章中介绍了一些更复杂的数据库内部技术。您了解了查询是如何在 MySQL 服务器内部表示的,因为它们是通过“大开关”进行解析和处理的。更重要的是,您发现了如何使用 MySQL 通过查询树类进行自己的数据库内部实验。了解这些技术会让您更好地理解 MySQL 内部组件是如何构建的。

在下一章中,我将通过一个查询树优化策略的示例实现向您展示更多关于内部查询表示的内容。如果您想知道为关系数据库系统构建一个优化器需要什么,下一章将向您展示一个使用查询树类的启发式查询优化器的例子。

练习

下面的列表代表了您可能希望作为实验(或作为课堂作业)来探索关系数据库技术的活动类型。

  1. 图 12-1 中的查询暴露了一个表格中的设计缺陷。这是什么?该缺陷是否违反了任何标准形式?如果有,是哪一个?
  2. 浏览TABLE结构并更改DBXP_SELECT存根以返回关于表及其字段的信息。
  3. 更改EXPLAIN DBXP_SELECT命令,产生类似于 MySQL EXPLAIN SELECT命令的输出。
  4. 修改build_query_tree函数来识别和处理LIMIT子句。
  5. 如何改变查询树query_node的结构以容纳HAVINGGROUP BYORDER子句?

奇怪的是,很少有文献对符号的选择做出解释。传统上,θ代表θ连接,image代表自然连接,但大多数文本互换了这些概念,导致所有连接都使用一个或另一个符号(有时两个都用)来表示。

2 你能发现经常被滥用的特征吗?提示:学期属性的定义域是什么?对列中的数据进行编码违反了哪条规则?

3 有些人会说这不应该,因为 MySQL 的内部结构是用来为优化器组织数据的。另一方面,查询树被设计为就地优化和执行。

4 虽然许多关于查询处理主题的文章对如何区分每个过程有不同的看法,但他们都同意必须出现某些不同的过程步骤。

5 一种被克努特和其他算法大师极力劝阻的做法。

6 无论如何,像命令行界面那样图形化是允许的。

十三、查询优化

第 12 章中显示的查询树类是为 DBXP 构建实验性查询优化和执行引擎的起点。在本章中,我将向您展示如何将优化器添加到查询树类中。我首先解释优化器中使用的试探法(或规则)的基本原理,然后开始编写代码。因为一些函数的代码很长,所以本章中的例子是摘录的。如果您正在编写示例代码,请下载本章的源代码,而不是从头开始输入代码。

查询优化器的类型

第一批查询优化器是为早期的数据库系统设计的,比如 System R 1 和 INGRES 。 2 这些优化器是为关系模型的特定实现而开发的,它们经受住了时间的考验,是如何实现优化器的例证。许多商业上可用的数据库系统都是基于这些工作。从那时起,优化器已经被创建用于关系模型的扩展,以包括面向对象和分布式数据库系统。

一个例子是火山优化器 ,它使用动态编程算法 3 在面向对象的数据库系统中生成基于成本优化的查询计划。另一个例子是关于如何在异构数据库系统中执行优化(类似于分布式系统,但是没有公共共享的组织概念)。在这些环境中,可以使用统计方法来推导优化策略。

查询优化需求产生独特需求的另一个领域是内存数据库系统。这些系统被设计成包含整个系统和计算机辅助存储器(即磁盘)中的所有数据。虽然这些应用中的大多数是作为嵌入式系统实现的,但是一些由系统集合组成的大型分布式系统使用内存数据库来加速内存数据库系统中的信息检索优化,这需要高效的算法,因为与处理查询本身的需要相比,优化检索的需要是微不足道的。4

所有对传统和非传统优化的研究都是基于系统优化器的领域。System R optimizer 是一个基于成本的优化器,它使用收集到的关于数据库和数据的信息,或者统计数据,来形成对查询如何执行的成本估计。此外,将查询的内部表示安排成不同但等价的内部表示(它们生成相同的答案)的概念提供了一种存储替代形式的机制。这些可选形式中的每一种都被称为一个查询计划 。成本最低的计划被选为执行查询的最有效方式。

系统 R 工作中确定的一个关键特性是选择性的概念——基于包含属性及其值的引用的表达式的评估来预测结果。选择性是决定合取选择中的简单表达式测试顺序的核心。最具选择性的表达式(即具有最小选择性的表达式)将检索最少数量的元组(行)。因此,该表达式应该是查询中第一个操作的基础。合取选择可以被认为是“交集”条件。相反,析取选择是“联合”条件。在析取条件中,顺序没有影响。

某些查询优化器,比如 System R,并不处理所有可能的连接顺序。相反,它们将搜索限制在某些类型的连接顺序上,已知这些连接顺序会产生更有效的执行。例如,可以对多路连接进行排序,以便首先执行生成最少可能结果的条件。类似地,System R 优化器只考虑那些连接顺序,其中每个连接的右操作数是初始关系之一。这种连接顺序称为左深连接顺序。左深连接顺序对于流水线执行特别方便,因为右操作数通常是一个关系(相对于中间关系),因此每个连接只有一个输入被流水线化。管道的使用是数据库实验项目的优化器和执行引擎的一个关键元素。

基于成本的优化器

基于成本的优化器通过使用等价规则从给定的查询生成一系列查询评估计划,并根据收集的关于执行查询所需的关系和操作的度量(或统计)选择成本最低的计划。对于一个复杂的查询,许多等价的计划是可能的。

基于成本的优化的目标是利用从过去的查询中收集的索引和统计信息来安排查询执行和表访问。微软 SQL Server 和甲骨文等系统使用基于成本的优化器。

数据库系统中负责获取和处理统计数据(以及许多其他实用功能)的部分被称为数据库目录 。该目录维护关于引用关系和每个引用关系上可用的访问路径的统计信息。这些将在后面的访问路径选择中使用,以选择最有效的计划(成本最低)。例如,System R 为以下每个表维护统计信息:

  • 每个关系的基数

  • 段中包含每个关系的元组的页数

  • 段中包含关系元组(阻塞因子或填充)的数据页的比例

  • 对于每个索引:

  • 每个索引中不同键的数量

  • 每个索引中的页数

这些统计数据来自系统内的几个来源。加载关系和创建索引时会创建统计信息。然后由用户命令 5 定期更新,该命令可由任何用户运行。System R 不会实时更新这些统计数据,因为这会在系统编目中产生额外的数据库操作和锁定瓶颈。统计数据的动态更新倾向于对修改关系内容的访问进行串行化,从而限制了系统在多用户环境中处理同时查询的能力。

在基于成本的优化中使用统计数据并不复杂。大多数受访的数据库专业人员似乎认为统计数据的收集和应用是查询优化的一个复杂而重要的元素。尽管基于成本的查询优化甚至混合优化方案使用成本和/或排名的统计,但优化方案既不复杂也不关键。例如,在属性中均匀分布值的概念。这个概念本身就证明了统计学应用的不精确性。统计计算在本质上很大程度上是分类的,它们不是为了生成精确的值而设计的。它们只是帮助确定一个查询执行计划是否通常比另一个更昂贵。

属性值的频率分布是预测查询结果大小的常用方法。通过形成属性的可能(或实际 6 )值的分布,数据库系统可以使用该分布,通过预测计划必须处理的元组(或行)的数量来计算给定查询计划的成本。然而,现代数据库系统只处理单个属性的频率分布,因为考虑所有可能的属性组合是非常昂贵的。这实质上对应于所谓的属性值独立性假设,尽管这很少是真的,但几乎所有的关系数据库系统都采用这种假设。

收集分布数据需要不断更新统计数据或对数据进行预测分析。另一个策略是使用均匀分布,其中属性值的分布被假设为对于所有不同的值都是相等的。例如,给定 5000 个元组和给定属性的 50 个可能值,均匀分布假设每个值表示 100 次。这种情况很少发生,而且通常是不正确的。尽管没有任何统计数据,但在许多情况下,这仍然是现实的合理近似值。

在最坏的情况下,动态编程的内存需求和运行时间随着查询大小(即连接的数量)呈索引增长,因为在每个步骤中生成的所有可行的部分计划都必须存储起来,以便在下一个步骤中使用。事实上,许多现代系统对可以提交的查询的大小有限制(通常在 15 个连接左右),因为对于较大的查询,优化器会由于非常高的内存需求而崩溃。然而,实践中看到的大多数查询包含的连接少于 10 个,并且该算法已被证明在这种情况下是有效的。它被认为是查询优化搜索策略的标准。收集的关于表(或关系)中的行(或元组)的统计数据包括:

  • 表中元组的数量
  • 包含行的块数(块数)
  • 以字节为单位的行的大小
  • 每个属性(或列)的不同值的数量
  • 每个属性的选择基数(有时表示为均匀分布)
  • 索引内部节点的扇出(产生子树的子节点数)
  • 索引的 B 树的高度
  • 索引叶级别的块数

将操作的最终结果写回磁盘的开销被忽略。不管使用什么样的查询评估计划,这个成本都不会改变;因此,不将其包括在计算中并不影响计划的选择。

今天大多数数据库系统使用一种形式的动态编程来生成所有可能的查询计划。虽然动态编程为成本优化提供了良好的性能,但它是一种复杂的算法,对于更复杂的查询可能需要更多的资源。虽然大多数数据库系统不会遇到这些类型的查询,但分布式数据库系统和高性能计算领域的研究人员已经探索了动态编程技术的替代方案和变体。Kossmann 和 Stocker 最近的研究表明,我们开始看到传统查询优化方法的局限性。 7 我们需要的是更高效的优化技术,这些技术能够生成遵循良好实践的执行计划,而不是穷尽式的探索。换句话说,我们需要在各种通用环境中表现良好的优化器,也需要在独特的数据库环境中表现良好的优化器。

启发式优化器

启发式优化的目标是应用确保查询执行良好实践的规则。使用启发式优化器的系统包括 INGRES 和各种学术变体。大多数系统通常使用启发式优化作为避免真正糟糕的计划的手段,而不是作为优化的主要手段。

启发式优化器在选择替代实现之前,使用关于如何将查询塑造成最佳形式的规则。试探法或规则的应用可以消除可能低效的查询。使用试探法作为形成查询计划的基础确保了查询计划在评估之前最有可能(但不总是)得到优化。这种试探法包括:

  • 尽可能早地执行选择操作。通常最好在投影之前执行选择,因为这样可以减少沿树向上发送的元组数量。
  • 尽早进行预测。
  • 确定哪些选择操作和连接操作产生最小的结果集,并首先使用这些操作(最左边的深度)。
  • 用连接操作替换笛卡尔积。
  • 在树中尽可能向下移动投影属性。
  • 识别其操作可以被流水线化的子树。

启发式优化器不是新技术。研究人员已经为各种专门目的创建了基于规则的优化器。一个例子是基于 Prairie 规则的查询优化器。这个基于规则的优化器允许根据给定的语言符号创建规则。使用控制优化器执行方式的规则来处理查询。在这种情况下,Prairie 优化器主要是一个基于成本的优化器,它使用规则来调整优化器。

除了 Prairie 和 INGRES 等早期原语之外,没有任何商业数据库系统实现纯粹的启发式优化器。对于那些具有启发式或基于规则的优化步骤的优化,它通常作为经典的基于成本的优化器的附加或预处理器来实现,或者作为优化中的预处理步骤来实现。

语义优化器

语义优化的目标是形成查询执行计划,该计划使用数据库的语义或拓扑以及其中的关系和索引来形成查询,以确保在给定数据库中执行查询的最佳实践。语义查询优化使用模式的知识(例如,完整性约束)来将查询转换成可以比原始版本更有效地回答的形式。

虽然还没有在商业数据库系统中作为主要的优化技术来实现,但是语义优化目前是大量研究的焦点。语义优化的前提是优化器对实际的数据库模式有基本的了解。当提交一个查询时,优化器使用它对系统约束的了解来简化或忽略一个特定的查询,如果它保证返回一个空的结果集的话。这项技术很有希望在未来的关系数据库系统中进一步提高查询处理效率。

参数优化器

Ioannidis 在他关于参数化查询优化的工作中描述了一种查询优化方法,该方法将启发式方法的应用与基于成本的优化相结合。生成的查询优化器提供了一种方法来生成一个较小的有效查询计划集,根据该计划可以估计成本,从而可以执行该计划集中成本最低的计划。 8 查询计划生成是使用一种随机算法创建的,称为 sipR。这允许利用参数查询优化的系统选择可以包括参数变化的不确定性(例如缓冲区大小)的查询计划,以选择在运行中或从存储中形成的最佳计划。

有趣的是,在他的工作中,Ioannidis 认为可能不需要使用动态编程算法,因此可以避免使用这些技术的开销。此外,他发现,在应用动态编程算法进行查询优化之前,使用试探法来修剪或调整查询的数据库系统通常是 System R 的原始算法的增强版本。Ioannidis 表明,对于小型查询(大约多达 10 个连接),动态编程优于随机化算法,而对于大型查询,情况正好相反。

启发式优化再探

启发式优化过程 使用一组已定义的规则来保证良好的执行计划。因此,启发式优化器产生良好计划的有效性仅仅基于其规则的有效性和完整性。

以下段落描述了用于创建 DBXP 查询优化器的规则。尽管这些规则非常基本,但是当它们应用于典型的查询时,最终的执行是接近最优的,具有快速的性能和准确的结果。

最初使用一些基本策略来构建查询树。具体来说,所有的执行都发生在查询树节点中。限制和投影是在分支上处理的,不会生成中间关系。连接总是作为两条路径的交集来处理。多路连接将使用一系列双向连接来形成。以下规则代表了形成一组启发式规则以生成良好的执行计划的最佳实践。DBXP 优化器被设计来应用这些规则,以便将查询树转换成确保高效执行的形式。 9

  1. 拆分包含项目的任何节点并连接或限制并连接。这一步是必要的,因为一些查询在WHERE子句 10 中指定了连接条件,从而可以“欺骗”优化器形成连接节点,这些节点的表达式部分不属于连接条件。
  2. 把所有的限制都推到树叶上。表达式根据它们各自的关系被分组到单独的查询树节点中。虽然有些复杂的表达式不能简化,但大多数可以很容易地简化为一个单一的关系式。通过在叶子上设置限制,减少了必须沿树向上传递的结果元组的数量。
  3. 将所有投影放置在树的最低点。投影应该放在高于限制的节点中,并且它们将通过从结果元组中消除不需要的属性来进一步减少通过树传递的数据量。应当注意,可以修改投影以包括操作所需的属性,例如驻留在投影查询树节点的父代中的连接。
  4. 将所有连接放在 join 子句中包含的关系的投影或限制的交叉点上。 11 这确保了对最昂贵的操作——连接——评估最少量的元组。对来自子节点的结果元组进行排序的中间查询树节点可能是必要的。这些中间节点称为实用操作,可以根据连接的类型对元组进行排序或分组,它们可以大大提高连接的性能。

image 注意可以使用其他启发式方法。前面的列表包含那些产生最大性能增益的。

Lee、Shih 和 Chen 对将选择和限制推下树的做法给出了一个有趣的反驳。 12 他们提出,在某些条件下,有些选择和投射可能比联结的代价更大。他们的论点提出了一种基于图论的查询优化器,可以更准确地预测复杂选择和投影情况下的查询优化。然而,一般情况下,可以使用我列出的规则为大多数查询构建“高效”的执行计划。

DBXP 查询优化器

尽管这些规则为形成最佳查询树提供了一整套操作,但它们并没有解决平衡多路连接或应用索引的问题。这些步骤被认为是基于成本的优化。由于这个原因,大多数启发式优化器被实现为两阶段优化,第一阶段生成优化的查询路径,第二阶段应用成本优化策略。

image 注意 DBXP 优化器是作为两遍操作实现的。第一个操作使用启发式算法重新排列树以供执行。第二遍遍历树,改变与被操作的属性上可用的索引有关系的节点的访问方法。我将成本优化过程的实现留给读者作为练习。

为启发式优化器创建全面的测试需要编写涵盖优化器中所有可能路径的 SQL 语句。本质上,您需要创建一个测试来测试所有可能的查询,包括有效的和无效的(无效的查询通常在 SQL 解析器代码中被捕获)。然而,实现启发式优化器只是 DBXP 引擎的第二部分。在前一章中,我们创建了基本的查询树内部表示,并删除了执行方法。在本章中,我们将创建优化器,但不能执行查询。您可以继续使用存根执行来测试优化器,但是您可以重用前一章中的代码来显示查询计划而不是查询结果,而不是显示查询结果。

记住这一点,让我们设计几个基本的查询来测试优化器,以显示它正在处理这些查询。我们将在下一章关注查询的执行。清单 13-1 展示了一个测试查询优化器的示例。

清单 13-1。 示例 DBXP 查询优化器测试(Ch13.test)

#
# Sample test to test the DBXP_SELECT optimizer
#

# Test 1:
DBXP_SELECT * FROM staff;

# Test 2:
DBXP_SELECT id FROM staff WHERE staff.id = '123456789';

# Test 3:
DBXP_SELECT id, dir_name FROM staff, directorate
WHERE staff.dno = directorate.dnumber;

# Test 4:
DBXP_SELECT * FROM staff JOIN tasking ON staff.id = tasking.id
WHERE staff.id = '123456789';

image 提示这些例子中使用的数据库包含在附录中。

您可以使用这个测试作为指南,并添加您自己的命令来探索新代码。有关如何使用 MySQL 测试套件创建和运行该测试的更多详细信息,请参考第 4 章

清除 DBXP_SELECT 命令

由于没有查询执行功能,查询命令可以优化,但不能执行。show plan 机制(EXPLAIN命令)可以作为演示优化器的一种手段。要添加这个功能,您可以打开sql_dbxp_parse.cc文件并改变DBXP_select_command()方法,如清单 13-2 所示。

清单 13-2。 对查询优化器进行测试

int DBXP_explain_select_command(THD *thd);

/*
  Perform DBXP_SELECT Command

  SYNOPSIS
    DBXP_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the SELECT command using the query tree and optimizer.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_select_command");
  DBXP_explain_select_command(thd);
  DBUG_RETURN(0);
}

这些变化改变了调用EXPLAIN命令代码的代码,而不是执行查询。这允许测试返回有效的结果集(查询计划),这样我们就可以在没有查询执行部分的情况下测试优化器。

image 注意我在DBXP_select_command()方法上面用了一个函数声明。这允许代码在不使用头文件的情况下向前调用DBXP_explain_select_command()方法。

还有一个对DBXP_explain_select_command()方法的必要改变。您需要添加对新优化方法的调用。这包括heuristic_optimization()cost_optimization()方法。我将在接下来的小节中更详细地讨论启发式优化。清单 13-3 显示了对EXPLAIN代码的修改。

清单 13-3。 修改解释命令代码

/*
  Perform EXPLAIN command.

  SYNOPSIS
    DBXP_explain_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the EXPLAIN SELECT command.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_explain_select_command(THD *thd)
{
  bool res= 0;

  DBUG_ENTER("DBXP_explain_select_command");

  /* Prepare the tables (check access, locks) */
  res = check_table_access(thd, SELECT_ACL, thd->lex->query_tables, 0, 1, 1);
  if (res)
    DBUG_RETURN(1);
  res = open_and_lock_tables(thd, thd->lex->query_tables, 0,
                             MYSQL_LOCK_IGNORE_TIMEOUT);
  if (res)
    DBUG_RETURN(1);

  /* Create the query tree and optimize it */
  Query_tree *qt = build_query_tree(thd, thd->lex,
           (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  qt->heuristic_optimization();
  qt->cost_optimization();

  /* create a field list for returning the query plan */
  List<Item> field_list;

  /* use the protocol class to communicate to client */
  Protocol *protocol= thd->protocol;

  /* write the field to the client */
  field_list.push_back(new Item_empty_string("Execution Path",NAME_LEN));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();

  /* generate the query plan and send it to client */
  show_plan(protocol, qt->root, qt->root, false);
  my_eof(thd); /* end of file tells client no more data is coming */

  /* unlock tables and cleanup memory */
  mysql_unlock_read_tables(thd, thd->lock);
  delete qt;
  DBUG_RETURN(0);
}

重要的 MySQL 结构和类

MySQL 源代码中有许多关键的结构和类。到目前为止,您已经在示例中看到了许多。一些更重要的问题记录在 MySQL 内部手册中。不幸的是,没有任何文件将它们全部列出。下面几节描述了在使用 DBXP 查询优化器(以及后面的查询执行代码)时会遇到的一些结构和类。这些包括TABLE结构、Field类和一些常见的Item迭代器(在第 3 章中讨论了Item类)。

表格结构

在编写优化器时,最重要的 MySQL 结构是TABLE结构。

这个结构很重要,因为它包含了一个表的所有相关数据。它包含所有内容,从指向适当存储处理程序类的指针,到执行查询时用于存储行的字段、键和临时缓冲区的列表。

虽然这个结构非常庞大(就像 MySQL 中最重要的结构一样),但是你会反复看到一些关键的属性。表 13-1 列出了TABLE结构的一些更重要的属性。关于TABLE结构的详细检查,参见handler.h文件。

表 13-1 。表结构概述

属性描述
文件对存储引擎对象的引用。
领域表的字段数组。
田地(复数);场;域;字段字段数组中的字段数。
然后指向表列表中下一个表的指针。
上一个指向表列表中上一个表的指针。

字段类

Field类包含创建、赋值和操作数据库中表的字段(或属性)的所有属性和方法。Field类在/sql/field.h文件中定义,在/sql/field.cc文件中实现。Field类实际上是一个基类,几种类型的字段都是从这个基类派生出来的。这些名为Field_XXX,的派生类可以在 MySQL 源代码的几个地方找到。

由于它只是一个基类, 13 类中的许多方法都打算被派生类覆盖(它们被定义为virtual)。然而,许多派生类具有相同的基本属性和方法。表 13-2 列出了你在使用 DBXP 源代码时会遇到的属性和方法。对于Field类的详细检查,参见field.h文件。

表 13-2 。字段类

属性/方法描述
光电带读数机(photoelectric tape reader)指向记录缓冲区中字段的指针。
空值 ptr指向记录缓冲区中一个(或多个)字节的指针,指示哪些属性可以包含 NULL。
表名与该字段关联的表名。
字段名称该字段的属性名。
字段长度字段的长度。指示可以存储的字节数。
is_null()检查字段是否为空。
move_field()将内存中字段的指针更改为指向不同的位置。
商店()用于将值存储到字段中的一系列重载方法。
val_str()以字符串形式获取字段的值。
val_int()以整数形式获取字段的值。
结果类型()获取字段的数据类型。
化学机械抛光()返回字段与传递的值的比较结果。

迭代器

MySQL 源代码中有三种类型的迭代器。在前面的章节中,你已经看到了这些迭代器。迭代器是一种特殊的结构,它使得创建和浏览对象列表变得很容易,它们通常以链表或数组的形式出现。MySQL 中的迭代器是作为模板类实现的,它将列表操作的数据类型作为参数。MySQL 迭代器是链表,但有些更像队列和堆栈。以下部分描述了 MySQL 中一些可用的迭代器类。这些迭代器在sql/sql_list.h头文件中定义。

模板<>班级列表

List模板类被实现为一个队列或堆栈,使用push_back()方法将项目推到列表的后面,或者使用push_front()方法将项目推到列表的前面。可以使用pop()方法检索项目,或者使用remove()方法删除项目。您可以通过使用数据项的next属性来循环遍历列表,但是列表通常用于形成项目的链表(例如List<Item> item_list),然后使用List_iterator类中的一个来快速循环遍历列表。这个类从base_list类派生而来(也在/sql/sql_list.h中定义)。

模板<>类列表 _ 迭代器

List_iterator类被实现为一个链表,该链表具有使用重载的++操作符在列表中移动的方法。可以使用ref()方法检索项目,或者使用remove()方法删除项目。可以通过发出rewind()方法从前面重新开始列表。该类从base_list类派生而来(也在/sql/sql_list.h中定义)。

模板<>类列表 _ 迭代器 _ 快速

List_iterator_fast类本质上与List_iterator类相同,但它是为快速向前遍历而优化的。它被实现为一个链表,并带有使用重载的++操作符在列表中移动的方法。可以使用ref()方法检索项目,或者使用remove()方法删除项目。该类从base_list类派生而来(也在/sql/sql_list.h中定义)。

例子

使用迭代器很容易。如果你想使用一个列表来操作条目,一个简单的列表,比如List<Item_field>, 将是最好的选择。如果想快速遍历字段列表,可以创建一个列表迭代器作为List_iterator<Item_field>List_iterator_fast<Item_field>。循环结构的例子如清单 13-4 所示。

清单 13-4。 示例迭代器

  /* create a list and populate with some items */
  List<Item> item_list;
  item_list.push_back(new Item_int((int32)
             join->select_lex->select_number));
  item_list.push_back(new Item_string(join->select_lex->type,
    strlen(join->select_lex->type), cs));
  item_list.push_back(new Item_string(message,strlen(message),cs));

../* start a basic list iterator to iterate through the item_list */
  List_iterator<Item_field> item_list_it(*item_list);

  /* control the iteration using an offset */
  while ((curr_item= item_list_it++))
  {
    /* do something */
  }

../* start a fast list iterator to iterate through the item_list */
  List_iterator_fast<Item_field> li(item_equal->fields);

  /* control the iteration using an offset */
  while ((item= li++))
  {
    /* do something */
  }

DBXP 助手类

我在第 11 章中提到了 DBXP 引擎中使用的两个额外的类(属性和表达式)。这些类旨在使优化器更容易编码和理解。它们是现有 MySQL 类(和结构)的封装,并且重用了 MySQL 代码中的许多可用方法。

第一个助手类是封装查询中使用的属性的类。这些属性在 MySQL 代码中表示为Item类。名为Attribute的助手类通过提供访问项目的公共接口,使得访问这些类变得更加容易。清单 13-5 显示了Attribute类的头文件。

清单 13-5。 属性类表头

#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"

class Attribute
{
public:
  Attribute(void);
  int remove_attribute(int num);
  Item *get_attribute(int num);
  int add_attribute(bool append, Item *new_item);
  int num_attributes();
  int index_of(char *table, char *value);
  int hide_attribute(Item *item, bool hide);
  char *to_string();
private:
  List<Item> attr_list;
  bool hidden[256];
};

第二个助手类封装了查询中使用的表达式。像属性一样,表达式在 MySQL 代码中表示为Item类实例。名为Expression的助手类为Item类提供了一个公共(且简化的)接口。清单 13-6 显示了Expression类的头文件。

清单 13-6。 表达式类表头

#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include <sql_string.h>

struct expr_node
{
  Item      *left_op;
  Item      *operation;
  Item      *right_op;
  Item      *junction;
  expr_node *next;
};

class Expression
{
public:
  Expression(void);
  int remove_expression(int num, bool free);
  expr_node *get_expression(int num);
  int add_expression(bool append, expr_node *new_item);
  int num_expressions();
  int index_of(char *table, char *value);
  int reduce_expressions(TABLE *table);
  bool has_table(char *table);
  int convert(THD *thd, Item *mysql_expr);
  char *to_string();
  bool evaluate(TABLE *table 1);
  int compare_join(expr_node *expr, TABLE *t1, TABLE *t2);
  int get_join_expr(Expression *where_expr);
private:
  expr_node *root;
  Field *find_field(TABLE *tbl, char *name);
  bool compare(expr_node *expr, TABLE *t1);
  int num_expr;
};

我使用一个结构来包含左操作数、运算符和右操作数形式的表达式。这是一种比 MySQL 类表示的表达式树更简化的方法,使得阅读优化器代码更容易。更简单的方法也使得在交互式调试器中评估条件更容易。

image 注意我在正文中省略了这些助手类的一些细节,因为它们是调用用于TABLE结构的 MySQL 方法以及ItemField类的非常简单的抽象。然而,这些文件包含在在线章节源代码中。这本书的源代码可以在 www.apress.com[的源代码部分下载。](www.apress.com)

这些助手类和头文件应该放在/sql目录中,并添加到 CMakeLists.txt 文件中。我将在“编译和测试代码”一节中向您展示如何做到这一点。

对现有代码的修改

实现优化器还需要一个小的修改:我们需要添加代码来使用新的AttributeExpression类。打开query_tree.h头文件,进行清单 13-7 所示的修改。正如你所看到的,我已经改变了where_exprjoin_expr属性来使用新的Expression类。同样,我更改了attributes属性以使用新的Attribute类。

清单 13-7。 对查询树类的修改

#include "attribute.h"
#include "expression.h"
#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include "records.h"

const int MAXNODETABLES = 4;
const int LEFTCHILD = 0;
const int RIGHTCHILD = 1;

class Query_tree
{
public:
  enum query_node_type          //this enumeration lists the available
  {                              //query node (operations)
    qntUndefined = 0,
    qntRestrict = 1,
    qntProject = 2,
    qntJoin = 3,
    qntSort = 4,
    qntDistinct = 5
  };

  enum join_con_type            //this enumeration lists the available
  {                              //join operations supported
    jcUN = 0,
    jcNA = 1,
    jcON = 2,
    jcUS = 3
  };

  enum type_join                //this enumeration lists the available
  {                              //join types supported.
    jnUNKNOWN      = 0,          //undefined
    jnINNER        = 1,
    jnLEFTOUTER    = 2,
    jnRIGHTOUTER   = 3,
    jnFULLOUTER    = 4,
    jnCROSSPRODUCT = 5,
    jnUNION        = 6,
    jnINTERSECT    = 7
  };

    enum AggregateType          //used to add aggregate functions
    {
        atNONE      = 0,
        atCOUNT     = 1
    };

  /*
    STRUCTURE query_node

    DESCRIPTION
      This this structure contains all of the data for a query node:

      NodeId -- the internal id number for a node
      ParentNodeId -- the internal id for the parent node (used for insert)
      SubQuery -- is this the start of a subquery?
      Child -- is this a Left or Right child of the parent?
      NodeType -- synonymous with operation type
      JoinType -- if a join, this is the join operation
      join_con_type -- if this is a join, this is the "on" condition
      Expressions -- the expressions from the "where" clause for this node
      Join Expressions -- the join expressions from the "join" clause(s)
      Relations[] -- the relations for this operation (at most 2)
      PreemptPipeline -- does the pipeline need to be halted for a sort?
      Fields -- the attributes for the result set of this operation
      Left -- a pointer to the left child node
      Right -- a pointer to the right child node
*/
  struct query_node
  {
    query_node();
    ∼query_node();
    int                 nodeid;
    int                 parent_nodeid;
    bool                sub_query;
    int                 child;
    query_node_type     node_type;
    type_join           join_type;
    join_con_type       join_cond;
    Expression          *where_expr;
    Expression          *join_expr;
    TABLE_LIST          *relations[MAXNODETABLES];
    int                 eof[MAXNODETABLES];
    int                 ndx[MAXNODETABLES];
    bool                preempt_pipeline;
    Attribute           *attributes;
    query_node          *left;
    query_node          *right;
  };

  struct record_buff
  {
    uchar *field_ptr;
    long field_length;
    record_buff *next;
    record_buff *prev;
    READ_RECORD *record;
  };

许多方法也需要添加到查询树类中。我没有描述每个方法及其实现的细节,而是在清单 13-8 中包含了查询树定义的其余部分。这段代码也被添加到query_tree.h文件中。

清单 13-8。 查询-树类的新方法

  query_node *root;              //The ROOT node of the tree

  Query_tree(void);
  ∼Query_tree(void);
  int init_node(query_node *qn);
  int heuristic_optimization();
  int cost_optimization();
  int insert_attribute(query_node *qn, Item *c);
  bool distinct;
  int prepare(query_node *qn);
  int cleanup(query_node *qn);
  bool Eof(query_node *qn);
  READ_RECORD *get_next(query_node *qn);
  List <Item> result_fields;

private:
  bool h_opt;              //has query been optimized (rules)?
  bool c_opt;              //has query been optimized (cost)?
  READ_RECORD *lbuff;
  READ_RECORD *rbuff;
  record_buff *left_record_buff;
  record_buff *right_record_buff;
  record_buff *left_record_buffer_ptr;
  record_buff *right_record_buffer_ptr;

  int push_projections(query_node *qn, query_node *pNode);
  query_node *find_projection(query_node *qn);
  bool is_leaf(query_node *qn);
  bool has_relation(query_node *qn, char *Table);
  bool has_attribute(query_node *qn, Item *a);
  int del_attribute(query_node *qn, Item *a);
  int push_restrictions(query_node *qn, query_node *pNode);
  query_node *find_restriction(query_node *qn);
  query_node *find_join(query_node *qn);
  int push_joins(query_node *qn, query_node *pNode);
  int prune_tree(query_node *prev, query_node *cur_node);
  int balance_joins(query_node *qn);
  int split_restrict_with_project(query_node *qn);
  int split_restrict_with_join(query_node *qn);
  int split_project_with_join(query_node *qn);
  bool find_table_in_tree(query_node *qn, char *tbl);
  bool find_table_in_expr(Expression *expr, char *tbl);
  bool find_attr_in_expr(Expression *expr, char *tbl, char *value);
  int apply_indexes(query_node *qn);
  bool do_restrict(query_node *qn, READ_RECORD *t);
  READ_RECORD *do_project(query_node *qn, READ_RECORD *t);
  READ_RECORD *do_join(query_node *qn);
  int find_index_in_expr(Expression *e, char *tbl);
  TABLE *get_table(query_node *qn);
  int insertion_sort(bool left, Field *field, READ_RECORD *rcd);
  int check_rollback(record_buff *cur_left, record_buff *curr_left_prev,
    record_buff *cur_right, record_buff *cur_right_prev);
};

注意有几个公共方法,包括heuristic_optimization()cost_optimization()。我还添加了一个公共属性distinct,,你可以用它来帮助实现不同的操作(参见本章末尾的练习)。其余的方法是优化代码的助手方法。我解释了一些更有趣的,并把平凡的留给你去探索。

现在我们有了一些助手类来使优化器更容易实现,我们需要将它们合并到将 MySQL 内部查询表示转换为 DBXP 查询树的转换代码中。打开sql_dbxp_parse.cc文件,定位build_query_tree()方法 。清单 13-9 显示了添加新的AttributeExpression类所必需的改变。

清单 13-9。 对构建-查询-树方法的修改


/*
  Build Query Tree

  SYNOPSIS
    build_query_tree()
    THD *thd            IN the current thread
    LEX *lex            IN the pointer to the current parsed structure
    TABLE_LIST *tables  IN the list of tables identified in the query

  DESCRIPTION
    This method returns a converted MySQL internal representation (IR) of a
    query as a query_tree.

  RETURN VALUE
    Success = Query_tree * -- the root of the new query tree.
    Failed = NULL
*/
Query_tree *build_query_tree(THD *thd, LEX *lex, TABLE_LIST *tables)
{
  DBUG_ENTER("build_query_tree");
  Query_tree *qt = new Query_tree();
  Query_tree::query_node *qn =
    (Query_tree::query_node *)my_malloc(sizeof(Query_tree::query_node),
    MYF(MY_ZEROFILL | MY_WME));
  TABLE_LIST *table;
  int i = 0;
  Item *w;
  int num_tables = 0;

  /* create a new restrict node */
  qn->parent_nodeid =1;
  qn->child = false;
  qn->join_type = (Query_tree::type_join) 0;
  qn->nodeid = 0;
  qn->node_type = (Query_tree::query_node_type) 2;
  qn->left = NULL;
  qn->right = NULL;
  qn->attributes = new Attribute();
  qn->where_expr = new Expression();
  qn->join_expr = new Expression();

  /* Get the tables (relations) */
  i = 0;
  for(table = tables; table; table = table->next_local)
  {
    num_tables++;
    qn->relations[i] = table;
    i++;
  }

  /* prepare the fields (find associated tables) for query */
  List <Item> all_fields;
  Name_resolution_context context;
  List_iterator <Item> it(thd->lex->select_lex.item_list);
  it++;
  if (lex->select_lex.with_wild)
  {
    bool found = FALSE;
    Field_iterator_table_ref field_iterator;
    for(table = tables; table; table = table->next_local)
    {
      field_iterator.set(table);
      for (; !field_iterator.end_of_fields(); field_iterator.next())
      {
        Item *item= field_iterator.create_item(thd);
        if (!found)
        {
          found= TRUE;
          it.replace(item); /* Replace '*' with the first found item. */
        }
        else
        {
          it.after(item);   /* Add 'item' to the SELECT list. */
        }
      }
    }
  }
if (setup_fields(thd, lex->select_lex.ref_pointer_array,
                   lex->select_lex.item_list, thd->mark_used_columns,
                   &all_fields, 1))
    DBUG_RETURN(NULL);
  qt->result_fields = lex->select_lex.item_list;

  /* get the attributes from the raw query */
  w = lex->select_lex.item_list.pop();
  while (w != 0)
  {
    uint unused_field_idx= NO_CACHED_FIELD_INDEX;
    TABLE_LIST *dummy;
    Field *f = NULL;
    for(table = tables; table; table = table->next_local)
    {
      f = find_field_in_table_ref(thd, table, ((Field *)w)->field_name,
                                  strlen(((Field *)w)->field_name),
                                  ((Field *)w)->field_name, NULL, NULL, NULL,
                                  FALSE, FALSE, &unused_field_idx, FALSE,
                                  &dummy);
      if (f)
      {
        qn->attributes->add_attribute(true, (Item *)f);
        break;
      }
    }
    w = lex->select_lex.item_list.pop();
  }

  /* get the joins from the raw query */
  if (num_tables > 0)  //indicates more than 1 table processed
    for(table = tables; table; table = table->next_local)
    {
      if (table->join_cond() != 0)
        qn->join_expr->convert(thd, (Item *)table->join_cond());
    }

  /* get the expressions for the where clause */
  qn->where_expr->convert(thd, lex->select_lex.where);

  /* get the join conditions for the joins */
  qn->join_expr->get_join_expr(qn->where_expr);

  /* if there is a where clause, set node to restrict */
  if (qn->where_expr->num_expressions() > 0)
    qn->node_type = (Query_tree::query_node_type) 1;

  qt->root = qn;
  DBUG_RETURN(qt);
}

此时,需要调整包含文件,以确保我们包含了编译代码所需的所有内容。例如,以下语句出现在query_tree.cc文件的顶部:

#include "query_tree.h"

query_tree.h 头文件包括属性和表达式头文件,以及所需的 MySQL includes 文件,如下所示。

#include "query_tree.h"
#include "sql_base.h"
#include "sql_acl.h"
#include "sql_parse.h"
#include "lock.h"

image 注意如果在编译时遇到奇怪的错误,请检查您的 CMakeLists.txt 文件中没有包含attributeexpressionquery_tree头文件。编译器将按照 include 指令自动包含这些文件。

启发式优化器的详细信息

启发式优化器是使用前面描述的规则模型实现的。启发式优化器中使用的每个方法都实现一些或所有规则。这些方法在表 13-3 中列出。

表 13-3 。启发式优化器中的启发式方法

方法描述
split_restrict_with_join()在树中搜索具有限制(有表达式)和连接表达式的节点。它将节点分为两个节点:一个用于限制,另一个用于连接。
split_project_with_join()在树中搜索具有投影(具有属性)和连接表达式的节点。它将节点分为两个节点:一个用于投影,一个用于连接。
split _ restrict _ with _ project()在树中搜索具有限制(有表达式)和投影(有属性)的节点。它将节点分为两个节点:一个用于限制,另一个用于投影。
find_restriction()在树中搜索不在叶节点上的限制节点。
push_restrictions()将限制沿树向下推至尽可能低的节点。它寻找限制可以驻留在叶子上的情况。这个方法在一个循环中与 find_restrictions()一起使用(当没有找到更多已经不在叶子上的限制时,循环结束)。
find_projection()在树中搜索不在叶子上的投影节点。
push_projections()将投影沿树向下推至可能的最低节点。它寻找投影可以驻留在叶子上或作为限制的父对象的情况。此方法在循环中与 find_projections()一起使用(当没有发现更多已经不在叶或作为限制的叶的父级的投影时,循环结束)。
find_join()在树中搜索联接节点。
push_joins()将联接沿树向下推至节点,作为合格限制和/或投影(在联接中的表上操作的那些)的父级。
prune_tree()识别树中已经优化掉并且不再有效的节点(没有属性或表达式,也没有连接或排序),并删除它们。

启发式优化器的实现读起来非常容易。清单 13-10 展示了heuristic_optimization()方法的源代码实现。

清单 13-10。DBXP 启发式优化方法 14

/*
  Perform heuristic optimization

  SYNOPSIS
    heuristic_optimization()

  DESCRIPTION
    This method performs heuristic optimization on the query tree. The
    operation is destructive in that it rearranges the original tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::heuristic_optimization()
{
  DBUG_ENTER("heuristic_optimization");
  query_node       *pNode;
  query_node       *nNode;

  h_opt = true;
  /*
    First, we have to correct the situation where restrict and
    project are grouped together in the same node.
  */
  split_restrict_with_join(root);
  split_project_with_join(root);
  split_restrict_with_project(root);

  /*
    Find a node with restrictions and push down the tree using
    a recursive call. continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_restriction(root);
  while(pNode != 0)
  {
    push_restrictions(root, pNode);
    nNode = find_restriction(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
  }

  /*
    Find a node with projections and push down the tree using
    a recursive call. Continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_projection(root);
  while(pNode != 0)
  {
    push_projections(root, pNode);
    nNode = find_projection(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
  }

  /*
    Find a join node and push it down the tree using
    a recursive call. Continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_join(root);
  while(pNode != 0)
  {
    push_joins(root, pNode);
    nNode = find_join(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
    else
      pNode = nNode;
  }

  /*
    Prune the tree of "blank" nodes
    Blank Nodes are:
     1) projections without attributes that have at least 1 child
     2) restrictions without expressions
     BUT...Can't delete a node that has TWO children!
  */
  prune_tree(0, root);

  /*
    Lastly, check to see if this has the DISTINCT option.
    If so, create a new node that is a DISTINCT operation.
  */
  if(distinct && (root->node_type != qntDistinct))
  {
    int i;
    pNode = (query_node*)my_malloc(sizeof(query_node),
            MYF(MY_ZEROFILL | MY_WME));
    init_node(pNode);
    pNode->sub_query = 0;
    pNode->attributes = 0;
    pNode->join_cond = jcUN;  /* (join_con_type) 0; */
    pNode->join_type = jnUNKNOWN;  /* (type_join) 0; */
    pNode->left = root;
    pNode->right = 0;
    for(i = 0; i < MAXNODETABLES; i++)
      pNode->relations[i] = NULL;
    pNode->nodeid = 90125;  // sentinel value to indicate node is not set
    pNode->child = LEFTCHILD;
    root->parent_nodeid = 90125;  // sentinel value to indicate node is not set
    root->child = LEFTCHILD;
    pNode->parent_nodeid =1;
    pNode->node_type = qntDistinct;
    pNode->attributes = new Attribute();
    pNode->where_expr = new Expression();
    pNode->join_expr = new Expression();
    root = pNode;
  }
  DBUG_RETURN(0);
}

请注意用于定位限制、投影和连接的循环。该代码被设计为使用前序遍历遍历树,应用规则,直到不再有违反规则的条件(即,没有“坏的”节点放置)。

下面的清单显示了前面描述的heuristic_optimization()方法中主要方法的一些源代码。为了节省空间,我没有列出次要的助手方法,因为它们是 MySQL 结构和类方法的简单抽象。您应该下载本章的源代码,并研究其他帮助器方法,看看它们是如何工作的。

split_restrict_with_join()方法在树中搜索具有where表达式的连接(因此既是连接又是限制),并将它们分成两个节点:一个连接和一个限制节点。清单 13-11 显示了这个方法的源代码。

清单 13-11。 分割制约合并

/*
  Split restrictions that have joins.

  SYNOPSIS
    split_restrict_with_join()
    query_node *QN IN the node to operate on

  DESCRIPTION
     This method looks for joins that have where expressions (thus are both
     joins and restrictions) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_restrict_with_join(query_node *QN)
{
  int j = 0;
  int i = 0;

  DBUG_ENTER("split_restrict_with_join");
  if(QN != 0)
  {
    if(((QN->join_expr->num_expressions() > 0) &&
      (QN->where_expr->num_expressions() > 0)) &&
       ((QN->node_type == qntJoin) || (QN->node_type == qntRestrict)))
    {
      bool isleft = true;
      /*
        Create a new node and:
          1) Move the where expressions to the new node.
          2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left or right child = new node;
          5) Set new node's id = current id + 200;
           6) set parent id, etc.
          7) determine which table needs to be used for the
             restrict node.
      */
      query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                             MYF(MY_ZEROFILL | MY_WME));
      init_node(new_node);
      new_node->node_type = qntRestrict;
      new_node->parent_nodeid = QN->nodeid;
      new_node->nodeid = QN->nodeid + 200;
      new_node->where_expr = QN->where_expr;
      new_node->join_expr = new Expression();
      QN->where_expr = new Expression();

      /*
        Loop through tables and move table that matches
        to the new node
      */
      for(i = 0; i < MAXNODETABLES; i++)
      {
        if (QN->relations[i] != NULL)
        {
          if (find_table_in_expr(new_node->where_expr,
                QN->relations[i]->table_name))
          {
            new_node->relations[j] = QN->relations[i];
            j++;
            if (i != 0)
              isleft = false;
            QN->relations[i] = NULL;
          }
        }
      }

      /* set children to point to balance of tree */
      new_node->right = 0;
      if (isleft)
      {
        new_node->child = LEFTCHILD;
        new_node->left = QN->left;
        QN->left = new_node;
      }
      else
      {
        new_node->child = RIGHTCHILD;
        new_node->left = QN->right;
        QN->right = new_node;
      }
      if (new_node->left)
        new_node->left->parent_nodeid = new_node->nodeid;
      j = QN->attributes->num_attributes();
      if ((QN->node_type == qntJoin) && (j > 0))
      {
        Attribute *attribs = 0;
        Item * attr;
        int ii = 0;
        int jj = 0;
        if ((QN->attributes->num_attributes() == 1) &&
            (strcasecmp("*",
            ((Field *)QN->attributes->get_attribute(0))->field_name) == 0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j,
            QN->attributes->get_attribute(0));
        }
        else
        {
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Item *f = (Item *)new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
          }
          j = attribs->num_attributes();
          new_node->attributes = new Attribute();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj >1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
             (char *)((Field *)attr)->table->s->table_name.str,
             (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
      else
      {
        QN->node_type = qntJoin;
        QN->join_type = jnINNER;
        new_node->attributes = new Attribute();
      }
    }
    split_restrict_with_join(QN->left);
    split_restrict_with_join(QN->right);
  }
  DBUG_RETURN(0);
}

split_project_with_join()方法在树中搜索具有属性的连接(因此既是连接又是投影),并将它们分成两个节点:一个连接和一个项目节点。清单 13-12 显示了这个方法的源代码。

清单 13-12。 拆分项目合并

/*
  Split projections that have joins.

  SYNOPSIS
    split_project_with_join()
    query_node *QN IN the node to operate on

  DESCRIPTION
     This method looks for joins that have attributes (thus are both
     joins and projections) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_project_with_join(query_node *QN)
{
  int j = 0;
  int i;

  DBUG_ENTER("split_project_with_join");
  if(QN != 0)
  {
    if((QN->join_expr->num_expressions() > 0) &&
       ((QN->node_type == qntJoin) || (QN->node_type == qntProject)))
    {
      /*
        Create a new node and:
          1) Move the where expressions to the new node.
          2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left or right child = new node;
          5) Set new node's id = current id + 300;
          6) set parent id, etc.
      */
      QN->node_type = qntJoin;
      QN->join_type = jnINNER;
      if (QN->left == 0)
      {
        query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                               MYF(MY_ZEROFILL | MY_WME));
        init_node(new_node);
        new_node->node_type = qntProject;
        new_node->parent_nodeid = QN->nodeid;
        new_node->nodeid = QN->nodeid + 300;
        for(i = 0; i < MAXNODETABLES; i++)
          new_node->relations[i] = 0;
        new_node->relations[0] = QN->relations[0];
        QN->relations[0] = 0;
        new_node->left = QN->left;
        QN->left = new_node;
        new_node->right = 0;
        new_node->child = LEFTCHILD;
        if (new_node->left != 0)
           new_node->left->parent_nodeid = new_node->nodeid;
        j = QN->attributes->num_attributes();
        new_node->attributes = new Attribute();
        new_node->where_expr = new Expression();
        new_node->join_expr = new Expression();
        if ((j == 1) &&
            (strcasecmp("*", ((Field *)QN->attributes->get_attribute(0))->field_name)==0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j, QN->attributes->get_attribute(0));
          if (QN->right != 0)
            QN->attributes->remove_attribute(0);
        }
        else if (j > 0)
        {
          Attribute *attribs = 0;
          Item * attr;
          int ii = 0;
          int jj = 0;
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Field *f = new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
          }
          j = attribs->num_attributes();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj >1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
      if (QN->right == 0)
      {
        query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                               MYF(MY_ZEROFILL | MY_WME));
        init_node(new_node);
        new_node->node_type = qntProject;
        new_node->parent_nodeid = QN->nodeid;
        new_node->nodeid = QN->nodeid + 400;
        for(i = 0; i < MAXNODETABLES; i++)
          new_node->relations[0] = 0;
        new_node->relations[0] = QN->relations[1];
        QN->relations[1] = 0;
        new_node->left = QN->right;
        QN->right = new_node;
        new_node->right = 0;
        new_node->child = RIGHTCHILD;
        if (new_node->left != 0)
          new_node->left->parent_nodeid = new_node->nodeid;
        j = QN->attributes->num_attributes();
        new_node->attributes = new Attribute();
        new_node->where_expr = new Expression();
        new_node->join_expr = new Expression();
        if ((j == 1) &&
            (strcasecmp("*", ((Field *)QN->attributes->get_attribute(0))->field_name)==0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j, QN->attributes->get_attribute(0));
          QN->attributes->remove_attribute(0);
        }
        else
        {
          Attribute *attribs = 0;
          Item * attr;
          int ii = 0;
          int jj = 0;
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Field *f = new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
            if (j == 0)
            {
              new_node->attributes->hide_attribute((Item *)f, true);
            }
          }
          j = attribs->num_attributes();
          new_node->attributes = new Attribute();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj >1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
    }
    split_project_with_join(QN->left);
    split_project_with_join(QN->right);
  }
  DBUG_RETURN(0);
}

split_restrict_with_project()方法在树中搜索具有属性的限制(因此既是投影又是限制),并将它们分成两个节点:一个限制节点和一个项目节点。清单 13-13 显示了这个方法的源代码。

清单 13-13。 用项目分割制约

/*
  Split restrictions that have attributes (projections).

  SYNOPSIS
    split_restrict_with_project()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for restrictions that have attributes (thus are both
    projections and restrictions) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_restrict_with_project(query_node *QN)
{
  DBUG_ENTER("split_restrict_with_project");
  if(QN != 0)
  {
    if(((QN->attributes->num_attributes() > 0) &&
      (QN->where_expr->num_expressions() > 0)) &&
      ((QN->node_type == qntProject) || (QN->node_type == qntRestrict)))
    {
      /*
        Create a new node and:
          1) Move the expressions to the new node.
           2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left child = new node;
          5) Set new node's id = current id + 1000;
          6) set parent id, etc.
      */
      query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                             MYF(MY_ZEROFILL | MY_WME));
      init_node(new_node);
      new_node->child = LEFTCHILD;
      new_node->node_type = qntRestrict;
      if(new_node->node_type == qntJoin)
      {
        new_node->join_cond = QN->join_cond;
        new_node->join_type = QN->join_type;
      }
      QN->node_type = qntProject;
      new_node->attributes = new Attribute();
      new_node->where_expr = QN->where_expr;
      new_node->join_expr = new Expression();
      QN->where_expr = new Expression();
      new_node->left = QN->left;
      new_node->right = QN->right;
      new_node->parent_nodeid = QN->nodeid;
      new_node->nodeid = QN->nodeid + 1000;
      if(new_node->left)
        new_node->left->parent_nodeid = new_node->nodeid;
      if(new_node->right)
        new_node->right->parent_nodeid = new_node->nodeid;
      for(int i = 0; i < MAXNODETABLES; i++)
      {
        new_node->relations[i] = QN->relations[i];
        QN->relations[i] = NULL;
      }
      QN->left = new_node;
      QN->right = 0;
    }
    split_restrict_with_project(QN->left);
    split_restrict_with_project(QN->right);
  }
  DBUG_RETURN(0);
}

find_restriction()方法从起始节点(QN)开始在树中搜索下一个限制。如果找到限制,则返回指向该节点的指针;否则,该方法返回NULL清单 13-14 显示了这个方法的源代码。

清单 13-14。 寻找制约

/*
  Find a restriction in the subtree.
  SYNOPSIS
    find_restriction()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a restriction and returns the node
    pointer.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = query_node * the node located
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_restriction(query_node *QN)
{
  DBUG_ENTER("find_restriction");
  query_node   *N;

  N = 0;
  if(QN != 0)
  {
    /*
      A restriction is a node marked as restrict and
      has at least one expression
    */
    if (QN->where_expr->num_expressions() > 0)
      N = QN;
    else
    {
      N = find_restriction(QN->left);
      if(N == 0)
        N = find_restriction(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_restriction()方法 从起始节点(QN)开始搜索树,并将约束节点(pNode)向下推到包含约束中指定的关系的节点。清单 13-15 显示了这个方法的源代码。

清单 13-15。 推送限制

/*
  Push restrictions down the tree.

  SYNOPSIS
    push_restrictions()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the restriction attributes

  DESCRIPTION
    This method looks for restrictions and pushes them down the tree to nodes
    that contain the relations specified.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_restrictions(query_node *QN, query_node *pNode)
{
  query_node       *NewQN=0;

  DBUG_ENTER("push_restrictions");
  if((QN != 0) && (pNode != 0) && (pNode->left != 0))
  {
    /*
      Conditions:
        1) QN is a join node
        2) QN is a project node
        3) QN is a restrict node
        4) All other nodes types are ignored.

      Methods:
        1) if join or project and the children are not already restrictions
           add a new node and put where clause in new node else
           see if you can combine the child node and this one
        2) if the node has the table and it is a join,
           create a new node below it and push the restriction
           to that node.
        3) if the node is a restriction and has the table,
           just add the expression to the node's expression list
    */

    /* if projection, move node down tree */
    if((QN->nodeid != pNode->nodeid) && (QN->node_type == qntProject))
    {
      if (QN->left != 0)
      {
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        init_node(QN->left);
        NewQN = QN->left;
        NewQN->left = 0;
      }
      else
      {
        NewQN = QN->left;
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        QN->left->left = NewQN;
        NewQN = QN->left;
      }
      NewQN->sub_query = 0;
      NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
      NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
      NewQN->right = 0;
      for(long i = 0; i < MAXNODETABLES; i++)
        NewQN->relations[i] = 0;
      NewQN->nodeid = QN->nodeid + 1;
      NewQN->parent_nodeid = QN->nodeid;
      NewQN->node_type = qntRestrict;
      NewQN->attributes = new Attribute();
      NewQN->where_expr = new Expression();
      NewQN->join_expr = new Expression();
      if (pNode->relations[0])
        NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
      if ((QN->relations[0] != NULL) && (QN->relations[0] == pNode->relations[0]))
      {
        if (QN->relations[0])
        {
          if (find_table_in_expr(pNode->where_expr, QN->relations[0]->table_name))
          {
            NewQN->relations[0] = QN->relations[0];
            QN->relations[0] = 0;
          }
        }
      }
      else
      {
        if (pNode->relations[0])
          if (find_table_in_tree(QN->left, pNode->relations[0]->table_name))
            NewQN->relations[0] = 0;
        pNode->where_expr = NULL;
        pNode->relations[0] = 0;
      }
    }
    /* if join, move restrict node down tree */
    else if((QN->nodeid != pNode->nodeid) &&
      ((QN->left == 0) || (QN->right == 0)) &&
      (QN->node_type == qntJoin))
    {
      if(QN->relations[0] != 0)
      {
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        NewQN = QN->left;
        NewQN->sub_query = 0;
        NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
        NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
        NewQN->left = 0;
        NewQN->right = 0;
        for(long i = 0; i < MAXNODETABLES; i++)
          NewQN->relations[i] = 0;
        NewQN->nodeid = QN->nodeid + 1;
        NewQN->parent_nodeid = QN->nodeid;
        NewQN->node_type = qntRestrict;
        NewQN->attributes = new Attribute();
        NewQN->where_expr = new Expression();
        NewQN->join_expr = new Expression();
        NewQN->relations[0] = QN->relations[0];
        QN->relations[0] = 0;
        if (pNode->relations[0])
          NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
      }
      else if(QN->relations[1] != 0)
      {
        QN->right = (query_node*)my_malloc(sizeof(query_node),
                    MYF(MY_ZEROFILL | MY_WME));
        NewQN = QN->left;
        NewQN->sub_query = 0;
        NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
        NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
        NewQN->left = 0;
        NewQN->right = 0;
        for(long i = 0; i < MAXNODETABLES; i++)
          NewQN->relations[i] = 0;
      }
      NewQN->nodeid = QN->nodeid + 1;
      NewQN->parent_nodeid = QN->nodeid;
      NewQN->node_type = qntRestrict;
      NewQN->attributes = new Attribute();
      NewQN->where_expr = new Expression();
      NewQN->join_expr = new Expression();
      NewQN->relations[0] = QN->relations[1];
      QN->relations[1] = 0;
      NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
    }
    push_restrictions(QN->left, pNode);
    push_restrictions(QN->right, pNode);
  }
   DBUG_RETURN(0);
}

find_projection()方法从起始节点(QN)开始搜索树,寻找树中的下一个投影。如果找到投影,则返回指向该节点的指针;否则,该方法返回NULL。清单 13-16 显示了这个方法的源代码。

清单 13-16。 寻找投影

/*
  Find a projection in the tree

  SYNOPSIS
    find_projection()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a projection and returns the node
    pointer.

  NOTES
    This finds the first projection and is biased to the left tree.
    This is a RECURSIVE method!

  RETURN VALUE
    Success = query_node * the node located or NULL for not found
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_projection(query_node *QN)
{
  DBUG_ENTER("find_projection");
  query_node   *N;

  N = 0;
  if(QN != 0)
  {
    /*
      A projection is a node marked as project and
      has at least one attribute
    */
    if((QN->node_type == qntProject) &&
       (QN->attributes != 0))
      N = QN;
    else
    {
      N = find_projection(QN->left);
      if(N == 0)
        N = find_projection(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_projection()方法 从起始节点(QN)开始搜索树,并将投影节点(pNode)向下推到包含投影中指定的关系的节点。清单 13-17 显示了这个方法的源代码。

清单 13-17。 推投影

/*
  Push projections down the tree.

  SYNOPSIS
    push_projections()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the projection attributes

  DESCRIPTION
    This method looks for projections and pushes them down the tree to nodes
    that contain the relations specified.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_projections(query_node *QN, query_node *pNode)
{
  DBUG_ENTER("push_projections");
  Item *   a;
  int         i;
  int         j;

  if((QN != 0) && (pNode != 0))
  {
    if((QN->nodeid != pNode->nodeid) &&
       (QN->node_type == qntProject))
    {
      i = 0;
      j = QN->attributes->num_attributes();
      /* move attributes to new node */
      while(i < j)
      {
        a = QN->attributes->get_attribute(i);
        if(has_relation(QN,
          (char *)((Field *)a)->table->s->table_name.str))
        {
          if(!has_attribute(QN, a))
            insert_attribute(QN, a);
          del_attribute(pNode, a);
        }
        i++;
      }
    }
    if(pNode->attributes->num_attributes() != 0)
    {
      push_projections(QN->left, pNode);
      push_projections(QN->right, pNode);
    }
  }
  DBUG_RETURN(0);
}

find_join ()方法 从起始节点(QN)开始在树中搜索下一个连接。如果找到一个连接,则返回一个指向该节点的指针;否则,该方法返回NULL。清单 13-18 显示了这个方法的源代码。

清单 13-18。 寻找加入

/*
  Find a join in the subtree.
  SYNOPSIS
    find_restriction()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a join and returns the
    node pointer.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = query_node * the node located
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_join(query_node *QN)
{
  DBUG_ENTER("find_join");
  query_node               *N;
  N = 0;

  if(QN != 0)
  {
    /*
      if this is a restrict node or a restrict node with
      at least one expression it could be an unprocessed join
      because the default node type is restrict
    */
    if(((QN->node_type == qntRestrict) ||
      (QN->node_type == qntRestrict)) && (QN->join_expr->num_expressions() > 0))
      N = QN;
    else
    {
      N = find_join(QN->left);
      if(N == 0)
        N = find_join(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_joins()方法 从起始节点(QN)开始搜索树,并将连接节点(pNode)沿树向下推到一个位置,在该位置,连接是两个节点的父节点,这两个节点包含连接的子节点中指定的关系。清单 13-19 显示了这个方法的源代码。

清单 13-19。 推归附

/*
  Push joins down the tree.

  SYNOPSIS
    push_restrictions()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the join

  DESCRIPTION
    This method looks for theta joins and pushes them down the tree to the
    parent of two nodes that contain the relations specified.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_joins(query_node *QN, query_node *pNode)
{
  DBUG_ENTER("push_joins");
  Item *lField;
  Item *rField;
  expr_node *node;

  if(!pNode->join_expr)
    DBUG_RETURN(0);
  node = pNode->join_expr->get_expression(0);
  if (!node)
    DBUG_RETURN(0);
  lField = node->left_op;
  rField = node->right_op;

  /* Node must have expressions and not be null */
  if((QN != NULL) && (pNode != NULL) &&
     (pNode->join_expr->num_expressions() > 0))
  {
    /* check to see if tables in join condition exist */
    if((QN->nodeid != pNode->nodeid) &&
       (QN->node_type == qntJoin) &&
        QN->join_expr->num_expressions() == 0 &&
       ((has_relation(QN->left,
         (char *)((Field *)lField)->table->s->table_name.str) &&
       has_relation(QN->right,
         (char *)((Field *)rField)->table->s->table_name.str)) ||
      (has_relation(QN->left,
        (char *)((Field *)rField)->table->s->table_name.str) &&
       has_relation(QN->right,
         (char *)((Field *)lField)->table->s->table_name.str))))
    {
      /* move the expression */
      QN->join_expr = pNode->join_expr;
      pNode->join_expr = new Expression();
      QN->join_type = jnINNER;
      QN->join_cond = jcON;
    }
    push_joins(QN->left, pNode);
    push_joins(QN->right, pNode);
  }
  DBUG_RETURN(0);
}

prune_tree()方法 在树上搜索作为对树执行启发式优化的结果的空白节点(不再具有任何操作或功能的节点)并删除它们。清单 13-20 显示了这个方法的源代码。

清单 13-20。 修枝树

/*
  Prune the tree of dead limbs.

  SYNOPSIS
    prune_tree()
    query_node *prev IN the previous node (parent)
    query_node *cur_node IN the current node pointer (used to delete).

  DESCRIPTION
    This method looks for nodes blank nodes that are a result of performing
    heuristic optimization on the tree and deletes them.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::prune_tree(query_node *prev, query_node *cur_node)
{
  DBUG_ENTER("prune_tree");
  if(cur_node != 0)
  {
    /*
      Blank Nodes are 1) projections without attributes
      that have at least 1 child, or 2) restrictions
      without expressions
    */
    if((((cur_node->node_type == qntProject) &&
       (cur_node->attributes->num_attributes() == 0)) ||
      ((cur_node->node_type == qntRestrict) &&
       (cur_node->where_expr->num_expressions() == 0))) &&
       ((cur_node->left == 0) || (cur_node->right == 0)))
    {
      /*
        Redirect the pointers for the nodes above and
        below this node in the tree.
      */
      if(prev == 0)
      {
         if(cur_node->left == 0)
        {
          cur_node->right->parent_nodeid =1;
          root = cur_node->right;
        }
        else
        {
          cur_node->left->parent_nodeid =1;
          root = cur_node->left;
        }
        my_free(cur_node);
        cur_node = root;
      }
      else
      {
        if(prev->left == cur_node)
        {
          if(cur_node->left == 0)
          {
            prev->left = cur_node->right;
            if (cur_node->right != NULL)
              cur_node->right->parent_nodeid = prev->nodeid;
          }
          else
          {
            prev->left = cur_node->left;
            if (cur_node->left != NULL)
              cur_node->left->parent_nodeid = prev->nodeid;
          }
          my_free(cur_node);
          cur_node = prev->left;
        }
        else
        {
          if(cur_node->left == 0)
          {
            prev->right = cur_node->right;
            if (cur_node->right != NULL)
              cur_node->right->parent_nodeid = prev->nodeid;
          }
          else
          {
            prev->right = cur_node->left;
            if (cur_node->left != NULL)
              cur_node->left->parent_nodeid = prev->nodeid;
          }
          my_free(cur_node);
          cur_node = prev->right;
        }
      }
      prune_tree(prev, cur_node);
    }
    else
    {
      prune_tree(cur_node, cur_node->left);
      prune_tree(cur_node, cur_node->right);
    }
  }
  DBUG_RETURN(0);
}

编译和测试代码

如果还没有,下载本章的源代码,并将文件放在源代码根目录下的/sql目录中。在示例代码中,您还会发现一个差异文件(ch13.diff ),您可以使用它将更改应用到服务器源代码文件(例如 mysqld.cc sql_cmd.h 等)。).或者,您可以使用第 12 章中的修改,因为对服务器代码的修改是相同的。

花一些时间浏览源代码,以便熟悉这些方法。如果您需要调试代码以使用您的配置,或者如果您想要添加其他增强功能或进行练习,现在花时间浏览代码将会有所帮助。

一旦下载了所有的源代码文件并检查了代码,就将这些文件添加到 CMakeLists.txt 文件中。详见第 12 章中的“将文件添加到 CMakeLists.txt 文件中”。您将添加attributeexpression辅助源文件(attribute.cc 和 expression.cc)。将文件添加到项目后,导航到源代码树的根目录,运行 cmake,并发出如下所示的命令。确保代码编译时没有编译错误。

cmake .
make

一旦安装并编译了新代码,运行服务器并执行测试。您可以运行之前创建的测试,也可以在 MySQL 客户端实用程序中输入命令。清单 13-21 显示了运行测试中列出的命令的预期输出。

清单 13-21。 示例测试运行

DBXP_SELECT * FROM staff' at line 1
+−−------------------------+
| Execution Path           |
+−−------------------------+
|      expert_mysql.staff  |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |    RESTRICT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |     PROJECT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|              |           |
|              |           |
|              V           |
|          Result Set      |
+−−------------------------+
25 rows in set (0.00 sec)

+−−--------------------------------------------------+
| Execution Path                                     |
+−−--------------------------------------------------+
|      expert_mysql.staff                            |
|           |                                        |
|           |                                        |
|           |                                        |
|           V                                        |
|      -------------------                           |
|      |     PROJECT     |                           |
|      -------------------                           |
|      | Access Method:  |                           |
|      |    iterator     |                           |
|      -------------------                           |
|           |               expert_mysql.directorate |
|           |                    |                   |
|           |                    |                   |
|           |                    |                   |
|           |                    V                   |
|           |               -------------------      |
|           |               |     PROJECT     |      |
|           |               -------------------      |
|           |               | Access Method:  |      |
|           |               |    iterator     |      |
|           |               -------------------      |
|           |                              |         |
|           |   ----------------------------         |
|           |   |                                    |
|           V   V                                    |
|      -------------------                           |
|      |      JOIN       |                           |
|      -------------------                           |
|      | Access Method:  |                           |
|      |    iterator     |                           |
|      -------------------                           |
|              |                                     |
|              |                                     |
|              V                                     |
|          Result Set                                |
+−−--------------------------------------------------+
36 rows in set (0.00 sec)

+−−----------------------------------------------+
| Execution Path                                 |
+−−----------------------------------------------+
|      expert_mysql.staff                        |
|           |                                    |
|           |                                    |
|           |                                    |
|           V                                    |
|      -------------------                       |
|      |    RESTRICT     |                       |
|      -------------------                       |
|      | Access Method:  |                       |
|      |    iterator     |                       |
|      -------------------                       |
|           |               expert_mysql.tasking |
|           |                    |               |
|           |                    |               |
|           |                    |               |
|           |                    V               |
|           |               -------------------  |
|           |               |     PROJECT     |  |
|           |               -------------------  |
|           |               | Access Method:  |  |
|           |               |    iterator     |  |
|           |               -------------------  |
|           |                              |     |
|           |   ----------------------------     |
|           |   |                                |
|           V   V                                |
|      -------------------                       |
|      |      JOIN       |                       |
|      -------------------                       |
|      | Access Method:  |                       |
|      |    iterator     |                       |
|      -------------------                       |
|              |                                 |
|              |                                 |
|              V                                 |
|          Result Set                            |
+−−----------------------------------------------+
36 rows in set (0.00 sec)

Query OK, 4 rows affected (0.00 sec)
mysql >

请注意输入的每个语句的查询计划有何不同。花些时间研究其他查询语句,看看优化器如何优化其他形式的查询。

image 注意DBXP_SELECT命令的输出一开始可能看起来有点奇怪(它们是查询计划),但是回想一下之前我们在sql_dbxp_parser.cc文件中删除了DBXP_select_command()方法以重定向到DBXP_explain_select_command()方法。我们将在下一章添加查询的执行。

摘要

在本章中,我介绍了最复杂的数据库内部技术——优化器。您了解了如何扩展查询树的概念,以包含一个在优化过程中使用树结构的查询优化器。更重要的是,您发现了如何构建启发式查询优化器。启发式优化器的知识应该让您更好地理解 DBXP 引擎,以及如何使用它来更深入地研究数据库技术。没有比优化器更深入的了!

在下一章中,我将通过一个查询树优化策略的示例实现向您展示更多关于查询执行的内容。下一章将通过使用查询树类将启发式查询优化器链接到一个也使用查询树结构的执行过程来完成 DBXP 引擎。

练习

下面列出了几个需要进一步探索的领域。它们代表了您可能希望作为实验(或作为课堂作业)来探索关系数据库技术的活动类型。

  1. 完成balance_joins()方法的代码。提示:您需要创建一个可以移动合取连接的算法,以便首先执行限制性最强的连接(位于树的最底层)。
  2. 完成cost_optimization()方法的代码。提示:您需要遍历树并指出可以使用索引的节点。
  3. 检查启发式优化器的代码。它涵盖了所有可能的查询吗?如果不是,有没有其他规则可以用来完成保险?
  4. 检查查询树和启发式优化器的代码。如何实现查询树类中列出的 distinct 节点类型?提示:参见heuristic_optimization()方法中prune_tree()方法后面的代码。
  5. 如何更改代码来识别无效查询?确定查询无效的条件是什么?如何测试这些条件?
  6. (高级)MySQL 目前不支持 intersect 操作(按日期定义)。更改 MySQL 解析器以识别新的关键字并处理查询,例如SELECT * FROM A INTERSECT B。这个操作有什么限制吗,它们在优化器中有反映吗?
  7. (高级)你将如何实现GROUP BYORDER BYHAVING子句?对优化器进行更改以启用这些子句。

1 P. G .塞林格、M. M .阿斯特拉姆、D. D .钱伯林、R. A .洛里斯、T. G .普莱斯。1979."关系数据库管理系统中的访问路径选择."ACM SIGMOD 数据管理国际会议记录,苏格兰阿伯丁:23–34。被一些人认为是“查询优化的圣经”

2 M .斯通布雷克,e .黄,p .克雷普斯。1976." INGRES 的设计和实现."数据库系统上的 ACM 事务1(3):189–222。

3

4 传统系统中的查询执行不仅包括处理查询,还包括从物理介质中访问数据。然而,内存系统没有与从物理介质中检索相关联的长访问时间。

这种做法至今仍被大多数商业数据库系统所采用。

6 实时统计的累加称为捎带统计生成。

7 D .科斯曼和 k .斯托克。2000."迭代动态规划:一类新的查询优化算法." ACM 数据库系统汇刊25(1):43–82。

8 Y. E .约安尼迪斯、R. T. Ng、K. Shim 和 T. Sellis。1997.“参数查询优化。” VLDB 日报6:132–151。

9 在这种情况下,高效的执行未必是最优方案。

10 数据库新手常用的技巧。

11 可能不允许使用索引进行连接操作。

12 李政道、施振荣、陈奕奕。2001."优化包含方法的查询的图论模型."LDB 日报 9:327–343。

13 它不是一个真正的抽象类,因为它包含了一些在源代码中定义的方法。一个真正的抽象类将所有方法定义为virtual,因此它们被用作接口而不是基类。

14 这段代码中的哨兵值来源于一张经典摇滚专辑。你知道乐队的名字吗?