MySQL-专家级教程-一-

125 阅读1小时+

MySQL 专家级教程(一)

原文:Expert MySQL, 2nd Edition

协议:CC BY-NC-SA 4.0

零、简介

MySQL 被认为是世界上最流行的开源数据库,也是业内发展最快的数据库系统。本书介绍了高级数据库系统的一些主题,研究了 MySQL 的体系结构,并提供了一个专家工作簿,用于检查、集成和修改企业环境中使用的 MySQL 源代码。这本书深入探讨了如何修改 MySQL 系统,以满足系统集成商和教育者的独特需求。

这本书是给谁的

我写这本书时考虑到了各种各样的读者。无论您已经从事数据库系统工作多年,或者可能已经上过数据库理论入门课,或者刚刚读过一本关于 MySQL 的好书,您都会从这本书中学到很多。最棒的是,你甚至可以接触到源代码。如果你曾经想知道是什么让像 MySQL 这样的数据库系统运行,这就是你的书!

这本书的结构

介绍的材料分为三个部分,后面有一个附录。每一部分都旨在呈现一系列主题,从 MySQL 和开源革命的介绍材料到 MySQL 系统的扩展和定制。甚至还介绍了如何构建一个实验性的查询优化器和执行引擎,作为 MySQL 查询引擎的替代。

第一部分

这本书的第一部分介绍了开发和修改开源系统的概念。第一部分提供了开始探索本书其余部分中介绍的更高级的数据库概念所必需的工具和资源。章节包括:

  1. MySQL 和开源革命——这一章比本书的其余部分更少技术含量,包含更多叙述。它指导您了解开源系统集成商的好处和责任。它强调了 MySQL 的快速增长及其在开源和数据库系统市场的重要性。此外,它为开源革命提供了一个清晰的视角。
  2. 数据库系统的剖析——本章涵盖了什么是数据库系统以及如何构建数据库系统的基础知识。MySQL 系统的剖析被用来说明现代关系数据库系统的关键组件。
  3. MySQL 源代码之旅——本章介绍了 MySQL 源代码的完整介绍,以及如何获得和构建该系统。向您介绍了源代码的机制,以及代码维护的编码指南和最佳实践。
  4. 测试驱动的 MySQL 开发——本章介绍了为 MySQL 系统生成高质量扩展的一个关键要素。软件测试与如何测试大型系统的通用实践一起被提出。具体的例子被用来说明测试 MySQL 系统的可接受的实践。

第二部分

本书的这一部分提供了工具,使用动手的方法来研究 MySQL 系统。它向您介绍如何修改 MySQL 代码,以及如何将该系统用作嵌入式数据库系统。示例和项目说明了如何调试源代码,如何修改 SQL 命令以扩展语言,以及如何构建自定义存储引擎。

  • 5.调试——本章为您提供调试技巧和技术,帮助您简化开发,减少失败。介绍了几种调试技术,以及每种技术的优缺点。
  • 6.嵌入式 MySQL——本章将向您介绍如何在企业应用中嵌入 MySQL 系统的教程。示例项目帮助您将所展示的技能应用到您自己的集成需求中。
  • 7.向 MySQL 添加函数和命令——本章介绍了对 MySQL 代码最流行的修改。向您展示了如何修改 SQL 命令以及如何构建定制的 SQL 命令。它展示了如何修改 SQL 命令以添加新参数、函数和新命令的示例。
  • 8.扩展 MySQL 高可用性—本章概述了 MySQL 的高可用性特性,包括复制源代码和如何扩展该特性以满足高可用性需求的示例。
  • 9.开发 MySQL 插件——本章介绍了 MySQL 中的可插拔架构。您将发现如何构建插件,并看到一个身份验证插件的详细示例。
  • 10.构建您自己的存储引擎—本章演示了可插拔存储引擎的特性。我们将使用示例和项目来探究该架构,这些示例和项目允许您构建一个示例存储引擎。

第三部分

本书的这一部分对 MySQL 系统进行了更深入的研究,并从内部人士的角度向您展示了该系统的工作原理。本节首先介绍更高级的数据库技术。理论和实践以一种严肃的方式呈现,使您能够应用所获得的知识来处理数据库系统的更复杂的主题。本节还提供了如何实现内部查询表示、替代查询优化器和替代查询执行机制的示例。详细讨论了示例和项目。第 12 章到 14 章提供了改变 MySQL 系统内部结构以使用替代机制执行所需的技能和技术。这些章节为你提供了如何构建和修改大型系统的独特见解。

  • 11.hapter 旨在通过研究 MySQL 架构来展示更高级的数据库技术。主题包括查询执行、多用户问题和编程考虑。
  • 12.内部查询表示——本章介绍 MySQL 内部查询表示。为您提供了一个替代查询表示的示例。还讨论了如何修改 MySQL 源代码来实现另一种查询表示。
  • 13.查询优化——本章介绍 MySQL 内部查询优化器。我们为您提供了一个备选查询优化器示例,它使用了上一章中的备选查询表示。它讨论了如何修改 MySQL 源代码来实现替代的查询优化器。
  • 14.查询执行——本章结合了前几章的技术,为您提供了如何修改 MySQL 系统以实现替代查询处理引擎技术的指导。

附录

本书的这一部分提供了一个关于 MySQL、数据库系统和开源软件的资源列表。

使用这本书来教授数据库系统的内部知识

许多优秀的数据库教科书提供了关系理论和实践的覆盖面。然而,很少有人提供适用于教室或实验室环境的材料。可供学生探索数据库系统内部工作的资源甚至更少。这本书为教师提供了一个通过动手实验来扩充数据库课程的机会。该文本可以在课堂环境中以三种方式使用:

  1. 该文本可用于增加本科生或研究生数据库入门课程的深度。第 1 部分第 2 部分可用于提供数据库系统中特殊主题的深入报道。建议的讲座主题包括在第 2 章第 3 章第 4 章第 6 章中提出的主题。除了更传统的数据库理论或系统文本之外,还可以使用这些。动手练习或课堂项目可从第 6 章第 7 章中抽取。
  2. 本科或研究生的高级数据库课程可以基于part 12;每章可以在 8 到 12 周内完成。讲座的其余部分可以讨论物理存储层的实施和存储引擎的概念。学期项目可以基于第 10 章,让学生构建自己的存储引擎。
  3. 为高年级本科生或研究生开设的关于数据库系统内部的专题课程可以基于整篇课文,授课基于前 11 章。学期专题可以从课文的第三部分衍生而来,学生实现数据库实验平台的其余功能。这些特性包括语言理论、查询优化器和查询执行算法的应用。

约定

在整本书中,我保持了一致的风格来呈现 SQL 和结果。当文本中出现一段代码、一个 SQL 保留字或一段 SQL 时,它会以固定宽度的 Courier 字体显示,例如:

select * from dual

在讨论 SQL 命令的语法和选项时,我使用了对话式的风格,这样您可以快速理解命令或技术。这意味着我没有复制更适合参考手册的大型语法图。

下载代码

本书中给出的例子的代码可以在 press 网站www.apress.com上找到。在该书的信息页面上的源代码/下载选项卡下可以找到一个链接。该选项卡位于页面相关标题部分的下方。

联系作者

如果你有任何问题或意见——甚至是你认为我应该知道的错误——你可以打电话到drcharlesbell@gmail.com联系我。

一、MySQL 和开源革命

开源系统正在迅速改变软件领域。世界各地的信息技术专业人员都注意到了开源软件供应商提供的高质量,在许多情况下是世界级的开发和支持。公司开始注意了,因为他们第一次有了商业专有软件供应商的替代品。小企业正在关注,因为开源软件可以显著降低他们信息系统的成本。个人之所以关注,是因为他们有了比以往更多的选择。使互联网成为今天这个样子的大部分基础都是基于开源软件系统,比如 Linux、Apache HTTP server、BIND、Sendmail、OpenSSL、MySQL 等等。

企业使用开源软件最常见的原因是成本。开源软件,就其本质而言,降低了总拥有成本(TCO ),并提供了一种可行的商业模式,企业可以在此基础上建立或改善其市场。对于开源数据库系统来说尤其如此,因为商业专有系统的成本很容易达到数万或数十万美元。

对于刚刚起步的小企业来说,这笔资金支出可能会影响其成长。例如,如果一家初创公司不得不花费大量储备,它可能无法将其产品推向市场,因此可能无法在竞争激烈的市场中站稳脚跟。开源为创业公司提供了推迟购买软件的机会,直到他们能够承担投资。然而,这并不意味着他们在用劣质组件构建基础设施。

开源软件曾经被许多人认为是仅限于爱好者或黑客,他们致力于颠覆大型商业软件公司的市场。虽然一些开发者可能觉得他们在扮演微软巨人的角色,但是开源社区完全不是这样。它并不自称是商业专有软件的替代品,而是提出开源哲学作为替代。正如您将在本章中看到的,开源不仅是商业软件的一个可行的替代方案,而且它还推动了一场软件开发、发展和营销方式的世界性革命。

image 在本书中,术语“黑客”指的是理查德·斯托尔曼的定义:“喜欢编程并喜欢耍小聪明的人,” 1 而不是人们通常认为的一个一心盗窃信用卡和破坏计算机系统的邪恶恶棍。

以下部分是为那些不熟悉开源软件或 MySQL 哲学的人提供的。如果你已经熟悉开源软件的理念,你可以跳到“用 MySQL 开发”一节

什么是开源软件?

开源软件是从对公司财产心态的有意识抵制中成长起来的。20 世纪 70 年代,在麻省理工学院(MIT)人工智能实验室工作时,理查德·斯托尔曼开始了一场代码共享运动。在让所有程序员都能使用常用代码的愿望驱使下,斯托曼看到了开发者合作社区的需要。这种哲学对 Stallman 和他的小团体很有效——直到行业集体决定软件是财产,不应该与潜在的竞争者分享。这导致许多麻省理工学院的研究人员被吸引到这些公司工作。最终,合作社区逐渐消失。

幸运的是,斯托曼抵制了这种趋势,离开了麻省理工学院,开始了 GNU (GNU 不是 Unix)项目和自由软件基金会(FSF)。GNU 项目的目标是生产一个免费的类似 Unix 的操作系统。这个系统将是免费的(包括访问源代码),任何人都可以使用。“免费”的概念是不禁止任何人使用和修改系统。

斯托曼的目标是重建在麻省理工学院运作良好的开发者合作社区。然而,他有先见之明,意识到这个系统需要版权许可来保证某些自由。(有些人把斯托曼对版权的理解称为“左版权”,因为它保障了自由,而不是限制了自由。)斯托曼创建了 GNU 公共许可证(GPL)。GPL 是一个巧妙的法律许可作品,它允许代码不受限制地被复制和修改,规定衍生作品(修改后的副本)必须在与原始版本相同的许可下发布,没有任何附加限制。从本质上讲,这是通过完全去除专有元素来利用版权法对抗版权。

不幸的是,斯托曼的 GNU 项目从未完全实现,但它的几个部分已经成为许多开源系统的基本元素。其中最成功的包括用于 C 编程语言的 GNU 编译器(GCC)和 GNU 文本编辑器(Emacs)。尽管 GNU 操作系统未能完成,但斯托尔曼和他的追随者们的开拓性努力使得 Linus Torvalds 在 1991 年用他的 Linux 操作系统填补了空白,当时它还处于婴儿期。Linux 已经成为斯托曼设想的免费的类 Unix 操作系统(参见“为什么 Linux 如此受欢迎?”).今天,Linux 是世界上最流行和最成功的开源操作系统。

为什么 LINUX 这么受欢迎?

Linux 是建立在开源模式上的类 Unix 操作系统。因此,任何人都可以免费使用、分发和修改它。Linux 使用了一种保守的内核设计,这种设计已经被证明是易于发展和改进的。自从 1991 年发布以来,Linux 已经在全世界范围内赢得了寻求提高其性能和可靠性的开发者的支持。有些人甚至声称 Linux 是所有操作系统中发展得最好的。自发布以来,Linux 已经获得了世界服务器和工作站安装的巨大市场份额。Linux 经常被认为是迄今为止最成功的开源努力。

我们可以从社区中较小的团体带来的许多变体中看到 Linux 的成功。这些变体中的许多,比如 Ubuntu,都是由控制产品发展的公司(Canonical)所拥有的。虽然仍然是 Linux,但 Ubuntu 是一个很好的例子,说明所有权如何通过核心产品的增值改造来推动创新和差异化。

自由软件运动有一个问题。“免费”的本意是保证使用、修改和分发的自由,而不是免费或免费到好人家(通常解释为“免费”是言论自由,而不是免费啤酒)。为了消除这种误解,开放源码倡议(OSI)成立了,后来采用并推广了“开放源码”一词来描述 GPL 所保证的自由;在www.opensource.org访问网站。

OSI 的努力改变了自由软件运动。软件开发者有机会区分真正没有成本的自由软件和作为合作社区一部分的开放软件。随着互联网的爆炸,合作社区已经成为一个全球性的开发者社区,确保了斯托尔曼愿景的延续。

因此,开放源码软件是一种被许可的软件,它保证开发者在加入一个合作团体时使用、复制、修改和分发他们的软件的权利,该团体的自然目标是发展和培养更高质量的软件。开源并不意味着零成本。这确实意味着任何人都可以参与软件的开发,并且可以免费使用软件。另一方面,许多开源系统是由销售软件支持服务的组织托管和分发的。这允许使用该软件的组织通过消除启动成本和在许多情况下节省大量维护成本来降低他们的信息技术成本。

今天所有的开源系统都是从 Stallman 和其他人努力创造的软件乌托邦的工作基础中汲取血统,Stallman 认为组织应该通过出售服务而不是私有产权来获得收入。有几个 Stallman 的设想变成现实的例子。GNU/Linux(以下简称为 Linux)运动已经产生了许多成功的(并且盈利的)公司,比如 Red Hat 和 Slackware,它们出售定制的发行版和对 Linux 的支持。另一个例子是 MySQL,它已经成为最成功的开源数据库系统。

尽管软件乌托邦的概念在今天可能还没有成为现实,但下载一整套系统和工具来驱动个人或商业计算机而不在软件本身上花费任何钱是可能的。任何人都可以下载和使用免费版本的软件,从操作系统和服务器系统(如数据库和 web 服务器)到生产力软件。

为什么要使用开源软件?

迟早有人会问为什么使用开源软件是个好主意。为了成功抵御来自商业专有软件支持者的挑战,你应该有一个可靠的答案。采用开源软件的最重要原因是:

  • 开源软件的使用成本很低甚至为零。这对于非营利组织、大学和社区组织来说尤其重要,因为它们的预算在不断缩减,每年都必须以更少的资源做更多的事情。
  • 您可以修改来满足您的特定需求。
  • 可用的许可机制比商业许可更加灵活。
  • 开源软件比商业专有软件更健壮(经过测试)。
  • 开源软件比商业专有软件更加可靠和安全。

虽然你可能不会被质疑或被要求证明采用开源软件的任何理由,但你可能会受到矛盾的挑战——也就是说,商业专有软件的支持者(开源的反对者)会试图通过陈述为什么不应该使用开源软件进行开发来质疑这些说法。让我们从商业专有软件的角度来研究一些不使用开源软件的更流行的理由,并用开源观点来反驳它们。

神话 1:商业专有软件培养更大的创造力

理由是:大多数企业级商业专有软件都提供了应用编程接口(API ),允许开发者扩展其功能,从而使软件更加灵活,并确保开发者有更大的创造力。

这其中有些是真的。API 确实允许开发者扩展软件,但他们通常以严格禁止开发者向基础软件添加功能的方式来扩展软件。这些 API 经常迫使开发者进入沙箱,进一步限制了她的创造力。

例如,微软。Net 语言 C#被公认为是一门非常好的语言。然而,API 不容易修改。事实上,只有在安装宿主产品 Visual Studio 时,才会收到二进制形式的库。您可以用类派生来扩充 API,但是严格地说,您不能编辑 API 本身的源代码。

image 注意沙箱经常被创建来限制开发者影响核心系统的能力,很大程度上是为了安全。API 越开放,邪恶的开发者就越有可能创建恶意代码来破坏系统或其数据。

开源软件也可能支持并提供 API,但它为开发者提供了查看核心系统实际源代码的能力。他们不仅可以看到源代码,还可以自由地(并被鼓励)修改它!(例如,当一个关键特性不可用时,或者您需要系统读取或写入特定格式时,您可能想要修改核心系统。)因此,开源软件比商业专有软件更能培养创造力。

误区二:商业专有软件比开源软件 更安全

理由是:在当今互联网连接的社会中,组织要求他们的信息系统比以往任何时候都更安全。商业专有软件本质上更安全,因为销售软件的公司在确保他们的产品能够抵御当今数字掠夺者的攻击方面有更大的利益。

尽管这一声明的目标很可能作为任何商业软件供应商的口头禅出现在会议室的墙上,但这一目标的实现,或者在某些情况下的营销声明,通常是误导性的或无法实现的。

研究表明,开源软件开发的本质有助于使软件更加安全,因为根据定义,开源软件是由一个团体和一个社区开发的,他们有兴趣为产品寻找最好的东西。事实上,对源代码的严格审查和开放确保了没有任何东西可以隐藏起来,无论是缺陷还是遗漏。因为源代码对所有人都是可用的,所以强化他的代码符合每个开源开发者的最大利益——不管是恶意的还是良性的。

误解 3:商业专有软件被测试的次数比开源软件多

说法是:软件商卖软件。他们出售的产品必须保持高质量标准,否则顾客不会购买。开源软件没有任何这样的压力,因此没有像商业专利软件那样严格测试。

这个论点很有说服力。事实上,它打动了所有信息技术收购代理人的心。他们相信,你花钱买的东西比免费获得的软件更可靠,没有缺陷。不幸的是,这些人忽略了开源软件的一个重要概念:它是由全球开发者社区开发的,其中许多人认为自己是缺陷侦探(测试员),并以发现和报告缺陷为荣。在某些情况下,开源软件公司为发现可重复错误的开发者提供奖励。

诚然,软件供应商雇佣软件测试人员(毫无疑问,他们是他们领域中的佼佼者),但更多的时候,商业软件项目被推向一个特定的截止日期,并从公司目标的角度关注产品的好处——通常由营销机会驱动。设置这些截止日期是为了确保战略发布日期或竞争优势。很多时候,这些截止日期迫使软件供应商在他们软件开发过程的某些部分上妥协——通常是后面的部分:测试。可以想象,减少测试人员对软件的访问(测试时间)意味着他们会发现更少的缺陷。

开源软件公司通过获得全球开发者社区的帮助和支持,确保他们的软件被更多的人更频繁地测试,这些人只考虑产品本身的好处,而不是通常被可能影响他们审查软件的能力的目标所驱动。事实上,一些开源社区成员在评估一个新特性或版本时有时会毫不留情。相信我,如果没有达到他们的期望,他们会让你知道的。

误解 4:商业专有系统比开源系统 拥有更复杂的功能和更完整的特性集

论点是:商业专有数据库系统是复杂的服务器系统。开源系统既不够大,也不够复杂,无法处理关键任务企业 数据。

尽管一些开源系统很好地模仿了它们所模仿的商业系统,但对于像 MySQL 这样的数据库系统来说就不一样了。MySQL 的早期版本不具备商业专有数据库系统中的所有功能,但从 5.0 版本开始,尤其是最新版本,MySQL 包含了一些主要功能,被认为是世界上最受欢迎的开源数据库系统。

此外,MySQL 已被证明能够提供大型企业对关键任务数据所需的可靠性、性能和可伸缩性,许多知名组织都在使用它。MySQL 是一个开源系统,它提供了最好的商业专有数据库系统的所有特性和功能。

误解 5:商业专有软件供应商反应更快,因为他们有专门的员工

论点是这样的:当一个软件系统被购买时,该软件附带了保证,即生产它的公司将提供帮助或帮助解决问题。因为没有人“拥有”开源系统,所以获得帮助要困难得多。

大多数开源软件都是由全球开发者社区构建的。然而,日益增长的趋势是将商业模式建立在开源哲学的基础上,并围绕它建立一家公司,为该公司监管的软件销售支持和服务。大多数主要的开源产品都以这种方式得到支持。例如,甲骨文公司拥有其 MySQL 产品的源代码。(关于甲骨文 MySQL 开源许可的完整描述,请参见www . MySQL . com/company/legal/licensing/opensource-license . htm。)

开源软件的开发者对问题的反应比商业开发者要快得多。事实上,许多人对公开他们的产品感到非常自豪,并密切关注世界对他们的看法。另一方面,与商业软件开发者直接对话几乎是不可能的。例如,微软有一个全面的支持机制,可以满足几乎任何组织的需求。然而,如果你想与微软产品的开发者交谈,你必须通过适当的渠道。这需要与支持层级的每一个阶段进行沟通——即使这样,你也不能保证与开发者取得联系。

另一方面,开源开发者使用互联网作为他们主要的交流方式。由于他们已经在互联网上,他们更有可能在论坛或新闻组中看到你的问题。此外,像 Oracle 这样的开源公司积极地监控他们的社区,并且可以快速地响应他们的客户。

因此,购买商业专有软件并不能保证你的响应时间比开源软件更快。在许多情况下,开源软件开发者比商业软件开发者反应更快(更容易接触到)。

如果他们想要证据呢?

当你试图在你的组织中采用开源软件时,这些仅仅是可能导致你悲伤的一些争论。一些研究人员试图证明开源软件比商业软件更好。一个是 James W. Paulson,他对开放源代码和商业专有软件(他称之为“封闭的”)进行了一项实证研究,该研究检验了前面的论点,并证明了开放源代码软件开发可以证明相对于商业专有软件开发的可测量的改进。参见 2004 年 4 月出版的 IEEE 软件工程汇刊中 Paulson 的文章“开源和闭源软件产品的实证研究”。

开源真的是商业软件的威胁吗?

直到最近,开源软件还不被认为是对商业专有软件巨头的威胁,但甲骨文的一个竞争对手开始展示出经典的 应对竞争威胁的迹象。尽管微软最近做出了开放的努力 2 ,微软继续公开反对开源软件,谴责 MySQL 是世界一流的数据库服务器,同时消极地忽视威胁,甲骨文却采取了一种截然不同的策略。

自从通过收购 Sun Microsystems 获得 MySQL 以来,Oracle 继续投入大量资源来增强 MySQL。Oracle 已经并将继续投资于开发,以使 MySQL 成为世界上最好的 web 数据库系统。

竞争的压力不仅限于 MySQL 和专有数据库系统。至少有一个开源数据库系统 Apache Derby 将自己标榜为 MySQL 的替代者,并且最近将其帽子扔进了环中,作为 LAMP 栈中“M”的替代者(参见“什么是 LAMP 栈?”).Apache Derby 的支持者列举了 MySQL 的许可问题和功能限制。既没有阻止 MySQL 的安装基础,也没有这些“问题”限制 MySQL 的日益普及。

什么是灯栈?

LAMP 代表 Linux,Apache,MySQL,以及 PHP/Perl/Python 。LAMP stack 是一组开源服务器、服务和编程语言,允许快速开发和部署高质量的 web 应用。关键组件包括

  • Linux :类似 Unix 的操作系统。Linux 以其高度的可靠性和速度以及支持的硬件平台的多样性而闻名。
  • Apache :以高可靠性和易于配置著称的 web 应用服务器。Apache 运行在大多数 Unix 操作系统上。
  • MySQL :许多 web 应用开发者的首选数据库系统。MySQL 以其速度和较小的执行占用空间而闻名。
  • PHP/Perl/Python :这些是脚本语言,可以嵌入到 HTML 网页中,用于事件的编程执行。这些脚本语言代表 LAMP 堆栈的活动编程元素。它们用于连接系统资源和后端数据库系统,为用户提供活动内容。虽然大多数 LAMP 开发者更喜欢 PHP 而不是其他脚本语言,但是每种语言都可以用来成功地开发 web 应用。

使用灯堆栈进行开发有很多好处。最大的是成本。所有 LAMP 组件都可以作为免费的开源许可证获得。组织可以在几个小时内下载、安装和开发 web 应用,而软件的初始成本很少或没有。

提供开源数据库系统的好处的一个有趣的指标是最近一些专有数据库供应商提供的“免费”版本。微软一直是开源软件的公开反对者,现在它提供了一个名为 SQL Server Express 的免费版本的 SQL Server 数据库系统。尽管下载该软件是免费的,并且您被允许将该软件与您的应用一起分发,但您可能看不到源代码或以任何方式修改它。该版本的功能集有限,如果不购买额外的软件和服务,就无法扩展到完整的企业级数据库服务器。

显然,甲骨文公司以其 MySQL 服务器产品开辟的道路证明了对专有数据库市场的威胁——商业专有软件行业正在认真对待这一威胁。尽管微软继续试图削弱开源软件市场,它也开始看到免费软件的智慧。

法律问题和 GNU 宣言

商业专有软件许可证旨在限制您的自由和使用。大多数商业许可证都明确声明,作为软件的购买者,您并不拥有该软件,但可以在非常特定的条件下使用它。在几乎所有情况下,这意味着您不能以任何方式复制、分发或修改系统。这些许可证也清楚地表明,源代码是由许可方独家拥有的,您,即被许可方,不允许查看或重新设计它。

image 注意本节是对通用公共许可证的一般性讨论。软件提供商通常有自己的许可证形式,并可能以微妙但不同的方式解释其合法性。请始终联系软件提供商,以澄清您希望行使的许可证的任何部分。如果您希望在自己的产品或服务中修改或包含软件的任何部分,这一点尤其正确。

开源系统通常使用基于 GNU 的许可协议(GNU 代表 GNU,而不是 Unix)授权,称为通用公共许可证(GPL)。更多详情见www.gnu.org/licenses/。大多数 GPL 许可证允许自由使用原始源代码,但有一个限制,即所有修改都必须公开或作为合法所有权返还给创作者。此外,大多数开放源码系统使用 GPL 协议,该协议声明它旨在保证您复制、分发和修改软件的权利。请注意,GPL 并不限制您使用软件的权利;事实上,它特别授予你以你想要的方式使用软件的权利。GPL 也保证了你访问源代码的权利。所有这些权利都在 GNU 宣言和 GPL 协议(【www.gnu.org/licenses/gp…

最有趣的是,GPL 特别允许你对原始源代码的发布收取发布费(或媒体费),并为你提供使用整个系统或修改系统的权利,以创建衍生产品,这也受同一 GPL 的保护。唯一的问题是,您需要将修改后的源代码提供给任何想要它的人。

这些限制并不妨碍你通过努力工作获得收入。相反,只要你通过原始所有者发布你的源代码,你就可以向你的客户收取衍生作品的费用。有些人可能会争辩说,这意味着你永远无法获得真正的竞争优势,因为你的源代码对每个人都是可用的,但在实践中恰恰相反。Canonical、Red Hat 和 Oracle 等供应商已经从基于 GPL 的商业模型中获利。

GPL 的唯一限制可能会让你犹豫,那就是对保证的限制,以及要求在你的软件中放置一个横幅说明作品的来源(原始和许可)。

如果你考虑到大多数商业许可都包含类似的条款,那么对明示担保的限制就不足为奇了。GPL 的独特之处在于无责任损失的概念。GPL 明确免除了创作者和您,即修改者(或发布者)因安装或使用软件而导致的损失或损害。Stallman 不希望法律行业因开源软件的责任问题而获利。逻辑很简单。您免费获得了该软件,并且您没有获得任何关于其性能或保护其免受因使用而造成的损害的保证。在这种情况下,没有交换条件,因此没有任何形式的保证。

开源运动的反对者将引用这一点作为避免开源软件的理由,声称这是“使用风险自担”,因此引入了太多的风险。虽然这是事实,但是当您从开源供应商那里购买支持时,这一论点就被削弱或无效了。开源供应商的支持选项通常包括某些责任权利和进一步的保护。这也许是购买开源软件支持的最有说服力的理由。在这种情况下,有交换条件,而且在许多情况下有可靠的保证。

在你的软件中一个可见的地方放置一个横幅的需求并不是那么繁重。GPL 只需要清楚地声明软件的来源和出处,并标记软件受 GPL 保护。这将告知使用本软件的任何人他们使用、复制、分发和修改本软件的权利(自由)。

也许 GNU 宣言中最重要的声明是“GNU 将如何可用”下面的陈述。在这一节中,宣言声明尽管每个人都可以修改和重新发布 GNU,但是没有人可以限制它的进一步重新发布。这意味着没有人可以把一个基于 GNU 宣言的开源系统变成一个专有系统或者进行专有修改。

属性

如果不包括财产问题,关于开放源码软件许可的讨论将是不完整的。财产仅仅是被拥有的东西。虽然通常认为财产是有形的东西,但在软件的情况下,这个概念变得有问题。当我们说软件是财产时,到底是什么意思?属性的概念适用于源代码、二进制文件(可执行文件)、文档,还是所有这些?

当谈到开源软件时,财产的概念通常是一个棘手的话题。如果软件是由全球开发者社区开发的,谁是所有者?在大多数情况下,开源软件始于某人或某个组织开发的项目。当软件足够成熟,对其他人有用时,项目就变成了开源。无论这是在早期阶段,当软件不精炼时,还是在后期,当软件达到一定的可靠性水平时,都不重要。重要的是启动项目的人或组织被认为是所有者。以 MySQL 为例,Oracle 公司发起了这个项目,因此它拥有 MySQL 系统。

根据 MySQL 坚持的 GPL,Oracle 拥有所有的源代码和在 GPL 下所做的任何修改。GPL 给了你修改 MySQL 的权利,但是它没有给你将源代码作为你的财产的权利。

甲骨文真的拥有 MYSQL 吗?

MySQL 作为一个组织进化的详细历史超出了本书的范围。MySQL 品牌、产品及其开发组织完全归甲骨文公司所有。作为 2010 年 1 月执行的 Sun Microsystems 合并的一部分,甲骨文收购了 MySQL。

尽管在欧洲有一些反垄断的争议,但合并是成功的,甲骨文承诺继续发展和演变 MySQL。到目前为止,Oracle 已经实现了这些承诺,并继续将 MySQL 发展成为世界领先的开源数据库系统。Oracle 继续像过去一样定位 MySQL:web 数据库(LAMP 中的 M)。

自收购以来,Oracle 发布了几个 MySQL 版本,包括更好的性能、集成 InnoDB 作为默认数据库、Windows 平台改进以及对复制的大量改进和创新,从而实现了高可用性功能。甲骨文确实是 MySQL 的所有者,也是迄今为止最大的保管者。

伦理的一面

当你第一次开始使用开源软件时,道德困境比比皆是。例如,开源软件可以免费下载,但你必须将你的任何改进交给原所有者。你怎么能从你不得不放弃的东西中赚钱呢?

要理解这一点,您必须考虑 Stallman 在开发 GNU 许可证模型时的目标:在全世界的开发者中建立一个合作和团结的社区。他希望源代码公开,生成的软件免费供任何人使用。你工作挣钱(获得报酬)的权利不受限制。你可以出售你的衍生作品。你不能声称拥有源代码的所有权。你在伦理上(和法律上!)必将回馈全球开发者社区。

当你修改开源软件供自己使用时,另一个道德困境出现了。例如,您下载了最新版本的 MySQL,并添加了一个特性,允许您对 SQL 命令使用自己的缩写快捷方式,因为您已经厌倦了键入冗长的 SQL 语句(我相信有人已经这样做了)。在这种情况下,你正在以一种只有利于你自己的方式修改系统。那么你为什么要交出你的修改呢?虽然这种困境对我们大多数人来说可能不是问题,但如果你继续使用带有你个人修改的软件并最终创作出一个衍生作品,它可能会成为你的问题。基本上,你所做的任何生产性的和有意义的修改必须被认为是原创者的财产,不管它的使用或使用的限制。

如果您将修改源代码作为一项学术练习(然而,我将在本书后面告诉您如何做),您应该在完成练习或实验后放弃修改。一些开源软件为这些类型的使用做了准备。大多数人认为探索和试验源代码是对软件的“使用”,而不是修改,因此允许在学术研究中使用源代码。

让革命继续!

自由的理念驱使理查德·斯托尔曼开始寻求改革软件开发。虽然自由是开源运动的催化剂,但它已经成为一场革命,因为组织现在可以通过投资低成本软件系统来避免在竞争对手手中过时,同时保持收入以在市场中竞争。

采用开源软件作为产品线一部分的组织可能是所有组织中最具革命性的。大多数已经采用了基于 GPL 的商业模式,这种模式允许他们获得开源系统带来的所有经验和健壮性,同时仍然为他们自己的想法和附加内容创造收入。

软件行业对开源软件既嗤之以鼻,又赞不绝口。一些人鄙视开源,因为他们认为这是对商业专有软件行业的攻击。他们还声称开源只是一种时尚,不会长久。他们认为生产、贡献或使用开源软件的组织已经过时了,并相信世界迟早会醒悟并忘记开源软件。有些人并不轻视开源,因为他们看不到盈利的可能性,因此认为这个想法是徒劳的。

另一些人将开源软件视为救世主,将我们从商业专有软件的暴君手中拯救出来,他们相信大型软件公司迟早会被迫将他们的产权模式转变为开源或其某种变体。真相大概在中间。我认为开源行业是一个充满活力、不断发展的行业,由志同道合的人组成,他们的目标是创建安全、可靠、健壮的软件。他们通过提供基于和支持开源软件的服务来赚钱。有时是通过许可或支持销售,有时是通过定制和咨询。

无论是哪种方法,好的开源软件都可以独立成为一门生意。同样,无论你的观点如何,你必须得出结论,开源运动已经在各地的软件开发者中引起了一场革命。

现在你已经对开源革命有了一个很好的介绍,你可以决定你是否同意它的理念。如果你这样做了(我真诚地希望我已经说服了你),那么欢迎来到全球开发者社区。革命万岁!

用 MySQL 开发

您已经了解了什么是开源软件,以及使用和开发开源软件的法律后果。现在您将学习如何使用 MySQL 开发产品。正如您将会看到的,MySQL 为开发者提供了一个独特的机会,他们可以利用主要的服务器软件技术,而不必将他们的开发局限于一组固定的规则或有限的 API 套件。

MySQL 是一个关系数据库管理系统,设计用于客户机/服务器架构。MySQL 也可以作为嵌入式 数据库库。当然,如果您以前使用过 MySQL,您会熟悉它的功能,并且毫无疑问会选择 MySQL 来满足您的部分或全部数据库需求。

在系统的最底层,服务器是使用 C 和 C++混合编写的多线程模型构建的。这个核心功能大部分是在 20 世纪 80 年代早期构建的,后来在 1995 年用结构化查询语言(SQL)层进行了修改。MySQL 是使用 GNU C 编译器(GCC)构建的,它为目标环境提供了极大的灵活性。这意味着 MySQL 可以在任何 Linux 操作系统上编译使用。甲骨文在开发微软视窗和麦金塔操作系统的变体方面也取得了相当大的成功。MySQL 的客户端工具大部分是用 C 编写的,以获得更好的可移植性和速度。客户端库和访问机制可用于。NET、Java、ODBC 和其他一些语言。

这个++是什么意思?

有一次,当我还是大学生的时候。我旁听了一门 C++课程,主要是作为学习这门语言的动力。我发现学习一门新的编程语言是徒劳的,如果没有掌握这门语言的动机——比如及格。第一天上课,一个学生(不是我)问老师++代表什么。他的回答是,“额外的东西。”基于这个异想天开且不完全符合历史的答案,以及 MySQL 源代码有真正的 C 和 C++的事实,它更像 C+/-而不是 C 或 C++。C++最初被它的创建者命名为“C with classes ”,但后来在 1983 年改为 C++,对增量运算符使用了一个糟糕的双关语。换句话说,C++就是进化加法 3 的 C。

MySQL 是使用并行开发路径构建的,以确保在计划和开发软件的新版本时,产品线继续发展。软件开发遵循一个分阶段的开发过程,在这个过程中,每个阶段都会产生多个版本。MySQL 开发过程的各个阶段是:

  1. 开发—新产品或功能集作为开发树的新路径被计划和实现。
  2. Alpha—实现了特性细化和缺陷修正(bug 修复)。
  3. Beta—特性被“冻结”(不能添加新特性),并实施额外的密集测试和缺陷修正。
  4. 发布候选版本——没有重大缺陷的稳定 beta 状态,代码被冻结(只有缺陷可能被修复)并进行最后几轮测试。
  5. 一般可用(GA)—如果没有发现重大缺陷,代码被声明为稳定的,并准备好生产发布。

在这些阶段中的任何一个阶段,您都会经常看到各种版本的 MySQL 软件。通常情况下,只有 beta 版、候选版和正式版可供下载,但根据功能的重要性或支持订阅提出的功能请求的状态,alpha 版也可能提供。

当某个特定功能代表现有功能的重大变化或显著改进某个特定功能时,可能会在 dev.mysql.com 上提供实验室版本。一个实验室版本被认为是该特性的预览版,因此,它是用于评估目的的。通常,实验室发布的文档很少或没有。事实上,甲骨文在实验室网站上声明,“不适合生产[使用]。”你可以从 labs.mysql.com/下载实验报告。

当一组特性代表了功能或性能的重大进步时,可能会提供一个开发里程碑版本(DMR) 。DMR 可能具有处于不同发展阶段的几个特征。一个 DMR 软件可能有大部分功能处于测试状态,但也有一些功能处于测试状态,甚至是接近发布候选状态。因此,DMR 是跟踪和准备采用 MySQL 开发中的主要进展的关键方法。你可以在 dev.mysql.com/downloads/m…找到 DMRs。

你可以在 http://dev . MySQL . com/doc/MySQL-development-cycle/en/index . html 上阅读更多关于 MySQL 开发、实验室发布和 DMRs 的信息。

并行开发策略允许 Oracle 在开发新功能的同时维护其当前版本。当开发和缺陷修复在 5.5 中继续时,在 5.6 中读到新的特性是很常见的。这似乎令人困惑,因为我们已经习惯了商业专有软件供应商对他们自己的开发策略保密。MySQL 版本号用于跟踪版本;它们包含一个由两部分组成的产品系列号和一个版本号。例如,版本 5.6.12 是 5.6 产品线的第十二个版本。

image 提示与 Oracle 通信时,始终包含完整的版本号。仅仅陈述“alpha 版本”或“最新版本”是不够清楚的,不足以恰当地满足您的需求。

这种多次发布的理念有一些有趣的副作用。遇到使用旧版本 MySQL 的组织并不罕见。事实上,我遇到过几个与我合作的机构,他们仍然在使用 4.x 版本的产品线。这一理念实际上消除了商业专有软件所经历的升级 Shell 游戏。也就是说,每当供应商发布一个新版本,它就停止对旧版本的开发,在许多情况下停止对旧版本的支持。随着主要架构的改变,客户被迫相应地改变他们的环境和开发工作。这增加了维护基于商业专有软件的产品线的大量成本。多版本哲学通过允许组织将他们自己的产品在流通中保持更长的时间,并保证持续的支持,将组织从这种负担中解放出来。即使出现新的架构变化,如 MySQL 版,组织也有更长的准备时间,因此可以以最有效的方式消耗资源,而不必匆忙或改变长期计划。

虽然您可以下载任何版本的 MySQL,但首先要考虑您对该软件的使用。如果您计划在自己的生产环境中将它用作企业服务器,您可能希望将下载限制在产品线的稳定版本上。另一方面,如果您正在使用 LAMP 栈或另一个开发环境构建一个新的系统,那么任何其他的发布阶段都可以用于开发工作。大多数用户将下载他们打算在其环境中使用的最新版本的稳定版本。

我应该使用这本书的哪个版本?

就本书中的练习和实验而言,MySQL 5.6 的任何版本(阶段)都适用。MySQL 5.6 是 MySQL 发展过程中的一个重要里程碑,不仅因为它的高级特性和性能改进,还因为它在架构和源代码方面的重大变化。虽然本书的某些部分可能适用于 5.1 或 5.5 版本(例如,添加新功能),但大多数示例都是针对 5.6 版本的。

对于任何新的开发,Oracle 建议使用最新的稳定版本。这意味着,如果您计划向 MySQL 添加功能,并且您正在参与全球开发者社区,那么您应该在服务器最稳定的时候向其添加新功能。这为您的代码获得成功提供了最大的机会。

还要考虑到,虽然版本的阶段可能表明了它相对于新特性的状态,但是您不应该自动地将不稳定性与早期阶段相关联,或者将稳定性与后期阶段相关联。根据您对软件的使用,稳定性可能会有所不同。例如,如果您正在开发中使用 MySQL 在 LAMP stack 中构建一个新的电子商务站点,并且您没有使用 alpha 阶段引入的任何新特性,那么您使用的稳定性实际上与任何其他阶段都是一样的。最好的经验是选择具有您在最新开发阶段需要的特性的版本。

克隆人战争?

如果你花时间研究 MySQL,你很可能遇到过至少一个声称是 MySQL 的衍生物的数据库系统。虽然一些供应商提供了 MySQL 的变体(有时称为 fork 或 port)——一些由小型创业公司支持,一个由 MySQL 的原始开发者支持——但这些供应商中没有一个能够与 Oracle 培育 MySQL 的专业知识和开发能力相媲美。

你可能会遇到一些变体,也许你可以用在这本书上。然而,考虑到 MySQL 在最近版本中的主要进步,您最好使用官方的 MySQL 源代码来探索源代码。一些变体最终可能会包含最新的功能,但目前它们不太可能与 Oracle 的版本保持同步。提供变体的供应商包括 MariaDB、SkySQL 和 Percona。

为什么要修改 MySQL?

修改 MySQL 不是一件小事。如果您是一名经验丰富的 C/C++程序员,并且理解关系数据库系统的构造,您可能会马上投入其中。我们其余的人需要考虑为什么要修改数据库服务器系统,并仔细计划我们的修改。

你想修改 MySQL 的原因有很多。也许您需要一个不可用的数据库服务器或客户机功能。或者,您可能有一个定制的应用套件,它需要特定类型的数据库行为,而不是必须适应商业专有系统,对您来说,修改 MySQL 来满足您的需求更容易、更便宜。最有可能的情况是,您的组织负担不起复制 MySQL 数据库系统的复杂性和精细化,但是您需要一些东西作为您的解决方案的基础。使您的应用成为世界级的,还有什么比基于世界级的数据库系统更好的方法呢?

image 如果一个特性真的有用,有人认为它有益,开源的好处在于这个特性会融入到产品中。某个地方的某个人会贡献并构建这个特性。

像所有有效的软件开发者一样,你必须首先计划好你要做什么。从您最熟悉的计划设备和材料开始,列出您认为需要数据库服务器(或客户机)做的所有事情。花些时间评估 MySQL,看看是否有你想要的特性已经存在,并记录下它们的行为。完成这项研究后,你会对差距所在有更好的了解。这种“差距分析”将为您提供一个集中的特性和所需修改的列表。一旦确定了需要添加的特性,就可以开始检查 MySQL 源代码并尝试添加新特性。

image 警告在计划修改时,一定要彻底调查当前的 MySQL 特性。您将需要检查和试验与您的需求相似的所有 SQL 命令。虽然您可能无法使用当前的功能,但是检查现有的功能将使您能够形成一个已知行为和性能的基线,您可以用它来比较您的新功能。可以肯定的是,全球开发者社区的成员将会仔细检查新的特性,并删除那些他们认为使用当前特性可以最好地实现的特性。

这本书将向您介绍 MySQL 源代码,并教您如何添加新功能,以及改变什么(和不改变什么)的最佳实践。

后面的章节还将详细介绍您获取源代码的选项,以及如何将您的更改合并到适当的代码路径(分支)中。您还将了解 Oracle 编码指南的详细内容,这些指南规定了您的代码应该是什么样子,以及您应该避免哪些代码结构。

在 MySQL 中可以修改什么?有限制吗?

开源软件的美妙之处在于,您可以访问该软件的源代码(由各自的开源许可证保证)。这意味着你可以访问整个软件的所有内部工作。你有没有想过一个特定的开源软件产品是如何工作的?您可以通过下载源代码并研究它来找到答案。

有了 MySQL,事情就没那么简单了。MySQL 中的源代码非常复杂,在某些情况下,很难阅读和理解。可以说代码的可理解性很低。原始开发者通常认为源代码具有“天才因素”,即使对于最好的 C/C++程序员来说,这也是一个挑战。

虽然 C/C++代码的复杂性挑战可能是一个问题,但它决不会限制您修改软件的能力。大多数开发者修改源代码来添加新的 SQL 命令或改变现有的 SQL 命令,以更好地满足他们的数据库需求。然而,机会远不止简单地改变 MySQL 的 SQL 行为。您可以更改优化器、内部查询表示,甚至查询缓存机制。

你可能遇到的一个挑战不会来自你的任何一个开发者;它可能来自你的高级技术涉众。例如,我曾经对 MySQL 源代码进行了重大修改,以解决一个具有挑战性的问题。组织中的高级技术涉众质疑我的项目的有效性,不是因为解决方案或它的设计,而是因为我正在修改服务器代码本身的基础。一个利益相关者坚持认为我的改变“违背了 30 年来的数据库理论,并尝试了真正的实现。”我当然希望你永远不要遇到这种行为,但是如果你遇到了,并且你已经研究过哪些功能是可用的,它们如何不能满足(或部分满足)你的需求,你的答案应该包含无可争议的事实。如果你确实得到了这个问题或者类似的问题,提醒你的高级技术涉众开源软件的优点是它可以被修改,而且经常被修改。你也可以考虑解释一下你的新特性是做什么的,以及它将如何为每个人改善整个系统。如果你能做到这一点,你就能度过难关。

在修改 MySQL 时,你可能面临的另一个挑战是“为什么要用 MySQL?”专家们会很快指出有几个开源数据库系统可供选择。最流行的是 MySQL、Firebird、PostgreSQL 和 Berkeley DB。在开发项目中选择 MySQL 而不是其他数据库系统的原因包括:

  • MySQL 是一个支持全套 SQL 命令的关系数据库管理系统。一些开源数据库系统,如 PostgreSQL,是对象关系数据库系统,使用 API 或库进行访问,而不是接受 SQL 命令。一些开源系统使用的架构可能不适合您的环境。例如,Apache Derby 基于 Java,可能无法为您的嵌入式应用提供最佳性能。
  • MySQL 是使用 C/C++构建的,它可以为几乎所有的 Linux 平台以及 Microsoft Windows 和 Macintosh 操作系统构建。有些开源系统可能不适合您选择的开发语言。如果您必须将系统移植到您正在运行的 Linux 版本,这可能是一个问题。
  • MySQL 被设计成客户机/服务器体系结构。一些开源系统不可扩展到基于客户端的嵌入式系统之外。例如,Berkeley DB 是一组客户端库,而不是一个独立的数据库系统。
  • MySQL 是一个成熟的数据库服务器,拥有世界领先的数据库系统所拥有的经过验证的稳定性记录。一些开源数据库系统可能没有 MySQL 的安装基础,或者可能不提供您在企业数据库服务器中需要的特性。

很明显,对于开发需求和修改发生的环境来说,挑战是独特的。无论您的需求是什么,您都可以确保您可以完全访问所有的源代码,并且您的修改只受您的想象力的限制。

MySQL 的双许可证

MySQL 被授权为 GPL 下的开源软件。服务器和客户端软件以及工具和库都包含在 GPL 中。Oracle 已经将 GPL 作为其商业模式的一个主要焦点。它坚定地致力于 GNU 开源社区。

image 提示【MySQL 的完整 GPLv2 许可文本可以在http://dev . MySQL . com/doc/ref man/5.6/en/license-GNU-GPL-2-0 . html找到。如果你打算修改 MySQL 或者你以前从未见过 GPL 许可,请仔细阅读。如果您对如何解释您使用的许可证有任何疑问,请联系 Oracle。

Oracle 通过向全球开发者社区公开其源代码获得了许多好处。源代码由公众定期审查评估,第三方组织定期审核源代码,开发过程促进了公开交流和反馈的论坛,源代码在许多不同的环境中编译和测试。没有其他数据库供应商能够在保持世界级的稳定性、可靠性和功能的同时做出这样的声明。

MySQL 也作为商业产品获得许可。商业许可证允许 Oracle 拥有源代码(如前所述)以及名称、徽标和文档(如书籍)的版权。这是独一无二的,因为大多数开源公司并不自称拥有任何东西——相反,他们的知识产权是他们的经验和专业知识。Oracle 保留了该软件的知识产权,同时利用全球开发者社区的支持来扩展和发展该软件。甲骨文拥有自己的 MySQL 开发团队,在全球拥有 100 多名工程师。尽管来自世界各地的开发者参与了 MySQL 的开发,但 Oracle 雇佣了其中的许多人。

自由开放源码例外

甲骨文的自由/开源软件例外条款允许在应用中使用 GPL 许可的客户端库,而不要求衍生作品受 GPL 约束。如果您正在开发使用 MySQL 客户端库的应用,请查看 MySQL FOSS 异常以了解完整的详细信息。

www.mysql.com/about/legal…

Oracle 提供了服务器的几个主要 MySQL 版本。大多数是商业产品,可能没有相应的 GPL 版本。例如,虽然您可以下载 MySQL Cluster 的 GPL 版本,但您不能下载 MySQL Cluster 运营商级版本 的商业版本。表 1-1 总结了 Oracle 目前提供的各种服务器版本(撰写本章时)及其基本许可费用。

表 1-1 。MySQL 服务器产品和定价

Table1.1

****Costs shown are representative as of the publication of this work. Contact Oracle for accurate and up-to-date pricing.

image 提示mysql.com/buy-mysql/了解有关甲骨文定价和购买选项的更多信息

.

那么,到底能不能修改 MySQL 呢?

在讨论了在 GNU 公共许可证下使用开源软件的局限性之后,您可能想知道,您是否真的可以修改它。答案是:看情况。

您可以在 GPL 下修改 MySQL,当然,前提是如果您打算发布您的更改,您必须将这些更改交给项目所有者,从而履行您参与全球开发者社区的义务。如果您出于个人或教育目的试验或使用修改,您没有义务交出您的更改。

问题的核心归结于修改的好处。如果你增加了别人感兴趣的能力,而不是你自己,你应该分享它们。无论在何种情况下,在进行任何想要共享的修改之前,请务必咨询 Oracle。

修改 MySQL 的指南

在处理诸如修改 MySQL 等系统的任务时要小心。关系数据库系统是一组复杂的服务,这些服务以提供快速、可靠的数据访问的方式分层。您不希望打开源代码,插入自己的代码来看看会发生什么(但是欢迎您尝试)。相反,计划您的更改,并仔细瞄准与您的需求相关的源代码部分。

在修改了 MySQL 这样的大型系统之后,我想传授一些简单的指导原则,让你在修改 MySQL 时有一个积极的体验。

首先,决定您将使用哪个许可证。如果您已经在开源许可下使用 MySQL,并且可以自己实现修改,请继续使用 GPL。在这种情况下,你必须延续开源咒语,并回馈社区以换取免费提供的东西。根据 GPL 的条款,开发者必须做出这些改变。如果您在商业许可下使用 MySQL 或需要修改支持,请购买合适的 MySQL 版本以匹配您的服务器(CPU 核心数),并就您的修改咨询 Oracle。但是,如果您不打算分发这些修改,并且您可以在 MySQL 的未来版本中支持它们,那么您不需要更改为商业许可或者将您的商业许可更改为 GPL。

另一个建议是创建一个开发者日志,记录你所做的每一个改变或者你发现的每一个有趣的发现。你不仅可以一步一步地记录你的工作,还可以用日志来记录你正在做的事情。通过回顾和阅读你过去的日志,你会惊奇地发现你的研究。我在我的工程笔记本上发现了许多潦草的金块信息。

在尝试源代码的同时,也要在源代码中做笔记。在更改前后用注释行或注释块对源代码进行注释。这使得使用您最喜欢的文本解析器或搜索程序来定位您的所有更改变得很容易。下面演示了一种注释更改的方法:

/* BEGIN MY MODIFICATION */
/* Purpose of modification: experimentation. */
/* Modified by: Chuck */
/* Date modified: 30 May 2012 */
if (something_interesting_happens_here)
{
do_something_really_cool;
}
/* END MY MODIFICATION */

最后,不要害怕探索 MySQL 网站上的免费知识库和论坛,或者寻求全球开发者社区的帮助。这些是你最大的财富。在你发帖到某个论坛之前,确保你已经做了功课。失去信心的最快方式是在论坛上发布一条消息,却得到某人简短(但礼貌地)引用文档的回复。让你的帖子简明扼要。你不需要详细解释你为什么要这么做——只需要发布你的问题,并提供关于你所遇到的问题的所有相关信息。确保你发布到正确的论坛。大多数都是有主持人的,如果你有任何疑问,请咨询主持人,以确保你在正确的论坛上发布你的主题。

image 提示planet.mysql.com/是一个了解 MySQL 社区动态的好网站 ,这里汇集了来自世界各地的许多关于 MySQL 的博客帖子。

一个真实世界的例子:TiVo

你有没有想过是什么让你的 TiVo 滴答作响?如果您知道它运行在一个嵌入式 Linux 版本上,您会感到惊讶吗?

吉姆·巴顿和迈克·拉姆齐在 1997 年设计了最初的 TiVo 产品。它被定位为基于家庭网络的多媒体服务器,为瘦客户机提供流媒体内容。自然,像这样的设备必须容易学习,甚至更容易使用,但最重要的是,它必须无错误地操作,并优雅地处理电源中断(和用户错误)。

Barton 正在试验几种形式的 Linux,在 Silicon Graphics (SGI)工作时,他赞助了一个 Linux 到 SGI Indy 平台的移植。主要由于稳定的文件系统、网络、内存处理和开发者工具的支持,Barton 相信将一个版本的 Linux 移植到 TiVo 平台是可能的,并且 Linux 可以处理 TiVo 产品的实时性能目标。

然而,巴顿和拉姆齐面临着来自同龄人的挑战。当时,许多人对开源持怀疑和蔑视的态度。商业软件专家断言,开放源码软件在实时环境中永远不可靠。此外,他们认为,基于 GPL 的商业专有产品不允许修改,如果他们继续下去,该项目将成为版权诉讼和无休止的法律辩论的噩梦。幸运的是,巴顿和拉姆齐没有被吓住,仔细研究了 GPL。他们的结论是,GPL 不仅是可行的,它还允许他们保护自己的知识产权。

尽管最初的 TiVo 产品旨在成为服务器,但巴顿和拉姆齐认为带宽不足以支持如此崇高的目标。相反,他们将产品重新设计成一种客户端设备,称为 TiVo 客户端设备(TCD),就像一台复杂的录像机。他们想提供付费服务来提供电视指南和 TCD 接口。这将允许家庭用户提前选择他们想要的节目,并对 TCD 进行编程以记录它们。实际上,他们创造了现在众所周知的数字录像机(DVR)。

TCD 硬件包括一台小型嵌入式计算机,带有硬盘和内存。使用 MPEG 2 编码器和解码器创建硬件接口来读取和写入视频(视频输入和视频输出)。附加的输入/输出(I/O)设备包括音频和电信(用于访问 TiVo 服务)。TCD 还必须允许多重处理能力,以便允许记录一个信号(通道)同时回放另一个信号(通道)。这些特性需要良好的内存和磁盘管理子系统。巴顿和拉姆齐意识到这些目标对任何控制系统都是一个挑战。此外,视频接口绝不能以任何方式中断或损坏。

Barton 和 Ramsay 最需要的是一个具有完善的磁盘子系统、支持多任务处理、能够优化硬件(CPU、内存)使用的系统。因此,Linux 是 TCD 操作系统的合理选择。生产目标和预算约束限制了 CPU 的选择。TCD 选择了 IBM PowerPC 403GCX 处理器。不幸的是,在选择的处理器上没有运行 Linux 的端口。这意味着巴顿和拉姆齐必须将 Linux 移植到处理器平台上。

虽然移植是成功的,但 Barton 和 Ramsay 发现他们需要对 Linux 内核进行一些专门的定制,以满足硬件的需求和限制。例如,它们绕过了文件系统缓冲区高速缓存,以便允许更快地移动或处理进出用户空间的视频信号。他们还增加了广泛的性能增强、日志记录和恢复功能,以确保 TCD 可以从断电或用户错误中快速恢复。

运行 TCD 的应用是在基于 Linux 的个人计算机上构建的,并毫不夸张地移植到修改后的 Linux 操作系统上——这证明了 Linux 操作系统的稳定性和互操作性。当 Barton 和 Ramsay 完成移植和应用工作后,他们进行了广泛的测试,并于 1999 年 3 月交付了世界上第一台 DVR。

TCD 是使用最广泛的消费产品之一,运行定制的嵌入式 Linux 操作系统。显然,TCD 的故事是一个通过修改开源软件可以实现什么的光辉例子。然而,故事并没有到此结束。Barton 和 Ramsay 发表了他们的 Linux 内核移植,并附有源代码。他们的增强已经融入了最新版本的 Linux 内核。

说服你的老板修改开源软件

如果你有一个想法和一个基于它的商业模式,走开源路线可以在将你的产品推向市场的过程中节省大量时间。事实上,您的项目可能会节省大量的开发收入,并允许您比竞争对手更快地将产品推向市场。如果你需要修改开源软件,这一点尤其正确——你已经做了功课,可以展示使用开源软件的成本效益。

不幸的是,许多经理已经被商业专有软件世界所限制,拒绝接受基于开源软件的产品来产生收入的概念。那么如何改变他们的想法呢?用 TiVo 的故事作为弹药。向你的老板展示你从 TiVo 故事和本章剩余部分中获得的知识,以驱散关于 GPL 和开源软件可靠性的神话。不过,要小心。如果你像大多数开源专家一样,你的热情经常会被解释为对高级技术人员的威胁。

列出坚持商业专有观点的技术利益相关者。让这些人参与关于开源软件的对话,并回答他们的问题。最重要的是,要有耐心。这些人并没有你想象的那么厚,最终他们会分享你的热情。

一旦你让高级技术人员接受了教育,并且有了开源思维,就用一个修改过的提议重新让你的管理层参与进来。一定要带上一个高级技术人员作为挡箭牌(和理智的声音)。在这种情况下,胜利正在扭转商业所有权统治的潮流。

摘要

在本章中,您探索了开源软件的起源以及 MySQL 成为世界级数据库管理系统的过程。您了解了什么是开源系统,以及它们与商业专有系统的比较。您看到了开源许可的弱点,并发现了作为全球开发者社区成员的责任。

您还了解了如何使用 MySQL 进行开发,并了解了源代码的特征和修改指南。您了解了 Oracle 的双许可实践以及根据您的需求修改 MySQL 的意义。最后,您看到了一个在商业产品中成功集成开源系统的例子。

在接下来的章节中,您将了解更多关于关系数据库系统的剖析,以及如何开始定制 MySQL 以满足您的需求。稍后,在本书的第 2 部分和第 3 部分,将向您介绍 MySQL 的内部工作原理,并探索代码中最私密的部分。

http://www . GNU . org/GNU/thegnupproject . html

2www.microsoft.com/en-us/openn…??

3www.research.att.com/∼bs/bs_faq.…??

二、数据库系统的剖析

虽然您可能知道关系数据库管理系统(RDBMS)的基础知识,并且是管理该系统的专家,但是您可能从未探索过数据库系统的内部工作方式。我们中的大多数人都接受过管理数据库系统的培训,并拥有这方面的经验,但是无论是学术培训还是专业培训都没有包括太多关于数据库系统构建方式的内容。数据库专业人员可能永远不需要这些知识,但是了解系统如何工作是有好处的,这样您就可以了解如何最好地优化您的服务器,甚至如何最好地利用它的特性。

虽然理解 RDBMS 的内部工作方式对于托管数据库甚至维护服务器或开发使用该系统的应用来说都不是必需的,但是了解系统的组织方式对于修改和扩展其功能来说是必不可少的。掌握最流行的数据库系统的基本原理以理解这些系统与 RDBMS 相比如何也是很重要的。

本章涵盖了 RDBMSs 包含的子系统的基础知识以及它们是如何构造的。我使用 MySQL 系统的剖析来说明现代 RDBMSs 的关键组件。那些已经研究过这种系统的内部工作原理,并希望了解 MySQL 架构的人可以跳到“MySQL 数据库系统”

数据库系统的类型

大多数数据库专业人员使用 RDBMSs,但是其他几种类型的数据库系统也越来越流行。下面几节简要概述了三种最流行的数据库系统:面向对象的、对象关系的和关系的。了解这些系统的体系结构和一般特性非常重要,这样才能充分认识到 Oracle 通过将 MySQL 开发为开源软件并向所有人公开系统的源代码所提供的机会。这允许我向你们展示盒子里发生了什么。

面向对象的数据库系统

面向对象数据库系统(OODBSs)是一种存储和检索机制,它通过将数据作为对象直接操作来支持面向对象编程(OOP)范例。它们包含真正的面向对象(OO)类型的系统,允许对象在应用和使用中持久化。然而,大多数缺乏标准的查询语言 1 (对数据的访问通常是通过编程接口),因此不是真正的数据库管理系统。

OODBSs 是 RDBMS 的一个有吸引力的替代方案,特别是在 RDBMS 的建模能力或性能不足以将数据作为对象存储在表中的应用领域。这些应用维护大量从不删除的数据,从而管理单个对象的历史。OODBSs 最不寻常的特性是,它通过指定可以通过 OOP 接口应用于这些对象的结构和操作来提供对复杂对象的支持。

OODBSs 特别适合于尽可能接近真实世界的建模,而不会在实体之间和实体内部强加不自然的关系。面向对象的哲学提供了一个整体的以及面向建模的真实世界的视图。这些视图对于处理难以捉摸的主题是必要的,例如建模时间变化,特别是在向结构化数据添加 OO 特性时。尽管有许多开放源码的面向对象数据库,但大多数都部分基于支持查询语言接口的关系系统,因此不是真正的面向对象数据库;相反,它们更像具有面向对象接口的关系数据库。真正的面向对象数据库需要通过编程接口进行访问。

面向对象数据库系统的应用领域包括地理信息系统、科学和统计数据库、多媒体系统、图像存档和通信系统、语义网解决方案和 XML 仓库。

OODBS 最大的适应性是对数据(或对象)及其行为(或方法)的裁剪。大多数 OODBS 系统集成商依赖 OO 方法来描述数据,并在设计中用这种表达能力来构建他们的解决方案。因此,面向对象的数据库系统是用特定的实现来构建的,而不是通用的或一般化的,具有像 RDBMSs 那样的语句-响应类型的接口。

对象关系数据库系统

对象关系数据库管理系统是面向对象理论在关系数据库管理系统中的应用。ORDBMSs 提供了一种机制,允许数据库设计者实现面向对象数据的结构化存储和检索机制。ORDBMSs 提供了关系模型的基础——含义、完整性、关系等——同时扩展了模型,以一种以对象为中心的方式存储和检索数据。在许多情况下,实现纯粹是概念性的,因为 OO 概念到关系概念的映射充其量只是尝试性的。对关系技术的修改或扩展包括对 SQL 的修改,允许表示对象类型、标识、操作封装和继承。这些通常作为复杂类型松散地映射到关系理论。尽管很有表现力,但 SQL 扩展并不允许真正的对象操作和对 OODBSs 的控制。一个流行的 ORDBMS 是 ESRI 的 ArcGIS 地理数据库环境。其他例子包括 Illustra、PostgreSQL 和 Informix。

关系数据库管理系统中使用的技术通常基于关系模型。大多数关系数据库管理系统是使用现有的商业关系数据库管理系统(RDBMSs)如 Microsoft SQL Server 实现的。因为这些系统是基于关系模型的,所以它们受到将 OO 概念转换成关系机制的问题的困扰。在面向对象的应用中使用关系数据库的许多问题包括:

  • OO 概念模型不容易映射到数据表。
  • 复杂的映射意味着复杂的程序和查询。
  • 复杂的程序意味着维护问题。
  • 复杂的程序意味着可靠性问题。
  • 复杂的查询可能无法优化,可能会导致性能下降。
  • 对象概念到复杂类型 2 的映射比关系系统更容易受到模式变化的影响。
  • 全选的 OO 性能...WHERE 查询速度较慢,因为它涉及多次连接和查找。

尽管这些问题看起来很严重,但是通过在底层关系数据库和面向对象应用之间进行通信的面向对象应用层的应用,它们很容易被减轻。这些应用层允许将对象转换成结构化的(或持久的)数据存储。有趣的是,这种做法违反了 ORDBMS 的概念,因为您现在使用面向对象的访问机制来访问数据,这并不是创建 ORDBMS 的原因。创建它们是为了通过提供查询语言的扩展来允许在 RDBMS 中存储和检索对象。

虽然 ORDBMSs 和 oodbs 很相似,但是 oodbs 在哲学上有很大的不同。OODBSs 试图通过编程接口和平台向 OO 编程语言添加数据库功能。相比之下,ORDBMSss 试图使用传统的查询语言和扩展向 RDBMS 添加丰富的数据类型。OODBSs 试图实现与 OOP 语言的无缝集成。ORDBMS 不尝试这种级别的集成,通常需要一个中间应用层来将信息从面向对象应用翻译到 ORDBMS 甚至主机 RDBMS。类似地,面向对象数据库的目标是以面向对象观点作为其核心工程观点的应用。ORDBMSs 针对支持大量数据的大型数据存储和基于对象的系统(如 GIS 应用)进行了优化。最后,OODBSs 的查询机制以使用专门的 OO 查询语言的对象操作为中心。ORDBMS 查询机制适合于使用 SQL 标准的扩展来快速检索大量数据。与具有优化查询机制的真正面向对象数据库不同,如对象描述语言(ODL)和对象查询语言(OQL),ORDBMSs 使用的查询机制是 SQL 查询语言的扩展。

GIS 应用的 ESRI 产品套件包含一个名为 Geodatabase(地理数据库的简称)的产品,它支持地理数据元素的存储和管理。地理数据库是一种支持空间数据的对象关系数据库。这是作为 ORDBMS 实现的空间数据库的一个例子。

image 空间数据库系统不需要在 ORDBMSs 甚至 OODBSs 中实现。ESRI 已经选择将地理数据库作为一个 ORDBMS 来实现。更重要的是,GIS 数据可以存储在 RDBMS 中,RDBMS 已经扩展为支持空间数据。看哪!这正是 MySQL 发生的事情。Oracle 向 MySQL RDBMS 添加了一个空间数据引擎。

尽管 ORDBMSs 是基于关系数据库平台的,但它们也提供了一些数据封装和行为层。大多数关系数据库管理系统是关系数据库管理系统的特殊形式。那些提供 ORDBMSs 的数据库供应商通常通过修改 SQL 来包含对象描述符和空间查询机制,从而构建对语句响应接口的扩展。这些系统通常是为特定的应用而构建的,并且像 OODBSs 一样,它们的一般用途是有限的。

关系数据库系统

RDBMS 是一种基于数据关系模型的数据存储和检索服务,由 E. F. Codd 于 1970 年提出。这些系统是结构化数据的标准存储机制。大量的研究致力于改进 Codd 提出的基本模型,正如 C. J. Date 在数据库关系模型:回顾和分析中所讨论的。 3 这种理论和实践的演变最好地记录在第三个宣言中。

关系模型是存储库(数据库)的直观概念,可以通过使用一种称为查询语言的机制来检索、更新和插入数据,从而方便地查询存储库。关系模型已经被许多厂商实现,因为它具有完善的系统理论、坚实的数学基础和简单的结构。最常用的查询机制是结构化查询语言(SQL),它类似于自然语言。虽然关系模型中不包含 SQL,但是 SQL 提供了关系模型在 RDBMSs 中实际应用的一个组成部分。

数据被表示为关于某个实体的相关信息(属性)。属性的值集被形成为一个元组(有时被称为记录)。然后将元组存储在包含具有相同属性集的元组的表中。然后,表可以通过对域、键、属性和元组的约束与其他表相关联。

记录还是元组:有区别吗?

许多人错误地认为记录是元组的通俗说法。一个重要的区别是元组是一组有序的元素,而记录是没有顺序感的相关项目的集合。但是,列的顺序在记录的概念中很重要。有趣的是,在 SQL 中,查询的结果可以是一条记录,而在关系理论中,每个结果都是一个元组。许多文本交替使用这些术语,给许多人造成了混淆。

在探索 MySQL 架构和源代码时,我们会遇到专门用来描述结果集中的一行或数据更新的一行的术语 record。虽然 MySQL 中的记录数据结构是有序的,但与元组的相似之处仅此而已。

大多数实现选择的查询语言是结构化查询语言(SQL)。SQL 是在 20 世纪 80 年代作为一种标准提出的,目前是一种行业标准。不幸的是,许多人似乎认为 SQL 是基于关系理论的,因此是一个合理的理论概念。这种误解可能是由工业带来的一种现象助长的。几乎所有的 RDBMSs 都实现某种形式的 SQL。这种流行错误地忽略了 SQL 的许多缺点,包括:

  • SQL 不支持关系模型所描述的域。
  • 在 SQL 中,表可以有重复的行。
  • 结果(表)可以包含未命名的列和重复的列。
  • 主机数据库系统对空值(缺失值)的实现已经被证明是不一致和不完整的。因此,许多人错误地将空值的错误处理与 SQL 联系起来,而事实上,SQL 只是返回数据库系统提供的结果。 5

RDBMSs 中使用的技术多种多样。一些系统被设计来优化关系模型的一些部分或者模型对数据的一些应用。RDBMSs 的应用范围从简单的数据存储和检索到具有复杂数据、过程和工作流的复杂应用套件。这可以是简单的存储光盘或 DVD 收藏的数据库,或者是设计用来管理酒店预订系统的数据库,甚至是设计用来管理网上信息的复杂的分布式系统。正如我在第 1 章中提到的,许多网络和社交媒体应用实现了 LAMP 堆栈,MySQL 由此成为存储托管数据的数据库。

关系数据库系统提供了最健壮的数据独立性和数据抽象。通过使用关系的概念,RDBMS 提供了一种真正通用的数据存储和检索机制。当然,缺点是这些系统非常复杂,需要大量的专业知识来构建和修改。

在下一节中,我将介绍一个典型的 RDBMS 体系结构,并研究该体系结构的每个组件。稍后,我将研究一个 RDBMS (MySQL)的特定实现。

MYSQL 是关系数据库系统吗?

许多数据库理论家会告诉你,世界上真正的 RDBMSs 非常少。他们还会指出,什么是关系型,什么不是关系型,很大程度上取决于您对数据库系统所支持的特性的定义,而不是系统符合 Codd 的关系型模型的程度。

从纯营销的角度来看,MySQL 提供了许多被认为是 RDBMSs 所必需的特性。这些包括但不限于使用外键将表相互关联的能力、关系代数查询机制的实现以及索引和缓冲机制的使用。显然,MySQL 提供了所有这些特性,甚至更多。

那么 MySQL 是 RDBMS 吗?这取决于你对关系的定义。如果你考虑 MySQL 的特性和演变,你应该得出结论,它确实是一个 RDBMS。然而,如果您坚持 Codd 关系模型的严格定义,您将会得出结论,MySQL 缺少模型中表示的一些特性。但是话说回来,许多其他 RDBMSs 也是如此。

关系数据库系统体系结构

RDBMS 是一个复杂的系统,由专门的机制组成,用于处理存储和检索信息所需的所有功能。RDBMS 的体系结构经常被比作操作系统的体系结构。如果你考虑 RDBMS 的使用,特别是作为一个客户机主机的服务器,你会发现它们与操作系统有许多共同之处。例如,拥有多个客户机意味着系统必须支持许多请求,这些请求可能会也可能不会读写相同的数据或来自相同位置的数据(比如一个表)。因此,RDBMSs 必须有效地处理并发性。类似地,RDBMSs 必须为每个客户机提供对数据的快速访问。这通常是通过使用文件缓冲技术来实现的,该技术将最近或最常用的数据保存在内存中,以便更快地访问。并发性要求内存管理技术类似于操作系统中的虚拟内存系统。与操作系统的其他相似之处包括网络通信支持和优化算法,旨在最大限度地提高查询执行的性能。

我将从用户的角度开始我们对体系结构的探索,从发出查询到检索数据。下面几节写的是让你可以跳过你熟悉的,读你感兴趣的。但是,我鼓励您阅读所有章节,因为它们详细介绍了典型的 RDBMS 是如何构建的。

客户端应用

大多数 RDBMS 客户机应用都是作为独立的可执行程序开发的,这些程序通过通信路径(例如,诸如套接字或管道之类的网络协议)连接到数据库。有些通过编程接口直接连接到数据库系统,其中数据库系统成为客户机应用的一部分。在这种情况下,我们称数据库为嵌入式系统。有关嵌入式数据库系统的更多信息,请参见第 6 章

大多数通过通信路径连接到数据库的系统都是通过一组称为数据库连接器的协议来实现的。数据库连接器最常基于开放式数据库连接(ODBC) 6 模型。MySQL 还支持 Java (JDBC)、PhP、Python 和微软的连接器。NET(参见“MySQL 连接器”).大多数 ODBC 连接器的实现也支持网络协议上的通信。

ODBC 是应用编程接口(API)的规范。ODBC 设计用于将 SQL 命令传输到数据库服务器,检索信息,并将其提供给调用应用。ODBC 实现包括一个设计为使用 API 的应用,它充当 ODBC 库的中介,一个支持 API 的核心 ODBC 库,以及一个为特定数据库系统设计的数据库驱动程序。我们通常将客户端访问、API 和驱动程序的集合称为连接器。因此,ODBC 连接器充当客户机应用和数据库服务器之间的“解释器”。ODBC 已经成为几乎所有关系(和大多数对象关系)数据库系统的标准。数百个连接器和驱动程序可用于各种各样的客户机和数据库系统。

当我们考虑客户机应用时,我们通常会考虑向数据库服务器发送信息和从数据库服务器检索信息的程序。甚至我们用来配置和维护数据库服务器的应用也是客户端应用。大多数这些实用程序通过与数据库应用相同的网络路径连接到服务器。有些使用 ODBC 连接器或类似 Java 数据库连接(JDBC) 的变体。少数使用专门的协议来管理服务器,以达到特定的管理目的。其他的,比如 phpMyAdmin,使用端口或套接字。

不管它们的实现如何,客户机应用都向数据库系统发出命令,并检索这些命令的结果,解释和处理这些结果,然后将它们呈现给用户。标准的命令语言是 SQL。客户端通过 ODBC 连接器向服务器发出 SQL 命令,ODBC 连接器使用驱动程序指定的网络协议将命令传输到数据库服务器。该过程的图形描述如图 2-1 中的所示。

9781430246596_Fig02-01.jpg

图 2-1。客户端应用/数据库服务器通信

MYSQL 连接器

Oracle 为开发者提供了几个数据库连接器,使应用能够与 MySQL 进行交互。这些连接器可以用于与行业标准兼容的应用和工具,包括 ODBC 和 JDBC。这意味着任何使用 ODBC 或 JDBC 的应用都可以使用 MySQL 连接器。适用于 Oracle 的 MySQL 连接器有:

  • 连接器/ODBC–用于 Windows、Linux、Mac OS X 和 Unix 平台的标准 ODBC 连接器。
  • connector/J[ava]–用于 Java 平台和开发。
  • 连接器/网络–窗户。Net 应用开发。
  • 连接器/MXJ–用于在 Java 应用中嵌入 MySQL 服务器的 MBean。
  • 连接器/c++–c++开发。
  • connector/C(libmysql)–C 应用开发。
  • 连接器/Python–Python 应用开发。

您可以在 www.mysql.com/downloads/c…](www.mysql.com/downloads/c…)

查询界面

诸如 SQL 的查询语言是一种可以表示向数据库系统提出的问题的语言(它具有语法和语义)。事实上,在数据库系统中使用 SQL 被认为是它们成功的主要原因之一。SQL 提供了几个语言组,形成了使用数据库系统的全面基础。数据库专业人员使用数据定义语言 (DDL) 来创建和管理数据库。任务包括创建和修改表、定义索引和管理约束。数据操作语言 (DML) 被数据库专业人员用来查询和更新数据库中的数据。任务包括添加和更新数据以及查询数据。这两种语言组构成了数据库系统支持的大多数命令。

SQL 命令是使用专门的语法形成的。下面给出了 SQL 中的一个 SELECT 命令的语法。符号用斜体表示用户定义的变量,用方括号([])表示可选参数。

SELECT [DISTINCT] listofcolumns
FROM listoftables
[WHERE expression (predicates in CNF)]
[GROUP BY listofcolumns]
[HAVING expression]
[ORDER BY listof columns];

该命令的语义是: 7

  1. 形成来自子句的中的表的笛卡尔积,从而仅形成那些出现在其他子句中的引用的投影。
  2. 如果存在一个 WHERE 子句,则应用引用的给定表的所有表达式。
  3. 如果存在一个 GROUP BY 子句,则在指定属性的结果中形成组。
  4. 如果存在具有子句的*,则为组应用过滤器。*
  5. 如果存在一个 ORDER BY 子句,则以指定的方式对结果进行排序。
  6. 如果存在 DISTINCT 关键字,则从结果中删除重复的行。

前面的代码示例代表了大多数 SQL 命令;所有这样的命令都有必需的部分,大多数还有可选的部分以及基于关键字的修饰符。

一旦查询语句通过网络协议(称为运输传输到客户端,数据库服务器就必须解释并执行命令。从这一点开始,查询语句被简称为查询,因为它表示数据库系统必须提供答案的问题。此外,在接下来的部分中,我假设查询属于 SELECT 类型,其中用户已经发出了一个数据请求。然而,所有的查询,无论是数据操作还是数据定义,在系统中都遵循相同的路径。也正是在这一点上,我们考虑在数据库服务器本身中执行的操作。这个过程的第一步是解读客户的要求——也就是说,查询必须被解析并分解成可以执行的元素。

查询处理

在以客户机/服务器模式运行的数据库系统的环境中,数据库服务器负责处理客户机提出的查询并相应地返回结果。这被称为查询传送 ,其中查询被传送到服务器并返回有效负载(数据)。查询传送的好处是减少了查询的通信时间,并且能够利用服务器资源,而不是使用更有限的客户端资源来执行查询。该模型还允许将数据在服务器上的存储和检索方式与数据在客户机上的使用方式分开。换句话说,客户机/服务器模型支持数据独立性。

数据独立性是 Codd 在 1970 年引入的关系模型的主要优点之一:将物理实现逻辑模型分离。按 Codd, 8

必须保护大型数据库的用户,使他们不必知道数据在机器中是如何组织的。。。当数据的内部表示改变时,终端用户的活动和大多数应用应该保持不受影响。

这种分离允许开发一组强大的逻辑语义,独立于特定的物理实现。数据独立(Elmasri 和 Navathe 9 称之为物理数据独立)的目标是每个逻辑元素独立于所有物理元素(见表 2-1 )。例如,数据与由元组(行)排列的属性(字段)的关系(表)的逻辑布局完全独立于数据在存储介质上的存储方式。

表 2-1 。数据库设计的逻辑和物理模型

逻辑模型物理模型
查询语言排序算法
关系代数存储机制
关系演算索引机制
雷尔瓦斯数据表示法

数据独立性的一个挑战是数据库编程变成了一个两部分的过程。首先,编写逻辑查询——描述查询应该做什么。其次,是物理计划的编写,它向展示了如何实现逻辑查询。

逻辑查询通常可以以许多不同的形式编写,例如 SQL 之类的高级语言或代数查询树。 10 例如,在传统的关系模型中,一个逻辑查询可以用关系演算或关系代数来描述。关系演算在关注需要计算什么方面更好。关系代数更接近于提供一种算法,让您找到您正在查询的内容,但是仍然省略了查询评估中涉及的许多细节。

物理计划是一个查询树,其实现方式可以被数据库系统的查询执行引擎理解和处理。查询树 是一种树形结构,其中每个节点包含一个查询操作符,并且有多个子节点,这些子节点对应于操作中涉及的表的数量。查询树可以通过优化器转换成执行计划。这个计划可以看作是查询执行引擎可以执行的程序。

查询语句在执行之前要经过几个阶段;解析、验证、优化、计划生成/编译和执行。图 2-2 描述了一个典型的数据库系统会采用的查询处理步骤。对每个查询语句进行有效性分析,并检查语法是否正确以及查询操作的标识。然后,解析器以中间形式输出查询,以允许优化器形成高效的查询执行计划。然后,执行引擎执行查询,并将结果返回给客户端。这个过程在图 2-2 中显示,一旦解析完成,查询将被验证错误,然后被优化;选择并编制计划;最后执行查询。

9781430246596_Fig02-02.jpg

图 2-2。查询处理步骤

这个过程的第一步是将逻辑查询从 SQL 转换成关系代数中的查询树。这一步由解析器完成,通常包括将 SQL 语句分成几个部分,然后从那里构建查询树。下一步是将逻辑代数中的查询树翻译成物理计划。通常,许多计划都可以实现查询树。寻找最佳执行计划的过程称为查询优化 。也就是说,对于某些查询执行性能度量(例如,执行时间),我们希望找到具有最佳执行性能的计划。在优化器的搜索空间内,计划应该是最优的或接近最优的。优化器首先将关系代数查询树复制到它的搜索空间中。然后,优化器通过形成备选执行计划(到有限迭代)来扩展搜索空间,然后搜索最佳计划(执行最快的计划)。

在这个通用级别上,优化器可以被视为 SQL 语言查询编译器的代码生成部分。事实上,在一些数据库系统中,编译步骤将查询翻译成可执行程序。然而,大多数数据库系统将查询翻译成可以使用内部执行步骤库执行的形式。这种情况下的代码编译会生成由查询执行引擎解释的代码,只是优化器的重点是生成“非常高效”的代码。例如,优化器使用数据库系统的目录来获得关于查询所引用的存储关系的信息(例如,元组的数量),这是传统编程语言编译器通常不做的。最后,优化器将最佳物理计划从其内存结构中复制出来,并发送给查询执行引擎,查询执行引擎使用存储的数据库中的关系作为输入来执行计划,并生成与查询条件匹配的行的表。

所有这些活动都需要额外的处理时间,并通过迫使数据库实施者将查询优化器和执行引擎的性能作为其整体效率的一个因素来考虑,从而给该过程带来了更大的负担。这种优化的成本很高,因为有许多备选执行计划使用不同的访问方法(读取数据的方式)和不同的执行顺序。因此,可以为一个查询生成无限数量的计划。然而,数据库系统通常将问题局限于少数已知的最佳实践。

大量查询计划的一个主要原因是,许多重要运行时参数的不同值都需要优化,这些参数的实际值在优化时是未知的。数据库系统对数据库内容(例如,关系属性中的值分布)、物理模式(例如,索引类型)、系统参数的值(例如,可用缓冲区的数量)以及查询常量的值做出某些假设。

查询优化器

有些人错误地认为查询优化器执行了查询执行阶段的所有步骤。正如您将看到的,查询优化只是查询执行过程中的一个步骤。以下段落详细描述了查询优化器,并说明了优化器在查询执行过程中的作用。

查询优化是查询编译过程的一部分,它将高级非过程语言(如 SQL)中的数据操作语句翻译成更详细的过程操作符序列,称为查询计划。查询优化器通常通过估计许多备选计划的成本来选择一个计划,然后选择其中最便宜的(执行速度最快的)。

使用基于计划的方法进行查询优化的数据库系统假定可以使用许多计划来产生任何给定的查询。虽然这是真的,但不是所有的计划在执行查询所需的资源数量(或成本)上都是相等的,也不是所有的计划都在相同的时间内执行。那么,目标就是找到成本最低和/或运行时间最少的计划。当设计用于嵌入式集成或在小平台上运行的系统(资源可用性低)与对更高吞吐量(或时间)的需求相比时,资源使用或成本使用的区别是经常遇到的权衡。

图 2-3 描述了一个基于计划的查询处理策略,其中查询沿着箭头的路径进行。SQL 命令被传递给查询解析器,在那里被解析和验证,然后被转换成内部表示,通常基于关系代数表达式或查询树,如前所述。然后将查询传递给查询优化器,查询优化器检查所有等价的代数表达式,为每个组合生成不同的计划。然后,优化器选择成本最低的计划,并将查询传递给代码生成器,代码生成器将查询转换为可执行的形式,可以是直接可执行的,也可以是解释性代码。然后,查询处理器执行查询,并在结果集中一次返回一行。

这种常见的实现方案是大多数数据库系统的典型特征。然而,随着时间的推移,运行数据库系统的机器已经有所改进。查询计划不再有不同的执行成本。事实上,大多数查询计划的执行成本大致相同。这种认识导致一些数据库系统实现者采用一种查询优化器,这种查询优化器使用一些众所周知的最佳实践或规则(称为试探法)来优化查询。一些数据库系统使用基于一种形式的混合优化技术,同时在执行过程中保持其他技术的某些方面。

9781430246596_Fig02-03.jpg

图 2-3。基于计划的查询处理

执行查询优化的四种主要方法是

  • 基于成本的优化
  • 试探优化
  • 语义优化
  • 参数优化

虽然没有任何优化技术可以保证最佳的执行计划,但是所有这些方法的目标都是为查询生成一个有效的执行,保证正确的结果。

基于成本的优化器通过使用等价规则从给定的查询生成一系列查询评估计划,并根据收集的关于执行查询所需的关系和操作的度量(或统计)选择成本最低的计划。对于一个复杂的查询,许多等价的计划是可能的。基于成本的优化的目标是利用从过去的查询中收集的索引和统计信息来安排查询执行和表访问。Microsoft SQL Server 和 Oracle 等系统使用基于成本的优化器。

启发式优化器在选择替代实现之前,使用关于如何将查询塑造成最佳形式的规则。试探法或规则的应用可以消除可能低效的查询。使用试探法来形成查询计划可以确保查询计划在评估之前最有可能(但不总是)得到优化。启发式优化的目标是应用确保查询执行“良好”实践的规则。使用启发式优化器的系统包括 Ingres 和各种学术变体。这些系统通常使用启发式优化来避免真正糟糕的计划,而不是作为优化的主要手段。

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

参数查询优化将启发式方法的应用与基于成本的优化相结合。所得到的查询优化器提供了一种生成较小的有效查询计划集的方法,可以根据这些计划来估计成本,从而可以执行该计划集中成本最低的计划。

使用混合优化器的数据库系统的一个例子是 MySQL。MySQL 中的查询优化器是围绕 select-project-join 策略设计的,该策略结合了基于成本的优化器和使用已知优化机制的启发式优化器,从而减少了基于成本的优化可以选择的最小执行路径。这种策略确保了一个整体“好”的执行计划,但是它不一定产生最好的计划。这种策略已经被证明对于在不同环境中运行的各种各样的查询非常有效。MySQL 的内部表示已经表现得足够好,可以与最大的生产数据库系统的执行速度相媲美。

使用基于成本的优化器的数据库系统的一个例子是微软的 SQL Server。SQL Server 中的查询优化器是围绕经典的基于成本的优化器设计的,该优化器将查询语句转换为可以高效执行并返回所需结果的过程。优化器使用从过去的查询中记录的值和数据库中数据的特征收集的信息或统计数据来创建表示相同查询的替代过程。统计数据应用于每个过程,以预测哪个过程可以更有效地执行。一旦确定了最有效的过程,就开始执行并将结果返回给客户机。

通过使用未绑定的参数,例如用户谓词,查询的优化可能会变得复杂。例如,当执行存储过程时,如果存储过程中的查询接受来自用户的参数,则创建未绑定参数。在这种情况下,查询优化可能是不可能的,或者它可能不会产生最低的成本,除非在执行之前获得了谓词的一些知识。如果很少有记录满足谓词,那么即使是基本索引也远远优于文件扫描。如果许多记录都符合条件,则情况正好相反。如果在执行优化时因为谓词未绑定而导致选择性未知,那么在这些备选计划中的选择应该推迟到执行时进行。

选择性问题可以通过构建优化器来解决,这些优化器可以采用谓词作为开放变量,并通过基于历史查询执行生成所有可能发生的查询计划,以及利用基于成本的优化器的统计信息(包括谓词属性的频率分布)来执行查询计划规划。

查询的内部表示

在数据库系统中,可以使用几种不同形式的原始 SQL 命令来表示查询。这些替代形式的存在是由于 SQL 中的冗余、特定约束下的子查询和连接的等价性,以及可以从 WHERE 子句中的谓词得出的逻辑推理。拥有替代形式的查询给数据库实现者带来了一个问题,因为查询优化器必须为查询选择最佳的访问计划,而不管用户最初是如何形成查询的。

一旦查询优化器形成了有效的执行计划(启发式和混合优化器)或者选择了最有效的计划(基于成本的优化器),查询就被传递到流程的下一个阶段:执行。

查询执行

数据库系统可以使用几种方法来执行查询。大多数使用迭代解释执行策略。

迭代方法提供了产生可用于处理离散操作(连接、项目等)的调用序列的方法。),但它们的设计并不包含内部表示的功能。将查询转换成迭代方法使用了函数式编程和程序转换技术。一些可用的算法从基于代数的查询规范生成迭代程序。例如,一些算法将查询规范翻译成递归程序,在算法生成执行计划之前,递归程序通过转换规则集进行简化。另一种算法使用两级翻译。第一级使用一组较小的转换规则来简化内部表示,第二级在生成执行计划之前应用功能转换。

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

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

一个经常混淆的领域是编译的概念。一些数据库专家认为编译后的查询是迭代查询执行计划的实际编译,但是在 Date 的工作中,编译后的查询只是一个已经优化并存储以供将来执行的查询。我不会使用单词 compiled,,因为 MySQL 查询优化器和执行引擎不会存储查询执行计划以备后用(MySQL 查询缓存是一个例外)。我不认为我们可以在这里比较或提及查询缓存。对已执行查询的评估甚至与计划无关,而是与接收的 SQL 和存储的 SQL 之间的文献比较有关,直接与一组已检索的信息相关,查询执行也不需要任何编译或汇编工作。有趣的是,存储过程的概念符合第二个类别;它被编译(或优化)以便以后执行,并且可以对满足其输入参数的数据运行多次。

查询执行评估查询树(或由内部结构表示的查询)的每个部分,并为每个部分执行方法。支持的方法反映了在关系代数、投影、限制、联合、交集等中定义的那些操作。对于这些操作中的每一项,查询执行引擎都会执行一个方法来评估传入的数据,并将处理后的数据传递给下一步。例如,在项目操作中只返回数据的某些属性(或列)。在这种情况下,查询执行引擎将剥离不符合限制规范的属性的数据,并将剩余的数据传递给树(或结构)中的下一个操作。表 2-2 列出了支持的最常见操作,并简要描述了每种操作。

表 2-2。查询操作

操作描述
限制返回与 WHERE 子句的条件(谓词)匹配的元组(某些系统以相同或相似的方式处理 HAVING 子句)。这个操作通常被定义为 SELECT。
项目返回在计算的元组的列列表中指定的属性。
加入返回与称为连接条件(或连接谓词)的特殊条件相匹配的元组。联接有多种形式。请参阅“连接”了解每个连接的描述。

加入

连接操作可以采取多种形式。这些经常被数据库专业人员所混淆,在某些情况下要不惜一切代价避免。SQL 的表达能力允许将许多连接写成 WHERE 子句中的简单表达式。虽然大多数数据库系统能够正确地将这些查询转换成连接,但这被认为是一种懒惰的形式。下面列出了在 RDBMS 中可能遇到的联接类型,并对每种类型进行了描述。连接操作可以有连接条件(theta 连接)、要比较的属性值的匹配(equijoins)或没有条件(笛卡尔乘积)。连接操作细分为:

  • Inner: 两个关系的连接,返回匹配的元组。
  • Outer (left,right,full): 返回 from 子句中提到的至少一个表或视图中的所有行,只要这些行满足任何 WHERE 搜索条件。所有行都是从用左外部联接引用的左表中检索的;右表中的所有行都在右外部联接中被引用。两个表中的所有行都在完全外部联接中返回。不匹配行的属性值作为空值返回。
  • *右外:*两个关系的连接,返回匹配的元组,加上右边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Full outer: 两个关系的连接返回两个关系中的所有元组,将另一个关系中指定的不匹配属性保留为空(null)。
  • *叉积:*两个关系的连接,将第一个关系中的每个元组映射到另一个关系中的所有元组。
  • Union: 集合运算,其中只返回来自两个具有相同模式的关系的匹配。
  • Intersect: 集合运算,其中仅返回来自具有相同模式的两个关系的不匹配。

决定如何执行查询(或选择的查询计划)只是事情的一半。另一件要考虑的事情是如何访问数据。有许多方法可以从磁盘(文件)中读取和写入数据,但是选择最佳的方法取决于查询试图做什么。创建文件访问机制是为了最小化从磁盘访问数据的成本,并最大化查询执行的性能。

文件存取

文件访问机制,也称为物理数据库设计,在数据库系统开发的早期就很重要。然而,由于操作系统支持的通用文件系统的有效性和简单性,文件访问的重要性已经降低。今天,文件访问仅仅是文件存储和索引最佳实践的应用,例如将索引文件从数据文件中分离出来,并分别放在单独的磁盘输入/输出(I/O)系统上以提高性能。一些数据库系统使用不同的文件组织技术来使数据库适应特定的应用需求。在这方面,MySQL 可能是最独特的,因为它支持许多文件访问机制(称为存储引擎)。

必须满足明确的目标,以最小化数据库系统中的 I/O 成本。这些包括利用允许通过有效的访问路径仅有效检索相关数据的磁盘数据结构,以及组织磁盘上的数据,使得检索相关数据的 I/O 成本最小化。因此,最重要的性能目标是最小化磁盘访问(或磁盘 I/o)的数量。

有许多处理数据库设计的技术可用。可用于文件访问机制(数据文件的实际物理实现)的更少。此外,许多研究人员同意,最佳的数据库设计(从物理角度来看)通常是不可实现的,而且也不应该追求。优化是不可实现的,主要是因为现代磁盘子系统的效率大大提高了。相反,正是这些技术和研究的知识允许数据库实现者以尽可能好的方式实现数据库系统,以满足那些将使用该系统的人的需求。

要创建一个性能良好的结构,您必须考虑许多因素。早期的研究人员考虑根据数据的内容或上下文将数据分割成子集。例如,包含相同部门编号的所有数据将被分组在一起,并与相关数据的引用一起存储。这个过程可以永久化,因为集合可以被分组在一起以形成超集,从而形成分层的文件组织。

在这种配置中访问数据涉及在最高级别扫描集合,以便只访问和扫描那些对于获得所需信息是必要的集合。这个过程大大减少了要扫描的元素数量。将要扫描的数据项放在一起可以最大限度地减少搜索时间。将磁盘上的数据整理成结构化文件称为文件组织 。我们的目标是设计一种访问方法,它提供了一种逐个立即处理事务的方式,从而允许我们保存真实世界情况的最新存储图片。

随着操作系统的发展,对文件组织技术进行了修订,以确保更高的存储和检索效率。现代数据库系统带来了新的挑战,目前公认的方法可能不足以应对这些挑战。对于在磁盘速度更快、数据吞吐量更高的硬件上执行的系统来说尤其如此。此外,理解数据库设计方法,不仅在教科书中描述,而且在实践中,将增加对数据库系统的要求,从而增加进一步研究的动力。例如,最近工业上对冗余和分布式系统的采用引发了这些领域中的额外研究,以利用新的硬件和/或增加数据可用性、安全性和恢复的需求。

由于从磁盘访问数据的成本很高,因此使用缓存机制,有时也称为缓冲区 ,可以显著提高从磁盘读取数据的性能,从而降低存储和检索数据的成本。这个概念包括复制部分数据,或者是为了下次磁盘读取,或者是基于一种算法,该算法旨在将最常用的数据保留在内存中。有效地处理磁盘和主存之间的差异是高质量数据库系统的核心。应该理解数据库系统使用磁盘还是使用主存之间的权衡。请参见表 2-3 以了解物理存储(磁盘)和辅助存储(内存)之间的性能权衡。

表 2-3 。性能权衡

问题主内存与磁盘
速度主存至少比磁盘快 1000 倍。
储存空间同样的成本,磁盘可以容纳比内存多几百倍的信息。
坚持当电源关闭时,磁盘保存数据,主内存会忘记一切。
存取时间主存开始发送数据需要纳秒,而磁盘需要毫秒。
块大小主存储器可以一次访问一个字,磁盘可以一次访问一个块。

数据库物理存储的进步已经见证了许多关于存储策略和缓冲机制的相同改进,但是在探索性地研究物理存储的基本元素方面几乎没有发生。一些人从硬件层面探讨了这个话题,而另一些人则从更实用的层面探讨了我们到底需要存储什么。持久存储的主题在很大程度上被遗忘了,因为主机操作系统中提供了强大而高效的机制。

文件访问机制用于存储和检索数据库系统包含的数据。大多数文件访问机制都有附加的功能层,允许更快地定位文件中的数据。这些层被称为索引机制 。索引机制提供了访问路径(数据将被搜索和检索的方式),旨在基于称为的数据子部分来定位特定数据。索引机制的复杂程度不一,从简单的关键字列表到旨在最大化关键字搜索的复杂数据结构。

目标是快速有效地找到我们想要的数据,而不必请求和读取非绝对必要的更多磁盘块。这可以通过保存标识数据(或键)的值和记录在磁盘上的位置来实现,以形成数据的索引。此外,读取索引数据比读取所有数据更快。使用索引的主要好处是,它允许我们有效地搜索大量数据,而不必检查,或者在许多情况下阅读每一项,直到找到我们要搜索的内容。因此,索引与搜索包含存储在磁盘上的数据的大文件的方法有关。这些方法被设计用于数据的快速随机存取以及数据的顺序存取。

大多数(但不是全部)索引机制涉及一个存储键和磁盘块地址的树形结构。例子包括 B 树、B +树和散列树。这些结构通常由一个或多个算法遍历,这些算法被设计成最小化在结构中搜索关键字所花费的时间。大多数数据库系统在其索引机制中使用这种或那种形式的 B 树。这些树算法提供了非常快的搜索速度,而不需要很大的存储空间。

在查询执行期间,解释查询执行方法访问分配的索引机制,并通过指定的访问方法请求数据。然后,执行方法读取数据,通常一次读取一条记录;通过评估表达式来分析查询与谓词的匹配;然后通过任何转换传递数据,最后传递到服务器的传输部分,将数据发送回客户端。

查询结果

一旦处理了查询中引用的表中的所有元组,就沿着相同的或者有时是替代的通信路径将元组返回给客户端。然后,元组被传递到 ODBC 连接器,以便封装并呈现给客户端应用。

关系数据库体系结构概述

在这一节中,我详细介绍了通过典型的关系数据库系统架构查询数据的步骤。正如您将看到的,查询从客户端发出的 SQL 命令开始;然后,使用通信路径(网络)通过 ODBC 连接器将其传递给数据库系统。查询被解析、转换成内部结构、优化和执行,结果返回给客户机。

现在,我已经向您简要介绍了处理查询所涉及的所有步骤,并且您已经看到了数据库系统子组件的复杂性,现在是时候来看一个真实的例子了。在下一节中,我将深入介绍 MySQL 数据库系统架构。

MySQL 数据库系统

MySQL 源代码是高度组织化的,使用许多结构化的类构建(有些是复杂的数据结构,有些是对象,但大多数是结构)。虽然通过添加插件使系统更加模块化的努力正在进行中,但源代码还不是真正的模块化架构,但现在随着新的插件机制的出现,已经非常接近了。当您探索架构时,理解这一点很重要,更重要的是,当您以后探索源代码时。这意味着您有时会发现源代码中没有清晰的架构元素划分。关于 MySQL 源代码的更多信息,包括如何获得它,参见第三章。

虽然有些人可能会将 MySQL 架构描述为一个由一组模块化子组件构建的基于组件的系统,但事实是,尽管它高度组织化,但它既不是基于组件的,也不是模块化的。源代码是使用 C 和 C++混合构建的,并且在系统的许多功能中使用了许多对象。该系统不是真正意义上的面向对象编程中的面向对象。相反,该系统是建立在函数库和数据结构的基础上的,这些函数库和数据结构被设计成围绕体系结构优化源代码的组织,其中一些部分是使用对象编写的。

然而,MySQL 架构是高度组织化的子系统的智能设计,这些子系统和谐地工作以形成有效且高度可靠的数据库系统。我在本章前面描述的所有技术都存在于系统中。实现这些技术的子系统设计良好,并使用整个系统中相同精度的源代码来实现。有趣的是,许多有成就的 C 和 C++程序员评论源代码的优雅和简洁。我经常发现自己惊叹于代码的宁静复杂和优雅。事实上,甚至代码作者自己也承认,他们的代码有一种天才的直觉,这种直觉通常只有在彻底分析后才能被完全理解或欣赏。你也会惊奇地发现,一旦你弄明白了,一些源代码是多么的有效,多么的简单。

image 注意事实证明,MySQL 系统对一些人来说很难学习,当出现问题时,诊断起来也很麻烦。然而,很明显,一旦掌握了 MySQL 体系结构和源代码的复杂性,这个系统就非常容易适应,并且有希望成为实验数据库工作的第一个也是最好的平台。

这意味着 MySQL 架构和源代码对于 C++程序员新手来说通常是具有挑战性的。如果你发现自己开始重新考虑接手源代码,请继续阅读;我将是你导航源代码的向导。但是让我们先来看看这个系统是如何构成的。

MySQL 系统架构

MySQL 架构最好被描述为子系统的分层系统。虽然源代码不是作为单独的组件或模块编译的,但是子系统的源代码是以分层的方式组织的,这种方式允许子系统被分离(封装)在源代码中。大多数子系统依赖基础库来实现低级功能(例如,线程控制、内存分配、网络、日志和事件处理以及访问控制)。基本库、基于这些库构建的子系统、甚至是基于其他子系统构建的子系统共同构成了抽象的 API,即 C 客户端 API。这个强大的 API 允许 MySQL 系统在更大的应用中作为独立的服务器或嵌入式数据库系统使用。

该架构为 SQL 接口、查询解析、查询优化和执行、缓存和缓冲以及可插拔存储引擎提供了封装。图 2-4 描绘了 MySQL 架构及其子系统。在图的顶部是提供对客户机应用的访问的数据库连接器。正如您所看到的,几乎任何您想要的编程环境的连接器都存在。在绘图的左侧,辅助工具按管理和企业服务分组列出。关于管理和企业服务工具的完整讨论,请参见迈克尔·克鲁肯伯格和杰伊·皮普斯的Pro MySQL12这是一篇关于 MySQL 所有管理事务的极好参考。

体系结构中从连接器往下的下一层是连接池层。这一层处理客户端连接的所有用户访问、线程处理、内存和进程缓存需求。该层之下是数据库系统的核心。这里是解析和优化查询以及管理文件访问的地方。下一层是可插拔存储引擎层。在这一层,MySQL 体系结构的部分优点大放异彩。可插拔存储引擎层允许构建系统来处理各种不同的数据或文件存储和检索机制。这种灵活性是 MySQL 独有的。目前没有其他数据库系统能够通过提供几种数据存储机制来提供数据库调优能力。

image 注意可插拔存储引擎功能从版本 5.1 开始提供。

可插拔存储引擎下面是系统的最低层,即文件访问层。在这一层,存储机制读写数据,系统读写日志和事件信息。这一层与线程、进程和内存控制最接近操作系统。

让我们从系统中从客户端应用到数据的流程开始讨论 MySQL 架构。第一层遇到了曾经的客户端连接器(ODBC,。NET、JDBC、C API 等。)已经将 SQL 语句传送到服务器的是 SQL 接口。

9781430246596_Fig02-04.jpg

图 2-4。 MySQL 服务器架构(版权 Oracle。经许可转载。)

SQL 接口

SQL 接口提供了接收命令和向用户传输结果的机制。MySQL SQL 接口是根据 ANSI SQL 标准构建的,接受与大多数 ANSI 兼容的数据库服务器相同的基本 SQL 语句。尽管 MySQL 中支持的许多 SQL 命令都有非 ANSI 标准的选项,但 MySQL 开发者已经非常接近 ANSI SQL 标准。

从网络通信路径接收到数据库服务器的连接,并为每个连接创建一个线程。线程化进程是 MySQL 服务器中可执行路径的核心。MySQL 是作为真正的多线程应用构建的,每个线程独立于其他线程执行(除了某些助手线程)。传入的 SQL 命令存储在类结构中,通过将结果写出到网络通信协议,将结果传输到客户端。一旦创建了一个线程,MySQL 服务器就会尝试解析 SQL 命令,并将各个部分存储在内部数据结构中。

解析器

当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。MySQL 解析器是使用一个用 Bison 编译的大型 Lex-YACC 脚本实现的。解析器构建一个查询结构,用于将内存中的查询语句(SQL)表示为可用于执行查询的树结构(也称为抽象语法树)。

image 提示sql _ yacc . YY、sql_lex.h 和 lex.h 文件是您开始在 MySQL 中构建自己的 SQL 命令或数据类型的地方。这些文件将在第 7 章的中详细讨论。

被许多人认为是 MySQL 源代码中最复杂和最优雅的部分,解析器是使用 lex 和 YACC 实现的,它们最初是为编译器构造而构建的。这些工具用于构建词法分析器,该分析器读取 SQL 语句并将语句分解成多个部分,将命令部分、选项和参数分配给变量和列表结构。这个结构(被形象地命名为 Lex)是 SQL 查询的内部表示。因此,查询过程中的每个其他步骤都会使用该结构。Lex 结构包含正在使用的表的列表、引用的字段名、连接条件、表达式以及存储在单独空间中的查询的所有部分。

解析器的工作方式是读取 SQL 语句,并将表达式(由标记和符号组成)与源代码中定义的规则进行比较。这些规则用 Lex 和 YACC 构建到代码中,然后用 Bison 编译形成词法分析器。如果您检查 C 形式的解析器(一个名为/sql/sql_yacc.cc 的文件),您可能会被 switch 语句的简洁和庞大所淹没。 13 检查解析器的一个更好的方法是在编译之前查看 Lex 和 YACC 格式(一个名为/sql/sql_yacc.yy 的文件)。这个文件包含了为 YACC 写的规则,更容易破译。解析器的构造说明了 Oracle 的开源哲学在起作用:为什么要创建自己的语言处理程序,而 Lex、YACC 和 Bison 等特殊的编译器构造工具就是为做这件事而设计的呢?

一旦解析器识别出一个正则表达式并将查询语句分成几个部分,它就将适当的命令类型分配给线程结构,并将控制返回给命令处理器(它有时被认为是解析器的一部分,但更准确地说是主代码的一部分)。命令处理器被实现为一个大的 switch 语句,支持每个命令的用例。查询解析器只检查 SQL 语句的正确性。它不验证所引用的表或属性(字段)是否存在,也不检查语义错误,例如没有使用 GROUP BY 子句的聚合函数。相反,验证工作留给了优化器。因此,来自解析器的查询结构被传递给查询处理器。从那里,控制切换到查询优化器。

莱克斯和 YACC

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

查询优化器

一些人认为 MySQL 查询优化器子系统命名不当。所使用的优化器是一个选择-项目-连接策略,它试图通过首先执行任何限制(选择)来缩小要处理的元组的数量,然后执行投影来减少结果元组中的属性(字段)的数量,最后评估任何连接条件来重新构造查询。虽然不被认为是极其复杂的查询优化器类别中的一员,但是 SELECT-PROJECT-JOIN 策略属于启发式优化器类别。在这种情况下,试探法(规则)很简单:

  • 通过计算 WHERE (HAVING)子句中的表达式来消除多余的数据。
  • 通过将数据限制为属性列表中指定的属性来消除额外的数据。例外情况是在连接子句中使用的属性的存储可能不会保留在最终查询中。
  • 评估连接表达式。

这就产生了一种策略,可以确保已知良好的访问方法以高效的方式检索数据。尽管有批评性的评论,SELECT-PROJECT-JOIN 策略在执行事务处理中的典型查询时被证明是有效的。图 2-5 描绘了一个描述 MySQL 查询处理方法的框图。

9781430246596_Fig02-05.jpg

图 2-5。 MySQL 查询处理方法

优化器的第一步是检查表的存在和用户的访问控制。如果有错误,将返回相应的错误消息,并将控制权返回给线程管理器或侦听器。一旦确定了正确的表,就打开它们,并为并发控制应用适当的锁。

一旦所有维护和设置任务完成,优化器就使用内部查询结构(Lex)并评估查询的 WHERE 条件(限制操作)。结果作为临时表返回,为下一步做准备。如果存在 UNION 操作符,优化器会在继续之前执行循环中所有语句的 SELECT 部分。

优化器的下一步是执行预测。它们的执行方式与限制部分类似,同样将中间结果存储为临时表,并且只保存那些在 SELECT 语句的列规范中指定的属性。最后,针对使用 JOIN 类构建的任何 JOIN 条件来分析该结构,然后调用 join::optimize() 方法。在这个阶段,通过评估表达式并消除任何导致死分支或始终为真或始终为假的条件(以及许多其他类似的优化)来优化查询。优化器试图在执行连接之前消除查询中任何已知的不良条件。这样做是因为连接是所有关系运算符中最昂贵和耗时的。请注意,连接优化步骤是为所有具有 WHEREHAVING 子句的查询执行的,不管是否有任何连接条件。这使得开发者能够将所有的表达式求值代码集中在一个地方。一旦连接优化完成,优化器就使用一系列条件语句将查询路由到适当的库方法来执行。

查询优化器和执行引擎可能是第二难理解的领域,因为它的 SELECT-PROJECT-JOIN 优化器方法。使事情变得复杂的是,服务器的这一部分是 C 和 C++代码的混合,其中典型的 select 执行被写成 C 方法,而 join 操作被写成 C++对象。在第 13 章中,我将向你展示如何编写你自己的查询优化器,并使用它来代替 MySQL 优化器。

查询执行

查询的执行由一组旨在实现特定查询的库方法来处理。例如, mysql_insert() 方法就是为了插入数据而设计的。同样,还有一个 mysql_select() 方法,用于查找并返回与 WHERE 子句匹配的数据。这个执行方法库位于多个具有相似名称的文件下的源代码文件中(例如, sql_insert.ccsql_select.cc )。所有这些方法都有一个 thread 对象作为参数,该对象允许方法访问内部查询结构并简化执行。使用网络通信路径库返回每种执行方法的结果。查询执行库方法显然是使用查询执行的解释模型实现的。

查询缓存

虽然不是它自己的子系统,查询缓存应该被认为是查询优化和执行子系统的重要部分。查询缓存是一项了不起的发明,它不仅缓存查询结构,还缓存查询结果本身。这使系统能够检查经常使用的查询,并简化整个查询优化和执行阶段。这是 MySQL 独有的另一项技术。其他数据库系统会缓存查询,但不会缓存实际结果。如您所知,查询缓存还必须考虑到结果“脏”的情况,即自上次运行查询以来发生了一些变化(例如,对基表运行了 INSERTUPDATEDELETE ),缓存的查询可能需要偶尔清除。

image 提示查询缓存默认开启。如果要关闭特定 SQL 语句的查询缓存,使用 SQL_NO_CACHE SELECT 选项: SELECT SQL_NO_CACHE id,lname FROM myCustomer。否则,可以使用服务器变量(query_cache_type、query_cache_size 也释放缓冲区)全局禁用它。

如果您不熟悉这项技术,请尝试一下。找到一个有足够数量元组的表,并执行一个复杂的查询,比如一个连接或复杂的 WHERE 子句。记录执行所用的时间,然后再次执行相同的查询。注意时差。这是运行中的查询缓存。

清单 2-1 展示了使用 SHOW 命令显示与查询缓存相关的系统变量的练习。请注意多次运行查询是如何将查询添加到缓存中的,随后的调用是如何从缓存中读取查询的。还要注意,SQL_NO_CACHE 选项不影响查询缓存变量(因为它不使用查询缓存)。

***清单 2-1。***MySQL 查询缓存在行动

mysql> CREATE DATABASE test_cache;
Query OK, 1 row affected (0.00 sec)

mysql> CREATE TABLE test_cache.tbl1 (a int);
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO test_cache.tbl1 VALUES (100), (200), (300);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> USE test_cache;
Database changed

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 0     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select length(now()) from tbl1;
+---------------+
| length(now()) |
+---------------+
|            19 |
|            19 |
|            19 |
+---------------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 0     |
+---------------+-------+
1 row in set (0.01 sec)

mysql> show status like "Qcache_inserts";
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| Qcache_inserts | 1     |
+----------------+-------+
1 row in set (0.00 sec)

mysql> show status like "Qcache_queries_in_cache";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Qcache_queries_in_cache | 1     |
+-------------------------+-------+
1 row in set (0.00 sec)

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 1     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SELECT SQL_NO_CACHE * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 1     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 2     |
+---------------+-------+
1 r
mysql>

缓存和缓冲区

缓存和缓冲子系统负责确保最常用的数据(或结构,正如您将看到的)以最有效的方式可用。换句话说,数据必须是常驻的,或者随时可以读取。缓存大大增加了对数据请求的响应时间,因为数据在内存中,因此检索数据不需要额外的磁盘访问。创建缓存子系统是为了将所有的缓存和缓冲封装到一组松散耦合的库函数中。尽管您会发现缓存是在几个不同的源代码文件中实现的,但它们被认为是同一个子系统的一部分。

在这个子系统中实现了许多缓存。大多数缓存机制使用相同或相似的概念,将数据存储为链表中的结构。缓存在代码的不同部分中实现,以根据缓存的数据类型定制实现。让我们来看看每个缓存。

表格缓存

创建表缓存是为了最小化打开、读取和关闭表的开销。磁盘上的 FRM 文件)。因此,它被设计为在内存中存储关于表的元数据。这是通过利用一种叫做 Unireg 的数据分块机制来实现的。Unireg 是 MySQL 的创始人创建的一种格式,曾经用于编写 TTY 应用。它将数据存储在称为屏幕的数据段中,这些数据段最初是为在监控器上显示数据而设计的。Unireq 使存储数据和显示(屏幕)更容易,从而加快了数据刷新。您可能已经猜到,这是一项过时的技术,它显示了 MySQL 源代码的时代。好消息是,正在计划重新设计表缓存,并最终替换或删除 Unireg 机制。

关于 FRM 档案的一句话

如果您检查 MySQL 安装的数据目录,您将看到一个名为 data 的文件夹,其中包含为每个创建的数据库命名的子文件夹。在这些文件夹中,您会看到以表格名称命名的文件,文件扩展名为*。frm* 。许多 MySQL 开发者称这些文件为“FRM 文件”。因此,database1 中名为 table1 的表有一个名为*/data/database 1/table 1 . frm*的 FRM 文件。

当您试图打开这些文件时,您会看到它们是二进制文件,无法通过正常方式读取。事实上,这些文件的格式多年来一直是个谜。因为 FRM 文件包含表的元数据,所以所有的列定义和表选项(包括索引定义)都存储在该文件中。这意味着应该可以从 FRM 文件中提取重建 CREATE TABLE 语句所需的数据。不幸的是,考虑到 Unireg 的接口和唯一性,要解析这些文件中的信息并不容易。幸运的是,有人正在努力通过 Python 工具解密 FRM 文件,该工具是 MySQL Workbench 的 MySQL 工具插件的一部分。如果您需要读取一个 FRM 文件来恢复一个表,请参阅在线 MySQL 实用程序文档了解更多细节:【dev.mysql.com/doc/workben…

这使得线程可以更快地读取表的模式,而不必每次都重新打开文件。每个线程都有自己的表缓存结构列表。这允许线程维护它们自己的表视图,这样,如果一个线程正在改变一个表的模式(但是还没有提交改变),另一个线程可以使用具有原始模式的那个表。使用的结构很简单,包含了一个表的所有元数据信息。这些结构存储在内存中的链表中,并与每个线程相关联。

缓冲池

缓冲池是 InnoDB 存储引擎使用的特殊缓存。它缓存表和索引数据,允许从内存中读取最常用的数据和索引,而不是从磁盘中重新读取。缓冲池显著提高了性能。根据您的访问模式,您可以调整缓冲池来分配更多的物理内存。这是用于调整 InnoDB 存储引擎性能的关键参数之一。

InnoDB 使用其他几种缓存。如果您需要 InnoDB 安装的额外性能,请参考在线参考手册中的“InnoDB 性能调优和故障排除”一节,了解 InnoDB 调优最佳实践。

记录缓存

创建记录缓存是为了增强存储引擎的顺序读取。因此,记录缓存通常只在表扫描期间使用。它的工作方式类似于预读缓冲区,一次检索一个数据块,从而减少扫描期间的磁盘访问。较少的磁盘访问通常等同于性能的提高。有趣的是,记录缓存也用于顺序写入数据,首先将新的(或更改的)数据写入缓存,然后在缓存满时写入磁盘。这样,写入性能也得到提高。这种顺序行为(称为引用局部性)是记录缓存最常与 MyISAM 存储引擎一起使用的主要原因,尽管它不限于 MyISAM。记录缓存以不可知的方式实现,不会干扰用于访问存储引擎 API 的代码。开发者不需要做任何事情来利用记录缓存,因为它是在 API 的层中实现的。

密钥缓存

键缓存是常用索引数据的缓冲区。在这种情况下,它是索引文件(B 树)的一个数据块,专门用于 MyISAM 表。磁盘上的 MYI 文件)。索引本身作为链表存储在键缓存结构中。第一次打开 MyISAM 表时,会创建一个键缓存。每次索引读取时都会访问键缓存。如果在缓存中找到一个索引,就从那里读取它;否则,必须从磁盘中读取新的索引块,并将其放入缓存中。缓存大小有限,可通过更改 key_cache_block_size 配置变量进行调整。因此,并非索引文件的所有块都适合内存。那么,系统如何跟踪哪些块已经被使用了呢?

缓存实现了一个监控系统来跟踪索引块的使用频率。已经实现了键缓存来跟踪索引块有多“热”。在这种情况下,Warm 指的是一段时间内索引块被访问的次数。暖值包括块 _ 冷块 _ 暖块 _ 热。随着块冷却,新块变热,冷块被清除,热块被添加。这种策略是最近最少使用的(LRU)页面替换策略,与操作系统中用于虚拟内存管理和磁盘缓冲的算法相同,即使面对复杂得多的页面替换算法,这种策略也被证明是非常有效的。以类似的方式,键缓存跟踪已经改变的索引块(称为“变脏”)。清除脏块时,其数据会在被替换之前写回磁盘上的索引文件。相反,当清除干净的块时,数据只是从内存中删除。

image 实践表明,LRU 算法的表现在最佳算法的 80%以内。在这个时间宝贵、简单确保可靠的世界里,80%的解决方案是双赢的。

特权缓存

特权缓存用于存储用户帐户上的授权数据。这些数据以与访问控制列表(ACL)相同的方式存储,ACL 列出了用户对系统中某个对象拥有的所有权限。特权高速缓存被实现为存储在先进后出(FILO)哈希表中的结构。在用户身份验证和初始化期间,当读取授权表时,收集高速缓存的数据。将这些数据存储在内存中很重要,因为这样可以节省大量读取授权表的时间。

主机名缓存

主机名缓存是另一个助手缓存,类似于特权缓存。它也是作为结构的堆栈来实现的。它包含到服务器的所有连接的主机名。这似乎令人惊讶,但这种数据经常被请求,因此需求量很大,是专用缓存的候选对象。

多方面的

MySQL 源代码中还实现了许多其他小型缓存机制。一个是连接缓冲区缓存,在复杂的连接操作中使用。例如,一些连接操作需要将一个元组与第二个表中的所有元组进行比较。在这种情况下,缓存可以存储读取的元组,以便可以实现连接,而不必多次将第二个表重新读取到内存中。

通过可插拔存储引擎访问文件

MySQL 最好的特性之一是支持不同存储引擎或文件类型的能力。这允许数据库专业人员通过选择最能满足其应用需求的存储引擎来调整其数据库性能。示例包括使用存储引擎为需要事务处理的高度活跃的数据库提供事务控制,或者每当表被多次读取但很少被更新时使用内存存储引擎(例如,查找表)。

Oracle 在 5.1 版中添加了新的体系结构设计,使添加新的存储类型变得更加容易。这种新机制被称为 MySQL 可插拔存储引擎。Oracle 通过可插拔存储引擎努力使服务器具有可扩展性。可插拔存储引擎是作为文件访问层的抽象而创建的,并作为一个 API 而构建,Oracle(或任何人)可以使用它来构建称为存储引擎的专用文件访问机制。API 提供了一组用于读写数据的方法和访问实用程序。这些方法结合起来形成了一个标准化的模块化体系结构,允许存储引擎对每个存储引擎使用相同的方法(这就是为什么它被称为可插拔的本质,所有存储引擎都使用相同的 API 插入到服务器中)。它还支持使用存储引擎插件。

可插拔 VS 插件

可插拔是指一个公共接口可能有几种不同的实现,允许实现的交换,而系统的其他部分保持不变。插件是模块(编译模块)的二进制形式,它实现了可插入的接口。因此,插件是可以在运行时改变的东西。InnoDB 存储引擎也是一个插件,MySQL 中的几个模块也是如此,它们计划将服务器的其他部分变成插件。

要启用插件存储引擎,使用安装插件命令。例如,要加载示例插件存储引擎,发出以下命令(示例适用于 Linux 操作系统):

mysql >安装插件示例 SONAME ' ha _ example.so

同样,要拔出存储引擎,请使用卸载插件命令:

mysql >卸载插件示例;

或者,您可以使用 mysql_plugin 客户端工具来启用和禁用存储引擎插件。

也许最有趣的是数据库实现者(您!)可以为给定数据库中的每个表分配不同的存储引擎,甚至可以在创建表后更改存储引擎。这种灵活性和模块化允许您根据需要创建新的存储引擎。要更改表的存储引擎,请发出如下命令:

ALTER TABLE MyTable
ENGINE = InnoDB;

可插拔存储引擎可能是 MySQL 最不寻常的特性。对于该体系结构的文件访问层,没有任何其他数据库系统能够达到这种级别的灵活性和可扩展性。以下部分描述了服务器中可用的所有存储引擎,并简要概述了如何创建自己的存储引擎。我将在第 10 章中向你展示如何创建你自己的存储引擎。

存储引擎的优势和劣势多种多样。例如,MySQL 中提供的一些存储引擎支持并发。从版本 5.6 开始,MySQL 的默认存储引擎是 InnoDB。InnoDB 表支持用于并发控制的记录锁定(有时称为行级锁定);当更新正在进行时,在操作完成之前,任何其他进程都不能访问表中的该行。因此,InnoDB 表类型为在预期有许多并发更新的情况下使用提供了优势。但是,这些存储引擎中的任何一个都可以在只读环境中很好地运行。例如 web 服务器或 kiosk 应用。

image 提示您可以通过设置 STORAGE_ENGINE 配置服务器变量来更改默认存储引擎。

MySQL 的早期版本默认使用 MyISAM 存储引擎。MyISAM 支持用于并发控制的表级锁定。也就是说,当更新正在进行时,在操作完成之前,任何其他进程都不能访问同一表中的任何数据。由于使用索引顺序访问方法(ISAM)原则进行了优化,MyISAM 存储引擎也是最快的可用类型。Berkeley Database (BDB) 表支持页级锁定进行并发控制;当更新正在进行时,在操作完成之前,任何其他进程都不能从与被修改的数据相同的页面访问任何数据。

我们讨论过的并发操作是在数据库系统中使用专门的命令实现的,这些命令构成了一个事务子系统。目前,列出的存储引擎中只有三个支持事务:InnoDB 和 NDB。事务提供了一种机制,允许一组操作作为单个原子操作执行。例如,如果为一个银行机构建立一个数据库,将资金从一个账户转移到另一个账户的宏操作将优选地被完整地执行(资金从一个账户转移到另一个账户),而不会中断。事务允许将这些操作封装在一个原子操作中,如果在所有操作完成之前发生错误,该原子操作将取消任何更改,从而避免数据从一个表中删除,并且永远不会进入下一个表。包含在事务命令中的 SQL 语句形式的一组示例操作是:

START TRANSACTION;
UPDATE SavingsAccount SET Balance = Balance – 100
WHERE AccountNum = 123;
UPDATE CheckingAccount SET Balance = Balance + 100
WHERE AccountNum = 345;
COMMIT;

实际上,如果需要更快的访问速度,大多数数据库专业人员会指定 MyISAM 表类型,如果需要事务支持,则指定 InnoDB。幸运的是,MySQL 提供了为数据库中的每个表指定表类型的工具。事实上,数据库中的表不必是同一类型。这种多样的存储引擎允许为各种应用调整数据库。

有趣的是,您可以通过编写自己的表处理程序来扩展这个存储引擎列表。MySQL 提供了示例和代码存根,使系统开发者可以访问这个特性。扩展存储引擎列表的能力使得 MySQL 支持复杂的专有数据格式和访问层成为可能。

innodbt1 版

InnoDB 是一个 Oracle 存储引擎,早于 Oracle 通过收购 Sun MicroSystems 收购 MySQL 。InnoDB 最初是由 inno base(www.innodb.com)授权的第三方存储引擎,并在 GNU 公共许可(GPL)协议下发布。Innobase 被 Oracle 收购,在收购 MySQL 之前,它被授权给 MySQL 使用。因此,现在 Oracle 同时拥有 MySQL 和 InnoDB,它消除了任何许可限制,并使两个产品开发团队能够协调开发。从这种关系中获得的好处最近在 MySQL 的最新版本中得到了显著的性能改进。

当您需要使用事务时,最常使用 InnoDB。InnoDB 支持传统的 ACID 事务(参见附带的侧栏)和外键约束。InnoDB 中的所有索引都是 B 树,索引记录存储在树的叶页面中。InnoDB 通过提供行级锁定改进了 MyISAM 的并发控制。InnoDB 是高可靠性和事务处理环境的首选存储引擎。

酸是什么?

酸代表原子性、一致性、隔离性和持久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。

  • 原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。也就是说,每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高事务环境(如金融市场)中运行的系统尤其重要。考虑一下资金转移的后果。通常,借记一个账户和贷记另一个账户需要多个步骤。如果在借记步骤后事务失败,并且没有将钱贷记回第一个帐户,该帐户的所有者将会非常生气。在这种情况下,从借方到贷方的整个事务必须成功,否则都不会成功。
  • 一致性意味着只有有效的数据才会存储在数据库中。也就是说,如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始之前的状态。相反,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。
  • 隔离意味着同时执行的多个事务不会相互干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务不能违反数据的情况(更改、删除等)。)正在另一个事务中使用。有很多方法可以解决这个问题。大多数系统使用一种叫做锁定的机制,在第一个事务完成之前防止数据被另一个事务使用。尽管隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。
  • 持久性意味着事务不会导致数据丢失,也不会丢失事务期间创建或更改的任何数据。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。

我的天啊

大多数 LAMP 堆栈、数据仓库、电子商务和企业应用都使用 MyISAM 存储引擎。MyISAM 文件是 ISAM 的扩展,通过额外的优化构建,如高级缓存和索引机制。这些表是使用压缩特性和索引优化来提高速度的。此外,MyISAM 存储引擎通过提供表级锁定来支持并发操作。MyISAM 存储机制为各种应用提供可靠的存储,同时提供快速的数据检索。当考虑读取性能时,MyISAM 是首选的存储引擎。

ISAM

ISAM 文件访问方法已经存在很长时间了。ISAM 最初由 IBM 创建,后来用于 System R,IBM 的实验性 RDBMS,被许多人认为是开创性的工作,是今天所有 RDBMS 的祖先。(有些人引用 Ingres 作为原始 RDBMS。)

ISAM 文件通过将数据组织成固定长度属性的元组来存储数据。元组以给定的顺序存储。这样做是为了加快从磁带访问的速度。是的,在过去,这是数据库实现者唯一的存储选择,当然,穿孔卡除外!(通常就是在这一点上,我会通过显示我的年龄来让自己尴尬。如果你也记得打孔卡,那么你和我可能会有一个很少有人会再次经历的经历——扔掉一副没有编号或打印的卡片(在卡片顶部打印数据过去需要很长时间,并且经常被跳过)。

ISAM 文件还有一个外部索引机制,通常实现为包含指针(磁带块数和计数)的哈希表,允许您将磁带快进到所需位置。这允许快速访问存储在磁带上的数据,就像磁带驱动器快进一样快。

虽然 ISAM 机制是为磁带创建的,但它也可以(而且经常)用于磁盘文件系统。ISAM 机制的最大优点是索引通常非常小,并且可以快速搜索,因为可以使用内存中的搜索机制来搜索它。一些较新版本的 ISAM 机制允许创建替代索引,从而允许通过多种搜索机制访问文件(表)。这种外部索引机制已经成为所有现代数据库存储引擎的标准。

MySQL 包含了一个 ISAM 存储引擎(当时称为表类型),但是 ISAM 存储引擎已经被 MyISAM 存储引擎所取代。未来的计划包括用更现代的事务存储引擎替换 MyISAM 存储引擎。

image 注意MySQL 的旧版本支持 ISAM 存储引擎。随着 MyISAM 的推出,Oracle 不再支持 ISAM 存储引擎。

存储器

内存存储引擎(有时称为堆表)是一个内存中的表,它使用哈希机制来快速检索经常使用的数据。因此,这些表比那些从磁盘存储和引用的表要快得多。它们的访问方式与其他存储引擎相同,但是数据存储在内存中,并且仅在 MySQL 会话期间有效。数据在关机(或崩溃)时被刷新和删除。内存存储引擎通常用于静态数据被频繁访问且很少被更改的情况。这种情况的例子包括邮政编码、州、县、类别和其他查找表。堆表也可以用于利用快照技术进行分布式或历史数据访问的数据库中。

image 提示您可以使用- init-file = file 启动选项自动创建基于内存的表。在这种情况下,指定的文件应该包含重新创建表的 SQL 语句。由于表是一次性创建的,所以可以省略 CREATE 语句,因为在系统重新启动时不会删除表定义。

合并

合并存储引擎是使用一组具有相同结构(元组布局或模式)的 MyISAM 表构建的,这些表可以作为单个表引用。因此,表是根据各个表的位置进行分区的,但是没有使用额外的分区机制。所有表必须驻留在同一台机器上(由同一台服务器访问)。使用单一操作或语句访问数据,例如选择更新插入删除。幸运的是,当在合并表上发出 DROP 时,只有合并规范被删除。原始表格没有改变。

这种表类型最大的好处就是速度。可以将一个大表分割成不同磁盘上的几个小表,使用合并表规范将它们组合起来,并同时访问它们。搜索和排序将执行得更快,因为每个表中需要操作的数据更少。例如,如果按谓词划分数据,则可以只搜索包含要搜索的类别的特定部分。同样,对表的修复更有效,因为修复几个较小的单个文件比修复单个大表更快更容易。据推测,大多数错误将局限于一个或两个文件内的区域,因此不需要重建和修复所有数据。不幸的是,这种配置有几个缺点:

  • 您只能使用相同的 MyISAM 表或架构来形成一个合并表。这限制了合并存储引擎在 MyISAM 表中的应用。如果合并存储引擎接受任何存储引擎,合并存储引擎将更加通用。
  • 不允许替换操作。
  • 已经证明索引访问比单个表的效率低。
  • 合并存储机制最适合用在超大型数据库(VLDB)应用中,例如数据驻留在一个或多个数据库的多个表中的数据仓库。

存档

档案存储引擎设计用于以压缩格式存储大量数据。存档存储机制最适合用于存储和检索大量很少访问的存档或历史数据。这种数据包括安全访问数据日志。虽然这不是您想要搜索甚至日常使用的东西,但是如果发生安全事故,关心安全的数据库专业人员会希望拥有它。

没有为归档存储机制提供索引,唯一的访问方法是通过表扫描。因此,归档存储引擎不应用于正常的数据库存储和检索。

联盟

联邦存储引擎被设计为从多个数据库系统创建单个表引用。因此,联邦存储引擎的工作方式类似于合并存储引擎,但是它允许您跨数据库服务器将数据(表)链接在一起。这种机制在目的上类似于其他数据库系统中可用的链接数据表。联邦存储机制最适合在分布式或数据集市环境中使用。

联邦存储引擎最有趣的方面是它不移动数据,也不要求远程表是同一个存储引擎。这说明了可插拔存储引擎层的真正威力。数据在存储和检索过程中被转换。

集群/NDB

集群存储引擎(称为 NDB,以区别于集群产品 14 )被创建来处理 MySQL 的集群服务器功能。当在高可用性和高性能环境中集群多个 MySQL 服务器时,几乎只使用集群存储机制。群集存储引擎不存储任何数据。相反,它将数据的存储和检索委托给集群中数据库使用的存储引擎。它管理跨集群分发数据的控制,从而提供冗余和性能增强。NDB 存储引擎还提供了用于创建可扩展集群解决方案的 API。

CSV〔??〕

CSV 存储引擎被设计为以表格形式创建、读取和写入逗号分隔值(CSV)文件。虽然 CSV 存储引擎不会将数据复制为另一种格式,但图纸布局或元数据会与服务器上指定的文件名一起存储在数据库文件夹中。这允许数据库专业人员快速导出存储在电子表格中的结构化业务数据。CSV 存储引擎不提供任何索引机制。

由于 CSV 存储引擎的简单性,其源代码为想要或需要开发自己的存储引擎的开发者提供了一个极好的起点。您可以在源代码树的/storage/csv/ha_tina.h 和/storage/csv/ha_tina.cc 文件中找到 CSV 存储引擎的源代码。

蒂娜是谁?

关于 CSV 存储引擎的源代码,一个有趣的事实是,它是以原作者的一个朋友的名字命名的,旨在成为一个特殊的,而不是通用的解决方案。幸运的是,存储引擎已被证明对更广泛的受众有用。不幸的是,一旦源文件被引入,就没有任何改变文件名的动机。

黑洞〔??〕

黑洞存储引擎,一个有着惊人效用的有趣特性,被设计成允许系统写数据,但是数据永远不会被保存。如果启用了二进制日志记录,则 SQL 语句将被写入日志。这允许数据库专业人员通过切换表类型来临时禁用数据库中的数据接收。在您想要测试应用以确保它正在写入您不想存储的数据的情况下,这可能会很方便,例如在出于过滤复制的目的而创建中继从属时。

自定义

自定义存储引擎代表您为增强数据库服务器而创建的任何存储引擎。例如,您可能希望创建一个读取 XML 文件的存储引擎。虽然您可以将 XML 文件转换成表格,但是如果您需要访问大量文件,您可能不希望这样做。下面是如何创建这样一个引擎的概述。

如果您考虑使用 XML 存储引擎来读取一组特定的相似 XML 文件,那么您要做的第一件事就是分析 XML 文件的格式或模式,并确定您希望如何解析 XML 文件的自描述性质。假设所有文件都包含相同的基本数据类型,但是具有不同的标签和标签顺序。在这种情况下,您决定使用样式表将文件转换成一致的格式。

一旦决定了格式,就可以开始开发新的存储引擎,方法是检查 MySQL 源代码中包含的示例存储引擎,该示例存储引擎位于名为的文件夹中。主源代码树上的\storage\example 。您会发现一个 makefile 和两个源代码文件( ha_example.h ,ha_example.cc),其中包含一组允许引擎工作的代码,但这些代码并不真正有趣,因为它不做任何事情。但是,您可以阅读程序员留下的注释,这些注释描述了您需要为自己的存储引擎实现的特性。比如打开文件的方法叫做 ha_example::open 。当您检查示例存储引擎文件时,可以在 ha_example.cpp 文件中找到此方法。清单 2-2 显示了一个打开方法的例子。

清单 2-2。 开表法

/**
  @brief
  Used for opening tables. The name will be the name of the file.

  @details
  A table is opened when it needs to be opened; e.g. when a request comes in
  for a SELECT on the table (tables are not open and closed for each request,
  they are cached).

  Called from handler.cc by handler::ha_open(). The server opens all tables by
  calling ha_open() which then calls the handler specific open().

  @see
  handler::ha_open() in handler.cc
*/
int ha_example::open(const char *name, int mode, uint test_if_locked)
{
  DBUG_ENTER("ha_example::open");

  if (!(share = get_share()))
    DBUG_RETURN(1);
  thr_lock_data_init(&share->lock,&lock,NULL);

  DBUG_RETURN(0);
}

image 提示您也可以在 Microsoft Windows 环境中创建存储引擎。在这种情况下,文件位于 Visual Studio 项目中。

清单 2-2 中的例子解释了 ha_example::open 方法是做什么的,并让你知道它是如何被调用的以及预期的返回。虽然源代码现在对你来说可能看起来很陌生,但是越读越清楚,对 MySQL 编码风格越熟悉。

image 注意MySQL 的早期版本(5.1 版之前)允许创建定制的存储引擎,但需要您重新编译服务器可执行文件才能获得更改。借助新的 5.1 版可插拔架构,模块化 API 允许存储引擎具有不同的实现和特性,并允许它们独立于 MySQL 系统代码进行构建。因此,您不需要直接修改 MySQL 源代码。您的新存储引擎项目允许您创建自己的定制引擎,然后编译并将其与现有的运行服务器相链接。

一旦熟悉了示例存储引擎及其工作方式,就可以复制文件并将其重命名为更适合新引擎的名称,然后开始修改文件以从 XML 文件中读取。像所有优秀的程序员一样,你从一次实现一个方法开始,测试你的代码,直到你满意它能正常工作。一旦您拥有了想要的所有功能,并且编译了存储引擎并将其链接到生产服务器,您的新存储引擎就可供任何人使用了。

虽然这听起来像是一个困难的任务,但实际上并不是,这是开始学习 MySQL 源代码的一个好方法。在第 7 章的中,我将返回创建一个定制存储引擎的详细步骤说明。

摘要

在这一章中,我介绍了典型的 RDBMS 的体系结构。虽然这不是一堂完整的数据库理论课,但本章让您了解了关系数据库体系结构的内部,现在您应该对机器内部发生的事情有所了解。我还研究了 MySQL 服务器架构,并解释了构成 MySQL 服务器架构的所有部分在源代码中的位置。

RDBMS 如何工作的知识和 MySQL 服务器体系结构的研究将为您扩展 MySQL 数据库系统的密集旅程做好准备。有了 MySQL 架构的知识,你现在就武装起来了(但不是很危险)。

在下一章中,我将带领您浏览 MySQL 源代码,这将使您能够开始扩展 MySQL 系统以满足您自己的需求。所以卷起你的袖子,带上你的极客; 15 我们要进入源代码了!

1 有一些值得注意的例外,但这是普遍的事实。

2 当在填充的数据存储中修改对象类型时尤其如此。根据所做的更改,对象的行为可能已经改变,因此可能不具有相同的含义。尽管这可能是一种有意的改变,但这种改变的影响可能比典型的关系系统更严重。

3 C. J. Date,《数据库关系模型:回顾与分析》(雷丁,马:Addison-Wesley,2001)。

4 C. J. Date 和 H. Darwen,《未来数据库系统的基础:第三宣言》(雷丁,马:艾迪生-卫斯理,2000 年)。

数据库系统处理空值的一些方式从荒谬到不直观。

6 有时定义为对象数据库连通性或在线数据库连通性,但公认的定义是开放数据库连通性。

7 M. Stonebraker 和 J. L. Hellerstein,*数据库系统中的读数,*第 3 版。,由迈克尔·斯通布雷克编辑(摩根·考夫曼出版社,1998)。

8 C. J. Date,《数据库关系模型:回顾与分析》(雷丁,马:Addison-Wesley,2001)。

9 R. Elmasri 和 S. B. Navathe,数据库系统基础,第 4 版。(波士顿:艾迪森-韦斯利出版社,2003 年)。

10 A. B .塔克,计算机科学手册,第二版。(佛罗里达州博卡拉顿:CRC 出版社,2004 年)。

11 统计在数据库中的使用源于最早的基于成本的优化器。事实上,商业数据库中的许多实用程序允许数据库专业人员检查和生成这些统计数据,以调整他们的数据库,从而更有效地优化查询。

12 M .克鲁肯伯格和 j .皮波斯。 Pro MySQL 。(加州伯克利:阿普莱斯出版社,2005 年)。

13 克鲁肯伯格和派普斯把这种体验比作心灵的融化。撇开轻浮不谈,对任何不熟悉 YACC 的人来说,这都是一个挑战。

14 关于 NDB API 的更多信息,参见 dev.mysql.com/doc/ndbapi/….

众所周知,我们中的许多人在编写代码时,会摆出斜躺在电脑椅上、喝着含咖啡因的现成饮料、听着震耳欲聋的音乐、手按键盘的姿势。

三、MySQL 源代码之旅

本章对 MySQL 源代码进行了完整的介绍,并解释了如何获取和构建该系统。我向您介绍了源代码的机制,以及如何维护代码的编码指南和最佳实践。我将重点放在处理查询的代码部分;这将为第 11 章及以后介绍的主题做好准备。我也给你一个简短的概述,介绍动态加载包含特性的库的插件系统。

入门指南

在这一节中,我将研究修改 MySQL 源代码背后的原则,以及如何获得源代码。让我们先回顾一下可用的许可选项。

了解许可选项

当计划对开源软件进行修改时,要考虑如何使用这些修改。更具体地说,您将如何获取源代码并使用它?根据你对修改的意图,你的选择会和其他人非常不同。您可能希望通过三种主要方式来修改源代码:

  • 深入了解 MySQL 是如何构建的;因此,你是在遵循本书中的例子,还是在进行自己的实验。
  • 为您或您的组织开发一项不会在组织外分发的功能。
  • 构建计划与其他人共享或推广的应用或扩展。

在第一章中,我讨论了在开源许可下修改软件的开源开发者的责任。既然 MySQL 是在 GPLv2 下发布的,也是在商业许可下发布的(一个双许可,我们必须考虑在双许可下源代码的使用。我将从 GPLv2 开始我们的讨论。

根据 GPL,在纯学术会议中修改源代码是允许的,这显然给了你修改和试验源代码的自由。您贡献的价值可能决定您的代码是否在 GPL 下发布。例如,如果您的代码修改被认为是单一的(它们只适用于有限的一组特殊用途的用户),那么该代码可能不包含在源代码库中。类似地,如果你的代码专注于学术实践的探索,那么除了你自己,代码可能对任何人都没有价值。很少有人会将测试源代码中实现的选项和特性的学术练习视为对 MySQL 系统的增值。另一方面,如果你的实验为系统带来了成功和有意义的贡献,大多数人会同意你有义务分享你的发现。出于本书的目的,您将继续修改源代码,就好像您不会共享您的修改一样。虽然我希望你会发现本书中的实验具有启发性和娱乐性,但我不认为在没有进一步开发的情况下,它们会被考虑采用到 MySQL 系统中。如果你采用这些例子,并从中做出一些精彩的东西,我祝福你。一定要告诉所有人你的想法是从哪里来的。

image 警告如果您正在规划一个计划以任何方式与任何人共享的项目,请联系 Oracle 的 MySQL 销售人员,了解您当前的许可以及支持您的目标的许可选项的可用性。

如果您正在修改供您或您的组织使用的 MySQL 源代码,并且您不想共享您的修改,您应该购买适当的 MySQL 商业许可证。MySQL 的商业许可条款允许您进行修改(甚至让 Oracle 来帮助您)并保留这些修改。

类似地,如果你正在修改源代码并打算发布修改,GPL 要求你免费发布修改后的源代码(但你可能会收取媒体费)。在这样做之前,您应该咨询 Oracle。

此外,您的更改不能成为专有的,并且您不能拥有 GPL 下的修改权利。如果您选择不自己发布您的更改,您应该将代码提交给 Oracle 考虑。如果被接受,它将成为甲骨文的财产。另一方面,如果您想对 MySQL 进行专有修改,以便在嵌入式系统或类似安装中使用,请在启动项目之前联系 Oracle 并讨论您的计划。

获取源代码

您可以从 MySQL 开发者网站(dev.mysql.com/downloads)下载 MySQL 源代码。在该网站上,您会看到下载所有 MySQL 开源产品的链接。(要使用这本书,您需要 MySQL 社区版。)你还会看到几个下载不同版本服务器的链接,包括:

  • 生产使用的当前版本(也称为普遍可用版本或 GA)
  • 软件的旧版本
  • 每个版本的文档

如果向下滚动,您会看到一个下拉框,允许您选择您的平台。这将下载服务器的二进制版本,包括在您的系统上安装和运行它所需的一切。您还会看到名为“源代码”的条目这是您将用来下载源代码的链接。

您还可以下载新版本服务器的源代码,称为“开发版本”您可以点击选项卡,看到一个类似的列表,用于选择平台或源代码。提醒一下,开发版本是最先进的功能预览,可能包含也可能不包含最终的生产代码,因此,不应考虑在生产环境中使用。出于本书的目的,您可以使用开发发布版本 5.6.5 或更高版本。

要了解本书中的示例,请从网站下载版本 5.6.5 或更高版本。我将在下一节提供安装 MySQL 的说明。该站点包含所有受支持环境的所有二进制文件和源代码。支持许多不同的平台。你会在页面底部找到源代码。为您的平台下载源代码和二进制文件(两次下载)。在本书中,我将使用 Ubuntu 和微软 Windows 7 中的例子。

image 提示如果你用的是 Windows,下载 MSI 安装程序。事实上,可以考虑下载 MySQL Windows installer。它包含了所有的 MySQL 组件,使得在 Windows 上安装 MySQL 成为一个简单快速的过程。在你的 Windows 系统上安装 MySQL 是最好的方法。

旧平台支持

如果您没有看到您选择的二进制发行版中列出您的平台,很可能您的平台太新,不再受支持,或者尚未包括在内。如果出现这种情况,您仍然可以下载源代码并自己构建。

image 除非另有说明,本书中的例子均取自 Linux 源代码发行版(mysql-5.6.5)。虽然 Linux 和 Windows 发行版的大部分代码都是相同的,但我强调了它们之间的差异。最值得注意的是,Windows 平台有一个稍微不同的vio实现。

MySQL 源代码

下载源代码后,将文件解压到系统上的一个文件夹中。如果愿意,您可以将它们解压缩到同一个目录中。当您这样做时,请注意有许多文件夹和许多源文件。您需要引用的主文件夹是/sql文件夹。这包含服务器的主要源文件。表 3-1 列出了最常访问的文件夹及其内容。

表 3-1。 MySQL 源文件夹

文件夹内容
/BUILD支持所有平台的编译配置和 make 文件。
/clientMySQL 命令行客户端工具。
/cmakeCMake 跨平台构建系统的配置文件。
/dbug调试中使用的实用程序(详见第 5 章。
/include基本系统包括文件和头文件。
/libmysql用于 MySQL 客户端应用以及创建嵌入式系统的 C 客户端 API。(更多详情见第 6 章。)
/libmysqld核心服务器 API 文件。也用于创建嵌入式系统。(更多详情见第 6 章。)
/mysql-testMySQL 系统测试套件。(更多详情请参见第 4 章。)
/mysys大多数核心操作系统 API 包装器和助手函数。
/plugin包含所有提供的插件的源代码的文件夹。
/regex正则表达式库。在查询优化器和执行中用于解析表达式。
/scripts一组基于 shell 脚本的实用程序。
/sql主系统代码。你应该从这个文件夹开始你的探索。
/sql-bench一套基准测试工具。
/storageMySQL 可插拔存储引擎源代码位于该文件夹中。还包括存储引擎示例代码。(详见第 7 章。)
/strings核心字符串处理包装器。使用这些来满足您所有的字符串处理需求。
/support-files一组预配置的配置文件,用于使用不同的选项进行编译。
/tests一组测试程序和测试文件。
/vio网络和套接字层代码。
/zlib数据压缩工具。

我建议你现在花些时间仔细阅读一些文件夹,熟悉文件的位置。您会发现许多类型的文件和各种 Perl 脚本分散在文件夹中。虽然没有过分简单化,但 MySQL 源代码在逻辑上是围绕源代码的功能而不是核心子系统组织的。一些子系统,如存储引擎和插件,位于文件夹层次结构中,但大多数位于文件夹结构中的几个位置。对于检查源代码时讨论的每个子系统,我列出了相关的源文件及其位置。

入门指南

理解 MySQL 系统的流程和控制的最佳方式是从典型查询的角度来看源代码。我在第 2 章的中展示了每个主要 MySQL 子系统的高级视图。我现在使用相同的子系统视图,向您展示典型的 SQL 语句是如何执行的。我使用的示例 SQL 语句是:

SELECT lname, fname, DOB FROM Employees WHERE Employees.department = 'EGR'

该查询选择工程部门每个人的姓名和出生日期。虽然不是很有趣,但这个查询在演示 MySQL 系统中的几乎所有子系统时会很有用。让我们从到达服务器进行处理的查询开始。

图 3-1 显示了示例查询通过 MySQL 源代码的路径。我已经抽出了主要的代码行,你应该把它们与第二章中确定的子系统联系起来。我还简化和省略了一些参数列表,使图形更容易阅读。虽然不是特定子系统的一部分,但是mysqld_main()函数负责初始化服务器和设置连接监听器。mysqld_main()函数在文件/sql/mysqld.cc中。

image Windows 系统执行win_main()方法 ,也位于mysqld.cc

9781430246596_Fig03-01.jpg

图 3-1。查询路径概述

查询的路径一旦到达服务器,就从 SQL 接口子系统开始(像大多数 MySQL 子系统一样,SQL 接口函数分布在一组松散关联的源文件上)。在您阅读这一部分和后面的部分时,我会告诉您这些方法在哪个文件中。handle_connections_socket()方法(位于in /sql/mysqld.cc)实现监听器循环,为每个检测到的连接创建一个线程。一旦线程被创建,控制就流向do_handle_one_connection()函数。do_handle_one_connection()功能识别命令,然后将控制传递给do_command开关(位于/sql/sql_parse.cc)。do_command开关将控制路由到适当的网络读取调用,以从连接中读取查询,并通过dispatch_command()函数(位于in /sql/sql_parse.cc)将查询传递给解析器。

查询传递到查询解析器子系统,在那里查询被解析并路由到优化器的正确部分。查询解析器内置了 Lex 和 YACC。Lex 用于标识语言的标记和文字以及语法。YACC 用于构建与 MySQL 源代码交互的代码。它捕获 SQL 命令,将命令的部分存储在内部查询表示中,并将命令路由到名为mysql_execute_command()(有点名不副实)的命令处理器。然后,该方法将查询路由到适当的子功能,在本例中是mysql_select()。这些方法位于/sql/sql_parse.cc/sql/sql_select.cc。这部分代码进入 SELECT-PROJECT-JOIN 查询优化器的 SELECT-PROJECT 部分。

image 提示项目或投影是一个关系数据库术语,描述将结果集限制为 SQL 命令的列列表中定义的那些列的查询操作。例如,SQL 命令SELECT fname, lname FROM employee只将雇员表中的fnamelname列“投影”到结果集中。

此时,查询优化器被调用,通过位于/sql/sql_resolver.cc的函数join->prepare()和位于/sql/sql_optimizer.cc的函数join->optimize()来优化查询的执行。接下来在位于/sql/sql_executor.cc的 join- > exec()中执行查询,控制传递给位于/sql/sql_executor.cc中的较低级别的do_select()函数,该函数执行限制和投影操作。最后,sub _select()函数调用存储引擎读取元组,对其进行处理,并将结果返回给客户端。这些方法位于/sql/sql_executor.cc中。在结果被写入网络后,控制返回到handle_connections_sockets循环(位于in /sql/mysqld.cc)。

image 提示类、结构、类、结构——这都是关于类和结构的!在研究 MySQL 源代码时,请记住这一点。对于服务器中的任何操作,至少有一个类或结构管理数据或驱动执行。学习常用的 MySQL 类和结构是理解源代码的关键,你会在本章后面的“重要的类和结构”中看到。

您可能认为代码并不像您听到的那样糟糕。对于简单的SELECT 语句,比如我正在使用的例子,这在很大程度上是正确的,但是正如你很快会看到的,它可以变得比这更复杂。既然您已经看到了这条路径,并且已经了解了一些主要函数在查询和子系统路径中的位置,那么打开源代码并寻找这些函数。您可以在/sql/mysqld.cc中开始搜索。

好的,这是一个旋风般的介绍,对吗?从这一点开始,我放慢了一点速度(好吧,慢了很多),更详细地浏览源代码。我还在每一节的末尾以表格的形式列出了示例所在的特定源文件。所以系紧安全带,我们要进去了!

我省略了与我们旅行无关的部分。这些可能包括条件编译指令、辅助代码和其他系统级调用。我对缺失的部分做了如下注释:...。我保留了许多原始注释,因为我相信它们将帮助您了解源代码,并让您对开发世界一流的数据库系统有所了解。最后,我用粗体突出显示了代码的重要部分,以便您在阅读时可以更容易地找到它们。

函数的作用是

服务器开始执行的mysqld_main()函数位于/sql/mysqld.cc中。它是服务器可执行文件加载到内存中时调用的第一个函数。这个函数中有数百行代码专门用于特定于操作系统的启动任务,还有大量的系统级初始化代码。清单 3-1 显示了代码的压缩视图,要点用粗体显示。

清单 3-1 。main()函数

int mysqld_main(int argc, char **argv)
{
  ...

  if (init_common_variables())

  ...

  if (init_server_components())

  ...
  /*
   Initialize my_str_malloc() and my_str_free()
  */
  my_str_malloc= &my_str_malloc_mysqld;
  my_str_free= &my_str_free_mysqld;

  ...

  if (mysql_rm_tmp_tables() || acl_init(opt_noacl) ||
      my_tz_init((THD *)0, default_tz_name, opt_bootstrap))

  ...

  create_shutdown_thread();

  ...

  handle_connections_sockets();

  ...

  (void) mysql_mutex_lock(&LOCK_thread_count);

  ...

  (void) mysql_mutex_unlock(&LOCK_thread_count);

  ...
}

第一个有趣的函数是init_common_variables()。这使用命令行参数来控制服务器的运行方式;它是服务器解释参数并以各种模式启动服务器的地方。该函数负责设置系统变量,并将服务器置于所需的模式。init-server-components()函数初始化数据库日志,供任何子系统使用。这些是您看到的事件、语句执行等的典型日志。

两个最重要的my_库函数是my_str_malloc()my_str_free()。这两个函数指针就是在服务器启动代码中的这一点(靠近开头)被设置的。您应该总是使用这些函数来代替传统的 C/C++ malloc()函数,因为 MySQL 函数有额外的错误处理,因此比基本方法更安全。acl_init()函数的工作是启动认证和访问控制子系统。这个密钥系统出现在服务器启动代码的早期。

现在你开始了解 MySQL 的成功之处:线程。创建了两个重要的助手线程。函数创建一个线程,它的任务是在收到信号时关闭服务器。我将在“进程与线程”侧栏中更详细地讨论线程。

在启动代码的这一点上,系统已经准备好接受来自客户端的连接。为了做到这一点,handle-connections-sockets()函数实现了一个监听器,它循环遍历等待连接的代码。接下来我将更详细地讨论这个函数。

我想在代码中指出的最后一点是多线程期间互斥访问的临界区保护代码的一个例子。临界区是必须作为一个集合执行的代码块,一次只能由一个线程访问。临界区通常是写入共享内存变量的区域,因此它们必须在另一个线程试图读取内存之前完成。Oracle 创建了一种常见并发保护机制的抽象,称为互斥(互斥的缩写)。如果您在代码中找到一个需要在并发执行期间保护的区域,请使用以下函数来保护代码。

你应该调用的第一个函数是mysql_mutex_lock([resource reference]。这会在代码的这一点锁定代码的执行。它不允许另一个线程访问指定的内存位置,直到您的代码调用解锁函数mysql_mutex_unlock([resource reference])。在来自mysqld_main()函数的例子中,互斥调用锁定了线程计数全局变量。

这是你第一次在引擎盖下潜水。感觉如何?你想要更多吗?继续读——你才刚刚开始。事实上,您还没有看到我们的示例查询是从哪里进入系统的。让我们接下来做那件事。

进程与线程

术语进程线程经常互换使用。这是不正确的,因为进程是一组有组织的计算机指令,它有自己的内存和执行路径。一个线程也是一组计算机指令,但是线程在一个主机的执行路径中执行,没有自己的内存。(有些人称线程为轻量级进程。虽然这是一个很好的描述,但对它们的称呼无助于区分。)它们存储状态(在 MySQL 中,是通过THD类)。因此,当谈到支持进程的大型系统时,我指的是允许系统的各个部分作为独立的进程执行并拥有自己的内存的系统。当谈到支持线程的大型系统时,我指的是允许系统的某些部分与系统的其他部分并发执行的系统,它们都与主机共享相同的内存空间。

大多数数据库系统使用进程模型来管理并发连接和助手功能。MySQL 使用多线程模型。与进程相比,使用线程有很多优点。最值得注意的是,线程更容易创建和管理(没有内存分配和隔离的开销)。线程也允许非常快速的切换,因为没有上下文切换发生。然而,线程确实有一个严重的缺点。如果事情变得不稳定(这是一个高度技术性的术语,用来描述奇怪的、无法解释的行为;在线程的情况下,它们通常是非常奇怪和有害的事件),如果问题很严重,很可能会影响整个系统。幸运的是,Oracle 和全球开发者社区已经非常努力地让 MySQL 的线程子系统变得健壮和可靠。这就是为什么你的修改必须是线程安全的。

处理连接并创建线程

在上一节中,您看到了系统是如何启动的,以及控制是如何流向等待用户连接的侦听器循环的。连接始于客户端,并被分解成数据包,由客户端软件放在网络上,然后流经网络通信路径,由服务器的网络子系统接收,并在服务器上重新形成数据。(关于通信包的完整描述可以在 MySQL 内部手册中找到。)这个流程可以在图 3-2 中看到。我将在下一章展示更多关于网络通信方法的细节。我还提供了一些例子,说明如何编写代码,使用这些函数向客户机返回结果。

9781430246596_Fig03-02.jpg

图 3-2。从客户端到服务器的网络通信

此时,系统处于 SQL 接口子系统中。也就是说,数据包(包含查询)已经到达服务器,并通过handle_connections_sockets()函数被检测到。该函数进入一个循环,等待变量abort_loop被设置为TRUE表 3-2 显示了管理连接和线程的文件位置。

表 3-2。连接和线程管理

源文件描述
/sql/net_serv.cc包含所有网络通信功能。有关如何通过网络与客户端或服务器通信的信息,请查看此处。
/include/mysql_com.h包含通信中使用的大多数结构。
/sql/sql_parse.cc包含除词法分析器之外的大多数查询路由和分析功能。
/sql/mysqld.cc除了 mysqld_main 和服务器启动函数之外,这个文件还包含创建线程的方法。

清单 3-2 提供了连接处理代码的浓缩视图。当检测到一个连接时(我已经隐藏了这部分代码,因为它对了解系统如何工作没有帮助),函数创建一个新的线程,调用恰当命名的create_new_thread()函数。正是在这个功能中,第一个主要结构被创建。THD类负责维护线程的所有信息。虽然没有在私有内存空间中分配给线程,但是THD类允许系统在执行过程中控制线程。我将在后面的部分公开一些THD类。

清单 3-2 句柄-连接-套接字功能

void handle_connections_sockets()
{

  ...

  DBUG_PRINT("general",("Waiting for connections."));

  ...

  while (!abort_loop)
  {

  ...

    /*
    ** Don't allow too many connections
    */

    if (!(thd= new THD))

  ...

    create_new_thread(thd);
  }

  ...
}

好了,客户端已经连接到服务器了。接下来会发生什么?让我们看看create_new_thread()函数内部发生了什么。清单 3-3 显示了这个函数的一个浓缩视图。首先看到的是锁定线程数的互斥调用。正如您在mysqld_main()函数中看到的,这对于防止其他线程竞争对变量的写访问是必要的。创建线程时,会调用相关的解锁互斥体来解锁资源。

清单 3-3T5【create _ new _ thread()函数

static void create_new_thread(THD *thd)
{

...

  /*
    Don't allow too many connections. We roughly check here that we allow
    only (max_connections + 1) connections.
  */
  mysql_mutex_lock(&LOCK_connection_count);
  if (connection_count >= max_connections + 1 || abort_loop)
  {
    mysql_mutex_unlock(&LOCK_connection_count);
...
    close_connection(thd, ER_CON_COUNT_ERROR);
    delete thd;
...
  }

  ++connection_count;

  if (connection_count > max_used_connections)
    max_used_connections= connection_count;
  mysql_mutex_unlock(&LOCK_connection_count);
  /* Start a new thread to handle connection. */
  mysql_mutex_lock(&LOCK_thread_count);
...

  thd->thread_id= thd->variables.pseudo_thread_id= thread_id++;
  MYSQL_CALLBACK(thread_scheduler, add_connection, (thd));
...

}

一件非常有趣的事情发生在函数的早期。注意MYSQL_CALLBACK()宏。该宏旨在重用驻留在连接池中的线程。这有助于加快速度,因为创建线程虽然比创建进程快,但会花费一些时间。让线程准备就绪是一种连接缓存机制。为以后使用而保存的线程被称为连接池

如果没有可供重用的连接(线程),系统会通过调用pthread_create()函数创建一个。这里发生了非常奇怪的事情。注意这个函数调用的第三个参数。看似变量的东西实际上是函数的起始地址(函数指针)。pthread_create()使用这个函数指针来关联服务器中线程开始执行的位置。

既然查询已经从客户机发送到服务器,并且已经创建了一个线程来管理执行,那么控制就传递给 do_ handle_one_connection()函数。清单 3-4 显示了 do_ handle_one_connection()函数的浓缩视图。在这个视图中,我注释掉了处理初始化THD类以供使用的一大段代码。如果您感兴趣,稍后可以仔细看看代码(位于/sql/sql_connect.cc)。现在,让我们看看这个函数内部的基本工作。

清单 3-4do _ handle _ one _ connection()函数

void do_handle_one_connection(THD *thd_arg)
{
  THD *thd= thd_arg;

...

    while (thd_is_connection_alive(thd))
    {
      mysql_audit_release(thd);
      if (do_command(thd))
        break;
    }
    end_connection(thd);

...

}

在这种情况下,我们研究的唯一感兴趣的函数调用是do_command(thd)函数。它位于一个循环中,对于从网络通信代码中读取的每个命令,该循环都循环一次。虽然在这一点上有点神秘,但这是我们这些输入过堆叠 SQL 命令(同一行中有多个命令)的人感兴趣的。正如您在这里看到的,这是 MySQL 处理这种可能性的地方。对于每个命令读取,该函数将控制传递给开始从网络读取查询的函数。

此时,系统从网络中读取查询,并将其放入THD类进行解析。这发生在do_command()函数中。清单 3-5 显示了do_command()函数的浓缩视图。我留下了一些更有趣的注释和代码来演示 MySQL 源代码的健壮性。

清单 3-5。do _ command()函数

bool do_command(THD *thd)
{
  bool return_value;
  char *packet = 0;
  ulong packet_length;
  NET *net= &thd->net;
  enum enum_server_command command;

...

  net_new_transaction(net);

...

  packet_length= my_net_read(net);

...

  if (packet_length == packet_error)
  {
    DBUG_PRINT("info",("Got error %d reading command from socket %s",
         net->error,
         vio_description(net->vio)));

...

  command= (enum enum_server_command) (uchar) packet[0];

  if (command >= COM_END)
    command= COM_END; // Wrong command

  DBUG_PRINT("info",("Command on %s = %d (%s)",
                     vio_description(net->vio), command,
                     command_name[command].str));

  ...

  my_net_set_read_timeout(net, thd->variables.net_read_timeout);

  DBUG_ASSERT(packet_length);

  return_value= dispatch_command(command, thd, packet+1, 
                                    (uint) (packet_length-1));

...

}

首先要注意的是一个包缓冲区和一个NET结构的创建。这个包缓冲区是一个字符数组,存储从网络上读取的原始查询字符串,并存储在NET结构中。下一个创建的项目是一个命令结构,它将用于将控制传递给适当的解析器函数。my_net_read()功能从网络中读取数据包,并将它们存储在NET结构中。数据包的长度也存储在NET结构的packet_length变量中。您在这个函数中看到的最后一件事是对dispatch_command()的调用,从这里您可以开始看到命令是如何通过服务器代码路由的。

好吧,你开始有所进展了。dispatch_command()功能的工作是将控制路由到服务器中能够最好地处理输入命令的部分。由于您正在进行一个普通的SELECT查询,系统通过将command变量设置为COM_QUERY.将它识别为一个查询,其他命令类型用于识别语句、更改用户、生成统计数据和许多其他服务器功能。对于这一章,我将只看查询命令(COM_QUERY)。清单 3-6 显示了该函数的一个浓缩视图。为了简洁起见,我省略了 switch 中所有其他命令的代码(我也省略了注释分隔符),但是我保留了大多数命令的 case 语句。花点时间浏览列表。大多数名字都是不言自明的。如果您要对另一种类型的查询进行这种探索,您可以通过在这个函数中查找所标识的类型并按照 case 语句中的代码来找到自己的方法。我还包括了出现在函数代码前的大函数注释块。花点时间看看这个。在这一章的后面,我会更深入地探讨这一点。

清单 3-6。dispatch _ command()函数

/**
  Perform one connection-level (COM_XXXX) command.

  @param command         type of command to perform
  @param thd             connection handle
  @param packet          data for the command, packet is always null-terminated
  @param packet_length   length of packet + 1 (to show that data is
                         null-terminated) except for COM_SLEEP, where it
                         can be zero.

...

  @retval
    0   ok
  @retval
    1   request of thread shutdown, i. e. if command is
        COM_QUIT/COM_SHUTDOWN
*/
bool dispatch_command(enum enum_server_command command, THD *thd,
        char* packet, uint packet_length)
{

  ...
  switch (command) {
    case COM_INIT_DB:
    ...
    case COM_REGISTER_SLAVE:
    ...
    case COM_TABLE_DUMP:
    ...
    case COM_CHANGE_USER:
    ...
    case COM_STMT_EXECUTE:
    ...
    case COM_STMT_FETCH:
    ...
    case COM_STMT_SEND_LONG_DATA:
    ...
    case COM_STMT_PREPARE:
    ...
    case COM_STMT_CLOSE:
    ...
    case COM_STMT_RESET:
    ...
    case COM_QUERY:
    {
      if (alloc_query(thd, packet, packet_length))
        break;          // fatal error is set

    ...

      if (opt_log_raw)
        general_log_write(thd, command, thd->query(), thd->query_length());

    ...

      mysql_parse(thd, thd->query(), thd->query_length(), &parser_state);

    ...
    }
    case COM_FIELD_LIST:        // This isn't actually needed
    ...
    case COM_QUIT:
    ...
    case COM_BINLOG_DUMP_GTID;
    ...
    case COM_BINLOG_DUMP:
    ...
    case COM_REFRESH:
    ...
    case COM_SHUTDOWN:
    ...
    case COM_STATISTICS:
    ...
    case COM_PING:
    ...
    case COM_PROCESS_INFO:
    ...
    case COM_PROCESS_KILL:
    ...
    case COM_SET_OPTION:
    ...
    case COM_DEBUG:
    ...
    case COM_SLEEP:
    ...
    case COM_DELAYED_INSERT:
    ...
    case COM_CONNECT;
    case COM_TIME;
    ...
    case COM_END:
    ...
    default:
    ...
}

当控制权传递给COM_QUERY处理程序时发生的第一件事是,通过alloc _query()函数将查询从packet数组复制到thd->query成员变量。通过这种方式,线程现在拥有了查询的副本,该副本将在整个执行过程中一直伴随着它。还要注意,代码将命令写入常规日志。这将有助于稍后调试系统问题和查询问题。清单 3-6 中感兴趣的最后一个函数调用是mysql_parse()函数调用。至此,代码可以正式从 SQL 接口子系统转移到查询解析器子系统。如您所见,这种区别是语义上的,而不是语法上的。

解析查询

最后,解析开始。这是服务器处理查询时内部运行的核心。解析器代码和系统的其他部分一样,位于几个地方。如果你意识到虽然它是高度组织的,但是代码的结构与架构不匹配,那么这就不难理解了。

您现在正在检查的函数是mysql_parse()函数(位于/sql/sql_parse.cc)。它的工作是在查询缓存中检查先前执行的具有相同结果集的查询的结果,然后将控制传递给词法分析器(parse_sql()),最后将命令传递给查询优化器。清单 3-7 显示了mysql_parse()函数的浓缩视图。

清单 3-7MySQL _ parse()函数

/**
  Parse a query.

  @param       thd     Current thread
  @param       rawbuf  Begining of the query text
  @param       length  Length of the query text
  @param[out]  found_semicolon For multi queries, position of the character of
                               the next query in the query text.
*/

void mysql_parse(THD *thd, char *rawbuf, uint length,
                 Parser_state *parser_state)
{
  int error __attribute__((unused));

  ...

  if (query_cache_send_result_to_client(thd, rawbuf, length) <= 0)
  {
    LEX *lex= thd->lex;

  ...

    bool err= parse_sql(thd, parser_state, NULL);

  ...

      error= mysql_execute_command(thd);

  ...

    }

  ...

  }
  else
  {
    /*
      Query cache hit. We need to write the general log here.
      Right now, we only cache SELECT results; if the cache ever
      becomes more generic, we should also cache the rewritten
      query string together with the original query string (which
      we'd still use for the matching) when we first execute the
      query, and then use the obfuscated query string for logging
      here when the query is given again.
    */
    thd->m_statement_psi= MYSQL_REFINE_STATEMENT(thd->m_statement_psi,
                                                sql_statement_info[SQLCOM_SELECT].m_key);
    if (!opt_log_raw)
      general_log_write(thd, COM_QUERY, thd->query(), thd->query_length());
    parser_state->m_lip.found_semicolon= NULL;
  }
  ...
}

首先要注意的是检查查询缓存的调用。查询缓存存储所有最频繁请求的查询,包括结果。如果查询已经在查询缓存中,我们跳到 else,这样就完成了!剩下的工作就是将结果返回给客户机。不需要解析、优化甚至执行。这有多酷?

为了便于研究,我们假设查询缓存不包含示例查询的副本。在这种情况下,该函数创建一个新的LEX结构来包含查询的内部表示。这个结构由 Lex/YACC 解析器填充,如清单 3-8 所示。这段代码在 sql/sql_yacc.yy 中。

清单 3-8 选择 Lex/YACC 解析代码摘录

/*
  Select : retrieve data from table
*/

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

/* Need select_init2 for subselects. */
select_init:
          SELECT_SYM select_init2
        | '(' select_paren ')' union_opt
        ;

select_paren:
          SELECT_SYM select_part2
          {
            if (setup_select_in_parentheses(Lex))
              MYSQL_YYABORT;
          }
        | '(' select_paren ')'
        ;

/* The equivalent of select_paren for nested queries. */
select_paren_derived:
          SELECT_SYM select_part2_derived
          {
            if (setup_select_in_parentheses(Lex))
              MYSQL_YYABORT;
          }
        | '(' select_paren_derived ')'
        ;

select_init2:
          select_part2
          {
            LEX *lex= Lex;
            SELECT_LEX * sel= lex->current_select;
            if (lex->current_select->set_braces(0))
            {
              my_parse_error(ER(ER_SYNTAX_ERROR));
              MYSQL_YYABORT;
            }
            if (sel->linkage == UNION_TYPE &&
                sel->master_unit()->first_select()->braces)
            {
              my_parse_error(ER(ER_SYNTAX_ERROR));
              MYSQL_YYABORT;
            }
          }
          union_clause
        ;

select_part2:
          {
            LEX *lex= Lex;
            SELECT_LEX *sel= lex->current_select;
            if (sel->linkage != UNION_TYPE)
              mysql_init_select(lex);
            lex->current_select->parsing_place= SELECT_LIST;
          }
          select_options select_item_list
          {
            Select->parsing_place= NO_MATTER;
          }
          select_into select_lock_type
        ;

select_into:
          opt_order_clause opt_limit_clause {}
        | into
        | select_from
        | into select_from
        | select_from into
        ;

select_from:
          FROM join_table_list where_clause group_clause having_clause
          opt_order_clause opt_limit_clause procedure_analyse_clause
          {
            Select->context.table_list=
              Select->context.first_name_resolution_table=
                Select->table_list.first;
          }
        | FROM DUAL_SYM where_clause opt_limit_clause
          /* oracle compatibility: oracle always requires FROM clause,
             and DUAL is system table without fields.
             Is "SELECT 1 FROM DUAL" any better than "SELECT 1" ?
          Hmmm :) */
        ;

select_options:
          /* empty*/
        | select_option_list
          {
            if (Select->options & SELECT_DISTINCT && Select->options & SELECT_ALL)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "ALL", "DISTINCT");
              MYSQL_YYABORT;
            }
          }
        ;

select_option_list:
          select_option_list select_option
        | select_option
        ;

select_option:
          query_expression_option
        | SQL_NO_CACHE_SYM
          {
            /* 
              Allow this flag only on the first top-level SELECT statement, if
              SQL_CACHE wasn't specified, and only once per query.
             */
            if (Lex->current_select != &Lex->select_lex)
            {
              my_error(ER_CANT_USE_OPTION_HERE, MYF(0), "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_CACHE)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "SQL_CACHE", "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_NO_CACHE)
            {
              my_error(ER_DUP_ARGUMENT, MYF(0), "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else
            {
              Lex->safe_to_cache_query=0;
              Lex->select_lex.options&= ∼OPTION_TO_QUERY_CACHE;
              Lex->select_lex.sql_cache= SELECT_LEX::SQL_NO_CACHE;
            }
          }
        | SQL_CACHE_SYM
          {
            /* 
              Allow this flag only on the first top-level SELECT statement, if
              SQL_NO_CACHE wasn't specified, and only once per query.
             */
            if (Lex->current_select != &Lex->select_lex)
            {
              my_error(ER_CANT_USE_OPTION_HERE, MYF(0), "SQL_CACHE");
              MYSQL_YYABORT;
            } 
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_NO_CACHE)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "SQL_NO_CACHE", "SQL_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_CACHE)
            {
              my_error(ER_DUP_ARGUMENT, MYF(0), "SQL_CACHE");
              MYSQL_YYABORT;
            }
            else
            {
              Lex->safe_to_cache_query=1;
              Lex->select_lex.options|= OPTION_TO_QUERY_CACHE;
              Lex->select_lex.sql_cache= SELECT_LEX::SQL_CACHE;
            }
          }
        ;

select_lock_type:
          /* empty */
        | FOR_SYM UPDATE_SYM
          {
            LEX *lex=Lex;
            lex->current_select->set_lock_for_tables(TL_WRITE);
            lex->safe_to_cache_query=0;
          }
        | LOCK_SYM IN_SYM SHARE_SYM MODE_SYM
          {
            LEX *lex=Lex;
            lex->current_select->
              set_lock_for_tables(TL_READ_WITH_SHARED_LOCKS);
            lex->safe_to_cache_query=0;
          }
        ;

select_item_list:
          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)++;
          }
        ;

select_item:
          remember_name table_wild remember_end
          {
            THD *thd= YYTHD;

            if (add_item_to_list(thd, $2))
              MYSQL_YYABORT;
          }
        | remember_name expr remember_end select_alias
          {
            THD *thd= YYTHD;
            DBUG_ASSERT($1 < $3);

            if (add_item_to_list(thd, $2))
              MYSQL_YYABORT;
            if ($4.str)
            {
              if (Lex->sql_command == SQLCOM_CREATE_VIEW &&
                  check_column_name($4.str))
              {
                my_error(ER_WRONG_COLUMN_NAME, MYF(0), $4.str);
                MYSQL_YYABORT;
              }
              $2->item_name.copy($4.str, $4.length, system_charset_info, false);
            }
            else if (!$2->item_name.is_set())
            {
              $2->item_name.copy($1, (uint) ($3 - $1), thd->charset());
            }
          }
        ;

我包含了来自 Lex/YACC 解析器的摘录,展示了如何识别SELECT标记并通过 YACC 代码进行解析。要阅读这段代码(以防你不知道莱克斯或 YACC),注意代码中的关键词(或标记)(它们位于冒号的左下方,如select:)。这些关键字用于指导解析器的流程。这些关键字右侧的标记定义了解析查询时必须出现的顺序。例如,看看select:关键词。在它的右边,你会看到一个select_init2关键字,它并没有提供太多的信息。然而,如果你向下看代码,你会在左边看到select_init:关键字。这允许 Lex/YACC 作者以一种类似宏的形式指定某些行为。另外,请注意在select_init关键字下有花括号。这是解析器将查询分成几部分并将条目放入LEX结构的地方。直接符号,如SELECT,在头文件(/sql/lex.h)中定义,在解析器中显示为SELECT_SYM。现在花点时间浏览一下代码。你可能需要反复练习几次。如果你没有学习过编译器构造或文本解析,这可能会令人困惑。

如果你在想,“真是个怪物”,那么你可以放心,你是正常的。莱克斯/YACC 代码对大多数开发者来说都是一个挑战。我强调了一些重要的代码语句,它们应该有助于解释代码是如何工作的。让我们过一遍。为了方便起见,我在这里再次重复了示例SELECT语句:

SELECT lname, fname, DOB FROM Employees WHERE Employees.department = 'EGR'

再看第一个关键词。注意select_init代码块如何将LEX结构的sql_command设置为SQLCOM_SELECT。这很重要,因为查询路径中的下一个函数在一个大型 switch 语句中使用它来进一步控制通过服务器的查询流。示例SELECT语句在字段列表中有三个字段。让我们试着在解析器代码中找到它们。寻找add_item_to_list()函数调用。这就是解析器检测字段并将它们放入LEX结构的地方。您还会看到调用解析器代码的几行代码,这些代码标识了字段列表的*选项。现在您已经得到了sql_command成员变量集和标识的字段。那么,FROM子句在哪里被检测到呢?寻找以FROM join_table_list where_clause开头的代码语句。这段代码是解析器的一部分,用于识别FROMWHERE子句(以及其他)。处理这些子句的解析器的代码没有包含在清单 3-8 中,但是我想你已经明白了。如果您打开sql_yacc.yy源文件(位于/sql),您现在应该能够找到所有这些语句,并看到LEX结构的其余部分是如何用FROM子句中的表列表和WHERE子句中的表达式填充的。

我希望这次解析器代码之旅有助于减轻通常伴随着检查 MySQL 系统这一部分的震惊和恐惧。稍后当我演示如何添加你自己的命令 MySQL SQL lexicon 时,我将回到系统的这一部分(更多细节见第 8 章)。表 3-3 列出了与 MySQL 解析器相关的源文件。

表 3-3。MySQL 解析器

源文件描述
/sql/lex.h解析器支持的所有关键字和标记的符号表
/sql/lex_symbol.h符号表的类型定义
/sql/sql_lex.h法律结构的定义
/sql/sql_lex.ccLex 类的定义
/sql/sql_yacc.yy莱克斯/YACC 解析器代码
/sql/sql_parse.cc包含除词法分析器之外的大多数查询路由和分析功能

image 注意不要编辑文件sql_yacc.ccsql_yacc.hlex_hash.h。这些文件由其他实用程序生成。详见第 7 章。

为优化准备查询

尽管从 MySQL 文档来看,解析器结束和优化器开始的界限并不清楚(存在矛盾),但是从优化器的定义来看,源代码的路由和控制部分可以被视为优化器的一部分。为了避免混淆,我将把下一组函数称为优化器的准备阶段。

这些准备功能中的第一个是mysql_execute_command()功能(位于/sql/sql_parse.cc)。这个名称使您相信您实际上正在执行查询,但事实并非如此。该函数执行优化查询所需的许多设置步骤。复制了LEX结构,并设置了几个变量来帮助查询优化和后期执行。你可以在清单 3-9 中的函数的浓缩视图中看到这些操作。

清单 3-9 。函数的作用是

/**
  Execute command saved in thd and lex->sql_command.

  @param thd                       Thread handle

...

  @retval
    FALSE       OK
  @retval
    TRUE        Error
*/

int
mysql_execute_command(THD *thd)
{
  int res= FALSE;
  int  up_result= 0;
  LEX  *lex= thd->lex;
  /* first SELECT_LEX (have special meaning for many of non-SELECTcommands) */
  SELECT_LEX *select_lex= &lex->select_lex;
  /* first table of first SELECT_LEX */
  TABLE_LIST *first_table= select_lex->table_list.first;
  /* list of all tables in query */
  TABLE_LIST *all_tables;
  /* most outer SELECT_LEX_UNIT of query */
  SELECT_LEX_UNIT *unit= &lex->unit;
#ifdef HAVE_REPLICATION
  /* have table map for update for multi-update statement (BUG#37051) */
  bool have_table_map_for_update= FALSE;
#endif
  DBUG_ENTER("mysql_execute_command");

  ...

switch (lex->sql_command) {

...

  case SQLCOM_SHOW_STATUS_PROC:
  case SQLCOM_SHOW_STATUS_FUNC:
  case SQLCOM_SHOW_DATABASES:
  case SQLCOM_SHOW_TABLES:
  case SQLCOM_SHOW_TRIGGERS:
  case SQLCOM_SHOW_TABLE_STATUS:
  case SQLCOM_SHOW_OPEN_TABLES:
  case SQLCOM_SHOW_PLUGINS:
  case SQLCOM_SHOW_FIELDS:
  case SQLCOM_SHOW_KEYS:
  case SQLCOM_SHOW_VARIABLES:
  case SQLCOM_SHOW_CHARSETS:
  case SQLCOM_SHOW_COLLATIONS:
  case SQLCOM_SHOW_STORAGE_ENGINES:
  case SQLCOM_SHOW_PROFILE:
  case SQLCOM_SELECT:
  {
    thd->status_var.last_query_cost= 0.0;
    thd->status_var.last_query_partial_plans= 0;

    if ((res= select_precheck(thd, lex, all_tables, first_table)))
      break;

    res= execute_sqlcom_select(thd, all_tables);
    break;
  }

...

在这个函数中会发生许多有趣的事情。您将会注意到另一个 switch 语句,它使用了SQLCOM关键字。在示例查询中,您看到解析器将成员变量lex->sql_command设置为SQLCOM_SELECT。我已经在的清单 3-9 中为您提供了该案例陈述的精简视图。我没有包括许多其他的SQLCOM案例陈述。这是一个非常大的函数。因为它是查询处理的中心路由功能,所以它包含每个可能命令的案例。因此,源代码有几十页长。

让我们看看这个 case 语句的作用。注意 select_precheck()方法调用。该方法执行权限检查,查看用户是否可以使用表列表来执行命令,以验证访问权限。如果用户有访问权,处理继续到 execute_sqlcom_select()方法,如清单 3-10 所示。我将有关DESCRIBE ( EXPLAIN)命令的部分代码留给您来检查并弄清楚它是如何工作的。

清单 3-10 。execute_sqlcom_command()函数

static bool execute_sqlcom_select(THD *thd, TABLE_LIST *all_tables)
{
  LEX       *lex= thd->lex;
  select_result *result= lex->result;
  bool res;
  /* assign global limit variable if limit is not given */
  {
    SELECT_LEX *param= lex->unit.global_parameters;
    if (!param->explicit_limit)
      param->select_limit=
        new Item_int((ulonglong) thd->variables.select_limit);
  }
  if (!(res= open_and_lock_tables(thd, all_tables, 0)))
  {
    if (lex->describe)
    {
      /*
        We always use select_send for EXPLAIN, even if it's an EXPLAIN
        for SELECT ... INTO OUTFILE: a user application should be able
        to prepend EXPLAIN to any query and receive output for it,
        even if the query itself redirects the output.
      */
      if (!(result= new select_send()))
        return 1;                               /* purecov: inspected */
      res= explain_query_expression(thd, result);
      delete result;
    }
    else
    {
      if (!result && !(result= new select_send()))
        return 1;                               /* purecov: inspected */
      select_result *save_result= result;
      select_result *analyse_result= NULL;
      if (lex->proc_analyse)
      {
        if ((result= analyse_result=
               new select_analyse(result, lex->proc_analyse)) == NULL)
          return true;
      }
      res= handle_select(thd, result, 0);
      delete analyse_result;
      if (save_result != lex->result)
        delete save_result;
    }
  }
  return res;
}

image 注意有一次当我修改代码时,我需要找到所有EXPLAIN调用的位置,这样我就可以根据特定的需要修改它们。我到处寻找,直到在解析器中找到它们。在莱克斯/YACC 代码的中间,有一个注释,大意是DESCRIBE是早期 Oracle 兼容性问题遗留下来的,正确的术语是EXPLAIN。注释是有用的,如果你能找到它们的话。

下一个有趣的函数调用是对handle_select()的调用。你可能在想,“我们不是刚做了手柄的事情吗?”handle_select()是另一个函数mysql_select()的包装器。清单 3-11 显示了handle_select()功能的完整代码。清单顶部附近是select_lex->next_select()操作,它检查将多个SELECT结果追加到一组结果中的UNION命令。除此之外,代码只调用链中的下一个函数mysql_select()。此时,您终于可以过渡到查询优化器子系统了。表 3-4 列出了与查询优化器相关的源文件。

image 注意这可能是代码中最容易受到未定义子系统影响的部分。虽然代码仍然非常有组织,但是在源代码的这一点上,子系统的边界是模糊的。

清单 3-11handle _ select()函数

bool handle_select(THD *thd, select_result *result,
                   ulong setup_tables_done_option)
{
  bool res;
  LEX *lex= thd->lex;
  register SELECT_LEX *select_lex = &lex->select_lex;
  DBUG_ENTER("handle_select");
  MYSQL_SELECT_START(thd->query());

  if (lex->proc_analyse && lex->sql_command != SQLCOM_SELECT)
  {
    my_error(ER_WRONG_USAGE, MYF(0), "PROCEDURE", "non-SELECT");
    DBUG_RETURN(true);
  }

  if (select_lex->master_unit()->is_union() || 
      select_lex->master_unit()->fake_select_lex)
    res= mysql_union(thd, lex, result, &lex->unit, setup_tables_done_option);
  else
  {
    SELECT_LEX_UNIT *unit= &lex->unit;
    unit->set_limit(unit->global_parameters);
    /*
      'options' of mysql_select will be set in JOIN, as far as JOIN for
      every PS/SP execution new, we will not need reset this flag if 
      setup_tables_done_option changed for next rexecution
    */
    res= mysql_select(thd,
        select_lex->table_list.first,
        select_lex->with_wild, select_lex->item_list,
        select_lex->where,
        &select_lex->order_list,
        &select_lex->group_list,
        select_lex->having,
        select_lex->options | thd->variables.option_bits |
                      setup_tables_done_option,
        result, unit, select_lex);
  }
  DBUG_PRINT("info",("res: %d  report_error: %d", res,
       thd->is_error()));
  res|= thd->is_error();
  if (unlikely(res))
    result->abort_result_set();

  MYSQL_SELECT_DONE((int) res, (ulong) thd->limit_found_rows);
  DBUG_RETURN(res);
}

表 3-4。查询优化器

源文件描述
/sql/sql_parse.cc大部分解析器代码都在这个文件中
/sql/sql_select.cc包含一些优化功能和选择功能的实现
/sql/sql_prepare.cc包含优化程序的准备方法。
/sql/sql_executor.cc包含优化程序的执行方法。

优化查询

终于!你在优化器那里。然而,如果您去寻找那个名字的源文件或类,您将找不到它。虽然JOIN类包含一个名为optimize()的方法,但是优化器实际上是一个流控制和子功能的集合,旨在找到执行查询的最短路径。花哨的算法、查询路径和编译后的查询发生了什么变化?回想一下我们在第 2 章的架构讨论,MySQL 查询优化器是一个非传统的混合优化器,它结合了已知的最佳实践和基于成本的路径选择。在代码的这一点上,最佳实践部分开始发挥作用。

其中一个最佳实践的例子是标准化WHERE子句表达式中的参数。示例查询使用带有表达式Employees.department = 'EGR'WHERE子句,但是该子句可以写成'EGR' = Employees.department,仍然是正确的(它返回相同的结果)。这是一个传统的基于成本的优化器可以生成多个计划的例子——每个表达式变量一个计划。MySQL 使用的许多最佳实践的几个例子是:

  • 常数传播——使用常数移除传递连接词。比如你有 a=b='c'传递性定律陈述 a='c'这种优化消除了那些内部等式,从而减少了求值的次数。例如,SQL 命令**SELECT * FROM table 1 WHERE column 1 = 12 AND NOT(column 3 = 17 OR column 2 = column 1)将被简化为SELECT * FROM table 1 WHERE column 1 = 12 AND column 3<>17 AND column 2<>12
  • 死代码消除——消除始终为真的条件。例如,如果您有 a=b 和 1=1和 1=1 条件被移除。对于 always- false 条件也是如此,在这种情况下,可以删除 false 表达式,而不会影响子句的其余部分。例如,SQL 命令 SELECT * FROM table1,其中 column1 = 12,column2 = 13,column1 < column2 将被简化为 SELECT * FROM table1,其中 column1 = 12,column2 = 13 ** 范围查询——将 子句中的 转换为析取列表。例如,如果在(1,2,3)中有一个变换将是 a = 1 或 a = 2 或 a = 3这有助于简化表达式的计算。例如,SQL 命令**SELECT * FROM table 1 WHERE column 1 = 12 或 column1 = 17 或 column1 = 21 将简化为**SELECT * FROM table 1 WHERE column 1 IN(12,17,21) 。*

*我希望这一小部分例子能够让您对世界上最成功的非传统查询优化器的内部工作原理有所了解。简而言之,它对于数量惊人的查询非常有效。

嗯,我说得太快了。在优化领域,mysql_select()函数也没有太多变化。似乎mysql_select()函数只是锁定表,然后调用mysql_execute_select()函数。你又一次处于另一个模糊的边界。清单 3-12 显示了mysql_select()函数的摘录。

清单 3-12MySQL _ select()函数

/**
  An entry point to single-unit select (a select without UNION).

  @param thd                  thread handler
  @param tables               list of all tables used in this query.
                              The tables have been pre-opened.
  @param wild_num             number of wildcards used in the top level 
                              select of this query.
                              For example statement
                              SELECT *, t1.*, catalog.t2.* FROM t0, t1, t2;
                              has 3 wildcards.
  @param fields               list of items in SELECT list of the top-level
                              select
                              e.g. SELECT a, b, c FROM t1 will have Item_field
                              for a, b and c in this list.
  @param conds                top level item of an expression representing
                              WHERE clause of the top level select
  @param order                linked list of ORDER BY agruments
  @param group                linked list of GROUP BY arguments
  @param having               top level item of HAVING expression
  @param select_options       select options (BIG_RESULT, etc)
  @param result               an instance of result set handling class.
                              This object is responsible for send result
                              set rows to the client or inserting them
                              into a table.
  @param unit                 top-level UNIT of this query
                              UNIT is an artificial object created by the
                              parser for every SELECT clause.
                              e.g.
                              SELECT * FROM t1 WHERE a1 IN (SELECT * FROM t2)
                              has 2 unions.
  @param select_lex           the only SELECT_LEX of this query

  @retval
    false  success
  @retval
    true   an error
*/

bool
mysql_select(THD *thd,
             TABLE_LIST *tables, uint wild_num, List<Item> &fields,
             Item *conds, SQL_I_List<ORDER> *order, SQL_I_List<ORDER> *group,
             Item *having, ulonglong select_options,
             select_result *result, SELECT_LEX_UNIT *unit,
             SELECT_LEX *select_lex)
{
  bool free_join= true;
  uint og_num= 0;
  ORDER *first_order= NULL;
  ORDER *first_group= NULL;
  DBUG_ENTER("mysql_select");

  if (order)
  {
    og_num= order->elements;
    first_order= order->first;
  }
  if (group)
  {
    og_num+= group->elements;
    first_group= group->first;
  }

  if (mysql_prepare_select(thd, tables, wild_num, fields,
                       conds, og_num, first_order, first_group, having,
                       select_options, result, unit,
                       select_lex, &free_join))
  {
    if (free_join)
    {
      THD_STAGE_INFO(thd, stage_end);
      (void) select_lex->cleanup();
    }
    DBUG_RETURN(true);
  }

  if (! thd->lex->is_query_tables_locked())
  {
    /*
      If tables are not locked at this point, it means that we have delayed
      this step until after the prepare stage (i.e. this moment). This allows us to
      do better partition pruning and avoid locking unused partitions.
      As a consequence, in such a case, the prepare stage can rely only on
      metadata about tables used and not data from them.
      We need to lock tables now in order to proceed with the remaining
      stages of query optimization and execution.
    */
    if (lock_tables(thd, thd->lex->query_tables, thd->lex->table_count, 0))
    {
      if (free_join)
      {
        THD_STAGE_INFO(thd, stage_end);
        (void) select_lex->cleanup();
      }
      DBUG_RETURN(true);
    }

    /*
      Only register query in cache if it tables were locked above.

      Tables must be locked before storing the query in the query cache.
      Transactional engines must have been signalled that the statement started,
      which external_lock signals.
    */
    query_cache_store_query(thd, thd->lex->query_tables);
  }

  DBUG_RETURN(mysql_execute_select(thd, select_lex, free_join));
}

所有这些最佳实践在哪里?他们在JOIN班!对JOIN类中的优化器源代码的详细检查将花费比这本书更多的页面来呈现任何有意义的深度。简单地说,优化器很复杂,也很难检查。幸运的是,很少有人需要深入探究 MySQL。但是,欢迎您这样做!我将集中精力对optimizer from the mysql_execute_select() function进行更高层次的审查。

该函数中的下一个主要函数调用是join->exec()方法。不过,首先让我们看看在清单 3-13 中的mysql_execute_select()方法中发生了什么。

清单 3-13。MySQL _ execute _ select()函数

/**
  Execute stage of mysql_select.

  @param thd                  thread handler
  @param select_lex           the only SELECT_LEX of this query
  @param free_join            if join should be freed

  @return Operation status
    @retval false  success
    @retval true   an error

  @note tables must be opened and locked before calling mysql_execute_select.
*/

static bool
mysql_execute_select(THD *thd, SELECT_LEX *select_lex, bool free_join)
{
  bool err;
  JOIN* join= select_lex->join;

  DBUG_ENTER("mysql_execute_select");
  DBUG_ASSERT(join);

  if ((err= join->optimize()))
  {
    goto err; // 1
  }

  if (thd->is_error())
    goto err;

  if (join->select_options & SELECT_DESCRIBE)
  {
    join->explain();
    free_join= false;
  }
  else
    join->exec();

err:
  if (free_join)
  {
    THD_STAGE_INFO(thd, stage_end);
    err|= select_lex->cleanup();
    DBUG_RETURN(err || thd->is_error());
  }
  DBUG_RETURN(join->error);
}

现在我们可以在mysql_execute_select()函数中看到优化器代码的入口。我们看到了对在 prepare 方法中创建的现有 JOIN 类的引用。在代码中再往下一点,我们看到了我们期望的方法——optimize()调用。此后不久,我们看到了通过 JOIN 类执行查询的exec()方法。表 3-5 列出了与查询优化相关的更重要的源文件。

表 3-5。查询优化

源文件描述
/sql/abstract_query_plan.cc实现了一个抽象查询计划接口,用于检查查询计划的某些方面,而无需访问 mysqld 内部类(JOIN_TAB、SQL_SELECT 等)。)直接。
/sql/sql_optimizer.cc包含优化器核心功能
/sql/sql_planner.cc包含帮助优化器确定检索连接行的表顺序的类。
/sql/sql_select.h选择功能中用于支持the SELECT命令的结构定义
/sql/sql_select.cc包含一些优化功能和选择功能的实现
/sql/sql_union.cc用于执行联合操作的代码。

执行查询

与优化器一样,查询执行使用一组最佳实践来执行查询。例如,查询执行子系统检测特殊子句,如ORDER BYDISTINCT,,并将这些操作的控制路由到为快速排序和元组消除而设计的方法。

这种活动大部分发生在JOIN类的方法中。清单 3-14 展示了一个join::exec()方法的浓缩视图。请注意,还有另一个函数调用,调用的函数名称中包含了select。果然,还有一个调用需要调用一个名为do_select()的函数。看看这个函数调用的参数。您现在开始看到字段列表等内容。这是否意味着你正在接近读取数据?没错。事实上,do_select()函数正是为此而设计的高级包装器。

清单 3-14join::exec()函数

void
JOIN::exec()
{
  Opt_trace_context * const trace= &thd->opt_trace;
  Opt_trace_object trace_wrapper(trace);
  Opt_trace_object trace_exec(trace, "join_execution");
  trace_exec.add_select_number(select_lex->select_number);
  Opt_trace_array trace_steps(trace, "steps");
  List<Item> *columns_list= &fields_list;
  DBUG_ENTER("JOIN::exec");

...

  THD_STAGE_INFO(thd, stage_sending_data);
  DBUG_PRINT("info", ("%s", thd->proc_info));
  result->send_result_set_metadata(*fields,
                                Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF);
  error= do_select(this);
  /* Accumulate the counts from all join iterations of all join parts. */
  thd->inc_examined_row_count(examined_rows);
  DBUG_PRINT("counts", ("thd->examined_row_count: %lu",
                        (ulong) thd->get_examined_row_count()));

  DBUG_VOID_RETURN;
}

还有一个函数调用看起来很有意思。注意代码语句result->send_result_set_metadata ()。这个函数如其名所示。这个函数将字段头发送给客户端。正如您所猜测的,还有其他方法可以将结果发送给客户端。我将在第四章的中讨论这些方法。请注意thd->inc_examined_row_count=任务。这将记录计数值保存在THD类中。我们来看看那个do_select()功能。

你可以在清单 3-15 所示的do_select()方法中看到,一些重要的事情正在发生。请注意最后突出显示的代码语句。语句join->result->send_eof()看起来好像代码正在某处发送一个文件结束标志。它确实向客户端发送了一个文件结束信号。那么结果在哪里呢?它们在first_select()函数中生成(映射到sub_select()))。接下来让我们看看这个函数。

清单 3-15do _ select()函数

static int
do_select(JOIN *join)
{
  int rc= 0;
  enum_nested_loop_state error= NESTED_LOOP_OK;
  DBUG_ENTER("do_select");

...

  else
  {
    JOIN_TAB *join_tab= join->join_tab + join->const_tables;
    DBUG_ASSERT(join->tables);
    error= join->first_select(join,join_tab,0);
    if (error >= NESTED_LOOP_OK)
      error= join->first_select(join,join_tab,1);
  }

  join->thd->limit_found_rows= join->send_records;
  /* Use info provided by filesort. */
  if (join->order)
  {
    // Save # of found records prior to cleanup
    JOIN_TAB *sort_tab;
    JOIN_TAB *join_tab= join->join_tab;
    uint const_tables= join->const_tables;

    // Take record count from first non constant table or from last tmp table
    if (join->tmp_tables > 0)
      sort_tab= join_tab + join->tables + join->tmp_tables - 1;
    else
    {
      DBUG_ASSERT(join->tables > const_tables);
      sort_tab= join_tab + const_tables;
    }
    if (sort_tab->filesort &&
        sort_tab->filesort->sortorder)
    {
      join->thd->limit_found_rows= sort_tab->records;
    }
  }

  {
    /*
      The following will unlock all cursors if the command wasn't an
      update command
    */
    join->join_free(); // Unlock all cursors
  }
  if (error == NESTED_LOOP_OK)
  {
    /*
      Sic: this branch works even if rc != 0, e.g. when
      send_data above returns an error.
    */
    if (join->result->send_eof())
      rc= 1;                                  // Don't send error
    DBUG_PRINT("info",("%ld records output", (long) join->send_records));
  }

...

}

现在你有所进展了!花点时间浏览一下清单 3-16 。这个清单显示了sub_select()函数的一个精简视图。注意,代码以一个名为READ_RECORD的结构的初始化开始。READ_RECORD结构包含从表中读取的元组。系统初始化表,开始顺序读取记录,然后一次读取一条记录,直到所有记录都被读取。

清单 3-16sub _ select()函数

enum_nested_loop_state
sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records)
{
  DBUG_ENTER("sub_select");

  join_tab->table->null_row=0;
  if (end_of_records)
  {
    enum_nested_loop_state nls=
      (*join_tab->next_select)(join,join_tab+1,end_of_records);
    DBUG_RETURN(nls);
  }
  READ_RECORD *info= &join_tab->read_record;

...

  join->thd->get_stmt_da()->reset_current_row_for_warning();

  enum_nested_loop_state rc= NESTED_LOOP_OK;
  bool in_first_read= true;
  while (rc == NESTED_LOOP_OK && join->return_tab >= join_tab)
  {
    int error;
    if (in_first_read)
    {
      in_first_read= false;
      error= (*join_tab->read_first_record)(join_tab);
    }
    else
      error= info->read_record(info);

    DBUG_EXECUTE_IF("bug13822652_1", join->thd->killed= THD::KILL_QUERY;);

    if (error > 0 || (join->thd->is_error()))   // Fatal error
      rc= NESTED_LOOP_ERROR;
    else if (error < 0)
      break;
    else if (join->thd->killed) // Aborted by user
    {
      join->thd->send_kill_message();
      rc= NESTED_LOOP_KILLED;
    }
    else
    {
      if (join_tab->keep_current_rowid)
        join_tab->table->file->position(join_tab->table->record[0]);
      rc= evaluate_join_record(join, join_tab);
    }
  }

  if (rc == NESTED_LOOP_OK && join_tab->last_inner && !join_tab->found)
    rc= evaluate_null_complemented_join_record(join, join_tab);

  DBUG_RETURN(rc);
}

image 注意清单 3-16 中的代码比我展示的其他例子更加简洁。主要原因是这段代码使用了相当多的高级编程技术,比如递归和函数指针重定向。然而,所提出的概念对于示例查询是准确的。

控制返回到JOIN类,用于计算表达式和执行关系运算符。在结果被处理之后,它们被传输到客户端,然后控制返回到sub_select()函数,在这里发送文件结束标志来告诉客户端没有更多的结果。我希望这篇文章满足了您的好奇心,如果没有别的,也希望它提高了您对真实世界数据库系统复杂性的理解。请随意再次回顾此教程,直到您对基本流程感到满意为止。我将在下一节讨论一些更重要的类和结构。

支持库

MySQL 源代码树中有许多附加的库。Oracle 长期以来一直致力于封装和优化许多用于访问受支持的操作系统和硬件的常用例程。这些库中的大部分被设计成使代码与操作系统和硬件无关。这些库使得编写代码成为可能,这样特定的平台特性就不会迫使您编写专门的代码。这些库包括用于管理有效的字符串处理、哈希表、链表、内存分配等的库。表 3-6 列出了一些比较常见的库的用途和位置。

image 提示发现你正在使用的例程是否有库的最好方法是使用文本搜索工具查看/mysys目录下的源代码文件。大多数包装函数都有一个类似于它们原始函数的名字。例如,my_alloc.c实现了malloc包装器。

表 3-6。支持库

源文件公用事业
/mysys/array.c数组运算
/include/hash.h/mysys/hash.c散列表
/mysys/list.c合框架
/mysys/my_alloc.c存储器分配
/strings/*.c基本内存和字符串操作例程
/mysys/string.c字符串操作
/mysys/my_pthread.c穿线

重要的类和结构

MySQL 源代码中相当多的类和结构被认为是系统成功的关键因素。要全面了解 MySQL 源代码,需要学习系统中使用的所有关键类和结构的基础知识。了解什么存储在哪个类中或者结构包含什么可以帮助您很好地集成您的修改。以下部分描述了这些关键的类和结构。

项目 _ 类别

渗透到整个子系统的一个类是ITEM_ class。我称之为ITEM_是因为许多类都是从基类ITEM中派生出来的,甚至还有从基类中派生出来的类。这些衍生工具用于存储和操作系统中的大量数据(项目)。这些包括参数(如在WHERE子句中)、标识符、时间、字段、函数、数量、字符串和许多其他内容。清单 3-17 显示了一个ITEM基类的浓缩视图。该结构在/sql/item.h源文件中定义,在/sql/item.cc源文件中实现。附加的子类在以其封装的数据命名的文件中定义和实现。比如 function 子类在/sql/item_func.h中定义,在/sql/item_func.cc中实现。

清单 3-17ITEM _ Class

class Item
{
  Item(const Item &); /* Prevent use of these */
  void operator=(Item &);
  /* Cache of the result of is_expensive(). */
  int8 is_expensive_cache;
  virtual bool is_expensive_processor(uchar *arg) { return 0; }

public:
  static void *operator new(size_t size) throw ()
  { return sql_alloc(size); }
  static void *operator new(size_t size, MEM_ROOT *mem_root) throw ()
  { return alloc_root(mem_root, size); }
  static void operator delete(void *ptr,size_t size) { TRASH(ptr, size); }
  static void operator delete(void *ptr, MEM_ROOT *mem_root) {}

  enum Type {FIELD_ITEM= 0, FUNC_ITEM, SUM_FUNC_ITEM, STRING_ITEM,
      INT_ITEM, REAL_ITEM, NULL_ITEM, VARBIN_ITEM,
      COPY_STR_ITEM, FIELD_AVG_ITEM, DEFAULT_VALUE_ITEM,
      PROC_ITEM,COND_ITEM, REF_ITEM, FIELD_STD_ITEM,
      FIELD_VARIANCE_ITEM, INSERT_VALUE_ITEM,
             SUBSELECT_ITEM, ROW_ITEM, CACHE_ITEM, TYPE_HOLDER,
             PARAM_ITEM, TRIGGER_FIELD_ITEM, DECIMAL_ITEM,
             XPATH_NODESET, XPATH_NODESET_CMP,
             VIEW_FIXER_ITEM};

  enum cond_result { COND_UNDEF,COND_OK,COND_TRUE,COND_FALSE };

  enum traverse_order { POSTFIX, PREFIX };

  /* Reuse size, only used by SP local variable assignment, otherwize 0 */
  uint rsize;

  /*
    str_values's main purpose is to be used to cache the value in
    save_in_field
  */
  String str_value;

  Item_name_string item_name;  /* Name from select */
  Item_name_string orig_name;  /* Original item name (if it was renamed)*/

  /**
     Intrusive list pointer for free list. If not null, points to the next
     Item on some Query_arena's free list. For instance, stored procedures
     have their own Query_arena's.

     @see Query_arena::free_list
   */
  Item *next;
  uint32 max_length;                    /* Maximum length, in bytes */
  /**
     This member has several successive meanings, depending on the phase we're
     in:
     - during field resolution: it contains the index, in the "all_fields"
     list, of the expression to which this field belongs; or a special
     constant UNDEF_POS; see st_select_lex::cur_pos_in_all_fields and
     match_exprs_for_only_full_group_by().
     - when attaching conditions to tables: it says whether some condition
     needs to be attached or can be omitted (for example because it is already
     implemented by 'ref' access)
     - when pushing index conditions: it says whether a condition uses only
     indexed columns
     - when creating an internal temporary table: it says how to store BIT
     fields
     - when we change DISTINCT to GROUP BY: it is used for book-keeping of
     fields.
  */
  int marker;
  uint8 decimals;
  my_bool maybe_null; /* If item may be null */
  my_bool null_value; /* if item is null */
  my_bool unsigned_flag;
  my_bool with_sum_func;
  my_bool fixed;                        /* If item fixed with fix_fields */
  DTCollation collation;
  Item_result cmp_context;              /* Comparison context */
 protected:
  my_bool with_subselect;               /* If this item is a subselect or some
                                           of its arguments is or contains a
                                           subselect. Computed by fix_fields
                                           and updated by update_used_tables. */
  my_bool with_stored_program;          /* If this item is a stored program
                                           or some of its arguments is or
                                           contains a stored program.
                                           Computed by fix_fields and updated
                                           by update_used_tables. */

  /**
    This variable is a cache of 'Needed tables are locked'. True if either
    'No tables locks is needed' or 'Needed tables are locked'.
    If tables are used, then it will be set to
    current_thd->lex->is_query_tables_locked().

    It is used when checking const_item()/can_be_evaluated_now().
  */
  bool tables_locked_cache;
 public:
  // alloc & destruct is done as start of select using sql_alloc
  Item();
  /*
     Constructor used by Item_field, Item_ref & aggregate (sum) functions.
     Used for duplicating lists in processing queries with temporary
     tables
     Also it used for Item_cond_and/Item_cond_or for creating
     top AND/OR structure of WHERE clause to protect it from
     optimization changes in prepared statements
  */
  Item(THD *thd, Item *item);
  virtual ∼Item()
  {
#ifdef EXTRA_DEBUG
    item_name.set(0);
#endif
  } /*lint -e1509 */
  void rename(char *new_name);
  void init_make_field(Send_field *tmp_field,enum enum_field_types type);
  virtual void cleanup();
  virtual void make_field(Send_field *field);
  virtual Field *make_string_field(TABLE *table);
  virtual bool fix_fields(THD *, Item **);
  ...

};

LEX 结构

LEX结构负责查询及其部分的内部表示(内存存储)。然而,不仅仅如此。LEX结构用于以有组织的方式存储查询的所有部分。有字段、表、表达式以及构成任何查询的所有部分的列表。

当解析器发现查询的各个部分时,LEX结构由解析器填充。因此,当解析器完成时,LEX结构包含了优化和执行查询所需的一切。清单 3-18 显示了LEX结构的浓缩视图。该结构在/sql/sql_lex.h源文件中定义。

清单 3-18LEX 结构

struct LEX: public Query_tables_list
{
  SELECT_LEX_UNIT unit;                         /* most upper unit */
  SELECT_LEX select_lex;                        /* first SELECT_LEX */
  /* current SELECT_LEX in parsing */
  SELECT_LEX *current_select;
  /* list of all SELECT_LEX */
  SELECT_LEX *all_selects_list;

  char *length,*dec,*change;
  LEX_STRING name;
  char *help_arg;
  char* to_log;                                 /* For PURGE MASTER LOGS TO */
  char* x509_subject,*x509_issuer,*ssl_cipher;
  String *wild;
  sql_exchange *exchange;
  select_result *result;
  Item *default_value, *on_update_value;
  LEX_STRING comment, ident;
  LEX_USER *grant_user;
  XID *xid;
  THD *thd;

  /* maintain a list of used plugins for this LEX */

  DYNAMIC_ARRAY plugins;
  plugin_ref plugins_static_buffer[INITIAL_LEX_PLUGIN_LIST_SIZE];

  const CHARSET_INFO *charset;

...

};

网状结构

NET结构负责存储与客户端通信相关的所有信息。清单 3-19 显示了NET结构的浓缩视图。buff成员变量用于存储原始的通信数据包(当它们组合起来形成 SQL 语句时)。正如您将在后面的章节中看到的,帮助器函数填充、读取和传输来自客户端的数据包。两个例子是:

  • my_net_write(),将数据包从NET结构写入网络协议
  • my_net_read(),将数据包从网络协议读入NET结构

您可以在/include/mysql_com.h中找到整套网络通信功能。

清单 3-19 网络结构

typedef struct st_net {
#if !defined(CHECK_EMBEDDED_DIFFERENCES) || !defined(EMBEDDED_LIBRARY)
  Vio *vio;
  unsigned char *buff,*buff_end,*write_pos,*read_pos;
  my_socket fd; /* For Perl DBI/dbd */
  /*
    The following variable is set if we are doing several queries in one
    command ( as in LOAD TABLE ... FROM MASTER ),
    and do not want to confuse the client with OK at the wrong time
  */
  unsigned long remain_in_buf,length, buf_length, where_b;
  unsigned long max_packet,max_packet_size;
  unsigned int pkt_nr,compress_pkt_nr;
  unsigned int write_timeout, read_timeout, retry_count;
  int fcntl;
  unsigned int *return_status;
  unsigned char reading_or_writing;
  char save_char;
  my_bool unused1; /* Please remove with the next incompatible ABI change */
  my_bool unused2; /* Please remove with the next incompatible ABI change */
  my_bool compress;
  my_bool unused3; /* Please remove with the next incompatible ABI change. */
  /*
    Pointer to query object in query cache, do not equal NULL (0) for
    queries in cache that have not stored its results yet
  */
#endif
  /*
    Unused, please remove with the next incompatible ABI change.
  */
  unsigned char *unused;
  unsigned int last_errno;
  unsigned char error; 
  my_bool unused4; /* Please remove with the next incompatible ABI change. */
  my_bool unused5; /* Please remove with the next incompatible ABI change. */
  /** Client library error message buffer. Actually belongs to struct MYSQL. */
  char last_error[MYSQL_ERRMSG_SIZE];
  /** Client library sqlstate buffer. Set along with the error message. */
  char sqlstate[SQLSTATE_LENGTH+1];
  /**
    Extension pointer, for the caller private use.
    Any program linking with the networking library can use this pointer,
    which is handy when private connection specific data needs to be
    maintained.
    The mysqld server process uses this pointer internally,
    to maintain the server internal instrumentation for the connection.
  */
  void *extension;
} NET;

THD 类

在前面的源代码之旅中,您看到了许多对THD类的引用。事实上,对于每个连接,正好有一个THD对象。thread 类对于成功的线程执行至关重要,它参与了从实现访问控制到向客户端返回结果的所有操作。结果,THD类出现在服务器中运行的几乎每个子系统或功能中。清单 3-20 显示了一个THD类的浓缩视图。花点时间浏览一些成员变量和方法。如您所见,这是一个很大的类(我已经省略了很多方法)。该类在/sql/sql_class.h源文件中定义,并在/sql/sql_class.cc源文件中实现。

清单 3-20 。THD 类

class THD :public MDL_context_owner,
           public Statement,
           public Open_tables_state
{
private:

  ...

  String  packet; // dynamic buffer for network I/O
  String  convert_buffer;               // buffer for charset conversions
  struct  rand_struct rand; // used for authentication
  struct  system_variables variables;       // Changeable local variables
  struct  system_status_var status_var; // Per thread statistic vars
  struct  system_status_var *initial_status_var; /* used by show status */
  THR_LOCK_INFO lock_info;              // Locking info of this thread
  /**
    Protects THD data accessed from other threads:
    - thd->query and thd->query_length (used by SHOW ENGINE
      INNODB STATUS and SHOW PROCESSLIST
    - thd->mysys_var (used by KILL statement and shutdown).
    Is locked when THD is deleted.
  */
  mysql_mutex_t LOCK_thd_data;

  ...

};

读取记录结构

正如我们前面看到的,READ_RECORD结构用于包含来自存储引擎的元组,一旦优化器将它标识为要返回给用户的行。我们将存储引擎的讨论留到第 10 章中。清单 3-21 显示了READ_RECORD的结构。请注意,这里有回调到JOIN类方法的函数指针、引用THD类的变量、记录长度以及指向记录缓冲区本身的指针。如果您对了解行在系统中的存储方式感兴趣,可以研究一下这个类中的许多方法。

清单 3-21 。读取记录结构

struct READ_RECORD
{
  typedef int (*Read_func)(READ_RECORD*);
  typedef void (*Unlock_row_func)(st_join_table *);
  typedef int (*Setup_func)(JOIN_TAB*);

  TABLE *table;                                 /* Head-form */
  TABLE **forms;                                /* head and ref forms */
  Unlock_row_func unlock_row;
  Read_func read_record;
  THD *thd;
  SQL_SELECT *select;
  uint cache_records;
  uint ref_length,struct_length,reclength,rec_cache_size,error_offset;
  uint index;
  uchar *ref_pos; /* pointer to form->refpos */
  uchar *record;
  uchar *rec_buf;                /* to read field values  after filesort */
  uchar       *cache,*cache_pos,*cache_end,*read_positions;
  struct st_io_cache *io_cache;
  bool print_error, ignore_not_found_rows;

...

  Copy_field *copy_field;
  Copy_field *copy_field_end;
public:
  READ_RECORD() {}
};

MySQL 外挂程式

不提到架构中最重要和最新的创新之一,MySQL 系统之旅是不完整的。MySQL 现在支持一个插件工具,允许动态加载系统特性。这不仅意味着用户可以通过只加载她需要的东西来定制她的系统,还意味着 MySQL 的开发者可以以更加模块化的设计来开发功能。存储引擎子系统是为使用新的插件机制而重新设计的子系统的一个例子。还有很多其他的。我们将在后面的章节中更详细地看到插件是如何工作的。现在,让我们讨论插件是如何加载和卸载的,以及如何确定插件的状态。

mysql数据库中的plugins表用于在启动时加载插件。该表只包含两列,namedl,它们存储插件名和库名。启动时,除非用户关闭了插件,否则系统会加载在dl栏中指定的每个库,并为其各自的库启动在name栏中指定的每个插件。然后,可以手动修改这个表来管理插件,但是不推荐这样做,因为有些库可以包含多个插件。稍后,当我们检查mysql_plugin客户端应用时,您将看到这个概念的实际应用。

有些插件被认为是“内置”的,在默认情况下是可用的,在某些情况下是自动加载(安装)的。这包括许多存储引擎以及标准身份验证机制、二进制日志等。其他插件可以通过安装来使用,同样,也可以通过卸载来禁用。您可以在在线参考手册的“服务器插件”部分找到关于管理插件的完整文档。

安装和卸载插件

插件可以使用特殊的 SQL 命令作为启动选项来加载和卸载,也可以通过mysql_plugin客户端应用来加载和卸载。要加载一个插件,首先需要将正确的库放入由系统变量plugin_dir中的路径指定的插件目录中。您可以从 MySQL 中找到该变量的当前值,如下所示:

mysql> SHOW VARIABLES LIKE 'plugin_dir';
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| plugin_dir    | /usr/local/mysql/lib/plugin/ |
+---------------+------------------------------+
1 row in set (0.00 sec)

可以看到路径是/usr/local/mysql/lib/plugin/。当你构建你的插件或者安装一个现存的插件时,你必须首先把你的库放到插件目录中。然后,您可以执行类似于以下内容的安装插件命令:

mysql> INSTALL PLUGIN something_cool SONAME some_cool_feature.so;

在这里,我们正在加载一个名为something_cool的插件,它包含在名为some_cool_feature.so的已编译库模块中。

卸载插件更容易,如下所示。这里,我们正在卸载刚刚安装的同一个插件。

mysql> UNINSTALL PLUGIN something_cool;

插件也可以在启动时使用--plugin-load选项安装。这个选项可以被多次列出——每个插件一次——或者,它可以接受一个分号分隔的列表(没有空格)。如何使用此选项的示例包括:

mysqld ... --plugin-load=something_cool=some_cool_feature.so   
mysqld ... --plugin-load=something_cool=some_cool_feature.so;something_even_better=even_better.so

image 注意MySQL 文档使用术语安装卸载来动态加载和卸载插件。文档使用术语 load 来指定通过启动选项使用的插件。

还可以使用mysql_plugin客户端应用加载和卸载插件。该应用要求服务器停止工作。它将以引导模式启动服务器,加载或卸载插件,然后关闭引导的服务器。该应用主要用于停机期间的服务器维护,或者作为一种诊断工具,用于通过消除插件(以简化诊断)来尝试重启故障服务器。

客户端应用使用一个配置文件来保存关于插件的相关数据,比如库的名称和其中包含的所有插件。是的,一个插件库可能包含不止一个插件。以下是daemon_example插件的配置文件示例:

#
# Plugin configuration file. Place the following on a separate line:
#
# library binary file name (without .so or .dll)
# component_name
# [component_name] - additional components in plugin
#
libdaemon_example
daemon_example

要使用mysql_plugin应用安装(启用)或卸载(禁用)插件,请至少指定插件的名称:ENABLEDISABLEbasedirdatadirplugin-dirplugin-ini选项。如果 mysql_plugin 应用不在您的路径上,您可能还需要指定my-print-defaults选项。应用以静默方式运行,但是您可以打开详细度来查看应用的运行情况。(使用选项:-vvv)。下面展示了如何使用 mysql_plugin 客户端应用加载daemon_example插件。该示例从 MySQL 安装的 bin 文件夹中运行。

cbell$ sudo ./mysql_plugin --datadir=/mysql_path/data/ --basedir=/mysql_path/ --plugin-dir=../plugin/daemon_example/ --plugin-ini=../plugin/daemon_example/daemon_example.ini --my-print-defaults=../extra daemon_example ENABLE -vvv
# Found tool 'my_print_defaults' as '/mysql_path/bin/my_print_defaults'.
# Command: /mysql_path/bin/my_print_defaults mysqld > /var/tmp/txtdoaw2b
#    basedir = /mysql_path/
# plugin_dir = ../plugin/daemon_example/
#    datadir = /mysql_path/data/
# plugin_ini = ../plugin/daemon_example/daemon_example.ini
# Found tool 'mysqld' as '/mysql_path/bin/mysqld'.
# Found plugin 'daemon_example' as '../plugin/daemon_example/libdaemon_example.so'
# Enabling daemon_example...
# Query: REPLACE INTO mysql.plugin VALUES ('daemon_example','libdaemon_example.so');
# Command: /mysql_path/bin/mysqld --no-defaults --bootstrap --datadir=/mysql_path/data/ --basedir=/mysql_path/ < /var/tmp/sqlft1mF7
# Operation succeeded.

从输出中可以注意到,我必须依赖超级用户权限。如果您试图从安装在隔离 mysql 文件夹访问的平台(如 Linux 和 Mac OS X)上的服务器安装或卸载插件,您将需要使用这些权限。

还要注意,详细输出显示了应用正在做什么。在这种情况下,它用我们指定的插件的信息替换了mysql.plugin表中的任何行。类似地,将发出删除查询来禁用插件。

发现可用插件的状态

您可以通过检查INFORMATION_SCHEMA PLUGINS视图来发现系统上可用的插件。清单 3-22 是该视图输出的摘录。请注意,每个存储引擎以及每个插件的版本和状态都有条目。该视图还包含用于存储插件类型版本(创建插件时的系统版本)和作者的字段。通过使用EXPLAIN命令,您可以看到这个视图的所有字段。

清单 3-22 。信息 _ 模式。插件视图

mysql> SELECT plugin_name, plugin_version, plugin_status, plugin_type 
       FROM INFORMATION_SCHEMA.PLUGINS;
+-----------------------+----------------+---------------+--------------------+
| plugin_name           | plugin_version | plugin_status | plugin_type        |
+-----------------------+----------------+---------------+--------------------+
| binlog                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| mysql_native_password | 1.0            | ACTIVE        | AUTHENTICATION     |
| mysql_old_password    | 1.0            | ACTIVE        | AUTHENTICATION     |
| CSV                   | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MEMORY                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MyISAM                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MRG_MYISAM            | 1.0            | ACTIVE        | STORAGE ENGINE     |
| ARCHIVE               | 3.0            | ACTIVE        | STORAGE ENGINE     |
| BLACKHOLE             | 1.0            | ACTIVE        | STORAGE ENGINE     |
| FEDERATED             | 1.0            | DISABLED      | STORAGE ENGINE     |
| InnoDB                | 1.1            | ACTIVE        | STORAGE ENGINE     |
| INNODB_TRX            | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_LOCKS          | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_LOCK_WAITS     | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMP            | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMP_RESET      | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMPMEM         | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMPMEM_RESET   | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| PERFORMANCE_SCHEMA    | 0.1            | ACTIVE        | STORAGE ENGINE     |
| partition             | 1.0            | ACTIVE        | STORAGE ENGINE     |
+-----------------------+----------------+---------------+--------------------+
20 rows in set (0.00 sec)

现在,您已经浏览了源代码,并研究了系统中使用的一些重要的类和结构,我将重点转移到有助于您实现自己对 MySQL 系统的修改的项目上。让我们暂时离开源代码,考虑软件开发的编码指南和文档方面。

编码指南

如果我描述的源代码看起来有一种奇怪的格式,那可能是因为您的风格与源代码作者的风格不同。考虑这样一种情况,有许多开发者编写一个大型软件程序,如 MySQL,每个人都有自己的风格。可以想象,代码很快就会变得像一大堆杂乱的语句。为了避免这种情况,Oracle 发布了各种形式的编码指南。当您自己开始探索代码时,您将会看到,似乎有一些开发者没有遵循编码指南。唯一合理的解释是指导方针随着时间的推移而改变,这可能发生在大型项目的整个生命周期中。因此,代码的某些部分是使用一组规则编写的,而其他部分可能使用不同版本的规则。不管这个结果如何,开发者确实努力遵循了指导方针。

编码指南有一个巨大的列表,包含了为 MySQL 服务器编写 C/C++代码的注意事项。我已经捕捉到了最重要的指导方针,并在下面的段落中为您进行了总结。

一般准则

指南中最强调的一个方面是,您应该编写尽可能优化的代码。这个目标与敏捷开发方法相反,在敏捷开发方法中,您只编写您需要的代码,而将细化和优化留给重构。如果您使用敏捷方法进行开发,您可能希望等到重构之后再签入代码。

另一个非常重要的总体目标是避免使用直接的 API 或操作系统调用。您应该总是在相关的库中寻找包装函数。这些函数中有许多都经过优化,可以快速安全地执行。比如,千万不要用 C malloc()函数。而是使用sql_alloc ()my _alloc()功能。

所有代码行的长度必须少于 80 个字符。如果需要将一行代码延续到另一行,请对齐代码,以便参数垂直对齐,或者延续代码与缩进空间计数对齐。

注释是使用标准 C 风格的注释编写的,例如,/* this is a comment */。您应该在代码中自由地使用注释。

image 提示抵制使用 C++ // comment选项的冲动。MySQL 编码指南明确反对这种技术。

文件

源代码选择的语言是英语。这包括所有变量、函数名、常量和注释。编写和维护 MySQL 源代码的开发者遍布欧洲和美国。在源代码中选择英语作为默认语言很大程度上是由于美国计算机科学发展的影响。在许多欧洲国家,英语也作为第二语言在许多小学和中学教育项目中教授。

编写函数时,使用注释块来描述函数、其参数和预期的返回值。注释块的内容应该分成几个部分,每个部分的名称都要大写。您应该在注释后的第一行包含一个简短的函数描述名,并且至少要包含部分、概要、描述和返回值。您还可以包括可选部分,如警告、注释、另请参见、待办事项、错误和 REFERENCED_BY。各部分和内容描述如下:

  • 简介(必需)—简要概述该功能中的流程和控制机制。它应该允许读者理解函数的基本算法。这有助于读者理解该功能,并对其功能有一个大致的了解。该部分还包括所有参数的描述(用 IN 表示输入,用 OUT 表示输出,用 IN/OUT 表示其值可以更改的引用参数)。
  • 描述(必选)——功能的描述。它应该包括函数的目的和它的使用的简要描述。
  • 返回值(必选)—显示所有可能的返回值以及它们对调用者的意义。
  • 警告—包括这一部分来描述呼叫者应该注意的任何不寻常的副作用。
  • 注释—包括这一部分,为读者提供您认为重要的任何信息。
  • 参见—当您正在编写一个与另一个函数相关联的函数,或者需要另一个函数的特定输出,或者打算由另一个函数以特定的调用顺序使用的函数时,请包括这一节。
  • TODO—包含此部分,以传达该功能的任何未完成功能。完成这些项目后,请将其从本节中删除。我倾向于忘记做这件事,这经常会让我有点挠头,因为我已经完成了待办事项。
  • 错误—包含这一部分来记录您的函数所具有的任何异常错误处理。
  • REFERENCED _ BY—包含此部分以传达此函数与其他函数或对象之间关系的特定方面——例如,无论何时您的函数被另一个函数调用,该函数都是另一个函数的原语,或者该函数是友元方法甚至是虚方法。

image 提示 Oracle 建议没有必要为只有几行代码的短函数提供注释块,但是我建议为您创建的所有函数编写一个注释块。当您探索源代码并遇到许多很少或没有文档的小(和一些大)函数时,您会喜欢这个建议。

清单 3-23 中显示了一个函数注释块的例子。

清单 3-23 示例函数注释块

/**
  Find tuples by key.

  SYNOPSIS
    find_by_key()
    string key            IN     A string containing the key to find.
    Handler_class *handle IN     The class containing the table to be searched.
    Tuple *               OUT    The tuple class containing the key passed.

    Uses B Tree index contained in the Handler_class. Calls Index::find()
    method then returns a pointer to the tuple found.

  DESCRIPTION
    This function implements a search of the Handler_class index class to find
    a key passed.

  RETURN VALUE
    SUCCESS (TRUE)                 Tuple found.
    != SUCCESS (FALES)             Tuple not found.

  WARNING
    Function can return an empty tuple when a key hit occurs on the index but
    the tuple has been marked for deletion.

  NOTES
    This method has been tested for empty keys and keys that are greater or
    less than the keys in the index.

  SEE ALSO
    Query:;execute(), Tuple.h

  TODO
    * Change code to include error handler to detect when key passed in exceeds
    the maximum length of the key in the index.

  ERRORS
    -1                              Table not found.
    1                               Table locked.

  REFERENCED_BY
    This function is called by the Query::execute() method.
*/

功能和参数

我想特别指出这些项目,因为源代码中存在一些不一致的地方。如果您使用源代码作为格式化的指南,您可能会偏离编码指南。函数及其参数应该对齐,以便参数垂直对齐。这适用于定义函数和从其他代码调用它。同样,在声明变量时,变量也应该对齐。对齐的间距不是这些项目垂直外观的问题。您还应该添加关于每个变量的行注释。行注释应该从第 49 列开始,并且不能超过最大 80 列的规则。如果变量的注释超过 80 列,请将该注释放在单独的一行。清单 3-24 显示了函数、变量和参数的对齐类型的例子。

清单 3-24 变量、函数和参数对齐示例

int     var1;                                 /* comment goes here */
long    var2;                                 /* comment goes here too */
/* variable controls something of extreme interest and is documented well */
bool    var3;

return_value *classname::classmethod(int  var1,
                                    int  var2
                                    bool var3);

if (classname->classmethod(myreallylongvariablename1,
                          myreallylongvariablename2,
                          myreallylongvariablename3) == -1)
{
  /* do something */
}

image 警告如果你在 Windows 上开发,你的编辑器的换行符可能设置不正确。当您在文件中放置一个换行符时,Windows 中的大多数编辑器都会发出一个 CRLF ( /r/n)。Oracle 要求您使用单个 LF ( /n),而不是 CRLF。这是在 Windows 上创建的文件与在 UNIX 或 Linux 上创建的文件之间常见的不兼容。如果您使用的是 Windows,请检查您的编辑器,并对其配置进行适当的更改。

命名惯例

Oracle 更喜欢使用带下划线的小写字母而不是大写字母为变量指定有意义的名称。例外情况是类名的使用,它需要有首字母大写。枚举应该以短语enum_为前缀。所有的结构和定义都应该用大写字母书写。命名约定的例子如清单 3-25 所示。

清单 3-25 示例命名约定

class My_classname;
int   my_integer_counter;
bool  is_saved;

#define CONSTANT_NAME 12;

int my_function_name_goes_here(int variable1);

间距和缩进

MySQL 编码指南规定每个缩进层次的间距应该总是两个字符。千万不要用制表符。如果您的编辑器允许,请更改编辑器的默认行为以关闭自动格式,并用两个空格替换所有制表符。当使用 Doxygen(我稍后将讨论)或行解析工具等文档工具在文本中定位字符串时,这一点尤其重要。

当标识符和运算符之间有空格时,变量和运算符之间不包含空格,运算符和操作数之间只包含一个空格(运算符的右侧)。同样,函数中的左括号后面也不能有空格,但是参数之间要有一个空格,最后一个参数名和右括号之间不能有空格。最后,包含一个空行来描述变量声明与控制代码,控制代码与方法调用,块注释与其他代码,函数与其他声明。清单 3-26 描述了一段格式正确的代码摘录,包含一个赋值语句、一个函数调用和一个控制语句。

清单 3-26 间距和缩进

return_value= do_something_cool(i, max_limit, is_found);
if (return_value)
{
  int var1;
  int var2;

  var1= do_something_else(i);

  if (var1)
  {
    do_it_again();
  }
}

在源代码的某些部分,花括号的对齐方式也不一致。MySQL 编码指南规定花括号应该和它上面的控制代码对齐,正如我在所有例子中展示的那样。如果您需要缩进另一个级别,请使用与花括号中的代码相同的列对齐方式(两个空格)。如果在代码块中执行一行代码,也没有必要使用花括号。

花括号区域中的一个奇怪之处是 switch 语句。应该编写一个 switch 语句,将 switch 条件后面的左花括号对齐,并将右花括号与 switch 关键字对齐。case 语句应该与 switch 关键字在同一列中对齐。清单 3-27 说明了这一准则。

清单 3-27 开关语句示例

switch (some_var) {
case 1:
   do_something_here();
   do_something_else();
   break;
case 2:
   do_it_again();
   break;
}

image 注意前面代码中的最后一个break不需要。为了完整起见,我通常将它包含在我的代码中。

文档实用程序

检查源代码的另一个有用的方法是使用自动文档生成器,它读取源代码并生成基于函数和基于类的方法列表。这些程序列出了所使用的结构,并提供了它们在源代码中的使用方式和位置的线索。这对于研究 MySQL 很重要,因为源代码依赖于许多关键结构来操作和操纵数据。

一个这样的项目叫做 Doxygen。Doxygen 的好处是,它也是开源的,由 GPL 管理。当您调用 Doxygen 时,它读取源代码并生成一组可读性很高的 HTML 文件,这些文件从函数之前的源代码中提取注释,它还列出了函数原语。Doxygen 可以阅读 C、C++和 Java 等编程语言。Doxygen 是研究复杂系统(如 MySQL)的有用工具——尤其是当您考虑到代码中有数百个位置调用了基本库函数时。

Doxygen 可用于 UNIX 和 Windows 平台。要在 Linux 上使用该程序,请从 Doxygen 网站http://www.doxygen.com下载源代码。

下载安装后,按照安装说明(也在网站上)进行操作。Doxygen 使用配置文件来生成输出的外观以及输入中包含的内容。要生成默认配置文件,请发出以下命令:

doxygen -s -g /path_to_new_file/doxygen_config_filename

指定的路径应该是您想要存储文档的路径。一旦有了默认配置文件,就可以编辑该文件并更改参数以满足您的特定需求。有关选项及其参数的更多信息,请参见 Doxygen 文档。您通常会指定要处理的文件夹、项目名称以及其他与项目相关的设置。一旦设置了所需的配置,就可以通过发出以下命令为 MySQL 生成文档:

doxygen </path_to_new_file/Doxygen_config_filename>

image 注意根据您的设置,Doxygen 可能会运行很长时间。如果您希望 Doxygen 在合理的时间内生成文档,请避免使用高级绘图命令。

最新版本的 Doxygen 可以使用提供的 GUI 在 Windows 上运行。GUI 允许您使用向导创建配置文件,该向导将引导您逐步完成该过程,并创建基本配置文件、允许您设置自己的参数的专家模式,以及加载配置文件的能力。我发现使用向导界面生成的输出足以满足偶然到深入的查看。

我建议在深入研究源代码之前,花一些时间运行 Doxygen 并检查输出文件。这将节省你大量的查找时间。光是这些结构就值得贴在你显示器旁边的墙上,或者贴在你的工程日志上。Doxygen 可以生成的文件类型示例如图 3-3 所示。

9781430246596_Fig03-03.jpg

图 3-3。示例 MySQL Doxygen 输出

保存工程日志

许多开发者记录他们的项目。一些比另一些更详细,但大多数在会议和电话交谈中做笔记,从而为口头交流提供书面记录。如果你没有记录工程日志的习惯,你应该考虑这样做。我发现日志是我工作中的一个重要工具。是的,写东西确实需要更多的努力,如果你试图包含所有你认为重要的各种图纸和电子邮件,日志可能会变得混乱(我的日志经常塞满了从重要文件上剪下的剪报,就像某种工程师的剪贴簿)。然而,回报可能是巨大的。

当你在研究 MySQL 源代码的时候做一些调查工作时,尤其如此。把你的每一个发现都记录下来。写下每一个顿悟、重要的设计决策、重要纸质文档的片段,甚至偶尔的啊哈!随着时间的推移,你会建立起你的发现的纸质记录(我的一个前老板称之为她的纸质大脑!)这将证明对于评论和您自己的文档工作是非常宝贵的。如果你真的使用日志,做日志条目或粘贴重要的文件片段,你很快就会发现各种日志并不适合被很好地组织。大多数工程师(比如我)更喜欢无法重组的有线条的精装期刊(除非你用很多剪刀和胶水)。其他人更喜欢便于重组的活页日志。如果你打算使用精装期刊,可以考虑在使用过程中建立一个“活”索引。

image 提示如果你的日记本没有编号,花几分钟时间在每一页上都标上页码。

有许多方法可以建立生活索引。你可以在页面顶部或页边空白处写下任何有趣的关键词。这可以让你快速浏览日志,找到感兴趣的项目。活索引的特点是能够随着时间的推移添加引用。我发现创建 living index 的最好方法是使用电子表格列出你写在日志页上的所有术语,并在旁边写上页码。我大约每周更新一次电子表格,打印出来,贴在我的日志前面。我见过一些期刊前面有一个口袋,但磁带的方法也很有效。随着时间的推移,您可以对索引项和参考页码进行重新排序,以使列表更易于阅读;您也可以在日志的前面放置一个更新的列表,这样您可以更容易地找到页面。

考虑使用工程日志。到了向上级汇报进展的时候,你不会后悔的。当你被要求报告六个月或更久以前做的事情时,它也可以为你节省大量的重复工作。

跟踪您的更改

当您创建的代码对读者来说不直观时,请始终使用注释。例如,代码语句if (found)非常简单明了。如果变量评估为TRUE,将执行控制语句后的代码。然而,代码if (func_call_17(i, x, lp))需要一些解释。当然,您希望编写的所有代码都是不言自明的,但有时这是不可能的。当您访问支持库函数时尤其如此。有些名称不直观,参数列表可能会令人困惑。当你编码时,记录下这些情况,你的生活将会得到改善。

编写注释时,可以使用行内注释、单行注释或多行注释。行内注释从第 49 列开始写,不能超过 80 列。单行注释应该与它所引用的代码对齐(缩进标记),并且不应该超过 80 列。同样,多行注释应该与它们解释的代码对齐,并且不应该超过 80 列,但是它们应该将开始和结束注释标记放在单独的行上。清单 3-28 说明了这些概念。

清单 3-28 。注释位置和间距示例

if (return_value)
{
  int     var1;                                 /* comment goes here */
  long    var2;                                 /* comment goes here too */

  /* this call does something else based on i */
  var1= do_something_else(i);

  if (var1)
  {
    /*
      This comment explains
      some really interesting thing
      about the following statement(s).
    */
    do_it_again();
  }
}

image 提示永远不要使用重复的*来强调代码的某些部分。它分散了读者对代码的注意力,看起来杂乱无章。此外,将所有这些东西排列起来太麻烦了——尤其是当你以后编辑评论的时候。

如果您正在使用诸如 bazaar 这样的源代码控制应用修改 MySQL 源代码,您不必担心跟踪您的更改。Bazaar 为您提供了几种方法来检测和报告哪些变化是您自己的,哪些是别人的。如果您没有使用源代码管理应用,您可能会忘记哪些更改是您自己的,特别是当您直接对现有的系统函数进行更改时。在这种情况下,很难区分你写的和已经存在的。保持工程日志对这个问题有很大帮助,但是有一个更好的方法。

您可以在更改前后添加注释,以指示哪些代码行是您修改的。例如,您可以在代码前放置一个像/* BEGIN CAB MODIFICATION */这样的注释,在代码后放置一个像/* END CAB MODIFICATION */这样的注释。这允许您将更改括起来,并帮助您使用许多文本和行解析实用程序轻松地搜索更改。这种技术的一个例子显示在清单 3-29 中。

清单 3-29 评论您对 MySQL 源代码的修改

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds my revision note to the MySQL version number. */
   /* original code:   */
   /*strmov(end, "."); */
   strmov(end, "-CAB Modifications");
/* END CAB MODIFICATION */

请注意,我还包括了修改的原因和原始代码的注释行(这个例子是虚构的)。使用这种技术将帮助您快速访问您的更改,并增强您以后诊断问题的能力。

如果您进行了修改以便在您的组织中使用,并且不打算与 Oracle 共享这些更改,这种技术也会很有帮助。如果您不共享这些更改,每次 Oracle 发布您想要使用的新版本系统时,您都将被迫对源代码进行修改。在源代码中使用注释标记将帮助您快速确定哪些文件需要更改,以及这些更改应该是什么。如果你创造了一些新的功能,你可能最终会想要分享这些功能,如果没有别的原因,只是为了避免每次 MySQL 新版本发布时都进行修改。

image 注意虽然在使用配置控制下的源代码(BitKeeper)时这种技术没有被禁止,但通常是不鼓励的。事实上,开发者以后可能会完全删除你的评论。仅当您做出不打算与任何人共享的更改时,才使用此技巧。

首次构建系统

既然您已经看到了 MySQL 源代码的内部工作方式,并遵循了源代码中的典型查询路径,那么是时候开始了。如果您已经在使用 MySQL 源代码,并且您正在阅读本书以了解更多关于源代码以及如何修改它的信息,您可以跳过这一节。

在开始之前,我建议您下载源代码(如果您还没有下载的话),然后下载并安装适用于您选择的平台的可执行文件。将编译好的二进制文件放在手边很重要,以防在实验过程中出错。试图在没有参考点的情况下诊断一个修改过的 MySQL 源代码版本的问题是相当具有挑战性的。如果在遇到困难的调试问题时可以恢复到基本编译的二进制文件,您将会节省很多时间。我将在第 5 章中更详细地介绍调试。如果你发现自己遇到了系统问题,你可以重新安装二进制文件,让你的 MySQL 系统恢复正常。

编译源代码很容易。如果您使用的是 Linux,打开一个命令 shell,切换到源代码树的根目录,运行cmake和 make 命令。cmake 脚本将检查系统的依赖性并创建适当的 makefiles。下面概述了首次在 Linux 上构建源代码的典型构建过程:

$ cmake .

-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Looking for SHM_HUGETLB
-- Looking for SHM_HUGETLB - found
-- Looking for sys/types.h
-- Looking for sys/types.h - found
-- Looking for stdint.h
-- Looking for stdint.h - found
-- Looking for stddef.h
-- Looking for stddef.h - found

...

-- Configuring done
-- Generating done
-- Build files have been written to: /source/mysql-5.6.6

$ make

[  0%] Built target INFO_BIN
[  0%] Built target INFO_SRC
[  0%] Built target abi_check
[  0%] Building C object zlib/CMakeFiles/zlib.dir/adler32.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/compress.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/crc32.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/deflate.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/gzio.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/infback.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inffast.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inflate.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inftrees.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/trees.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/uncompr.c.o
[  1%] Building C object zlib/CMakeFiles/zlib.dir/zutil.c.o
Linking C static library libzlib.a
[  1%] Built target zlib
[  1%] Building CXX object extra/yassl/CMakeFiles/yassl.dir/src/buffer.cpp.o

...

Linking CXX executable my_safe_process
[100%] Built target my_safe_process

$

image 提示关于设置复杂条件的更多信息,参见http://www.cmake.org/cmake/help/documentation.html。您还可以使用cmake-gui应用通过图形界面设置选项。你可以从 cmake.org 网站下载cmakecmake-gui。在线参考手册还包含一些使用条件编译 MySQL 的好例子。

您可以使用 Microsoft Visual Studio 编译 Windows 平台源代码。要在 Windows 上编译系统,请打开 Visual Studio 命令窗口(确保运行 vcvarsall.bat 批处理文件以加载所需的路径),然后发出cmake .命令,后跟devenv mysql.sln /build debug

image 注意您必须在当前目录中包含告知 cmake 开始工作的点。

这将构建服务器的调试版本。如果您希望从源代码树安装服务器,请参见 MySQL 参考手册http://dev.mysql.com/doc/refman/5.6/en/source-installation.html中的“源代码安装概述”一节。

运行 cmake 命令后,您还可以从 Visual Studio 的源代码分发树的根目录中打开mysql.sln项目工作区。在那里,您将活动项目设置为mysqld类,并将项目配置设置为mysqld - Win32 nt。当您单击 Build mysqld 时,该项目将编译任何必要的库,并将它们链接到您指定的项目。带一杯新鲜的饮料来娱乐自己,因为第一次建立所有的库可能需要一段时间。不管你使用哪个平台,编译后的可执行文件都会被放在client_releaseclient_debug文件夹中,这取决于你选择的编译选项。

image 注意大多数编译问题都可以追溯到开发工具配置不当或缺少库。有关如何解决最常见编译问题的详细信息,请参考 MySQL 论坛。

关于新编译的二进制文件(除非有问题),您首先会注意到的是,您无法判断该二进制文件是否是您编译的!您可以检查文件的日期,以查看可执行文件是否是您刚刚创建的文件,但是没有办法从客户端知道这一点。尽管 Oracle 不推荐这种方法,其他人也可能会回避这种方法,但是您可以更改 MySQL 编译的版本号,以表明它是您编译的版本号。

让我们假设您想一眼就识别出您的修改。例如,您希望在客户端窗口中看到一些指示,表明服务器是您的修改版本。您可以更改版本号来显示这一点。图 3-4 是这种修改的一个例子。

9781430246596_Fig03-04.jpg

图 3-4。带有版本修改的 MySQL 命令示例

请注意,在头和发出命令SELECT Version();的结果中,返回的版本号是您编译的服务器的相同版本号加上我放在字符串中的附加标签。要自己做这个改变,只需编辑mysqld.cpp文件中的set_server_version()函数,如清单 3-30 中的所示。在示例中,我加粗了一行代码,您可以添加它来创建这种效果。

清单 3-30 修改 set_server_version 函数

/*
  Create version name for running mysqld version
  We automaticly add suffixes -debug, -embedded and -log to the version
  name to make the version more descriptive.
  (MYSQL_SERVER_SUFFIX is set by the compilation environment)
*/

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 CAB MODIFICATION */
  /* Reason for modification: */
  /* This section adds my revision note to the MySQL version number. */
  strmov(end, "-CAB MODIFICATION");
  /* END CAB MODIFICATION */
}

还要注意,我已经包含了我前面提到的修改意见。这将帮助您确定您更改了哪些代码行。这一变化还有一个好处,即新的版本号将显示在其他 MySQL 工具中,如 MySQL Workbench。图 3-5 显示了在 MySQL Workbench 中运行SELECT @@version查询的结果,该查询针对的是修改后编译的代码。

9781430246596_Fig03-05.jpg

图 3-5。使用 MySQL Workbench 访问修改后的 MySQL 服务器

image 警告我有没有说过这是不被认可的方法?如果您正在使用 MySQL 进行您自己的实验,或者您正在修改源代码供您自己使用,那么您可以按照我的建议去做。然而,如果您正在创建将在以后添加到基本源代码中的修改,您应该而不是实现这种技术。

摘要

在本章中,你学习了几种获取源代码的方法。无论您选择下载源代码树的快照或 GA 版本源代码的副本,还是使用开发者里程碑版本来获得最新和最好的版本,您都可以获得并开始使用源代码。这就是开源的美妙之处!

也许这一章最吸引人的地方是你对 MySQL 源代码的引导之旅。我希望通过一个简单的系统查询,您在理解 MySQL 源代码的探索中取得了很大的进展。我还希望,如果你在编译源代码时遇到了问题,你不会沮丧地扔掉这本书。一个优秀的开源开发者的主要素质是她系统地诊断和调整环境以适应当前项目需求的能力。如果你遇到问题,不要绝望。解决问题是学习周期的自然组成部分。

您还研究了 MySQL 编码指南文档中的主要元素,并看到了一些代码格式和文档指南的示例。虽然不完整,但我提供的编码指南足以让您感受到 Oracle 希望您如何编写修改的源代码。如果你遵循这些简单的指导方针,你以后就不会被要求遵守。

在接下来的两章中,我将带您了解软件开发中经常被忽视的两个非常重要的概念。下一章将向您展示如何应用测试驱动的开发方法来探索和扩展 MySQL 系统,下一章将讨论 MySQL 源代码的调试。*