MySQL排错指南

978-7-115-39728-7
作者: 【美】Sveta Smirnova(斯维特 斯米尔诺娃)
译者: 李宏哲杨挺
编辑: 傅道坤
分类: MySQL

图书目录:

详情

本书由Oracle公司的技术支持工程师编写,详细阐述了MySQL故障诊断及处理中的知识,教会读者如何深入浅出地定位、分析并解决各种MySQL数据库的故障。本书由Oracle公司的技术支持工程师编写,详细阐述了MySQL故障诊断及处理中的知识,教会读者如何深入浅出地定位、分析并解决各种MySQL数据库的故障。

图书摘要

版权信息

书名:MySQL排错指南

ISBN:978-7-115-39728-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]Sveta Smirnova

  译    李宏哲 杨 挺

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2012 by O’Reilly Media. Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2015. Authorized translation of the English edition, 2015 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


本书由Oracle公司的技术支持工程师编写,详细阐述了MySQL故障诊断及处理中的知识,教会读者如何深入浅出地定位错误和异常方法,分析并解决各种MySQL数据库的故障。

本书共分为7章,其内容涵盖了解决MySQL问题的基本技巧、MySQL中的并发问题、服务配置的影响、MySQL硬件和运行环境相关的问题、复制备份中的故障排除、故障排除使用的技术和工具,以及一些MySQL故障排除的最佳实践。此外,本书的附录中还包含了可以帮助读者解决MySQL疑难问题的一些有用资源。

本书适合MySQL数据库开发及运维人员阅读。


解决系统故障可谓是每个系统专家遭遇过的最头疼的问题之一,虽然修复问题或执行解决方案通常是比较容易的部分,但通过诊断分析找出问题的原因才是真正的挑战。

经验丰富的管理员经历过各种尝试和失败,都意识到解决问题最好的方式是通过标准化的步骤来定位问题,列出所有可能的原因,然后依次测试,直到找到解决方案。这种方式听起来初级,但很有效(尽管对于资深系统专家来说不够高效)。

MySQL是一个专业、复杂、成熟、强大的数据库系统,可以满足大量客户的需求。MySQL 的安装和配置都很简单。事实上,大部分默认安装根本不需要任何配置。然而,MySQL 作为一个拥有众多功能层次的系统,有时也会有故障,会产生警告甚至错误。

有时候,这些警告和错误提供的信息明确(比如,你可能经历过或者在文档中有记录),足以帮助你立即解决问题。也有时候,你遭遇的问题没有已知的解决方案,或者该问题是与你的应用、数据库环境相关的特定问题。寻找这类警告、错误或者其他MySQL问题的解决方案可以说是一项让人望而却步的工作。

当遭遇这种问题的时候,数据库专家一般都从各种各样的资料或者记录过相似问题和解决方案的文档中查找线索。大多数时候,你会发现针对问题的建议繁多且相似,或者其中给出的解决方案对你的情况并不适用。

针对这种情形,人们普遍的做法是在互联网上搜索错误消息。通常,你会搜索到各种各样的信息,包括从电子邮件的归档到个人博客,甚至是一些可能与错误消息相关或者不相关的评论。这往往很浪费时间而且容易造成困惑。你需要的其实是一个可以告诉你如何解决MySQL问题的参考指南。

这本书不仅仅满足上述需求,还建立了一种几乎适用于解决所有系统问题的方式、方法。书中展示出的方法结构清晰、周密并且可重复运用。结合现实工作中的示例,本书定义了一种合理地分析和修复MySQL问题的方法,这是一个标志性的成果。

Sveta用第一手的经验、丰富的MySQL知识与诊断技巧为读者讲解诊断和修复几乎所有可能遇到的MySQL问题的基本技能——这也使本书成为MySQL专家的必备书籍。

我自认为是一个MySQL专家,我的技术来自于丰富的经验。我不敢说我知道MySQL的所有细节。读完这本书,我可以说它提升了我的技能。如果一个像我这样经验丰富的专家都能从此书获益,那么每个MySQL用户都应该阅读此书。特别是所有MySQL数据库管理员、顾问以及数据库开发人员都应该阅读本书。

Charles Bell博士,Oracle公司

MySQL High Availability(O′Reilly)和Expert MySQL(Apress)图书的作者


Sveta Smirnova是Oracle公司MySQL支持团队Bug验证小组的首席技术支持工程师。

本书封面上的动物为马来西亚臭獾,也称为爪哇臭獾、巽他臭獾或印尼臭獾。臭獾属还包括另外一个物种——巴拉望臭獾。长期以来,臭獾一直被认为是獾家族的一部分,但是最近的DNA研究表明,它们与臭鼬的关系更为密切。

臭獾有棕黑色的皮毛,而且有白色或黄色的嘴,背部后面有一条类似于臭鼬的长条纹。它们的长嘴上面有像猪一样的鼻子,在前脚上长着细长而弯曲的爪子。它们一般有12~20英寸长(包括一条很短的尾巴在内),体重可达8磅。

臭獾主要分布在印尼、马来西亚以及菲律宾,它们生活在森林里以及附近的开阔地带,以及多山岛屿的高海拔地带。作为夜行动物,它们生活在地下的洞穴中,它们能自己挖洞,也会与豪猪共用洞穴。它们以蛋类、昆虫、植物和腐肉为食,它们的爪子和嘴用来挖掘蚯蚓。它们通常一窝幼崽有2~3只,但是人们对它们的生活习性以及繁殖情况知之甚少。

Lydekker在他的Royal Natural History(《皇家自然史》)中,将这种物种从后部腺体发出来的“臭气”称为具有极端臭味的喷雾。臭獾的这种分泌物用来防御肉食动物,其中包括爪哇鹰雕、野猫和老虎。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

业界评论

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


我从2006年5月开始,作为首席技术支持工程师在MySQL AB公司MySQL支持团队的bug校验组工作,然后我到了Sun公司,最后是在Oracle公司。在日常工作中,我经常遇到用户受困于某个问题而不知所措的情况。虽然有已经被证实可用的方法去定位并快速修复问题,但是用户往往很难从大量的信息中筛选出这些可用的信息。尽管有数以千百计的著名书籍、博文和网页都详细介绍了MySQL服务器方方面面的问题,但这正是我感觉困难的地方:这些信息都关注于如何让MySQL服务器正常地工作,而忽略了定位错误和异常的方法。

当这些信息综合到一起的时候,它们能够详细解释MySQL操作的每个方面。但是,如果你不知道问题是如何发生的,你可能会从文档中提到的大量建议中忽略掉真正的原因。即使你向专家咨询问题产生的原因,他们也可能只会给出很多的建议,你仍需要找出真正的原因。否则,你做出的任何修改可能只是临时解决了问题,甚至反而使问题更糟。

了解问题的来源非常重要,有时对SQL语句或者配置选项的一个修改就可以解决问题。掌握错误的原因可以让你永久地修复它并保证以后不再发生。

我写这本书的目的是告诉读者我经常使用什么方法来确定导致 SQL 应用程序或MySQL配置错误的原因,以及如何解决这些问题。

本书是写给具有一定MySQL知识基础的读者的。我会尽量使书中的内容对初学者和高级用户都有帮助。读者需要知道SQL语句并且简单了解MySQL服务器是如何工作的,至少从用户手册或者初学者指南中了解一二。最好读者有实际的使用经验或者已经遇到了难以解决的问题。

我不想重复介绍其他资料中已有的内容,我会更偏重于补充定位错误和异常行为的方法。因此,在本书中你将获得的是如何修复应用程序的指导,而不是有关应用程序和服务器行为的细节介绍。想了解更多信息,可阅读MySQL Reference Manual(http://dev.mysql.com/doc/refman/5.5/en/
index.html
)。

本书将围绕着帮助读者定位问题和寻找原因的目标进行组织。我会介绍我逐步解决问题的经过,而不会罗列出一大堆不相关的信息或者空想出的方法。

 提示:弄清问题是什么非常重要。

例如,当解释MySQL安装缓慢的时候,你需要确认哪里缓慢:是仅应用程序相关的部分缓慢,还是所有发送到MySQL服务器的查询运行缓慢?你也最好知道,是否同样的安装以前也缓慢,并且该问题是一直存在还是周期性地重现。

另一个例子是关于错误的行为。你需要知道什么是错误的操作,产生了什么结果和你预期的结果是什么。

我现在已经很擅长介绍解决问题的方法。很多问题都有不同的解决方式,最佳的解决方案取决于应用程序和用户的需求。如果我面面俱到地介绍各种解决问题的方法,那么本书的篇幅可能是现在的10倍,这可能让你忽略了适合自己的方法。我的目标是使读者从起步就处于正确的道路上,以便可以快速解决各种问题。而修正问题的其他细节可以在各种资料中查到,其中许多资料都会在我们学习的过程中被引用和提及。

本书包括7章和1个附录。

第1章,“基础”,介绍解决问题的基本技巧,你几乎会在任何场合使用到这些技巧。本章仅覆盖单线程问题,即在隔离条件下,独立连接产生的问题。之所以我从这种隔离的甚至有些理想化的条件下开始介绍,是因为你需要掌握这些在多线程应用中隔离问题的技术。

第2章,“你不孤单:并发问题”,介绍应用在多线程环境下运行或者应用与其他应用中的事务有交互的情况下产生的问题。

第3章,“配置选项对服务器的影响”,包括两部分内容。第一部分是调试和修复由配置项产生的问题的参考指南。第二部分是关于重要配置项的索引。也就是说,这部分可以根据需要查阅而不用通篇阅读。第二部分也包括解决由于特定配置引发的问题的推荐方法,以及如何测试你是否真正解决了问题。我会尽可能介绍其他参考中没有的技术,并且尽量把所有常见配置项问题集中在一起。我还把它们进行分组,便于你方便地检索到产生问题的原因。

第4章,“MySQL环境”,介绍其他关于硬件和服务器运行环境方面的问题。这是一个大话题,不过大部分的信息是针对操作系统的,而且通常只能由操作系统管理员解决。因此,本章列出了一些MySQL数据库管理员(DBA)必须关注的要点。阅读完该章后,你应该知道什么时候是你的环境的问题,以及如何向系统管理员清楚地解释问题。

第5章,“复制故障诊断”,本章侧重于复制场景下产生的问题。事实上,本书通篇都在讨论复制的问题,不过其他章节阐述的是复制和其他问题的关系,本章仅仅针对复制问题。

第6章,“故障排查技术与工具”,本章补充介绍在之前解决问题的过程中略过的或者无法详细介绍的故障排除技术和工具。本章的目的是补充前几章遗漏的一些细节,你也可以把它作为参考索引。我先给出原则,然后列出可用工具。我不会列出没有使用过的工具,我列出的都是我自己每天使用的工具,也就是MySQL项目组开发的工具(当然现在属于Oracle公司)。我也使用第三方工具来帮助我每天处理bug和进行服务支持。

第7章,“最佳实践”,本章主要介绍安全、高效地解决问题的习惯和方法。本章不会介绍用于设计MySQL应用程序的最佳实践,因为这在其他资料中有详细的介绍,而是重点讲述有助于定位问题和避免问题发生的最佳实践。

附录,“资源信息”,本附录包含我日常工作中用到的可以帮助解决疑难问题的资源。当然,本书也用到了其中一部分资源,我已在相应的位置添加了参考信息。

在过去的几年,诞生了很多MySQL分支,其中最重要的莫过于Percona服务器和MariaDB。但本书不会介绍它们,因为在日常工作中我主要用的都是MySQL,我无法介绍平常不使用的数据库。然而,由于它们是MySQL的分支,因此你也可以使用本书介绍的方法。只有当你用到了分支版本中特有的功能时,你才需要了解针对该产品的内容。

为了节约篇幅,同时也避免介绍一个全新领域的知识,我略过了MySQL群集相关的问题。如果你在使用MySQL群集的过程中遇到了SQL或者应用程序相关的问题,那么你可以采用解决其他存储引擎问题的方式去解决该问题。也就是说,本书也适用于发生在群集环境上的类似问题。但是,解决MySQL群集特有的问题需要用到MySQL群集的知识,在这里我并没有介绍它们。

不过,本书用大量篇幅介绍与MyISAM和InnoDB引擎相关的问题。因为它们是目前最受欢迎的存储引擎,安装量巨大。它们分别是过去和现在默认的存储引擎:5.5版本之前是MyISAM;5.5版开始是InnoDB。

关于本书的示例,我还要多说几句。它们有的是专门为本书设计的,有的是为了讨论我提到过的问题而构造的。尽管有些示例来自于实际支持的案例和bug报告,但是所有的代码都是全新的并且不会涉及任何机密。有些地方介绍了一些客户需求,那也不是真实的。不过,这里描述的问题都是真实的,且会多次遇到,只是改用了不同的代码、名称和环境。

我尽可能地使所有示例简单、通用、易懂。因此大部分示例中都使用了MySQL命令行客户端。在MySQL安装包中,始终都含有此客户端。

这也解释了为何本书不介绍每个安装版本的所有问题;那不可能在一本书中完全涵盖。相反,本书会尽量给出一些引子,你可以在此基础上进行扩展。

我准备用C语言API去举例说明本书中讨论的功能。做出这种选择并不容易,因为有各种语言的MySQL API。我不可能在本书中全部使用它们,也不想去猜测哪种语言的API更流行。我发现它们中的大部分看起来和C的API很相似(有很多甚至是在C的API上进行封装),因此我认为这会是最佳选择。即使你使用完全不同语法的API,比如ODBC,这部分也会很有帮助,因为你会知道应该搜索什么。

有些示例使用PHP。我这么做是因为我日常使用的语言是PHP,这样做可以展示我实际的代码示例。真实的示例往往更适用于展示,因为它们反映了现实生活中读者最可能遭遇的问题。并且,MySQL PHP API也是基于C的API封装的,而且使用了同样的名字,因此读者可以很容易与本书中讨论的C的函数进行比较[1]

我没有使用JDBC和ODBC的示例,因为这些API都太特殊。同时,它们的调试技术又很雷同,尽管不是完全一致。它们主要是语法有所不同。我觉得详细介绍这两种连接方式不但不会教会读者更多解决问题的技巧,反而会给读者造成困扰[2]

 提示

这个图标用来强调一个提示、建议或一般说明。

 

 警告

这个图标用来说明一个警告或注意事项。

本书的目的是为了帮助读者完成工作。一般而言,你可以在你的程序和文档中使用本书中的代码,而且也没有必要取得我们的许可。但是,如果你要复制的是核心代码,则需要和我们打个招呼。例如,你可以在无须获取我们许可的情况下,在程序中使用本书中的多个代码块。但是,销售或分发O’Reilly图书中的代码光盘则需要取得我们的许可。通过引用本书中的示例代码来回答问题时,不需要事先获得我们的许可。但是,如果你的产品文档中融合了本书中的大量示例代码,则需要取得我们的许可。

在引用本书中的代码示例时,如果能列出本书的属性信息是最好不过。一个属性信息通常包括书名、作者、出版社和ISBN。例如:“MySQL Troubleshooting by Sveta Smirnova (O’Reilly). Copyright 2012 Sveta Smirnova, 978-1-449-31200-8.”

在使用书中的代码时,如果不确定是否属于正常使用,或是否超出了我们的许可,请通过permissions@oreilly.com与我们联系。

如果你想就本书发表评论或有任何疑问,敬请联系出版社。

美国:

  O’Reilly Media Inc.

  1005 Gravenstein Highway North

  Sebastopol, CA 95472

中国:

  北京市西城区西直门南大街2号成铭大厦C座807室(100035)

  奥莱利技术咨询(北京)有限公司

我们还为本书建立了一个网页,其中包含了勘误表、示例和其他额外的信息。你可以通过如下地址访问该网页:

http://www.oreilly.com/catalog/9781449312008

关于本书的技术性问题或建议,请发邮件到:

bookquestions@oreilly.com

欢迎登录我们的网站(http://www.oreilly.com),查看更多我们的书籍、课程、会议和最新动态等信息。

Facebook: http://facebook.com/oreilly

Twitter: http://twitter.com/oreillymedia

YouTube: http://www.youtube.com/oreillymedia

我要感谢所有帮助完成本书的人,没有他们的帮助,本书将无法完成。

首先,我要感谢Andy Oram编辑,他给了我很大的帮助,使本书更加具有可读性。他给我指出了本书中介绍不够细致的地方。他还帮我深入分析了潜在读者的技能储备,建议我增加针对初学者的介绍,同时删除了人人皆知的冗余细节。

我也要感谢整个MySQL支持团队。他们将专业知识同团队的每个成员分享,我从中学到了很多很多。这里我不一一列举他们的名字,我想说的是感谢我从2006年加入MySQL支持团队以来共事过的所有同事,包括那些已经离职的、跳到开发部门的,或者跳到其他公司的同事。

感谢Charles Bell,是他督促我撰写本书。他对本书进行了审校并提出了很多改进意见。Charls在Oracle公司的MySQL复制与备份团队工作,并且是两本MySQL专著的作者。他对于本书内容和版式的建议都非常有用。

我要感谢本书所有的审校人员。

最后,感谢我的家人。

[1] mysqlnd使用它自己实现的客户端协议,但是函数名称仍然使用跟C API一样的命名方式。

[2] 你可以从http://dev.mysql.com/doc/refman/5.5/en/connector-j-reference.html获取更多的关于Connector /J(JDBC)的详细信息,从http://dev.mysql.com/doc/refman/5.5/en/connector-odbc-reference.html获取关于Connector/ODBC的信息。


当解决疑难问题的时候,为了节约时间,你可以从最简单的情况开始,然后一步步从简入繁。在MySQL支持团队工作的时候,我每个月解决成百上千的问题。其中的大部分都是从零散的请求信息开始的,最终的解决方案可能也很基础,我们将会从一些示例中看到这点。不过有些时候,我们确实会遭遇很大的挑战。所以,我们应时刻牢记从最基础的开始。

典型的基础类问题不外乎执行一个查询但是返回非预期的结果。这类问题的表现形式可能是很明显的错误,也可能是在你明知有匹配记录的情况下却没有返回结果,或者其他应用程序中罕见的行为。简而言之,本章内容是建立在你充分了解应用程序的运行状况以及查询应该返回结果的基础上的。对于不了解错误来源的情况将在本书后面讨论。

即使你遇到的是最诡异的错误,或者你无法确定应用程序中的错误原因,你也应该从最基础的部分开始。6.6节会深入讨论这个过程,此过程又称为构造最小化测试用例(creating a minimal test case)。

这个错误听起来十分简单,但仍可能很难发现。我建议你像处理其他问题一样,非常细心地查找可能出现的SQL语法错误。

类似如下错误,很容易被发现:

  

在这个示例中,很显然用户少转入了个“m”,错误消息也很清楚(输出结果根据页面设置进行宽度调整):

  

遗憾的是,不是所有的语法错误都这么显而易见。我曾经处理过一个问题,它的查询语句是这样的:

  

这是一个版本迁移导致的问题;该语句在5.0版本中运行正常,但是在5.1版本中出现错误。问题的原因在于,在5.1版本中,“accessible”是一个保留字。给该语句加上引号(反引号还是双引号取决于你的SQL格式),即可重新正常运行:

  

实际场景中的查询语句看起来可能十分繁琐,包括大段的JOIN和复杂的WHERE条件。所以,即使是简单的错误在其他大量语句的干扰下也很难被查找出来。我们当时第一步的任务就是简化复杂的查询,使它变成像刚才看到的那样只有一行SELECT的语句。这就是一个最简化测试的示例。当我们看到简化后只有一行的语句也有同样的bug时,我们就能很快意识到原有程序是因为保留字的问题而产生了错误。

但是,当你不知道查询是什么样的时候该怎么办?比如,查询是由应用程序自动生成的,或是在存储库中由第三方库动态生成的。

考虑如下PHP代码:

  

从这段脚本中很难直接定位错误。幸运的是,我们可以通过调整代码,使用输出函数打印查询语句。在PHP语言中,可以使用echo运算符。因此,修改代码,如下所示:

  

当程序输出将要提交的语句的时候,问题暴露出来了:

  

如果你仍然没有发现错误,你可以在MySQL命令行客户端中尝试执行这个查询:

  

问题在于每一行都少了一个右单引号。返回PHP代码,需要把:

  

改为:

  

即可。

遗憾的是,你不可能始终使用输出函数。比如,我之前提到的那个场景,SQL语句是在编译好的第三方库中生成的。你的应用程序可能只是使用了库提供的高级的抽象接口,比如CRUD(新建、读、更新、删除)接口。或者在生产环境下,你不希望用户看到在使用特定参数对特定查询进行测试时的查询。在这种情况下,可以检查MySQL的通用查询日志。这里用一个新示例来说明其是如何工作的。

这是一段有问题的PHP代码:

  

这段代码更新了例1.1中定义的表。

例1.1 一般问题情形的示例表

  

现在启用通用查询日志。该日志包含MySQL服务器接收到的每条独立查询。很多产品不会在日常运行中使用该日志,因为它在高负载的情况下增长十分迅速,并且写入日志会消耗MySQL服务器的资源,这些资源很可能用于更重要的用途。从5.1版本开始,你可以临时打开通用查询日志,方便随时记录你需要的查询。可以通过下面命令打开日志:

  

也可以将日志记录在表中,这可以帮你方便地分类索引日志文件项,因为你可以像查询其他表一样访问查询日志表:

  

现在可以运行应用程序。经过迭代地执行问题代码后,查询通用日志记录表,以查找有问题的查询:

  

  

注意上述代码中2.row中的查询语句:

  

错误再次显而易见:在语句的结尾有个多余的逗号。这个问题是由下面这部分PHP代码产生的:

  

如果字符串确实是以逗号结尾的,那么rtrim函数本应移除结尾的逗号。但是现在这行实际上是以空格结尾的,因此rtrim函数没有移除任何字符。

既然我们已经发现了应用程序中产生错误的原因,我们就可以关闭通用查询日志:

  

在这节,我们学到一些重要的东西:

这是用户反馈的另一个非常常见的问题,主要的现象有:用户看不到更新的结果、展示的顺序错误或者查询到了非预期的结果。

这个问题主要有两方面的原因:一方面是你的SELECT查询有误;另一方面是数据库中的数据和你想象的不同。我先介绍第一种情况。

在我规划本节示例的时候,我考虑要么使用真实的示例,要么使用我自己设计的小场景。真实的示例可能占用大量篇幅,但是我自己设计的示例可能对你没有什么帮助,因为没有人会写出那样的代码。因此,我选择使用典型的真实示例作为示例,只是大幅简化了它们。

第一个示例是用户在大量使用join时的常见错误。我们将使用之前介绍的例1-1中的表。这张表包含了MySQL中会引起一些常见使用错误的特性,这些特性是我在MySQL支持团队中收集的。每个错误在items表中都有一行记录。我还有一张关联资源信息的links表。因为条目和关联信息之间是多对多的关系,所以我通过items_links关联表把它们联系起来。下面是items表和items_links表的定义(在这个示例中不需要links表):

  

我编写的第一条查询正常运行,并且似乎返回了合理的结果:

  

……直到我把返回的数值与关联总数进行比较的时候,我才发现:

  

查询到的关联信息数比关联表的记录还多,这怎么可能?

我们再来检查一下我特意编写的这个查询。它很简单,仅仅包含两部分,一个子查询:

  

和一个外部查询:

  

子查询是开始错误排查的好切入点,因为它可以独立运行。因此,我们可以预期一个完整的结果集:

  

令人惊讶的是,我们居然有一个输入错误,事实上items_links表中并没有id字段,而是iid字段(代表items的ID)。如果我们重写该条查询,让它使用正确的标识符,它便可正常运行:

  

 提示

如果你通过表名.列名的格式指定完整的列名,那么你可以从一开始就避免这个错误,因为你会立即获得错误:

  

MySQL命令行客户端是一个非常好的测试工具,该工具包含在MySQL的安装包中。第6章将讨论这个重要的工具。

然而,为什么MySQL在执行原始查询语句的时候没有返回同样的错误呢?这是因为在items表有一个名为id的列,因此MySQL认为我们想要执行一个依赖子查询,结果实际上从items_links表中查询了items.id。“依赖子查询”是指引用外部查询中字段的查询。

我们也可以借助EXPLAIN EXTENDS命令,通过SHOW WARNINGS来查找这个错误。如果我们用该命令运行原始查询,会得到:

  

  

EXPLAIN EXTENDED输出的2.row表明该子查询实际上是依赖的:select_type是DEPENDENT SUBQUERY。

在结束这个示例之前,我想再介绍一个可以在请求语句涉及很多表的时候,帮助你避免迷茫的小技巧。要知道当你面对10个甚至更多表的连接时,即使你很了解它们应该怎么连接,你也会感到迷茫。

上面示例中一个值得注意的地方是SHOW WARNINGS的输出信息。MySQL服务器不是总按照语句输入的顺序执行它,而是调用优化器去构造一个更好的执行计划,因此用户通常都会很快得到返回结果。在EXPLAIN EXTENDED之后,SHOW WARNINGS命令展示的就是优化后的查询。

在该示例中,SHOW WARNINGS的输出包含两个主要信息。第一个是:

  

这条信息明确指出服务器是通过items表而不是items_links表解析id的值。

第二条信息包含了优化过的语句:

  

这个输出信息也指出服务器是从items表接受id的值。

现在我们来对比一下正确的查询和之前列出的错误查询的EXPLAIN EXTENDED的结果:

  

这次优化过的语句看起来完全不同了,并且确实像我们预期的那样比较items.id和items_links.iid的值。

在正确的查询中,select_type的值仍然是DEPENDENT SUBQUERY。我们已经通过items_links表来解析字段的名称了,为什么结果仍是那样?答案从SHOW WARNINGS中下面这部分输出开始:

  

子查询仍然显示是依赖的,因为外部查询子句中的id需要子查询去检查与内部查询对应的每行里的iid值。这个问题在MySQL社区bug数据库的12106号报告的讨论中提出。

SELECT查询运行异常可能有很多不同的原因,但是查找问题的一般方法总是相同的。

如果SELECT查询返回了非预期的结果集,这并不总是意味着查询语句本身有错误,也有可能是因为你以为已经进行了插入、更新或者删除等操作,而事实上它们并未生效。

在你调查这种可能之前,你应该先完全仔细检查前一节讨论的SELECT语句编写错误的问题。在SELECT语句编写正确并且能够返回你想要的值的情况下,现在我开始调查由数据本身的问题导致错误的可能性。为了确认问题是由数据本身而非SELECT语句产生的,我尝试精简语句,使其变成某个独立表的简单查询。如果是小表,那么移除所有的WHERE条件和GROUP BY语句,然后通过“野蛮”的SELECT * FROM table - name检查所有的数据。对于大表来说,用WHERE条件来筛选出你想要的值是明智的选择。如果你仅关心查询结果集的条数是否和预期的一致,也可以考虑用COUNT(*)来显示条数。

一旦你确定SELECT查询工作正常,那就意味着是数据不一致产生的问题,你就需要定位哪里出了问题。有很多可能的原因:使用了错误的备份、错误的UPDATE语句,或者从节点与主节点之间同步异常(这里先仅列出这些最常见的可能)。在这一节中,我们会看到一些关于DELETE或UPDATE操作在随后的SELECT查询中没有生效的示例。下一节会介绍一些令人困惑的情况,在这些情况中,问题会在被触发很久后才出现,当然我们也会告诉你如何反向定位这类错误。本章并不涉及事务中的问题,这类问题将在第2章中讨论。这里展示的场景都是基于数据库中的数据已经稳定的前提,也就是说,所有使用到的事务都已经完成了。我将继续使用从现实场景中简化过的示例。

我们从可能的最佳情形开始,即错误发生后立即提示数据不一致的问题。我们将使用下面的初始数据集:

  

在应用程序中,临时表包含从主日志表中查询出来的部分结果集。这是一个保存日常常用数据经常使用的技术手段,当你只需要用到主表中的一小部分数据并且用户不想改变主表中的数据或者锁定主表的时候,可以使用临时表。

所以在这个示例中,当使用完结果集后,用户想要同时删除两个表中的相应行。通常人们很难想象用一个查询语句去做多件事情。不过现实可以与你的设想不同,并且你还会得到非预期的结果或负面影响:

  

如果用户注意观察输出的DELETE语句的相应结果,就会立即发现出了问题。DELETE操作没有影响到任何行意味着它什么都没做。然而,一条语句的输出通常不是这么显而易见,有时候它并不可见,因为SQL语句是在程序或者脚本内部执行的,并且没有人会去监控执行结果。通常情况下,你应该始终检查语句执行的返回信息,从而了解有多少行数据受影响且它们的值是否与你预期的一致。在应用程序中,你必须明确检查信息功能。

继续下面的讨论。如果你立即执行SELECT查询,你可能会很惊讶,以为查询语句出现了错误或者查询缓存没有清除:

  

如果把SELECT语句改为查询行的数量,就可以确认这不是缓存或者其他相关的问题。这个小例子也告诉我们可以通过对同一张表进行不同的查询方式来确认数据的一致性:

  

这里COUNT(*)仍然返回一个正数,这表明表是非空的。细心的用户应该已经注意到,DELETE操作实际上没有删除任何行。为了找出原因,我们可以将DELETE语句改为相应的SELECT语句。这样做可以告诉我们哪些行满足了删除条件。

尽管这个简单的示例中没有WHERE语句,但是这个技巧对于包含WHERE语句的删除和更新操作同样有效。SELECT语句返回的行即为DELETE操作将要删除的行或者UPDATE操作将要更新的行:

  

与之前的结果一致,这里也返回空集合。这就是为什么没有删除任何行!然而,现在仍不清楚产生这个现象的具体原因,但是既然我们有一个SELECT查询,就可以利用第一节提到的相关技术。在这个场景中,最佳选择就是用EXPLAIN命令执行SELECT语句然后分析输出结果:

  

  

输出中最后的信息表明查询语句被修饰成了内部连接(inner JOIN),该内部连接仅当另一张表也有满足条件的行时才会同时返回两张表的行。对于t1表中的每一行,在t2表中应至少有一行的值匹配。在这个示例中,因为t2表是空的,自然连接操作返回空集合。

我们刚刚学习了另一个有助于找出UPDATE或DELETE语句错误原因的重要技巧:把语句转换成具有相同JOIN和WHERE条件的SELECT语句。针对SELECT查询,可以使用EXPLAIN EXTENDED[1]命令去获取实际的执行计划,同时也可以避免直接操作结果集带来的危险或者修改了错误的行。

这里有一个的使用UPDATE的更复杂示例。我们仍使用items表:

  

  

description和additional字段是TEXT类型的。在这个示例中,我们将使用一个错误的语句,该语句想要把表中的中NULL值替换成更有语义的文本(一个替换成“no description”,另一个替换成“no additional comments”):

  

该语句会更新一些数据(“影响到3行”),让我们检查一下现在表中数据是否合理:

  

正如我们所见,有3行记录的description字段的值被修改,不过值是0而不是我们预期的“no description”。并且,additional字段的值根本没有改变。为了定位该问题发生的原因,我们应该检查警告。注意服务器返回的这些语句,我们看到有共3个警告:

  

这条消息看起来很奇怪。为什么上述语句执行后,这里会报告关于DOUBLE的警告,而description和additional字段的类型都是TEXT的。

  

  

我们还想知道为什么additional字段完全没有变化,并且我们也没有得到任何警告。

我们把该语句拆分成小段,然后分别检查每段都做了什么:

  

这是UPDATE语句惯用的开头,没有什么问题:

  

该段使用SET语句。我们来检查一下它实际做了什么。AND关键字在这里究竟意味什么?我们在语句中加上圆括号来突出一下运算符号优先级:

  

所以,实际上这个语句计算了下列表达式:

  

然后将值赋给description字段。计算等式会产生一个布尔类型的结果,表示为LONGLONG类型的值。为了证明这点,以--column-type-info选项打开MySQL命令行客户端,然后再次运行SELECT查询:

  

  

我们可以清楚地看到表达式的结果是0,这个值随后被插入了description字段。并且因为我们对additional字段的更新已被这个奇怪的表达式所覆盖了,所以没有值插入该字段中,也就看不到服务器端给出任何关于该字段的信息。

现在可以修改上述语句中的逻辑错误了:

  

如果需要你也可以检查WHERE语句,不过在这个示例里它没有错误。

这个示例表明返回值和查询执行信息的重要性。我们来进一步讨论它们。

正如前一节看到的一样,数据库会返回一些关于每个查询的重要信息,有些信息直接展现在MySQL的访问客户端中,而有些信息则需要通过如SHOW WARNINGS等命令才能得到。当从应用程序中调用SQL语句的时候,获取这些返回信息并确认没有异常情况发生同样重要。所有语言的MySQL API都提供了获取服务器返回信息的接口。本节将讨论这些接口。这里仅涉及C的API,因为我必须选择一种语言的API,并且大部分其他语言的API都是基于C的API的[2]

受影响的行数

我们从之前见过的输出开始,每次插入、更新或者删除后都会显示有多少行数据插入、更新或者删除了:

  

这个信息代表查询正常执行并且修改了N行数据。

要在应用程序中获取相同的信息,可以调用:

  

如果有改变发生,该函数会返回一个正数,如果没有改变那么返回0,−1代表反生错误。

对于UPDATE语句,如果客户端设置了CLIENT_FOUND_ROWS,那么该函数将会返回满足WHERE条件的行数,这个数并不总是和实际更改的行数一致。

 提示

对于Connector/J来说,默认不启用受影响的行,因为这不是JDBC兼容的特性并且会使其DML语句依赖于匹配的行数而不是受影响的行数的大多数应用程序产生错误。不过对于INSERT...ON DUPLICATE KEY UPDATE类型的语句会返回正确的更新数量。连接字符串属性useAffectedRows告诉Connector/J在连接到服务器的时候是否设置CLIENT_FOUND_ROWS标志。

匹配的行数

输出中表示该数目的字符串是:

  

该输出表明有多少行满足WHERE条件。

下面的C函数:

  

以字符串格式返回关于最近的查询的补充信息。

对于更新操作来说,它返回的字符串类似:

  

其中,每个#依次对应代表匹配的行数、修改的行数和警告数目。可以通过解析该行中的“matched: #”获悉有多少行被查找出来。

被修改的行数

输出中代表该数目的字符串是:

  

该输出表明有多少行实际上修改了。需要注意的是,匹配行数M和修改的行数P是可以不同的。例如,假设你想要修改的列已经包含你指定的值,在这种情况下,该列会被认为是匹配的而不是修改的。

在应用程序中,像之前一样用mysql_info()获取信息,不过这次是解析“Changed: #”。

警告:数目和消息

输出中表示这部分信息的字符串是:

  

如果服务器在处理请求过程中探查到一些不寻常的情况或者值得报告的情况,你将会获得警告。不过查询仍然会执行并且会修改数据。无论如何请确保检查警告信息,因为它们会帮助你获悉潜在的问题。

在应用程序中,有很多不同的方式去获取警告。仍可以使用mysql_info()函数,然后解析“Warinings: #”。也可以调用:

  

如果有警告,可以执行SHOW WARNINGS命令去获取关于究竟发生了什么的文本消息。另一个选择是:

  

该函数将返回最近的SQL状态(SQLSTATE)。例如,“42000”代表语法错误,“00000”代表没有错误和警告。

 提示

SQLSTATE的值由ANSI SQL标准定义,用于表明语句的执行状态。执行状态被设置成标准中定义的状态码,表明一个请求是成功完成还是返回异常。SQLSTATE以字符串形式返回。要了解MySQL服务器可能返回哪些状态码,可以阅读MySQL参数手册中的“服务器错误码和错误消息”一节。

错误

检查错误也总是很有用。下面的函数返回最近SQL语句的错误值。

  

该函数返回最近一次错误的MySQL错误代码。例如,语法错误会生成数字1064,0意味着没有错误。

  

该函数返回最近一次错误的文本信息。对于语法错误,它会返回类似以下的内容。

  

这有利于保存存储于不同日志文件中的信息,使你可以在任何时候检查它们。

 提示

MySQL官方文档包含MySQL服务器可能返回的错误列表以及客户端错误列表。

 

通过perror获取错误字符串

perror工具是MySQL发行包中携带的一款用于解决问题的非常有用的工具。perror能够提供与给定错误代码相关联的MySQL及其所在操作系统的错误信息。可以从MySQL命令行客户端、信息函数或其他错误日志文件中的错误消息中后面括号的部分获取错误代码。下面是一些示例:

当命令产生错误的时候会在MySQL客户端输出这些错误代码,这些错误代码可以通过mysql_error()函数由程序获取。然而,当你面对一个错误码不知所措的时候,可以使用perror工具。

如果你严格地检查查询和更新的结果,就会发现很多问题,这些问题可能持续数周而未被察觉,然后慢慢变得越来越严重,直到最后无可避免地引发很多让人苦恼的问题。然而,问题确实在慢慢地接近你。有时,SELECT查询突然开始返回错误的结果,但是你对该查询的经验使你确信它没有什么问题。

在这种情况下,你应该反向模拟用户操作,直到发现错误的根源。如果幸运,你会一步就发现问题的原因。不过通常你会进行多步操作,有时甚至消耗很长的时间。

大部分这种问题是由于复制环境中主从节点的数据不一致造成的。一个常见的错误情形是期望唯一值的时候出现了重复值(例如,如果用户使用INSERT ON DUPLICATE KEY UPDATE语句,但是主从服务器中的表结构是不同的)。在这样的环境设置下,用户往往直到从节点执行SELECT语句的时候才会发现问题,而不会在INSERT发生时就注意到问题。在循环复制时情况会更糟糕。

为了说明这个问题,我们将使用一个存储过程从保存其他查询结果的临时表向另一个表插入数据。这是另一个常用技巧,用于当用户想要处理大表中的数据,同时担心意外修改数据的风险,或者担心在使用这些大表时对其他应用造成堵塞的情形。

我们来创建表并填充临时数据。在实际应用中,临时表会用于保存等待存入主表的计算结果集:

  

现在向临时表中插入数据:

  

存储例程将临时表中的数据移入主表。它在迁移前会先确认数据在临时表中。我们的版本如下:

  

在调用该存储例程时,如果指定的临时表不存在则会创建新的临时表。这样做可以避免由于临时表不存在而产生问题,但同时也会带来新问题。

 提示

该示例使用MAX函数检查表中是否至少存在一行记录。推荐用MAX计数,因为InnoDB表不会保存记录的行数,而是在每次调用COUNT函数的时候现进行计算。因此,MAX(indexed_field)函数比COUNT快。

如果从服务器在第一个插入之后,存储过程调用之前重启,那么从服务器中的临时表将会是空的并且从服务器上的主表没有任何数据。在这种情况下,我们访问主节点会得到:

  

  

与此同时,在从服务器上得到:

  

更糟的是,如果我们在存储过程调用后向t1表中插入数据,从服务器中的数据将会完全混乱。

假设我们注意到应用程序中主从表读取数据时的错误。现在我们应该弄清数据是怎么插入从表的:是直接更新从服务器还是从主节点复制的数据?

 警告

MySQL复制不会帮你检查数据一致性,因此对同一个对象,同时使用SQL复制线程和从节点上的用户线程更新会使数据与主服务器不同,这会导致随后的复制事件失败。

因为我们在示例中模拟这种情形,所以我们知道发生数据损坏问题的关键点:从服务器在第一次插入之后,存储过程调用之前重启了。在实际场景中,问题一般会在用户执行下面查询的时候被发现:

  

当你从SELECT查询中获得非预期结果时,你需要找出该问题发生的原因,是由于查询本身的问题,还是由于早些时候的一些错误引起的。刚才展示的插入非常简单,除非表损坏了,否则它不可能产生错误,因此我们必须回头检查一下表是如何修改的。

通常的示例是在建立在从服务器只读的复制环境下,因此我们可以确保错误产生有两种可能的原因:要么是主服务器插入了错误的数据,要么是数据在复制时损坏。

所以,首先检查主服务器的数据是否有错误:

  

主服务器数据正常,因此问题的原因在于复制层。然而,这是怎么发生的?复制看起来运行正常[3],因此我们猜想是主节点有逻辑错误。当发现了这个可能的原因的时候,你应该去分析存储过程并在主节点上调用以寻找修复方案。

如前所述,在向临时表中插入数据完成复制并清空临时表的事件之后,且在调用查询并向主表插入数据的存储过程之前,重启服务器。因此,从服务器仅是重新创建一个空的临时表并且没有插入任何数据。

在这种情况下,可以选择转换成基于行的复制或者重写存储过程,使其不依赖于已经存在的临时表。另一种方法是清空然后重新填充表,这样突然重启不会导致从服务器数据丢失。

有人可能觉得这个示例太过人为了,你不可能预知服务器何时会突然重启。没错,但是重启确实每时每刻都有可能发生。因此,你需要考虑这样的错误。

事实上,从服务器一个接一个地复制二进制日志事件,当数据在一个原子事件(例如,一个事务或者存储过程调用)中产生时,从服务器不会受上述情况的影响。不过回过头来说,这个示例仅仅是为了说明现实生活中发生的事件背后的原理。

关于复制错误的更多详细信息会在第5章进行介绍。

单服务器示例

我曾经处理过一个存储由不同的切割系统产生的度量数据的Web应用程序。用户可以添加一个系统,然后编辑保存度量数据的规则。

我第一次遇到错误的时候,我测试了一个含有系统列表的Web页面:

该列表不该包含重复的系统,因为描述同样的规则两次是没有意义的。因此我非常惊奇地看到有很多同名的条目。

输出数据的代码使用的是对象,并且,我无法仅通过阅读代码查看发送到MySQL服务器的语句是什么样的:

我通过日志获取了真实的查询,它看来是正确的:

接下来,我检查了表的内容:

SELCET语句准确地返回了表中存在的数据集。我转而检查更新表的代码:

我再次通过日志获取真实的请求语句:

该语句也是正确的!id似乎是自增的字段,因此会自动设置。

不过同时,该语句也暴露了潜在的问题:它一定在没有检查唯一性的情况下重复执行了。带着这种假设,我决定检查一下表的定义:

问题的源头很显然了:name字段没有定义成UNIQUE(唯一的)。当创建表的时候,我通常使用id作为唯一标识符,但是我也会使用MySQL的特性在INSERT时去给id生成一个自增的值,没有什么使我避免重复使用同一个name。

为了解决这个问题,我手动删除了多余的行并且增加了唯一(UNIQUE)索引。

我们已经介绍完了与错误结果相关的问题,接下来将介绍其他一些经常发生的问题。

SQL应用程序的一个常见问题就是性能退化。这一节将会介绍当你面对性能问题时的一些基本操作。不必担心细节,仅仅关注本质的思想就好。随着知识的深入,你会发现你自己变得更加训练有素,能够更加高效地使用它们。

当我考虑本章应涵盖哪些问题时,我曾犹豫是否应该涉及性能相关的问题。市面上有很多详细介绍性能问题的资料,如MySQL参考手册中的“优化”章节以及O'Reilly出版的相关书籍。我会在本书的最后简单介绍一些有用的资料。你很容易就会把整个职业生涯都消耗在该问题上面,或者淹没在可用信息之中。

这里将主要介绍SELECT查询。本节最后会简单介绍一下如何处理修改数据的慢查询问题。

处理慢查询有3个主要技巧:调优查询本身、调优表(包括增加索引)和调优服务器。下面逐一详细介绍。

最强大的查询调优工具就是我们之前熟知的EXPLAIN。这个工具为用户提供了服务器实际上如何执行查询的详细信息。MySQL 参考手册已经详细介绍了MySQL的EXPLAIN工具,因此这里不再赘述。不过,我将会重点介绍输出信息中我认为是最重要、最有用的部分。

第一行中你要注意的是type,它展示了连接(join)的执行方式;还要注意rows,它展示了在查询执行过程中检查的行数的估计(例如,如果查询必须扫描整个表,那么rows展示的数值和该表中的行数相等)。多表连接需要检查的行数是每个表中检查行数的笛卡儿积。也就是说,如果请求在第一个表中检查20行,另一个表中检查30行,那么连接一共执行了600次检查。EXPLAIN会包含JOIN中每个表中的行。我们将通过下面的示例进行说明。

即使在操作单表时,EXPLAIN也会报告连接。这可能听起来有些奇怪,因为MySQL的内部优化器把每个请求都当成一个连接,哪怕是单表上的连接。

我们来回顾一下前面介绍过的EXPLAIN输出:

  

检查的行数是10乘以1,因为子查询对于外部查询的每一行执行一次。第一个查询的类型是index,这意味着全部索引都将被读取。第二个查询的类型是index_subquery。这是一个索引查找函数,工作方式类似于ref类型。因此,在这个示例中,优化器将从itmes表中读取全部索引记录,并且对于从itmes表中查询到10行记录中的每一行,对应从items_links表中读取一行记录。

该如何确认这是一个对该查询合理的执行方式呢?首先,重复查询结果并检查查询的实际执行时间:

  

MySQL服务器检查了10行然后返回结果是4。这有多快?为了回答这个问题,统计每个表中的行数:

  

itmes表中有10行记录,每行都有一个唯一的ID。items_links表中有6行记录,没有唯一的ID(iid)。对于当前的数据规模来说,这个设计看起来可以,不过与此同时,这也透露出一个潜在的问题。目前,links数小于items数,并且数目的差异不是很大,但是如果数目差距巨大,那就更加值得注意了。

为了验证这个猜想,同时也为了举例说明查询调优的方法,我将向items表中插入一些数据。id字段定义为INT NOT NULL AUTO_INCREMENT PRIMARY KEY,这样可以确保对新插入的行不会存在关联(link)。这样,我就可以模仿现实中用户想从一个大表中查询少量link的场景(在该场景中是6个)。下面的语句展现了一个快速构造数据的小技巧,即重复地从表中选择所有行,然后再插入更多行:

  

现在,看一下查询的执行计划是否有所变化:

  

  

查询的执行计划并没有变化──这次为了6个link检查了5136行!有没有什么方式可以重写一下这个查询,使其运行得更快一些呢?

子查询的类型是index_subquery。这意味着优化器使用索引查询函数完全替代了子查询。SHOW WARNINGS的输出展示了查询是如何重写的:

  

输出信息看起来令人生畏,不过至少在这里可以看到一些连接。如果我们重写查询,让在其上执行连接的列更加明显,那又会怎么样?我们也将重写子查询,使之变成显式JOIN;在当前版本的MySQL中,这个方法能够显著地提高性能:

  

  

结果看起来挺让人振奋的,因为它没有搜索items表中的所有行。不过,这个查询结果正确吗?

  

我们得到了6行,而不是4行。这是因为我们要求返回所有匹配的行,这里有相同的link被匹配了两次。可以通过添加DISTINCT关键字来修复这个问题:

  

 提示

可以通过查询重写技巧来确认是否需要添加 DISTINCT 关键字。只要将count(*)替换成items.id,就可以看到重复的值。

加上DISTINCT后,该查询一样高效吗?我们再次通过EXPLAIN来验证一下:

  

它仍然检查了6行记录。因此,我们可以认为对于这个特定的数据集,该查询得到了优化。本章后面将会解释为何数据结构及其容量会有影响。

在该示例中,数据集是小规模的,所以即使在我的笔记本电脑上,我也无法让其真正执行得特别缓慢。不过,原始的和优化过的查询的执行时间的确有很大不同。下面是原始查询的时间:

  

下面是优化过的查询的时间:

  

  

对于如此小的数据集,查询的时间仍然降低了一半!在测试中,虽然仅提高了0.11秒,不过如果对于上百万行,那么效率提升效果就会更好。

上一节介绍了调优查询的过程。在全部示例中,EXPLAIN的输出都包括索引信息。那么,如果表没有索引会如何?或者如果没有使用到索引呢?你该如何选择在什么时候、什么地方、添加哪种索引呢?

当结果有限的时候,MySQL服务器会使用索引。因此,在与WHERE、JOIN和GROUP BY语句相关的列上添加索引可以加速查询。在与ORDER BY语句相关的列上添加索引也会有效果,因为它将使服务器更高效地排序。

在掌握这些规则的前提下,添加索引就成为了一个很简单的工作。考察之前示例中的表,但没有任何索引:

  

  

如你所见,没有指定任何索引。我们在这些表上试验一个没有优化过的查询,然后再优化它:

  

类型变成了 ALL,这是最耗时的类型,因为这表示会读取所有行。该查询这次检查了6 * 5137 = 30 822行。这甚至比之前示例中我们认为的慢查询还要糟。

来仔细检查一下这个查询:

  

这个查询返回结果集中唯一非空值的数目。应该在items.id列上添加索引,以使该查询使用索引。

该查询的另一部分是:

  

这里有items表中id字段和items_links表中iid字段的连接。因此,应该在这两列上添加索引。

  

现在看一下添加索引对查询计划的影响:

  

这看起来比之前好了很多,只有一点不好: items表这次的类型是ref,比之前的eq_ref要差。这是因为我们添加的是一个简单索引,而原始表在该列已经有唯一索引了。我们也可以简单地修改该临时表,因为ID是唯一的并且也应该如此:

  

  

现在,当已经使用了执行更快的eq_ref类型的时候,可以删除items.id字段上多余的索引。这在你关心数据修改的查询速度的时候尤为重要,因为每次更新索引都会消耗时间。下一节会讨论何时该停止优化。

你刚刚学习了索引是如何影响查询执行的以及何时应该添加索引。

选择你自己的执行计划

索引实际上也有减慢查询的时候。在这种情况下,应该删除索引或者使用会忽略索引(IGNORE INDEX)的语句(如果其他的查询还需要用到该索引)。也可以使用强制索引(FORCE INDEX)使优化器知道你想要使用的索引。这些语句对于查询调优也非常有用,比如当你想要了解特定索引对性能会有怎样影响的时候。只需要通过EXPLAIN命令执行语句,然后分析输出。

尽管使用IGNORE INDEX和FORCE INDEX 可能听起来不错,但是你应该避免在生产环境中使用,除非你已经准备好在今后的每个升级版本中都逐一检查使用了该语句查询。

因为优化器总是试图选择最佳的执行计划,随着版本的升级,可能对于同一个JOIN会使用不同的执行计划,所以这种检查是必要的。当你没有强制使用或者忽略索引的时候,优化器会按照它认为最佳的计划执行。但是,如果你明确指定优化器在多表JOIN的某个表中应该如何使用索引,那么这个规则可能会造成其他影响,并且这个最终的执行计划在新版本中可能会比之前要差。

在对单一表的查询中使用IGNORE INDEX和FORCE INDEX相对安全。对所有其他的情况,必须在更新后检查确保查询的执行计划没有改变。

在产品中使用IGNORE INDEX和FORCE INDEX的另一个问题是对于指定表的最佳执行计划依赖于其存储的数据。一般的步骤是,优化器检查表的统计数据然后依此调整计划,当然在你使用了IGNORE INDEX和FORCE INDEX的时候不会这样做。如果你使用这些语句,你就必须定期检查在应用程序的生命周期它们是否还有效。

前面讨论了简单查询。即使是简单查询,我们仍找到了优化的方向,有时经过一步一步的调优我们获得了更好的结果。当你处理包含很多JOIN条件,或者WHERE子句和GROUP BY字句中包含很多字段的复杂查询时,你就会拥有更多的选择。可以认为你总会找到使性能更好的方法,并且这种优化永无止境。因此,现在问题是,什么时候可以认为查询优化合理并可以停止进一步优化。

深入了解性能优化的技术自然可以帮助你选择合适的解决方案。不过,哪怕你自认不是专家,我们也有一些基本原则可以帮你决定停止优化。

首先,你应该了解查询都做了什么。例如,下面的查询:

  

始终会返回表中的所有列,该语句没有什么可优化的空间。

不过,即使你查询所有列,添加JOIN语句也会使情况改变:

  

这会产生优化效果,因为ON条件限制了结果集。当然,同样的分析也适用于WHERE和GROUP BY条件。

其次,你应该通过EXPLAIN输出查看连接类型。尽管你想要获得可能的最佳的JOIN类型,但是你应时刻意识到你的数据的约束。例如,非唯一的行永远不会产生eq_ref或者更好的类型。

当你优化查询的时候,你的数据是非常重要的。对于同样的执行计划,不同的数据可能会产生完全不同的结果。最简单的示例就是比较表中只有一行记录和表中超过50%的行都有相同值的结果。在这样情况下,使用索引会降低性能而不是提升性能。

你需要牢记的另一件事情是索引在修改表时的影响。尽管索引通常会提高SELECT查询的性能,但是它会略微降低修改数据的查询的性能,尤其是INSERT语句。因此,有些时候为了加快插入的速度,允许SELECT查询略慢是明智的。要时刻牢记考察你整个应用程序的性能,而不仅仅是某一个查询的性能。

假如你已经对查询进行了完全的优化,找不到进一步优化的方法,但是它仍然很慢,那么还有没有办法可以提高它的性能呢?有的。有很多服务器选项可以让你调节对查询有影响的因素,比如内存中临时表的大小、排序缓冲区等。有些针对特定存储引擎(如InnoDB)的选项,也会对查询优化很有用。

第3章将详细介绍这些选项。这里仅对如何使用它们进行性能优化做一个概述。

调整服务器的配置从某种程度来说是一个影响全局的行为,因为每个修改都可能对该服务器上的每个查询造成影响(对于指定引擎的选项,会影响每个使用该引擎的表上的查询)。不过有些选项是针对特定类型的优化的,如果你的请求没有满足条件,它将没有任何作用。

第一个需要检查的选项是缓冲区大小(buffer size)。每个缓冲区都有其存在的特定原因。一般的规律是大缓冲区意味着高性能──不过仅当请求可以针对该缓冲区扮演的特定角色使用大容量缓存的时候。

当然,增加缓冲区大小是有代价的。下面是一些大缓冲区可能带来的影响。我不是想要阻止你使用大缓冲区,因为在合理的环境下它是提高性能非常有效的手段。你仅需要牢记下面的要素然后合理地调整大小。

交换区(Swapping)

大容量缓冲区可能会导致使用到操作系统级别的交换区从而造成性能缓慢,这取决于系统内存大小。通常情况下,MySQL服务器在它所需的所有内存都来自物理内存的时候运行最快。当它使用到交换区的时候,性能显著下降。

当为缓冲区分配的内存大小超过服务器的物理内存大小的时候就会使用到交换区。请注意,有一些缓冲区是针对每个用户线程的。要确定这些缓冲区究竟需要多少内存,可以用公式最大连接数* 缓冲区大小(max_connections * buffer_size)来计算。计算出所有缓冲区的内存和,并确保和小于mysqld服务器可以使用的内存大小。这个计算的值不是决定性的,因为mysqld实际上可以分配多于你明确指定大小的内存。

启动时间

mysqld需要分配的内存越多,其启动时间就越长。

过期数据

我们还会有伸缩性问题,大部分时候是来自线程间的缓存共享。在这些场景中,扩充缓冲区做缓存会产生内存碎片。你通常会在服务器运行数小时后发现内存碎片问题,该问题发生在旧的数据需要从缓冲区中移除以给新数据腾出空间的时候。这会导致高速运转的服务器突然变慢。第3章会给出这样的示例。

讨论完缓冲区,第3章讨论其他选项。届时,我们不仅关注性能优化选项(如优化器选项),还会关注一些控制高可用性的选项。事务运行得越安全,就需要更多的检查和更慢地执行性能。不过,要注意这些选项;只有在你可以为了性能牺牲安全的时候才调优它们。

当你调优分配的时候,把性能作为整体来考虑尤为重要,因为每个选项都会影响整个服务器。例如,如果你没有使用特定引擎,针对该存储引擎的选项调优不会有任何作用。这是显而易见的,不过我确实见过很多安装环境下有大量关于MyISAM引擎的选项,然而却使用的InnoDB存储引擎,或者相反的情况。如果你用一些通用配置作为模板,这些注意点就尤为重要。

我们讨论了影响SELECT性能的因素,在本节我们开始优化修改数据的查询。UPDATE和DELETE查询可以使用与SELECT语句一样的条件去限制受影响的行数。因此,可以使用相同的优化规则。

我们在1.3节中学习了如何把UPDATE和DELETE查询转换成SELECT查询,然后使用EXPLAIN进行调试。可以在5.6.3以下版本的系统上使用该技巧解决性能问题,从5.6.3版本开始增加了EXPLAIN对INSERT、UPDATE和DELETE查询的支持,不过,请牢记UPDATE和DELETE查询有时候与相应的SELECT查询的执行方式略有不同。

通过在查询计划前后查询Handler_%的状态可以检查是否使用了索引:

  

1 这里使用了SHOW STATUS命令,这是SHOW SEESION STATUS的同义命令,作用是查看当前会话的变量状态。

 提示

在测试前使用FLUSH STATUS查询可以方便地重置这些变量。

我们将继续介绍之前列表中的特定变量。你需要注意的是,因为这些是累积的值,所以它们会随着你的每次查询增长。现在我们开始优化1.3节中的查询示例,使其更新可以为空的列:

  

这条语句没有修改任何行,因为数据在之前损坏了:现在每个字段中的值是0而不是NULL。但是这个请求执行非常缓慢。我们来看一下处理程序变量:

  

  

可以看到Handler_read_rnd_next的值非常高,该值代表从datafile中读取下一个值的频繁程度。过高的值一般代表使用了全表扫描,这对性能是有影响的。Handler_read_key也是一个相关的变量,表示读取索引的请求数目。正常情况下该值相对于Handler_read_rnd_next来说不应该这么低,因为这意味着大部分行的读取都没有使用的索引。此外,Handler_commit和Handler_read_first的值也增长缓慢。它们分别代表事务提交的次数和读索引中第一项的次数。最后,Handler_read_first的值是1,表明我们请求服务器读取索引中第一条记录,这可以当作全索引扫描的标志。

希望对这些Handler_%状态变量的简介可以告诉你如何利用它们去检查查询是怎样执行的。对于该查询是否有提升空间这个问题将作为作业留给读者自己去解答。

我仅将对INSERT查询做些说明。它们没有条件去约束受影响的行数,因此表中的索引只会降低插入效率,因为每次插入都需要更新索引。插入的性能需要通过服务器选项调优。特别地,影响InnoDB存储引擎的选项会很有作用。

一种加速插入的方式是把多个插入合并成一条语句,这也叫做“批量插入”(bulk insert):

  

不过,请注意插入会阻塞行甚至是整张表,因此其他查询会在插入的过程中被拒绝访问。我将给出一个通用规则来结束本节内容:

我们刚刚学习了如何优化服务器选项才能显著提升性能。在本章我们也学习了如何优化特定查询以提升其运行速度。优化查询和优化服务器一般是解决性能问题的两种选择。那么,有没有通用的规则告诉我们该从哪个方向开始优化呢?

我认为恐怕没有。优化服务器选项看起来特别有效以至于许多人认为合理地的改变选项将会使mysqld运行得如火箭般高效。如果你也是那么想的,我不得不让你失望了:不好的查询写法仍会耗尽服务器资源。并且你可能在重启服务器后仅仅感受到几个小时的服务器高性能,然后它就又变慢了,因为每个查询都需要消耗很多的资源并且你的缓存将会充满。有时候,服务器会被数以百万计的查询淹没,需要更多的资源。

然后,优化每个查询也不是一个好选择。有些请求很少调用,所以没有必要在这些查询上浪费人力。还有的查询可能查询表中的所有行,这些查询就没有必要去尝试优化了。

我一般推荐“混合”的优化模式。先优化服务器选项,特别注意你使用的存储引擎相关的选项,然后优化查询。当优化完重要的查询后,回头再检查服务器选项,考虑是否有进一步的优化空间,然后再继续优化剩下的查询,反复如此,直到你对性能满意。

你也可以从自己的应用程序中最慢的查询开始或者找到那些可以通过简单的优化获得显著提升的查询,然后优化服务器选项。参考之前展示的状态变量,第6章将详细介绍它们。

最后同样重要的是:在性能优化中参考大量的信息以形成你自己的策略。

有时候,MySQL客户端会收到严重的错误消息“在请求中丢失与服务器的连接”或者“服务器已停止”。尽管我希望你永远不会遇到这个错误,但是有所准备总是有好处的。由MySQL安装本身引起的这个问题主要有两个原因:服务器问题(最有可能是崩溃)或者滥用连接选项(通常是超时选项或者max_allowed_packet)。

第3章将讨论连接相关的配置。第4章会讨论硬件问题和第三方软件相关的问题。这里简短地介绍一下如果遇到服务器崩溃该做些什么。

首先,确定你的服务器是否真的崩溃。你可以借助进程状态监控器进行确认。如果你在服务器崩溃后运行了mysqld_safe或者其他守护进程重启服务器,错误日志将会包含表明服务器已经重启的消息。当mysqld启动时,它始终会在错误日志文件中输出类似如下的信息:

  

因此,如果你找到了类似消息,那么服务器已经重启了。如果没有任何消息,并且服务器已经启动并在运行,那么丢失连接的问题最有可能是因为滥用连接选项导致的,这将在第3章进行讨论。

 提示

如果你记得MySQL服务器原来是何时启动的,你可以使用状态变量uptime,该变量的值代表服务器已启动的时间,单位是秒:

  

该信息也可以帮你检查mysqld是否是因为操作系统的重启而失败的。仅需要比较该变量的值和操作系统的启动时间即可。

依赖于错误日志文件是因为我工作经历的原因,比如当客户在服务器崩溃数小时后发现问题,甚至有时候是在mysqld计划重启后才发现问题。

如果你确认服务器已经重启了,你应该再次检查错误日志去搜索崩溃本身的信息。通常,你会从错误日志中获取足够的崩溃信息,从而避免发生同样的情况。第6章将讨论如何调查你可能会遇到的少数困难情况。现在,我们再次回到错误日志文件,来看一下在服务器崩溃情况下的典型内容示例。这里我将摘录出大量信息:

  

  

  

表明崩溃原因的关键行是:

  

这意味着MySQL服务器在向操作系统申请资源(例如,访问文件或者内存)后终止了,得到了错误代码11。在大多数操作系统里,这个信号代表分段错误(segmentation fault)。你可以在你的操作系统的用户手册中获取更详细的信息。对于UNIX和Linux系统可以执行man命令。在Windows操作系统里,相似的情况通常会产生类似“mysqld got exception 0xc0000005”的日志消息。查找Windows的用户手册可以获取该异常代码的含义。

下面是从某个线程中导致服务器崩溃的请求的相关日志中提取的摘要信息:

  

为了进一步分析,重新执行查询来检查其是否是崩溃的原因:

  

 提示

当我推荐你重现错误的时候,我假定你是在开发服务器而不是生产服务器中进行的。6.3节将讨论如何安全地在特定环境下进行问题调优。请不要尝试重试示例中的语句,这是一个已知的bug#47780,已经在当前版本修复了。从5.0.88、5.1.41、5.5.0和6.0.14版本开始,该bug已经修复。

当目前为止,你已经找到并确认崩溃的原因,然而你还需要重写请求,从而避免下次服务器再次崩溃。现在,我们可以从日志的堆栈信息中获得帮助:

  

  

与错误相关的行是调用Item_subselect和Item_singlerow_subselect的部分:

  

我是如何确定这是罪魁祸首的呢?在这个示例中,我发现在之前排错过程中的调用。不过经验告诉我,最好还是从头开始排查问题。最前面的几个函数通常是操作系统调用的,这些函数可能会和问题相关,不过在当前环境下没有任何帮助,因为对它们你什么都做不了,接下来是对MySQL库的调用。自上而下地检查这些函数,以便找到哪些是与你相关的。例如,对于String4copy或Item_cache_str5store函数你没什么可做的,但是你可以重写子查询,因此我们从这里开始。

这里,哪怕不去看mysqld的源码,我们也可以找到崩溃的原因。推测子查询可能是问题的所在,这是一个不错的假设,因为子查询可以很容易地转换成JOIN。尝试重写查询然后测试它:

  

新查询并没有崩溃,因此你需要做的就是把应用程序中的查询改成等价的形式。

这里我想补充一点关于bug的内容。当你遇到崩溃并确定了原因的时候,应检查一下MySQL的bug数据库,看看有无类似的问题。如果你找到可能与你相关的bug,确认它是否修复了。如果已经修复了,那么把你的服务器升级到bug已修复的版本(或者更新的版本)。这样做可以节约你的时间,因为你不再需要修改有问题的语句了。

如果你没找到与你相关的bug,尝试下载最新的MySQL版本,然后再执行查询。如果bug再次出现,请把它提交给我们。使用最新的稳定版本是非常重要的,因为它包含当前修复的所有bug,许多老的问题在这里不会重现。第6章中会讨论如何在沙盒环境下安全地测试崩溃的情形。

不仅是特定的查询会引起崩溃,而且服务器运行的环境也有可能引起崩溃。最常见的原因就是缺少可用的内存(RAM)。特别当用户分配超大缓冲区时最容易发生。正如我之前提到的那样,mysqld始终需要比所有缓冲区容量总和略多的内存。通常情况下,错误日志文件包含可用内存的粗略估计。它看起来如下所示:

  

这种估计不是精确的,不过仍然值得确认。上面展示的信息表明mysqld可以使用高达20GB的内存。虽然现在你可以很容易地获得强大的计算机,但是仍有必要确认你是否真的拥有20GB内存。

该环境中的另一个问题是,有其他的应用程序与MySQL服务器一起运行。在生产环境中,最好为MySQL指定一台专门的服务器,因为其他应用程序可能会占用你希望MySQL使用的资源。第4章将讨论如何调试其他应用程序对mysqld的影响。

实际上,本书讨论的任何问题在你使用的存储引擎下都可能会有细微的差别。这种情况将在本书中一直存在。本节展示一些不依赖于其他问题的存储引擎自身的特性。我们将使用针对MyISAM或InnoDB存储引擎的工具来解决一些问题,因为这两种引擎是最受欢迎和最频繁使用的存储引擎。如果你使用第三方的存储引擎,那么可以查阅它的用户手册来获取有用的工具。

与存储引擎相关的错误要么会反馈到客户端,要么会记录在错误日志文件中。一般情况下,存储引擎的名字也会出现的错误消息中。偶尔,你可能会获得一个用perror工具也查不到的未知错误码。这一般是表明问题来自某存储引擎的信号。

一个常见的存储引擎问题是数据损坏。这不一定总是存储引擎的错误,也可能是由于磁盘损毁、系统崩溃或者MySQL服务器崩溃等外部原因。例如,如果有人使用kill –9终止服务器的进程,那么就很有可能招致数据损坏。这里将讨论如果MyISAM和InnoDB引擎发生数据损坏该怎么做。不会讨论如何修复第三方存储引擎的崩溃;读者可以查询该存储引擎的文档以获得相关指导。作为针对一般情况的建议,可以尝试使用CHECK TABLE命令,很多存储引擎都支持该工具(MyISQM引擎的CHECK TABLE工具将在1.8.1节中详细介绍)。

数据损坏是一个很难诊断的问题,因为往往直到MyISQM服务器访问损坏的表时用户才会发现问题。并且,错误表现出来的特征也有可能产生误导。在最好的情况下,你会得到一条错误消息。然而,问题也可能表现为查询执行错误或者服务器停止。如果问题在某个特定表上突然出现,就始终要检查数据是否损坏。

 提示

一旦你怀疑是数据损坏了,就需要修复损坏的表。要养成始终在修复之前备份表文件的习惯,这样你就可以在出错的时候恢复数据了。

MyISAM引擎按照三个文件一组保存每张表:table_name.frm文件包含表的结构(schema),table_name.MYD文件存储数据,以及table_name.MYI文件保存索引。崩溃会损坏数据或者索引文件,或者二者都损坏了。在这种情况下,当访问表时,你就会获得类似“ERROR 126 (HY000): Incorrect key file for table './test/t1.MYI'; try to repair it”或“Table './test/t2' is marked as crashed and last(automatic?) repair failed”的错误消息。错误消息可能各种各样,不过可以检索关键字“repair”或“crashed”来判断是否是表损坏。

SQL语句CHECK TABLE和REPAIR TABLE专门针对数据损坏问题。在操作系统shell里,也可以使用myisamchk工具进行同样的工作。使用myisamchk工具的一个好处就是,可以不必访问正在运行的MySQL服务器。例如,用户可以在崩溃后,在服务器再次启动前尝试修复表。

1.通过SQL修复MyISAM表

CHECK TABLE命令不加参数可以展示当前表的状态:

  

这是一个损坏的表的输出示例。解决问题的第一步应该是执行不带参数的 REPAIR TABLE命令:

  

现在很幸运,表修复成功了。可以再次执行CHECK TABLE命令进行确认:

  

如果单纯地执行REPAIR TABLE没有起到什么效果,那么可以选择另外两个选项。REPAIR TABLE EXTENDED执行速度比REPAIR TABLE慢得多,但是可以修复99%的错误。作为最后的选择,可以执行REPAIR TABLE USE_FRM命令,该命令会不相信索引文件中的信息。它会删除索引并利用table_name.frm文件中的描述重建索引,并通过table_name.MYD文件填充键对应的值。

 提示

为了达到同样的目的,还可以使用mysqlcheck工具。该工具通过向服务器发送CHECK和REPAIR命令进行工作。它还有非常好用的选项,如 --all-databases,该参数可以帮助用户高效地执行表的维护。

mysqlcheck像其他客户端一样连接到MySQL服务器工作,因此它可以远程使用。

2.使用myisamchk修复MyISAM表

所有这些步骤也可以通过使用myisamchk完成,该命令包含许多额外的表维护选项。这里不会一一介绍该工具的所有特性,而只是重点介绍与表修复相关的特性。

myisamchk可以直接访问表文件,而无须启动MySQL服务器。在某些情况下,这是非常有用的。同时,myisamchk需要对表文件保持独立的、排他的访问,因此用户也应该避免在MySQL服务器运行过程中使用该工具。

 警告

如果必须在服务器运行期间使用myisamchk工具,那么先执行FLUSH TABLES和LOCK TABLE table_name WRITE语句,然后等待直到最后的查询返回命令提示符,接着再在并行会话中执行myisamchk。如果有除了myisamchk以外的进程在myisamchk运行期间访问表,就可能会产生更糟糕的损坏情况。

一条基本的恢复命令是:

  

其中,--backup选项通知myisamchk在尝试修复表之前先进行数据文件备份,--recover选项执行实际修复。如果这条命令还不够,可以使用--safe-recover选项。该选项会使用在早期的MySQL版本中就存在的恢复模式进行修复,并且会找到简单的--recover选项无法发现的问题。当然,还有更加严格的选项--extend-check。

也可以使用--sort-recover选项,该选项会使用排序来解析键,甚至在临时文件很大的时候。

在其他选项中,推荐你仔细研究非常有用的--description选项,该选项会输出表的描述信息。结合-v或者其等价的--verbose选项,将会输出额外的信息。可以指定两次甚至三次-v选项去获得更多的信息。

InnoDB在共享的表空间中存储其数据和索引。如果服务器在创建表时是以选项--innodb_file_per_table选项启动的,那么它也会有自己的数据文件,不过表的定义仍然在共享的表空间中。理解表文件是如何存储的,将有助于高效地维护数据目录和备份。

InnoDB是带有事务的存储引擎,并且其内部机制会自动修复大部分数据损坏错误。它会在服务器启动时进行修复。下面的摘要信息是在MySQL企业级备份(MEB)中执行mysqlbackup --copy-back命令完成备份后从错误日志中摘录的,它展示了一种典型的恢复情况[4]

  

不过,有时候数据损坏得很严重并且InnoDB无法在没有用户交互的情况下完成修复。在这种情况下,有--innodb_force_recovery启动选项。该选项可以设置为0~6的任意值(0意味着不强制修复,1是最低级别,6是最高级别)。当修复成功的时候,可以在已修复的表上执行特定类型的请求,不过应该避免执行某些特定的命令。不能执行修改数据的操作,不过该选项仍允许特定的SELECT选择语句和DROP语句。例如,在级别6的情况下,仅可以执行形如SELECT * FROM table_name且不带WHERE条件、ORDER BY排序或者其他语句的查询。

如果发生了损坏,可从1开始依次尝试每个级别的--innodb_force_recovery选项,直到可以启动服务器并且可以访问有问题的表为止。你之前的检查应该已经发现了哪个表损坏了。使用SELECT INTO OUTFILE将表转储到文件中,然后使用DROP和CREATE命令重新创建表。最后,用配置--innodb_force_recovery=0重新启动服务器,然后加载转储的数据。如果问题还存在,尝试找到其他损坏的表然后执行同样的过程直到服务器恢复正常。

当需要在--innodb_force_recovery选项的值是正数的情况下开始修复数据库时,错误日志通常会有类似下面的明确提示消息:

  
  

你也会从中发现关于自动修复失败和启动失败的信息。

 提示

InnoDB在写实际数据前会立即对数据、索引和日志页写校验和(checksum),并且在从磁盘读数据之后立即确认校验和。这可以避免大多数问题。通常,一旦遭遇InnoDB数据损坏,这就意味着磁盘或内存有问题。

MySQL有复杂的权限方案,这使得你可以精确地设置哪些用户和主机可以或不可以执行这个或那个操作。从5.5版本开始,MySQL也有了可插拔式的身份验证模式。

尽管它有很多优势,但是这个方案很复杂。例如,让user1@hostA、user2@hostA和user1@hostB不同会很容易混淆它们的权限。当用户名相同而主机名变化的时候更是如此。

MySQL允许在对象和连接层面设置访问规则。可以限制某个用户对于特定的表、列等的访问权限。

用户通常会遇到两类权限问题:

在解决这些问题之前,应该确认你是否可以连接到服务器。

当你作为解决问题的用户成功连接到服务器之后(后面的章节将讨论无法连接的情况),执行以下查询:

  

USER()函数会返回当用户连接到服务器时使用的连接参数。这些参数通常为指定的用户名和运行客户端的主机名。CURRENT_USER()函数会返回从权限表中选择的与访问权限相关的用户名和主机名对。mysqld用这些用户名和主机名对来检查数据库对象访问权限。通过比较这些函数的结果,可以找到mysqld使用的权限和预期不同的原因。一个典型的问题是对主机名使用通配符%:

  

如果此时我以sveta身份连接并尝试创建一个表,我就会获得下面的错误:

  

该问题在于,MySQL服务器认为sveta是sveta@localhost,而不是通配符:

  

如果你不理解为什么选择一台或另一台主机,可以进行如下查询:

  

MySQL在表中按照从访问最多的主机到访问最少的主机的顺序对行进行排序,然后使用第一个找到的值。因此,它把我当作sveta@localhost用户进行连接,此时该用户没有CREATE权限。

另一种权限问题是你无法作为指定用户进行连接。在这种情况下,通常可以从错误消息中了解问题产生的原因,错误消息一般如下所示:

  

在看到这条消息以后,你了解了用户凭证。作为root超级用户进行连接,然后检查该用户是否存在以及是否拥有所需权限:

  

在这个输出信息中,你可以看到用户'sveta'@'localhost'仅仅对book数据库有权限,而对books数据库没有权限。现在,可以修复这个错误:赋予sveta@localhost用户必要的权限。

前面的示例讨论用户缺失必要权限的情况。对于用户被授予过多权限的情况也可以同样处理;仅需要移除不必要的权限。

 警告

MySQL的权限与其管控对象是分离的:这意味着当你赋予某用户权限时mysqld 不会检查其是否存在,同时当授权对象被删除时也不会移除相应权限。这样做的好处是允许我们预先授予必要的权限,但同时也有可能在不经意的使用中带来潜在的问题。

作为最佳实践,我推荐你仔细学习MySQL的权限工作机制。尤其是在你想要在用户对象级别授予权限的时候,因为你需要理解在一个级别授权是如何影响其他授权的。同样,对于撤消权限情形也一样重要,甚至更重要,因为如果你以为已经撤消了某个权限但它依然存在,这就会造成意外的访问。

[1] 版本5.6.3开始,也可以在UPDATE和DELETE上使用EXPLAIN方法,不过把语句转换成SELECT查询仍然有效,因为你可以方便地检查和操作实际的结果集,而不是仅使用EXPLAIN命令。这尤其适用于复杂的JOIN操作,尤其是当EXPLAIN输出的检查的行比实际更新的行还要多的时候。

[2] 你可以在http://dev.mysql.com/doc/refman/5.5/en/c.html找到关于C API的详细描述。

[3] 第5章将详细介绍如何解决复制失败的问题,因此这里不再详细解释。

[4] MySQL企业级备份(MEB)以前也称作InnoDB热备份,是InnoDB表进行在线热备份和其他存储引擎的表进行在线备份的一个工具。第7章将讨论备份的方法。


相关图书

Web应用安全
Web应用安全
MySQL是怎样使用的 快速入门MySQL
MySQL是怎样使用的 快速入门MySQL
MySQL是怎样运行的:从根儿上理解 MySQL
MySQL是怎样运行的:从根儿上理解 MySQL
深入浅出MySQL:数据库开发、优化与管理维护(第3版)
深入浅出MySQL:数据库开发、优化与管理维护(第3版)
MySQL数据库管理实战
MySQL数据库管理实战
PHP、MySQL和JavaScript入门经典(第6版)
PHP、MySQL和JavaScript入门经典(第6版)

相关文章

相关课程