MySQL性能调优与架构设计

微风

2019/03/24 发布于 技术 分类

文字内容
1. 第 1 章 MySQL 基本介绍 前言: 作为最为流行的开源数据库软件之一,MySQL 数据库软件已经是广为人知了。但是为了 照顾对 MySQL 还不熟悉的读者,这章我们将对 MySQL 做一个简单的介绍。主要内容包括 MySQL 各功能模块组成,各模块协同工作原理,Query 处理的流程等。 1.1 MySQL Server 简介 1.1.1 什么是 MySQL MySQL 是由 MySQL AB 公司(目前已经被 SUN 公司收归麾下)自主研发的,目前 IT 行业 最流行的开放源代码的数据库管理系统之一,它同时也是一个支持多线程高并发多用户的关 系型数据库管理系统。 MySQL 数据库以其简单高效可靠的特点,在最近短短几年的时间就从一个名不见经传的 数据库系统,变成一个在 IT 行业几乎是无人不知的开源数据库管理系统。从微型的嵌入式 系统,到小型的 web 网站,至大型的企业级应用,到处都可见其身影的存在。为何一个开源 的数据库管理系统会变得如此的流行呢?在我 2003 年第一次接触 MySQL 之前,也是非常的 纳闷?或许在我大概的向您介绍一下其发展历程之后,心中的这个问题就会消失了。 1.1.2 艰难诞生 1985 年,瑞典的几位志同道合小伙子(以 David Axmark 为首 ) 成立了一家公司,这 就是 MySQL AB 的前身。这个公司最初并不是为了开发数据库产品,而是在实现他们想法的 过程中,需要一个数据库。他们希望能够使用开源的产品。但在当时并没有一个合适的选择 , 没办法,那就自己开发吧。 在最初,他们只是自己设计了一个利用索引顺序存取数据的方法,也就是 IS AM(Indexed Sequential Access Method)存储引擎核心算法的前身,利用 ISAM 结合 mSQL 来实现他们的 应用需求。在早期,他们主要是为瑞典的一些大型零售商提供数据仓库服务。在系统使用过 程中,随着数据量越来越大,系统复杂度越来越高,ISAM 和 mSQL 的组合逐渐不堪重负。在 分析性能瓶颈之后,他们发现问题出在 mSQL 上面。不得已,他们抛弃了 mSQL,重新开发了 一套功能类似的数据存储引擎,这就是 ISAM 存储引擎。大家可能已经注意到他们当时的主 要客户是数据仓库,应该也容易理解为什么直至现在,MySQL 最擅长的是查询性能,而不是
2. 事务处理(需要借助第三方存储引擎)。 软件诞生,自然该给她取一个好听并且容易记住的名字。时至今日,MySQL AB 仍然没 有公布当初为什么给这个数据库系统取名为 MySQL。据传 MySQL 是取自创始人之一 Monty Widenius 的女儿的名字或许大家会认为这仅仅是我的猜测,不以为然,其实也并不是完全 没有根据的。大家或许知道 MySQL 最近正在研发的用来替代 MyISAM 存储引擎的新一代存储 引擎产品 Maria,为什么叫 Maria? 笔者对 这个 问题 也比 较感 兴趣,曾经 和 MySQL 前 CTO David 沟通过 。得 到的 答案 是, Maria 是以他 小女 儿的 名字 命名 的。 看来 ,这 是几 位 MySQL 的创始 人 为自己的软件命名的一个习惯。 在 MySQL 诞生之初,其功能还非常粗糙,和当时已经成熟稳定运营多年的商业数据库管 理系统完全不能比。MySQL 之所以能够成功,和几个创始人最初采用的策略关系非常大。 1.1.3 寻求发展 MySQL 诞生的时候,正是互联网开始高速发展的时期。MySQL AB 通过优化 MySQL 满足了 互联网开发用者对数据库产品的需求:标准化查询语言的支持,高效的数据存取,不必关注 事务完整性,简单易用,而且成本低廉。当时大量的小公司都愿意采用 MySQL 作为数据库应 用系统的数据库管理系统,所以 MySQL 的用户数量不断的增长,进一步促进了 MySQL 自身 的不断改进和完善,进入了一个非常好的良性循环。 合理地把握需求, 准确地定位目标客户,为 MySQL 后面的发展铺平了道路。我们看到, MySQL 一开始就没有拿大型的企业管理软件的数据库系统来定位自己,没有将所有的 IT 行 业定位为自己的目标用户,而是选择的当时并不受重视的一小部分 Web 开发者作为自己的客 户来重点培养发展。这种做法或许值得我们的 IT 企业学习。 1.1.4 巨人崛起 可以说,正是 MySQL 最初抓住了互联网客户,造就了今天 MySQL 在互联网行业的巨大成 功。当然,MySQL 的高速发展,同时也离不开另外一个很关键的因素,那就是开放源代码。 在 2000 年的时候,MySQL 公布了自己的源代码,并采用 GPL(GNU General Public License)许可协议,正式进入开源世界。虽然在当时的环境下,开源还没有现在这样流行, 但是那是开源世界开始真正让大多数世人所接受并开始推崇的起步阶段。当然 MySQL 的成功 并不仅仅是因为以上的这些原因,但我们不能否认正是 MySQL 这一战略性质的策略让 MySQL 在进一步拓展自己的客户群 的路上一路东风。此后 MySQL 的发展路程我想就不需要我继续 再次罗嗦了,因为基本上都可以从 MySQL 的官方网站(http://www.mysql.com)得到相应的 答案。
3. 1.2 MySQL 与其他数据库的简单比较 前面我们简单介绍了 MySQL 的发展历程,从中了解了 MySQL 快速崛起的必要的条件。接 下来,我们通过 MySQL 在功能,性能,以及其易用性方面和其他主流的数据库做一个基本的 比较,来了解一下 MySQL 成为当下最流行的开源数据库软件的充分条件。 1.2.1 功能比较 作为一个成熟的数据库管理系统,要满足各种各样的商业需求,功能肯定是会被列入重 点参考对象的。MySQL 虽然在早期版本的时候功能非常简单,只能做一些很基础的结构化数 据存取操作,但是经过多年的改进和完善之后,已经基本具备了所有通用数据库管理系统所 需要的相关功能。 MySQL 基本实现了 ANSI SQL 92 的大部分标准,仅有少部分并不经常被使用的部分没有 实现。比如在字段类型支持方面,另一个著名的开源数据库 PostGreSQL 支持的类型是最完 整的,而 Oracle 和其他一些商业数据库,比如 DB2、Sybase 等,较 MySQL 来说也要相对少 一些。这一点,我们可以通过 TCX 的 crash-me 测试套件所得出的测试报告得知。在事务支 持方面,虽然 MySQL 自己的存储引擎并没有提供,但是已经通过第三方插件式存储引擎 Innodb 实现了 SQL 92 标准所定义的四个事务隔离级别的全部,只是在实现的过程中每一种 的实现方式可能有一定的区别,这在当前商用数据库管理系统中都不多见。比如,大家所熟 知的大名鼎鼎的 Oracle 数据库就仅仅实现了其中的两种(Serializable 和 Read Commited), 而 PostGreSQL,同样支持四种隔离级别。 不过在可编程支持方面,MySQL 和其他数据库相比还有一定的差距,虽然最新版的 MySQL 已经开始提供一些简单的可编程支持,如开始支持 Procedure,Function,Trigger 等,但 是所支持的功能还比较有限,和其他几大商用数据库管理系统相比,还存在较大的不足。如 Oracle 有强大的 PL/SQL,SQL Server 有 T-SQL,PostGreSQL 也有功能很完善的 PL/PGSQL 的支持。 整体来说,虽然在功能方面 MySQL 数据库作为一个通用的数据库管理系统暂时还无法和 PostGreSQL 相比,但是其功能完全可以满足我们的通用商业需求,提供足够强大的服务。 而且不管是哪一种数据库在功能方面都不敢声称自己比其他任何一款商用通用数据库管理 系统都强,甚至都不敢声称能够自己拥有某一数据库产品的所有功能。因为每一款数据库管 理系统都有起自身的优势,也有起自身的限制,这只能代表每一款产品所倾向的方向不一样 而已。 1.2.2 易用性比较 从系统易用性方面来比较,每一个使用过 MySQL 的用户都能够明显地感觉出 MySQL 在这
4. 方面与其他通用数据库管理系统之间的优势所在。尤其是相对于一些大型的商业数据库管理 系统如 Oracle、DB2 以及 Sybase 来说,对于普通用户来说,操作的难易程度明显不处于一 个级别。MySQL 一直都奉行简单易用的原则,也正是靠这一特性,吸引了大量的初级数据库 用户最终选择了 MySQL。也正是这一批又一批的初级用户,在经过了几年时间的成长之后, 很多都已经成为了高级数据库用户,而且也一直都在伴随着 MySQL 成长。 从安装方面来说,MySQL 安装包大小仅仅只有 100MB 左右,这几大商业数据库完全不在 一个数量级。安装难易程度也要比 Oracle 等商业数据库简单很多,不论是通过已经编译好 的二进制分发包还是源码编译安装,都非常简单。 再从数据库创建来比较,MySQL 仅仅只需要一个简单的 CREATE DATABASE 命令,即可 在瞬间完成建库的动作,而 Oracle 数据库与之相比,创建一个数据库简直就是一个非常庞 大的工程。当然,二者数据库的概念存在一定差别也不可否认。 1.2.3 性能比较 性能方面,一直是 MySQL 引以为自豪的一个特点。在权威的第三方评测机构多次测试较 量各种数据库 TPCC 值的过程中,MySQL 一直都有非常优异的表现,而且在其他所有商用的 通用数据库管理系统中,仅仅只有 Oracle 数据库能够与其一较高下。至于各种数据库详细 的性能数据,我这里就不便记录,大家完全可以通过网上第三方评测机构公布的数据了解具 体细节信息。 MySQL 一直以来奉行一个原则,那就是在保证足够的稳定性的前提下,尽可能的提高自 身的处理能力。也就是说,在性能和功能方面,MySQL 第一考虑的要素主要还是性能,MySQL 希望自己是一个在满足客户 99%的功能需求的前提下,花掉剩下的大部分精力来性能努力, 而不是希望自己是成为一个比其他任何数据库的功能都要强大的数据库产品。 1.2.4 可靠性 关于可靠性的比较,并没有太多详细的评测比较数据,但是从目前业界的交流中可以了 解到,几大商业厂商的数据库的可靠性肯定是没有太多值得怀疑的。但是做为开源数据库管 理系统的代表,MySQL 也有非常优异的表现,而并不是像有些人心中所怀疑的那样,因为不 是商业厂商所提供,就会不够稳定不够健壮。从当前最火的 Facebook 这样大型的网站都是 使用 MySQL 数据库,就可以看出,MySQL 在稳定可靠性方面,并不会比我们的商业厂商的产 品有太多逊色。而且排在全球前 10 位的大型网站里面,大部分都有部分业务是运行在 MySQL 数据库环境上,如 Yahoo,Google 等。 总的来说,MySQL 数据库在发展过程中一直有自己的三个原则:简单、高效、可靠。从 上面的简单比较中,我们也可以看出,在 MySQL 自己的所有三个原则上面,没有哪一项是做 得不好的。而且,虽然功能并不是 MySQL 自身所追求的三个原则之一,但是考虑到当前用户 量的急剧增长,用户需求越来越越多样化,MySQL 也不得不在功能方面做出大量的努力,来
5. 不断满足客户的新需求。比如最近版本中出现的 Eent Scheduler(类似于 Oracle 的 Job 功 能 ), Partition 功能,自主研发的 Maria 存储引擎在功能方面的扩展,Falcon 存储引擎对 事务的支持等等,都证明了 MySQL 在功能方面也开始了不懈的努力。 任何一种产品,都不可能是完美的,也不可能适用于所有用户。我们只有衡量了每一种 产品的各种特性之后,从中选择出一种最适合于自身的产品。 1.3 MySQL 的主要适用场景 据说目前 MySQL 用户已经达千万级别了,其中不乏企业级用户。可以说是目前最为流行 的开源数据库管理系统软件了。任何产品都不可能是万能的,也不可能适用于所有的应用场 景。那么 MySQL 到底在什么场景下适用什么场景下不适用呢? 1、Web 网站系统 Web 站点,是 MySQL 最大的客户群,也是 MySQL 发展史上最为重要的支撑力量,这一点 在最开始的 MySQL Server 简介部分就已经说明过。 MySQL 之所以能成为 Web 站点开发者们最青睐的数据库管理系统,是因为 MySQL 数据库 的安装配置都非常简单,使用过程中的维护也不像很多大型商业数据库管理系统那么复杂, 而且性能出色。还有一个非常重要的原因就是 MySQL 是开放源代码的,完全可以免费使用。 2、日志记录系统 MySQL 数据库的插入和查询性能都非常的高效,如果设计地较好,在使用 MyISAM 存储 引擎的时候,两者可以做到互不锁定,达到很高的并发性能。所以,对需要大量的插入和查 询日志记录的系统来说,MySQL 是非常不错的选择。比如处理用户的登录日志,操作日志等 , 都是非常适合的应用场景。 3、数据仓库系统 随着现在数据仓库数据量的飞速增长,我们需要的存储空间越来越大。数据量的不断增 长,使数据的统计分析变得越来越低效,也越来越困难。怎么办?这里有几个主要的解决思 路,一个是采用昂贵的高性能主机以提高计算性能,用高端存储设备提高 I/O 性能,效果理 想,但是成本非常高;第二个就是通过将数据复制到多台使用大容量硬盘的廉价 pc server 上,以提高整体计算性能和 I/O 能力,效果尚可,存储空间有一定限制,成本低廉;第三个 , 通过将数据水平拆分,使用多台廉价的 pc server 和本地磁盘来存放数据,每台机器上面都 只有所有数据的一部分,解决了数据量的问题,所有 pc server 一起并行计算,也解决了计 算能力问题,通过中间代理程序调配各台机器的运算任务,既可以解决计算性能问题又可以 解决 I/O 性能问题,成本也很低廉。在上面的三个方案中,第二和第三个的实现,MySQL 都 有较大的优势。通过 MySQL 的简单复制功能,可以很好的将数据从一台主机复制到另外一台 , 不仅仅在局域网内可以复制,在广域网同样可以。当然,很多人可能会说,其他的数据库同 样也可以做到,不是只有 MySQL 有这样的功能。确实,很多数据库同样能做到,但是 MySQL 是免费的,其他数据库大多都是按照主机数量或者 cpu 数量来收费,当我们使用大量的 pc server 的时候,license 费用相当惊人。第一个方案,基本上所有数据库系统都能够实现,
6. 但是其高昂的成本并不是每一个公司都能够承担的。 4、嵌入式系统 嵌入式环境对软件系统最大的限制是硬件资源非常有限,在嵌入式环境下运行的软件系 统,必须是轻量级低消耗的软件。 MySQL 在资源的使用方面的伸缩性非常大,可以在资源非常充裕的环境下运行,也可以 在资源非常少的环境下正常运行。它对于嵌入式环境来说,是一种非常合适的数据库系统, 而且 MySQL 有专门针对于嵌入式环境的版本。 1.4 小结 从最初的诞生,到发展成为目前最为流行的开源数据库管理软件,MySQL 已经走过了较 长的一段路,也正是这段不寻常的路,造就了今天 MySQL 的成就。 通过本章的信息,我想各位读者应该是比较清楚 MySQL 的大部分基本信息了,对于 MySQL 主要特长,以及适用场景,都有了一个初步的了解。在后续章节中我们将会针对这些 内容做更为详细深入的介绍。 第 2 章 MySQL 架构组成 前言 麻雀虽小,五脏俱全。MySQL 虽然以简单著称,但其内部结构并不简单。本章从 MySQL 物理组成、逻辑组成,以及相关工具几个角度来介绍 MySQL 的整体架构组成,希望能够让 读者对 MySQL 有一个更全面深入的了解。 2.1 MySQL 物理文件组成
7. 2.1.1 日志文件 1、错误日志:Error Log 错误日志记录了 MyQL Server 运行过程中所有较为严重的警告和错误信息,以及 MySQL Server 每次启动和关闭的详细信息。在默认情况下,系统记录错误日志的功能是关闭的, 错误信息被输出到标准错误输出(stderr),如果要开启系统记录错误日志的功能,需要在 启动时开启-log-error 选项。错误日志的默认存放位置在数据目录下,以 hostname.err 命 名。但是可以使用命令:--log-error[=file_name],修改其存放目录和文件名。 为了方便维护需要,有时候会希望将错误日志中的内容做备份并重新开始记录,这时候 就可以利用 MySQL 的 FLUSH LOGS 命令来告诉 MySQL 备份旧日志文件并生成新的日志文件。 备份文件名以“.old”结尾。 2、二进制日志:Binary Log & Binary Log Index 二进制日志,也就是我们常说的 binlog,也是 MySQL Server 中最为重要的日志之一。 当我们通过“--log-bin[=file_name]”打开了记录的功能之后,MySQL 会将所有修改数据 库数据的 query 以二进制形式记录到日志文件中。当然,日志中并不仅限于 query 语句这么 简单,还包括每一条 query 所执行的时间,所消耗的资源,以及相关的事务信息,所以 binlog 是事务安全的。 和错误日志一样,binlog 记录功能同样需要“--log-bin[=file_name]”参数的显式指 定才能开启,如果未指定 file_name,则会在数据目录下记录为 mysql-bin.****** (*代表0~ 9 之间的某一个数字,来表示该日志的序号)。 binlog 还有其他一些附加选项参数: “--max_binlog_size”设置 binlog 的最大存储上限,当日志达到该上限时,MySQL 会 重新创建一个日志开始继续记录。不过偶尔也有超出该设置的 binlog 产生,一般都是因为 在即将达到上限时,产生了一个较大的事务,为了保证事务安全,MySQL 不会将同一个事务 分开记录到两个 binlog 中。 “--binlog-do-db=db_name”参数明确告诉 MySQL,需要对某个(db_name)数据库记 录 binlog,如果有了“--binlog-do-db=db_name”参数的显式指定,MySQL 会忽略针对其他 数据库执行的 query,而仅仅记录针对指定数据库执行的 query。 “--binlog-ignore-db=db_name”与“--binlog-do-db=db_name”完全相反,它显式指 定忽略某个(db_name)数据库的 binlog 记录,当指定了这个参数之后,MySQL 会记录指定 数据库以外所有的数据库的 binlog。 “--binlog-ignore-db=db_name”与“--binlog-do-db=db_name”两个参数有一个共同 的概念需要大家理解清楚,参数中的 db_name 不是指 query 语句更新的数据所在的数据库, 而是执行 query 的时候当前所处的数据库。不论更新哪个数据库的数据,MySQL 仅仅比较当 前连接所处的数据库(通过 use db_name 切换后所在的数据库)与参数设置的数据库名,而 不会分析 query 语句所更新数据所在的数据库。 mysql-bin.index 文件(binary log index)的功能是记录所有 Binary Log 的绝对路 径,保证 MySQL 各种线程能够顺利的根据它找到所有需要的 Binary Log 文件。
8. 3、更新日志:update log 更新日志是 MySQL 在较老的版本上使用的,其功能和 binlog 基本类似,只不过不是以 二进制格式来记录而是以简单的文本格式记录内容。自从 MySQL 增加了 binlog 功能之 后, 就很少使用更新日志了。从版本 5.0 开始,MySQL 已经不再支持更新日志了。 4、查询日志:query log 查询日志记录 MySQL 中所有的 query,通过“--log[=fina_name]”来打开该功能。由 于记录了所有的 query,包括所有的 select,体积比较大,开启后对性能也有较大的影响, 所以请大家慎用该功能。一般只用于跟踪某些特殊的 sql 性能问题才会短暂打开该功能。默 认的查询日志文件名为 hostname.log。 5、慢查询日志:slow query log 顾名思义,慢查询日志中记录的是执行时间较长的 query,也就是我们常说的 slow query,通过设--log-slow-queries[=file_name]来打开该功能并设置记录位置和文件名, 默认文件名为 hostname-slow.log,默认目录也是数据目录。 慢查询日志采用的是简单的文本格式,可以通过各种文本编辑器查看其中的内容。其中 记录了语句执行的时刻,执行所消耗的时间,执行用户,连接主机等相关信息。MySQL 还提 供了专门用来分析满查询日志的工具程序 mysqlslowdump,用来帮助数据库管理人员解决可 能存在的性能问题。 6、Innodb 的在线 redo 日志:innodb redo log Innodb 是一个事务安全的存储引擎,其事务安全性主要就是通过在线 redo 日志和记录 在表空间中的 undo 信息来保证的。redo 日志中记录了 Innodb 所做的所有物理变更和事务 信息,通过 redo 日志和 undo 信 息 ,Innodb 保证了在任何情况下的事务安全性。 Innodb 的redo 日志同样默认存放在数据目录下,可以通过 innodb_log_group_home_dir 来更改设置日志的 存放位置,通过 innodb_log_files_in_group 设置日志的数量。 2.1.2 数据文件 在 MySQL 中每一个数据库都会在定义好(或者默认)的数据目录下存在一个以数据库名 字命名的文件夹,用来存放该数据库中各种表数据文件。不同的 MySQL 存储引擎有各自不同 的数据文件,存放位置也有区别。多数存储引擎的数据文件都存放在和 MyISAM 数据文件位 置相同的目录下,但是每个数据文件的扩展名却各不一样。如 MyISAM 用“.MYD”作为扩展 名,Innodb 用“.ibd”,Archive 用“.arc”,CSV 用“.csv”,等等。 1、“.frm”文件 与表相关的元数据(meta)信息都存放在“.frm”文件中,包括表结构的定义信息等。 不论是什么存储引擎,每一个表都会有一个以表名命名的“.frm”文件。所有的“.frm”文 件都存放在所属数据库的文件夹下面。 2、“.MYD”文件
9. “.MYD”文件是 MyISAM 存储引擎专用,存放 MyISAM 表的数据。每一个 MyISAM 表都会 有一个“.MYD”文件与之对应,同样存放于所属数据库的文件夹下,和“.frm”文件在一起 。 3、“.MYI”文件 “.MYI”文件也是专属于 MyISAM 存储引擎的,主要存放 MyISAM 表的索引相关信息。对 于 MyISAM 存储来说,可以被 cache 的内容主要就是来源于“.MYI”文件中。每一个 MyISAM 表对应一个“.MYI”文件,存放于位置和“.frm”以及“.MYD”一样。 4、“.ibd”文件和 ibdata 文件 这两种文件都是存放 Innodb 数据的文件,之所以有两种文件来存放 Innodb 的数据(包 括 索引 ),是因 为 Innodb 的数据存储方式能够通过配置来决定是使用共享表空间存放存储数 据,还是独享表空间存放存储数据。独享表空间存储方式使用“.ibd”文件来存放数据,且 每个表一个“.ibd”文件,文件存放在和 MyISAM 数据相同的位置。如果选用共享存储表空 间来存放数据,则会使用 ibdata 文件来存放,所有表共同使用一个(或者多个,可自行配 置)ibdata 文件。ibdata 文件可以通过 innodb_data_home_dir 和 innodb_data_file_path 两 个 参 数 共 同 配 置 组 成 , innodb_data_home_dir 配 置 数 据 存 放 的 总 目 录 , 而 innodb_data_file_path 配 置 每 一 个 文 件 的 名 称 。 当 然 , 也 可 以 不 配 置 innodb_data_home_dir 而直接在 innodb_data_file_path 参数配置的时候使用绝对路径来 完成配置。innodb_data_file_path 中可以一次配置多个 ibdata 文件。文件可以是指定大 小,也可以是自动扩展的,但是 Innodb 限制了仅仅只有最后一个 ibdata 文件能够配置成自 动扩展类型。当我们需要添加新的 ibdata 文件的时候,只能添加在 innodb_data_file_path 配置的最后,而且必须重启 MySQL 才能完成 ibdata 的添加工作。不过如果我们使用独享表 空间存储方式的话,就不会有这样的问题,但是如果要使用裸设备的话,每个表一个裸设备 , 可能造成裸设备数量非常大,而且不太容易控制大小,实现比较困难,而共享表空间却不会 有这个问题,容易控制裸设备数量。我个人还是更倾向于使用独享表空间存储方式。当然, 两种方式各有利弊,看大家各自应用环境的侧重点在那里了。 上面仅仅介绍了两种最常用存储引擎的数据文件,此外其他各种存储引擎都有各自的数 据文件,读者朋友可以自行创建某个存储引擎的表做一个简单的测试,做更多的了解。 2.1.3 Replication 相关文件: 1、master.info 文件: master.info 文件存在于 Slave 端的数据目录下,里面存放了该 Slave 的 Master 端的 相关信息,包括 Master 的主机地址,连接用户,连接密码,连接端口,当前日志位置,已 经读取到的日志位置等信息。 2、relay log 和 relay log index mysql-relay-bin.xxxxxn 文件用于存放 Slave 端的 I/O 线程从 Master 端所读取到 的 Binary Log 信息,然后由 Slave 端的 SQL 线程从该 relay log 中读取并解析相应的 日志信息,转化成 Master 所执行的 SQL 语句,然后在 Slave 端应用。 mysql-relay-bin.index 文件的功能类似于 mysql-bin.index ,同样是记录日志的存
10. 放位置的绝对路径,只不过他所记录的不是 Binary Log,而是 Relay Log。 3、relay-log.info 文件: 类似于 master.info,它存放通过 Slave 的 I/O 线程写入到本地的 relay log 的相关信 息。供 Slave 端的 SQL 线程以及某些管理操作随时能够获取当前复制的相关信息。 2.1.4 其他文件: 1、system config file MySQL 的系统配置文件一般都是“my.cnf”,Unix/Linux 下默认存放在"/etc"目录下, Windows 环境一般存放在“c:/windows”目 录 下 面 。“my.cnf”文件中包含多种参数选项组 (group),每一种参数组都通过中括号给定了固定的组名,如“[mysqld]”组中包括了 mysqld 服务启动时候的初始化参数,“[client]”组中包含着客户端工具程序可以读取的参数,此 外还有其他针对于各个客户端软件的特定参数组,如 mysql 程 序使 用的“[mysql]”,mysqlchk 使用的“[mysqlchk]”,等等。如果读者朋友自己编写了某个客户端程序,也可以自己设定 一个参数组名,将相关参数配置在里面,然后调用 mysql 客户端 api 程序中的参数读取 api 读取相关参数。 2、pid file pid file 是 mysqld 应用程序 在 Unix/Linux 环境下的一个进程文件,和许多其他 Unix/Linux 服务端程序一样,存放着自己的进程 id。 3、socket file socket 文件也是在 Unix/Linux 环境下才有的,用户在 Unix/Linux 环境下客户端连接 可以不通过 TCP/IP 网络而直接使用 Unix Socket 来连接 MySQL。 2.2 MySQL Server 系统架构 2.2.1 逻辑模块组成 总的来说,MySQL 可以看成是二层架构,第一层我们通常叫做 SQL Layer,在 MySQL 数 据库系统处理底层数据之前的所有工作都是在这一层完成的,包括权限判断,sql 解析,执 行计划优化,query cache 的处理等等;第二层就是存储引擎层,我们通常叫做 Storage Engine Layer,也就是底层数据存取操作实现部分,由多种存储引擎共同组成。所以,可以 用如下一张最简单的架构示意图来表示 MySQL 的基本架构,如图 2-1 所示:
11. 图 2-1 虽然从上图看起来 MySQL 架构非常的简单,就是简单的两部分而已,但实际上每一层中 都含有各自的很多小模块,尤其是第一层 SQL Layer,结构相当复杂的。下面我们就分别针 对 SQL Layer 和 Storage Engine Layer 做一个简单的分析。 SQL Layer 中包含了多个子模块,下面我将逐个做一下简单的介绍: 1、初始化模块 顾名思议,初始化模块就是在 MySQL Server 启动的时候,对整个系统做各种各样的初 始化操作,比如各种 buffer,cache 结构的初始化和内存空间的申请,各种系统变量的初始 化设定,各种存储引擎的初始化设置,等等。 2、核心 API 核心 API 模块主要是为了提供一些需要非常高效的底层操作功能的优化实现,包括各种 底层数据结构的实现,特殊算法的实现,字符串处理,数字处理等,小文件 I/O,格式化输 出,以及最重要的内存管理部分。核心 API 模块的所有源代码都集中在 mysys 和 strings 文件夹下面,有兴趣的读者可以研究研究。 3、网络交互模块 底层网络交互模块抽象出底层网络交互所使用的接口 api,实现底层网络数据的接收与 发送,以方便其他各个模块调用,以及对这一部分的维护。所有源码都在 vio 文件夹下面。 4、Client & Server 交互协议模块 任何 C/S 结构的软件系统, 都肯定会有自己独有的信息交互协议, MySQL 也 不 例 外 。MySQL 的 Client & Server 交互协议模块部分,实现了客户端与 MySQL 交互过程中的所有协议。 当然这些协议都是建立在现有的 OS 和网络协议之上的,如 TCP/IP 以及 Unix Socket。 5、用户模块 用户模块所实现的功能,主要包括用户的登录连接权限控制和用户的授权管理。他就像 MySQL 的大门守卫一样,决定是否给来访者“开门”。
12. 6、访问控制模块 造访客人进门了就可以想干嘛就干嘛么?为了安全考虑,肯定不能如此随意。这时候就 需要访问控制模块实时监控客人的每一个动作,给不同的客人以不同的权限。访问控制模块 实现的功能就是根据用户模块中各用户的授权信息,以及数据库自身特有的各种约束,来控 制用户对数据的访问。用户模块和访问控制模块两者结合起来,组成了 MySQL 整个数据库系 统的权限安全管理的功能。 7、连接管理、连接线程和线程管理 连接管理模块负责监听对 MySQL Server 的各种请求,接收连接请求,转发所有连接请 求到线程管理模块。每一个连接上 MySQL Server 的客户端请求都会被分配(或创建)一个 连接线程为其单独服务。而连接线程的主要工作就是负责 MySQL Server 与客户端的通信, 接受客户端的命令请求,传递 Server 端的结果信息等。线程管理模块则负责管理维护这些 连接线程。包括线程的创建,线程的 cache 等。 8、Query 解析和转发模块 在 MySQL 中我们习惯将所有 Client 端发送给 Server 端的命令都称为 query,在 MySQL Server 里面,连接线程接收到客户端的一个 Query 后,会直接将该 query 传递给专门负责 将各种 Query 进行分类然后转发给各个对应的处理模块,这个模块就是 query 解析和转发模 块。其主要工作就是将 query 语句进行语义和语法的分析,然后按照不同的操作类型进行分 类,然后做出针对性的转发。 9、Query Cache 模块 Query Cache 模块在 MySQL 中是一个非常重要的模块,他的主要功能是将客户端提交给 MySQL 的 Select 类 query 请求的返回结果集 cache 到内存中,与该 query 的一个 hash 值做 一个对应。该 Query 所取数据的基表发生任何数据的变化之后,MySQL 会自动使该 query 的 Cache 失效。在读写比例非常高的应用系统中,Query Cache 对性能的提高是非常显著的。 当然它对内存的消耗也是非常大的。 10、Query 优化器模块 Query 优化器,顾名思义,就是优化客户端请求的 query,根据客户端请求的 query 语 句,和数据库中的一些统计信息,在一系列算法的基础上进行分析,得出一个最优的策略, 告诉后面的程序如何取得这个 query 语句的结果。 11、表变更管理模块 表变更管理模块主要是负责完成一些 DML 和 DDL 的 query,如 :update,delte,insert, create table,alter table 等语句的处理。 12、表维护模块 表的状态检查,错误修复,以及优化和分析等工作都是表维护模块需要做的事情。 13、系统状态管理模块 系统状态管理模块负责在客户端请求系统状态的时候,将各种状态数据返回给用户,像 DBA 常用的各种 show status 命令,show variables 命令等,所得到的结果都是由这个模块 返回的。
13. 14、表管理器 这个模块从名字上看来很容易和上面的表变更和表维护模块相混淆,但是其功能与变更 及维护模块却完全不同。大家知道,每一个 MySQL 的表都有一个表的定义文件,也就是*.frm 文件。表管理器的工作主要就是维护这些文件,以及一个 cache,该 cache 中的主要内容是 各个表的结构信息。此外它还维护 table 级别的锁管理。 15、日志记录模块 日志记录模块主要负责整个系统级别的逻辑层的日志的记录,包括 error log,binary log,slow query log 等。 16、复制模块 复 制 模 块 又 可 分 为 Master 模 块 和 Slave 模 块 两 部 分 , Master 模 块 主 要 负 责 在 Replication 环境中读取 Master 端的 binary 日志,以及与 Slave 端的 I/O 线程交互等工作 。 Slave 模块比 Master 模块所要做的事情稍多一些,在系统中主要体现在两个线程上面。一 个是负责从 Master 请求和接受 binary 日志,并写入本地 relay log 中的 I/O 线程。另外一 个是负责从 relay log 中读取相关日志事件,然后解析成可以在 Slave 端正确执行并得到和 Master 端完全相同的结果的命令并再交给 Slave 执行的 SQL 线程。 17、存储引擎接口模块 存储引擎接口模块可以说是 MySQL 数据库中最有特色的一点了。目前各种数据库产品 中,基本上只有 MySQL 可以实现其底层数据存储引擎的插件式管理。这个模块实际上只是一 个抽象类,但正是因为它成功地将各种数据处理高度抽象化,才成就了今天 MySQL 可插拔存 储引擎的特色。 2.2.2 各模块工作配合 在了解了 MySQL 的各个模块之后,我们再看看 MySQL 各个模块间是如何相互协同工作的 。 接下来,我们通过启动 MySQL,客户端连接,请求 query,得到返回结果,最后退出,这样 一整个过程来进行分析。 当我们执行启动 MySQL 命令之后,MySQL 的初始化模块就从系统配置文件中读取系统参 数和命令行参数,并按照参数来初始化整个系统,如申请并分配 buffer,初始化全局变量, 以及各种结构等。同时各个存储引擎也被启动,并进行各自的初始化工作。当整个系统初始 化结束后,由连接管理模块接手。连接管理模块会启动处理客户端连接请求的监听程序,包 括 tcp/ip 的网络监听,还有 unix 的 socket。这时候,MySQL Server 就基本启动完成,准 备好接受客户端请求了。 当连接管理模块监听到客户端的连接请求(借助网络交互模块的相关功能),双方通过 Client & Server 交互协议模块所定义的协议“寒暄”几句之后,连接管理模块就会将连接 请求转发给线程管理模块,去请求一个连接线程。 线程管理模块马上又会将控制交给连接线程模块,告诉连接线程模块:现在我这边有连
14. 接请求过来了,需要建立连接,你赶快处理一下。连接线程模块在接到连接请求后,首先会 检查当前连接线程池中是否有被 cache 的空闲连接线程,如果有,就取出一个和客户端请求 连接上,如果没有空闲的连接线程,则建立一个新的连接线程与客户端请求连接。当然,连 接线程模块并不是在收到连接请求后马上就会取出一个连接线程连和客户端连接,而是首先 通过调用用户模块进行授权检查,只有客户端请求通过了授权检查后,他才会将客户端请求 和负责请求的连接线程连上。 在 MySQL 中,将客户端请求分为了两种类型:一种是 query,需要调用 Parser 也就是 Query 解析和转发模块的解析才能够执行的请求;一种是 command,不需要调用 Parser 就可 以直接执行的请求。如果我们的初始化配置中打开了 Full Query Logging 的功能,那么 Query 解析与转发模块会调用日志记录模块将请求计入日志,不管是一个 Query 类型的请求 还是一个 command 类型的请求, 都会被记录进入日志,所以出于性能考虑,一般很少打开 Full Query Logging 的功能。 当客户端请求和连接线程“互换暗号(互通协议)”接上头之后,连接线程就开始处理 客户端请求发送过来的各种命令(或者 query),接受相关请求。它将收到的 query 语句转 给 Query 解析和转发模块,Query 解析器先对 Query 进行基本的语义和语法解析,然后根据 命令类型的不同,有些会直接处理,有些会分发给其他模块来处理。 如果是一个 Query 类型的请求,会将控制权交给 Query 解析器。Query 解析器首先分析 看是不是一个 select 类型的 query,如果是,则调用查询缓存模块,让它检查该 query 在 query cache 中是否已经存在。如果有,则直接将 cache 中的数据返回给连接线程模块,然 后通过与客户端的连接的线程将数据传输给客户端。如果不是一个可以被 cache 的 query 类型,或者 cache 中没有该 query 的数据,那么 query 将被继续传回 query 解析器,让 query 解析器进行相应处理,再通过 query 分发器分发给相关处理模块。 如果解析器解析结果是一条未被 cache 的 select 语句,则将控制权交给 Optimizer, 也就是 Query 优化器模块,如果是 DML 或者是 DDL 语句,则会交给表变更管理模块,如果是 一些更新统计信息、检测、修复和整理类的 query 则会交给表维护模块去处理,复制相关的 query 则转交给复制模块去进行相应的处理,请求状态的 query 则转交给了状态收集报告模 块。实际上表变更管理模块根据所对应的处理请求的不同,是分别由 insert 处理器、delete 处理器、update 处理器、create 处理器,以及 alter 处理器这些小模块来负责不同的 DML 和 DDL 的。 在各个模块收到 Query 解析与分发模块分发过来的请求后,首先会通过访问控制模块检 查连接用户是否有访问目标表以及目标字段的权限,如果有,就会调用表管理模块请求相应 的表,并获取对应的锁。表管理模块首先会查看该表是否已经存在于 table cache 中,如果 已经打开则直接进行锁相关的处理,如果没有在 cache 中,则需要再打开表文件获取锁,然 后将打开的表交给表变更管理模块。 当表变更管理模块“获取”打开的表之后,就会根据该表的相关 meta 信息,判断表的 存储引擎类型和其他相关信息。根据表的存储引擎类型,提交请求给存储引擎接口模块,调 用对应的存储引擎实现模块,进行相应处理。
15. 不过,对于表变更管理模块来说,可见的仅是存储引擎接口模块所提供的一系列“标准” 接口,底层存储引擎实现模块的具体实现,对于表变更管理模块来说是透明的。他只需要调 用对应的接口,并指明表类型,接口模块会根据表类型调用正确的存储引擎来进行相应的处 理。 当一条 query 或者一个 command 处理完成(成功或者失败)之后,控制权都会交还给连 接线程模块。如果处理成功,则将处理结果(可能是一个 Result set,也可能是成功或者 失败的标识)通过连接线程反馈给客户端。如果处理过程中发生错误,也会将相应的错误信 息发送给客户端,然后连接线程模块会进行相应的清理工作,并继续等待后面的请求,重复 上面提到的过程,或者完成客户端断开连接的请求。 如果在上面的过程中,相关模块使数据库中的数据发生了变化,而且 MySQL 打开了 binlog 功能,则对应的处理模块还会调用日志处理模块将相应的变更语句以更新事件的形式记 录到相关参数指定的二进制日志文件中。 在上面各个模块的处理过程中,各自的核心运算处理功能部分都会高度依赖整个 MySQL 的核心 API 模块,比如内存管理,文件 I/O,数字和字符串处理等等。 了解到整个处理过程之后,我们可以将以上各个模块画成如图 2-2 的关系图:
16. 图 2-2
17. 2.3 MySQL 自带工具使用介绍 MySQL 数据库不仅提供了数据库的服务器端应用程序,同时还提供了大量的客户端工具 程序,如 mysql,mysqladmin,mysqldump 等等,都是大家所熟悉的。虽然有些人对这些工 具的功能都已经比较了解了,但是真正能将这些工具程序物尽其用的人可能并不是太多,或 者知道的不全,也可能并不完全了解其中的某种特性。所以在这里我也简单地做一个介绍。 1、mysql 相信在所有 MySQL 客户端工具中,读者了解最多的就是 mysql 了,用的最多的应该也非 他莫属。mysql 的功能和 Oracle 的 sqlplus 一样,为用户提供一个命令行接口来操作管理 MySQL 服务器。其基本的使用语法这里就不介绍了,大家只要运行一下“mysql --help”就 会得到如下相应的基本使用帮助信息: sky@sky:~$ mysql --help mysql Ver 14.14 Distrib 5.1.26-rc, for pc-linux-gnu (i686) using EditLine wrapper Copyright (C) 2000-2008 MySQL AB This software comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to modify and redistribute it under the GPL license Usage: mysql [OPTIONS] [database] -?, --help Display this help and exit. ... ... -e, --execute=name Execute command and quit. (Disables --force and history file) -E, --vertical Print the output of a query (rows) vertically. ... ... -H, --html -X, --xml Produce HTML output. Produce XML output ... ... --prompt=name Set the mysql prompt to this value. ... ... --tee=name Append everything into outfile. See interactive help (\h) also. Does not work in batch mode. Disable with --disable-tee. This option is disabled by default.
18. ... ... -U, --safe-updates Only allow UPDATE and DELETE that uses keys. --select_limit=# Automatic limit for SELECT when using --safe-updates --max_join_size=# Automatic limit for rows in a join when using --safe-updates ... ... --show-warnings Show warnings after every statement. ... ... 上面的内容仅仅只是输出的一部分,省略去掉了大家最常用的一些参数(因为大家应该 已经很熟悉了),留下了部分个人认为可能不是太经常用到,但是在有些情况下却能给我们 带来意料之外的惊喜的一些参数选项。 首先看看“-e, --execute=name”参数,这个参数是告诉 mysql,我只要执行“-e”后 面的某个命令,而不是要通过 mysql 连接登录到 MySQL Server 上面。此参数在我们写一些 基本的 MySQL 检查和监控的脚本中非常有用,我个人就经常在脚本中使用到他。 如果在连接时候使用了“-E, --vertical”参数,登入之后的所有查询结果都将以纵列 显示,效果和我们在一条 query 之后以“\G”结尾一样,这个参数的使用场景可能不是特别 多。 “-H, --html”与“-X, --xml”这两个参数很有意思的,在启用这两个参数之后,select 出来的所有结果都会按照“Html”与“Xml”格式来输出,在有些场合之下,比如希望 Xml 或者 Html 文件格式导出某些报表文件的时候,是非常方便的。 “--prompt=name”参数对于做运维的人来说是一个非常重要的参数选项,其主要功能 是定制自己的 mysql 提示符的显示内容。在默认情况下,我们通过 mysql 登入到数据库之后 , mysql 的提示符只是一个很简单的内容”mysql>“,没有其他任何附加信息。非常幸运的是 mysql 通过“--prompt=name”参数给我们提供了自定义提示信息的办法,可以通过配置显 示登入的主机地址,登录用户名,当前时间,当前数据库 schema,MySQL Server 的一些信 息等等。我个人强烈建议将登录主机名,登录用户名和所在的 schema 这三项加入提示内容, 因为当大家手边管理的 MySQL 越来越多,操作越来越频繁的时候,非常容易因为操作的时候 没有太在意自己当前所处的环境而造成在错误的环境执行了错误的命令并造成严重后果的 情况。如果我们在提示内容中加入了这几项之后,至少可以更方便的提醒自己当前所处环境 , 以尽量减少犯错误的概率。 我个人的提示符定义: "\\u@\\h : \\d \\r:\\m:\\s> ",显示效果: “sky@localhost : test 04:25:45>” “--tee=name”参数也是对运维人员非常有用的参数选项,用来告诉 mysql,将所有输 入和输出内容都记录进文件。在我们一些较大维护变更的时候,为了方便被查,最好是将整 个操作过程的所有输入和输出内容都保存下来。有了“--tee=name”参数,就再也不用通过 copy 屏幕来保存操作过程了。
19. “-U, --safe-updates”,“--select_limit=#”和“--max_join_size=#”三个参数都 是出于性能相关考虑的参数。使用“-U, --safe-updates”参数之后,将禁止所有不能使用 索引的 update 和 delete 操作的请求,“--select_limit=#”的使用前提是有“-U, --safeupdates”参数,功能是限制查询记录的条数, “--max_join_size=#”也需要与“-U, --safeupdates”一起使用,限制参与 join 的最大记录数。 “--show-warnings”参数作用是在执行完每一条 query 之后都会自动执行一次“show warnings”,显示出最后一次 warning 的内容。 上面仅仅介绍了部分不是太常使用但是很有特点的少数几个参数选项,实际上 mysql 程序支持非常多的参数选项,有其自身的参数,也有提交给 MySQL Server 的。mysql 的所 有参数选项都可以写在 MySQL Server 启动参数文件(my.cnf)的[mysql]参数 group 中,还 有部分连接选项参数会从[client]参数 group 中读取,这样很多参数就可以不用在每次执行 mysql 的时候都手工输入,而由 mysql 程序自己自动从 my.cnf 文件 load 这些参数。 如果读者朋友希望对 mysql 其他参数选项或者 mysql 的其他更国有图有更深入的了解, 可以通过 MySQL 官方参考手册查阅,也可以通过执行“mysql --help”得到帮助信息之后通 过自行实验来做进一步的深刻认识。当然如果您是一位基本能看懂 c 语言的朋友,那么您完 全可以通过 mysql 程序的源代码来发现其更多有趣的内容。 2、mysqladmin Usage: mysqladmin [OPTIONS] command command ... mysqadmin,顾名思义,提供的功能都是与 MySQL 管理相关的各种功能。如 MySQL Server 状态检查,各种统计信息的 flush,创 建/删除数据库,关闭 MySQL Server 等 等。mysqladmin 所能做的事情,虽然大部分都可以通过 mysql 连接登录上 MySQL Server 之后来完成,但是 大部分通过 mysqladmin 来完成操作会更简单更方便。这里我将介绍一下自己经常使用到的 几个常用功能: ping 命令可以很容易检测 MySQL Server 是否还能正常提供服务 sky@sky:~#'>sky:~#'>sky:~#'>sky:~# mysqladmin -u sky -ppwd -h localhost ping mysqld is alive status 命令可以获取当前 MySQL Server 的几个基本的状态值: sky@sky:~#'>sky:~#'>sky:~#'>sky:~# mysqladmin -u sky -ppwd -h localhost status Uptime: 20960 Threads: 1 Questions: 75 Slow queries: 0 Opens: 15 Flush tables:'>tables: 1 Open tables:'>tables: 9 Queries per second avg: 0.3 processlist 获取当前数据库的连接线程信息: sky@sky:~#'>sky:~#'>sky:~#'>sky:~# mysqladmin -u sky -ppwd -h localhost processlist +----+------+-----------+----+---------+------+-------+------------------+ Id User Host db Command Time State Info
20. +----+------+-----------+----+---------+------+-------+------------------+ 48 sky localhost Query 0 show processlist +----+------+-----------+----+---------+------+-------+------------------+ 上面的这三个功能是我在自己的一些简单监控脚本中经常使用到的,虽然得到的信息还 是比较有限,但是对于完成一些比较基本的监控来说,已经足够胜任了。此外,还可以通过 mysqladmin 来 start slave 和 stop slave,kill 某个连接到 MySQL Server 的线程等等。 3、mysqldump Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage: mysqldump [OPTIONS] database [tables] OR mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...] OR mysqldump [OPTIONS] --all-databases [OPTIONS] mysqldump 这个工具我想大部分读者可能都比较熟悉了,其功能就是将 MySQL Server 中的数据以 SQL 语句的形式从数据库中 dump 成文本文件。虽然 mysqldump 是做为 MySQL 的 一种逻辑备份工具为大家所认识,但我个人觉得称他为 SQL 生成导出工具更合适一点,因为 通过 mysqldump 所生成的文件,全部是 SQL 语句,包括数据库和表的创建语句。当然,通过 给 mysqldump 程序加“-T”选项参数之后,可以生成非 SQL 形式的指定给是的文本文件。这 个功能实际上是调用了 MySQL 中的“select * into OUTFILE from ...”语句而实现。也可 以通过“-d,--no-data”仅仅生成结构创建的语句。在声称 SQL 语句的时候,字符集设置这 一项也是比较关键的,建议每次执行 mysqldump 程序的时候都通过尽量做到“--defaultcharacter-set=name”显式指定字符集内容,以防止以错误的字符集生成不可用的内容。 mysqldump 所生成的 SQL 文件可以通过 mysql 工具执行。 4、mysqlimport Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage: mysqlimport [OPTIONS] database textfile ... mysqlimport 程序是一个将以特定格式存放的文本数据(如通过 “select * into OUTFILE from ...”所生成的数据文件)导入到指定的 MySQL Server 中的工具程序,比如 将一个标准的 csv 文件导入到某指定数据库的指定表中。mysqlimport 工具实际上也只是 “load data infile”命令的一个包装实现。 5、mysqlbinlog Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage: mysqlbinlog [OPTIONS] log-files mysqlbinlog 程序的主要功能就是分析 MySQL Server 所产生的二进制日志(也就是大 家所熟知的 binlog)。当我们希望通过之前备份的 binlog 做一些指定时间之类的恢复的时 候,mysqlbinlog 就可以帮助我们找到恢复操作需要做哪些事情。通过 mysqlbinlog,我们 可以解析出 binlog 中指定时间段或者指定日志起始和结束位置的内容解析成 SQL 语句,并 导出到指定的文件中,在解析过程中,还可以通过指定数据库名称来过滤输出内容。 6、mysqlcheck Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage:'>Usage: mysqlcheck [OPTIONS] database [tables]
21. OR OR mysqlcheck [OPTIONS] --databases DB1 [DB2 DB3...] mysqlcheck [OPTIONS] --all-databases mysqlcheck 工具程序可以检查(check),修 复( repair),分 析( analyze)和优化 (optimize)MySQL Server 中的表,但并不是所有的存储引擎都支持这里所有的四个功能, 像 Innodb 就不支持修复功能。实际上,mysqlcheck 程序的这四个功能都可以通过 mysql 连 接登录到 MySQL Server 之后来执行相应命令完成完全相同的任务。 7、myisamchk Usage:'>Usage:'>Usage:'>Usage: myisamchk [OPTIONS] tables[.MYI] 功能有点类似“mysqlcheck -c/-r”,对检查和修复 MyISAM 存储引擎的表,但只能对 MyISAM 存储引擎的索引文件有效,而且不用登录连接上 MySQL Server 即可完成操作。 8、myisampack Usage:'>Usage:'>Usage:'>Usage: myisampack [OPTIONS] filename ... 对 MyISAM 表进行压缩处理,以缩减占用存储空间,一般主要用在归档备份的场景下, 而且压缩后的 MyISAM 表会变成只读,不能进行任何修改操作。当我们希望归档备份某些历 史数据表,而又希望该表能够提供较为高效的查询服务的时候,就可以通过 myisampack 工 具程序来对该 MyISAM 表进行压缩,因为即使虽然更换成 archive 存储引擎也能够将表变成 只读的压缩表,但是 archive 表是没有索引支持的,而通过压缩后的 MyISAM 表仍然可以使 用其索引。 9、mysqlhotcopy Usage:'>Usage:'>Usage:'>Usage: mysqlhotcopy db_name[./table_regex/] [new_db_name directory] mysqlhotcopy 和其他的客户端工具程序不太一样的是他不是 c(或者 c++)程序编写的 , 而是一个 perl 脚本程序,仅能在 Unix/Linux 环境下使用。他的主要功能就是对 MySQL 中 的 MyISAM 存储引擎的表进行在线备份操作,其备份操作实际上就是通过对数据库中的表进 行加锁,然后复制其结构,数据和索引文件来完成备份操作,当然,也可以通过指定 “-noindices”告诉 mysqlhotcopy 不需要备份索引文件。 10、其他工具 除了上面介绍的这些工具程序之外,MySQL 还有自带了其他大量的工具程序,如针对离 线 Innodb 文 件 做 checksum 的 innochecksum , 转 换 mSQL C API 函 数 的 msql2mysql , dumpMyISAM 全文索引的 myisam_ftdump,分析处理 slowlog 的 mysqldumpslow,查询 mysql 相关开发包位置和 include 文件位置的 mysql_config, 向 MySQL AB 报告 bug 的 mysqlbug, 测 试 套 件 mysqltest 和 mysql_client_test , 批 量 修 改 表 存 储 引 擎 类 型 的 mysql_convert_table_format, 能 从 更 新 日 志 中 提 取 给 定 匹 配 规 则 的 query 语 句 的 mysql_find_rows,更改 MyIsam 存储引擎表后缀名的 mysql_fix_extensions,修复系统表 的 mysql_fix_privilege_tables,查看数据库相关对象结构的 mysqlshow,MySQL 升级工具 mysql_upgrade,通过给定匹配模式来 kill 客户端连接线程的 mysql_zap,查看错误号信息 的 perror,文本替换工具 replace,等 等一系列工具程序可供我们使用。如果您希望在 MySQL
22. 源代码的基础上做一些自己的修改,如修改 MyISAM 存储引擎的时候,可以利用 myisamlog 来进行跟踪分析 MyISAM 的 log。 2.4 小结 第 3 章 MySQL 存储引擎简介 前言 3.1 MySQL 存储引擎概述 MyISAM 存储引擎是 MySQL 默认的存储引擎,也是目前 MySQL 使用最为广泛的存储引擎 之一。他的前身就是我们在 MySQL 发展历程中所提到的 ISAM,是 ISAM 的升级版本。在 MySQL 最开始发行的时候是 ISAM 存储引擎,而且实际上在最初的时候,MySQL 甚至是没有存储引 擎这个概念的。MySQL 在架构上面也没有像现在这样的 sql layer 和 storage engine layer 这两个结构清晰的层次结构,当时不管是代码本身还是系统架构,对于开发者来说都很痛苦 的一件事情。到后来,MySQL 意识到需要更改架构,将前 端的业务逻辑和后端数据存储以 清晰的层次结构拆分开的同时,对 ISAM 做了功能上面的扩展和代码的重构,这就是 MyISAM 存储引擎的由来。 MySQL 在 5.1(不包括)之前的版本中,存储引擎是需要在 MySQL 安装的时候就必须和 MySQL 一起被编译并同时被安装的。也就是说,5.1 之前的 版本中,虽然存储引擎层和 sql 层的耦合已经非常少了,基本上完全是通过接口来实现交互,但是这两层之间仍然是没办法 分离的,即使在安装的时候也是一样。 但是从 MySQL5.1 开始,MySQL AB 对其结构体系做了较大的改造,并引入了一个新的概 念:插件式存储引擎体系结构。MySQL AB 在架构改造的时候,让存储引擎层和 sql 层各自 更为独立,耦合更小,甚至可以做到在线加载信的存储引擎,也就是完全可以将一个新的存 储引擎加载到一个正在 运行的 MySQL 中,而不影响 MySQL 的正常运行。插件式存储引擎的 架构,为存储引擎的加载和移出更为灵活方便,也使自行开发存储引擎更为方便简单。在 这 一点上面,目前还没有哪个数据库管理系统能够做到。 MySQL 的插件式存储引擎主要包括 MyISAM,Innodb,NDB Cluster,Maria,Falcon, Memory,Archive,Merge,Federated 等,其中最著名而且使用最为广泛的 MyISAM 和 Innodb 两种存储引擎。MyISAM 是 MySQL 最早的 ISAM 存储引擎的升级版本,也是 MySQL 默认的存储 引擎。而 Innodb 实 际上并不是 MySQ 公司的,而是第三方软件公司 Innobase(在 2005 年 被 Oracle 公司所收购)所开发,其最大的特点是提供了事务控制等特性, 所以使用者也非
23. 常广泛。 其他的一些存储引擎相对来说使用场景要稍微少一些,都是应用于某些特定的场景,如 NDB Cluster 虽然也支持事务,但是主要是用于分布式环境,属于一个 share nothing 的分 布式数据库存储引擎。Maria 是 MySQL 最新开发(还没有发布最终的 GA 版本)的对 MyISAM 的升级版存储引擎,Falcon 是 MySQL 公司自行研发的为了替代当前的 Innodb 存储引擎的一 款带有事务等高级特性的数据库存储引擎,目前正在研发阶段。Memory 存储引擎所有数 据 和索引均存储于内存中,所以主要是用于一些临时表,或者对性能要求极高,但是允许在西 噢他嗯 Crash 的时候丢失数据的特定场景下。Archive 是一 个数据经过高比例压缩存放的 存储引擎,主要用于存放过期而且很少访问的历史信息,不支持索引。Merge 和 Federated 在严格意义上来说,并不能算 作一个存储引擎。因为 Merge 存储引擎主要用于将几个基表 merge 到一起,对外作为一个表来提供服务,基表可以基于其他的几个存储引擎。而 Federated 实际上所做的事情,有点类似于 Oracle 的 dblink,主要用于远程存取其他 MySQL 服务器上面的数据。 3.2 MyISAM 存储引擎简介 MyISAM 存储引擎的表在数据库中,每一个表都被存放为三个以表名命名的物理文件。 首先肯定会有任何存储引擎都不可缺少的存放表结构定义信息的.frm 文件,另外还有.MYD 和.MYI 文件,分别存放了表的数据(.MYD)和索引数据(.MYI)。每个表都有且仅有这样三 个文件做为 MyISAM 存储类型的表的存储,也就是说不管这个表有多少个索引,都是存放在 同一个.MYI 文件中。 MyISAM 支持以下三种类型的索引: 1、B-Tree 索引 B-Tree 索引,顾名思义,就是所有的索引节点都按照 balance tree 的数据结构来 存储,所有的索引数据节点都在叶节点。 2、R-Tree 索引 R-Tree 索引的存储方式和 b-tree 索引有一些区别,主要设计用于为存储空间和多 维数据的字段做索引,所以目前的 MySQL 版本来说,也仅支持 geometry 类型的字段作索引。 3、Full-text 索引 Full-text 索引就是我们长说的全文索引,他的存储结构也是 b-tree。主要是为了 解决在我们需要用 like 查询的低效问题。 MyISAM 上面三种索引类型中,最经常使用的就是 B-Tree 索引了,偶尔会使用到 Fulltext,但是 R-Tree 索引一般系统中都是很少用到的。另外 MyISAM 的 B-Tree 索引有一个较 大的限制,那就是参与一个索引的所有字段的长度之和不能超过 1000 字节。 虽然每一个 MyISAM 的表都是存放在一个相同后缀名的.MYD 文件中,但是每个文件的存 放格式实际上可能并不是完全一样的,因为 MyISAM 的数据存放格式是分为静态(FIXED)固
24. 定长度、动态(DYNAMIC)可变长度以及压缩(COMPRESSED)这三种格式。当然三种格式中 是否压缩是完全可以任由我们自己选择的,可以在创建表的时候通过 ROW_FORMAT 来指定 {COMPRESSED DEFAULT},也可以通过 myisampack 工具来进行压缩,默认是不压缩的。而 在非压缩的情况下,是静态还是动态,就和我们表中个字段的定义相关了。只要表中有可变 长度类型的字段存在,那么该表就肯定是 DYNAMIC 格式的,如果没有任何可变长度的字段, 则为 FIXED 格式,当然,你也可以通过 alter table 命令,强行将一个带有 VARCHAR 类型字 段的 DYNAMIC 的表转换为 FIXED,但是所带来的结果是原 VARCHAR 字段类型会被自动转换成 CHAR 类型。相反如果将 FIXED 转换为 DYNAMIC,也会将 CHAR 类型字段转换为 VARCHAR 类型, 所以大家手工强行转换的操作一定要谨慎。 MyISAM 存储引擎的表是否足够可靠呢?在 MySQL 用户参考手册中列出在遇到如下情况 的时候可能会出现表文件损坏: 1、当 mysqld 正在做写操作的时候被 kill 掉或者其他情况造成异常终止; 2、主机 Crash; 3、磁盘硬件故障; 4、MyISAM 存储引擎中的 bug? MyISAM 存储引擎的某个表文件出错之后,仅影响到该表,而不会影响到其他表,更不 会影响到其他的数据库。如果我们的出据苦正在运行过程中发现某个 MyISAM 表出现问题了, 则可以在线通过 check table 命令来尝试校验他,并可以通过 repair table 命令来尝试修 复。在数据库关闭状态下,我们也可以通过 myisamchk 工具来对数据库中某个(或某些)表 进行检测或者修复。不过强烈建议不到万不得已不要轻易对表进行修复操作,修复之前尽量 做好可能的备份工作,以免带来不必要的后果。 另外 MyISAM 存储引擎的表理论上是可以被多个数据库实例同时使用同时操作的,但是 不论是我们都不建议这样做,而且 MySQL 官方的用户手册中也有提到,建议尽量不要在多个 mysqld 之间共享 MyISAM 存储文件。 3.3 Innodb 存储引擎简介 在 MySQL 中使用最为广泛的除了 MyISAM 之外,就非 Innodb 莫属了。Innodb 做为第三 方公司所开发的存储引擎,和 MySQL 遵守相同的开源 License 协议。 Innodb 之所以能如此受宠,主要是在于其功能方面的较多特点: 1、支持事务安装 Innodb 在功能方面最重要的一点就是对事务安全的支持, 这无疑是让 Innodb 成为 MySQL 最为流行的存储引擎之一的一个非常重要原因。而且实现了 SQL92 标准所定义的所有四个级 别(READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ 和 SERIALIZABLE)。对事务安 全的支持,无疑让很多之前因为特殊业务要求而不得不放弃使用 MySQL 的用户转向支持 MySQL,以及之前对数据库选型持观望态度的用户,也大大增加了对 MySQL 好感。 2、数据多版本读取
25. Innodb 在事务支持的同时,为了保证数据的一致性已经并发时候的性能,通过对 undo 信息,实现了数据的多版本读取。 3、锁定机制的改进 Innodb 改变了 MyISAM 的锁机制,实现了行锁。虽然 Innodb 的行锁机制的实现是通过 索引来完成的,但毕竟在数据库中 99%的 SQL 语句都是要使用索引来做检索数据的。所以, 行锁定机制也无疑为 Innodb 在承受高并发压力的环境下增强了不小的竞争力。 4、实现外键 Innodb 实现了外键引用这一数据库的重要特性,使在数据库端控制部分数据的完整性 成为可能。虽然很多数据库系统调优专家都建议不要这样做,但是对于不少用户来说在数据 库端加如外键控制可能仍然是成本最低的选择。 除了以上几个功能上面的亮点之外,Innodb 还有很多其他一些功能特色常常带给使用 者不小的惊喜,同时也为 MySQL 带来了更多的客户。 在物理存储方卖弄,Innodb 存储引擎也和 MyISAM 不太一样,虽然也有.frm 文件来存放 表结构定义相关的元数据,但是表数据和索引数据是存放在一起的。至于是每个表单独存放 还是所有表存放在一起,完全由用户来决定(通过特定配置),同时还支持符号链接。 Innodb 的物理结构分为两大部分: 1、数据文件(表数据和索引数据) 存放数据表中的数据和所有的索引数据,包括主键和其他普通索引。在 Innodb 中,存 在了表空间(tablespace)这样一个概念,但是他和 Oracle 的表空间又有较大的不同。首 先,Innodb 的表空间分为两种形式。一种是共享表空间,也就是所有表和索引数据被存放 在同一个表空间(一个或多个数据文件)中,通过 innodb_data_file_path 来指定,增加数 据文件需要停机重启。 另外一种是独享表空间,也就是每个表的数据和索引被存放在一个 单独的.ibd 文件中。 虽然我们可以自行设定使用共享表空间还是独享表空间来存放我们的表,但是共享表空 间都是必须存在的,因为 Innodb 的 undo 信息和其他一些元数据信息都是存放在共享表空间 里面的。共享表空间的数据文件是可以设置为固定大小和可自动扩展大小两种形式的,自动 扩展形式的文件可以设置文件的最大大小和每次扩展量。在创建自动扩展的数据文件的时 候,建议大家最好加上最大尺寸的属性,一个原因是文件系统本身是有一定大小限制的(但 是 Innodb 并不知道),还有一个原因就是自身维护的方便。另外,Innodb 不仅可以使用文 件系统,还可以使用原始块设备,也就是我们常说的裸设备。 当我们的文件表空间快要用完的时候,我们必须要为其增加数据文件,当然,只有共享 表空间有此操作。共享表空间增加数据文件的操作比较简单,只需要在 innodb_data_file_path 参数后面按照标准格式设置好文件路径和相关属性即可,不过这里 有一点需要注意的,就是 Innodb 在创建新数据文件的时候是不会创建目录的,如果指定目 录不存在,则会报错并无法启动。另外一个较为令人头疼的就是 Innodb 在给共享表空间增 加数据文件之后,必须要重启数据库系统才能生效,如果是使用裸设备,还需要有两次重启 。 这也是我一直不太喜欢使用共享表空间而选用独享表空间的原因之一。
26. 2、日志文件 Innodb 的日志文件和 Oracle 的 redo 日志比较类似,同样可以设置多个日志组(最少 2 个) ,同样采用轮循策略来顺序的写入,甚至在老版本中还有和 Oracle 一样的日志归档特性 。 如果你的数据库中有创建了 Innodb 的表,那么千万别全部删除 innodb 的日志文件,因为很 可能就会让你的数据库 crash,无法启动,或者是丢失数据。 由于 Innodb 是事务安全的存储引擎,所以系统 Crash 对他来说并不能造成非常严重的 损失,由于有 redo 日志的存在,有 checkpoint 机制的保护,Innodb 完全可以通过 redo 日 志将数据库 Crash 时刻已经完成但还没有来得及将数据写入磁盘的事务恢复,也能够将所有 部分完成并已经写入磁盘的未完成事务回滚并将数据还原。 Innodb 不仅在功能特性方面和 MyISAM 存储引擎有较大区别,在配置上面也是单独处理 的。在 MySQL 启动参数文件设置中,Innodb 的所有参数基本上都带有前缀“innodb_”,不 论是 innodb 数据和日志相关,还是其他一些性能,事务等等相关的参数都是一样。和所有 Innodb 相关的系统变量一样,所有的 Innodb 相关的系统状态值也同样全部以“Innodb_” 前缀。当然,我们也完全可以仅仅通过一个参数(skip-innodb)来屏蔽 MySQL 中的 Innodb 存储引擎,这样即使我们在安装编译的时候将 Innodb 存储引擎安装进去了,使用者也无法 创建 Innodb 的表。 3.4 NDB Cluster 存储引擎简介 NDB 存储引擎也叫 NDB Cluster 存储引擎,主要用于 MySQL Cluster 分布式集群环境, Cluster 是 MySQL 从 5.0 版本才开始提供的新功能。这部分我们可能并不仅仅只是介绍 NDB 存储引擎,因为离开了 MySQL CLuster 整个环境,NDB 存储引擎也将失去太多意义。 所以 这一节主要是介绍一下 MySQL Cluster 的相关内容。 简单的说,Mysql Cluster 实际上就是在无共享存储设备的情况下实现的一种内存数据 库 Cluster 环境,其主要是通过 NDB Cluster(简称 NDB)存储引擎来实现的。 一般来说,一个 Mysql Cluster 的环境主要由以下三部分组成: a) 负责管理各个节点的 Manage 节点主机: 管理节点负责整个 Cluster 集群中各个节点的管理工作,包括集群的配置,启动关闭 各节点,以及实施数据的备份恢复等。管理节点会获取整个 Cluster 环境中各节点的状态和 错误信息,并且将各 Cluster 集群中各个节点的信息反馈给整个集群中其他的所有节点。由 于管理节点上保存在整个 Cluster 环境的配置,同时担任了集群中各节点的基本沟通工作, 所以他必须是最先被启动的节点。 b) SQL 层的 SQL 服务器节点(后面简称为 SQL 节点),也就是我们常说的 Mysql Server: 主要负责实现一个数据库在存储层之上的所有事情,比如连接管理,query 优化和响 应,cache 管理等等,只有存储层的工作交给了 NDB 数据节点去处理了。也就是说,在纯粹 的 Mysql Cluster 环境中的 SQL 节点,可以被认为是一个不需要提供任何存储引擎的 Mysql 服务器,因为他的存储引擎有 Cluster 环境中的 NDB 节点来担任。所以,SQL 层各 Mysql 服
27. 务器的启动与普通的 Mysql 启动有一定的区别,必须要添加 ndbcluster 项,可以添加在 my.cnf 配置文件中,也可以通过启动命令行来指定。 c) Storage 层的 NDB 数据节点,也就是上面说的 NDB Cluster: NDB 是一个内存式存储引擎也就是说,他会将所有的数据和索引数据都 load 到内存中 , 但也会将数据持久化到存储设备上。不过,最新版本,已经支持用户自己选择数据可以不全 部 Load 到内存中了,这对于有些数据量太大或者基于成本考虑而没有足够内存空间来存放 所有数据的用户来说的确是一个大好消息。 NDB 节点主要是实现底层数据存储的功能,保存 Cluster 的数据。每一个 NDB 节点保存 完整数据的一部分(或者一份完整的数据,视节点数目和配置而定),在 MySQL CLuster 里 面叫做一个 fragment。而每一个 fragment,正常情况来讲都会在其他的主机上面有一份(或 者多分)完全相同的镜像存在。这些都是通过配置来完成的,所以只要配置得当,Mysql Cluster 在存储层不会出现单点的问题。一般来说, NDB 节点被组织成一个一个的 NDB Group, 一个 NDB Group 实际上就是一组存有完全相同的物理数据的 NDB 节点群。 上面提到了 NDB 各个节点对数据的组织,可能每个节点都存有全部的数据也可能只保存 一部分数据,主要是受节点数目和参数来控制的。首先在 Mysql Cluster 主配置文件(在管 理节点上面,一般为 config.ini)中,有一个非常重要的参数叫 NoOfReplicas,这个参数 指定了每一份数据被冗余存储在不同节点上面的份数,该参数一般至少应该被设置成 2,也 只需要设置成 2 就可以了。因为正常来说,两个互为冗余的节点同时出现故障的概率还是非 常小的,当然如果机器和内存足够多的话,也可以继续增大。一个节点上面是保存所有的数 据还是一部分数据,还受到存储节点数目的限制。NDB 存储引擎首先保证 NoOfReplicas 参 数配置的要求对数据冗余,来使用存储节点,然后再根据节点数目将数据分段来继续使用多 余的 NDB 节点,分段的数目为节点总数除以 NoOfReplicas 所得。 MySQL Cluster 本身所包含的内容非常之多,出于篇幅考虑,这里暂时不做很深入的介 绍,在本书的架构设计部分的高可用性设计一章中将会有更为详细的介绍与实施细节,大家 也可以通过 MySQL 官方文档来进一步了解部分细节。 3.5 其他存储引擎介绍 3.5.1 Merge 存储引擎: MERGE 存储引擎,在 MySQL 用户手册中也提到了,也被大家认识为 MRG_MyISAM 引擎。 Why?因为 MERGE 存储引擎可以简单的理解为其功能就是实现了对结构相同的 MyISAM 表 ,通 过一些特殊的包装对外提供一个单一的访问入口,以达到减小应用的复杂度的目的。要创建 MERGE 表,不仅仅基表的结构要完全一致,包括字段的顺序,基表的索引也必须完全一致。 MERGE 表本身并不存储数据,仅仅只是为多个基表提供一个同意的存储入口。所以在创 建 MERGE 表的时候,MySQL 只会生成两个较小的文件,一个是.frm 的结构定义文件,还有一 个.MRG 文件,用于存放参与 MERGE 的表的名称(包括所属数据库 schema)。之所以需要有所
28. 属数据库的 schema,是因为 MERGE 表不仅可以实现将 Merge 同一个数据库中的表,还可以 Merge 不同数据库中的表,只要是权限允许,并且在同一个 mysqld 下面,就可以进行 Merge。 MERGE 表在被创建之后,仍然可以通过相关命令来更改底层的基表。 MERGE 表不仅可以提供读取服务,也可以提供写入服务。要让 MERGE 表提供可 INSERT 服务,必须在在表被创建的时候就指明 INSERT 数据要被写入哪一个基表,可以通过 insert_method 参数来控制。如果没有指定该参数,任何尝试往 MERGE 表中 INSERT 数据的 操作,都会出错。此外,无法通过 MERGE 表直接使用基表上面的全文索引,要使用全文索引 , 必须通过基表本身的存取才能实现。 3.5.2 Memory 存储引擎: Memory 存储引擎,通过名字就很容易让人知道,他是一个将数据存储在内存中的存储 引擎。Memory 存储引擎不会将任何数据存放到磁盘上,仅仅存放了一个表结构相关信息 的.frm 文件在磁盘上面。所以一旦 MySQL Crash 或者主机 Crash 之后,Memory 的表就只剩 下一个结构了。Memory 表支持索引,并且同时支持 Hash 和 B-Tree 两种格式的索引。由于 是存放在内存中,所以 Memory 都是按照定长的空间来存储数据的,而且不支持 BLOB 和 TEXT 类型的字段。Memory 存储引擎实现页级锁定。 既然所有数据都存放在内存中,那么他对内存的消耗量是可想而知的。在 MySQL 的用户 手册上面有这样一个公式来计算 Memory 表实际需要消耗的内存大小: SUM_OVER_ALL_BTREE_KEYS(max_length_of_key + sizeof(char*) * 4) + SUM_OVER_ALL_HASH_KEYS(sizeof(char*) * 2) + ALIGN(length_of_row+1, sizeof(char*)) 3.5.3 BDB 存储引擎: BDB 存储引擎全称为 BerkeleyDB 存储引擎,和 Innodb 一样,也不是 MySQL 自己开发实 现的一个存储引擎,而是由 Sleepycat Software 所提供,当然,也是开源存储引擎,同样 支持事务安全。 BDB 存储引擎的数据存放也是每个表两个物理文件,一个.frm 和一个.db 的文件,数据 和索引信息都是存放在.db 文件中。此外,BDB 为了实现事务安全,也有自己的 redo 日 志 , 和 Innodb 一样,也可以通过参数指定日志文件存放的位置。在锁定机制方面,BDB 和 Memory 存储引擎一样,实现页级锁定。 由于 BDB 存储引擎实现了事务安全,那么他肯定也需要有自己的 check point 机 制 。BDB 在每次启动的时候,都会做一次 check point,并且将之前的所有 redo 日志清空。在运行 过程中,我们也可以通过执行 flush logs 来手工对 BDB 进行 check point 操作。
29. 3.5.4 FEDERATED 存储引擎: FEDERATED 存储引擎所实现的功能,和 Oracle 的 DBLINK 基本相似,主要用来提供对远 程 MySQL 服务器上面的数据的访问借口。如果我们使用源码编译来安装 MySQL,那么必须手 工指定启用 FEDERATED 存储引擎才行,因为 MySQL 默认是不起用该存储引擎的。 当我们创建一个 FEDERATED 表的时候,仅仅在本地创建了一个表的结构定义信息的文件 而已,所有数据均实时取自远程的 MySQL 服务器上面的数据库。 当我们通过 SQL 操作 FEDERATED 表的时候,实现过程基本如下: a、SQL 调用被本地发布 b、MySQL 处理器 API(数据以处理器格式) c、MySQL 客户端 API(数据被转换成 SQL 调用) d、远程数据库-> MySQL 客户端 API e、转换结果包(如果有的话)到处理器格式 f、处理器 API -> 结果行或受行影响的对本地的计数 3.5.5 ARCHIVE 存储引擎: ARCHIVE 存储引擎主要用于通过较小的存储空间来存放过期的很少访问的历史数据。 ARCHIVE 表不支持索引,通过一个.frm 的结构定义文件,一个.ARZ 的数据压缩文件还有一 个.ARM 的 meta 信息文件。由于其所存放的数据的特殊性,ARCHIVE 表不支持删除,修改操 作,仅支持插入和查询操作。锁定机制为行级锁定。 3.5.6 BLACKHOLE 存储引擎: BLACKHOLE 存储引擎是一个非常有意思的存储引擎,功能恰如其名,就是一个“黑洞”。 就像我们 unix 系统下面的“/dev/null”设备一样,不管我们写入任何信息,都是有去无回 。 那么 BLACKHOLE 存储引擎对我们有什么用呢?在我最初接触 MySQL 的时候我也有过同样的疑 问,不知道 MySQL 提供这样一个存储引擎给我们的用意为何?但是后来在又一次数据的迁移 过程中,正是 BLACKHOLE 给我带来了非常大的功效。在那次数据迁移过程中,由于数据需要 经过一个中转的 MySQL 服务器做一些相关的转换操作,然后再通过复制移植到新的服务器上 面。可当时我没有足够的空间来支持这个中转服务器的运作。这时候就显示出 BLACKHOLE 的功效了,他不会记录下任何数据,但是会在 binlog 中记录下所有的 sql。而这些 sql 最 终都是会被复制所利用,并实施到最终的 slave 端。 MySQL 的用户手册上面还介绍了 BLACKHOLE 存储引擎其他几个用途如下: a、SQL 文件语法的验证。 b、来自二进制日志记录的开销测量,通过比较允许二进制日志功能的 BLACKHOLE 的性
30. 能与禁止二进制日志功能的 BLACKHOLE 的性能。 c、因为 BLACKHOLE 本质上是一个“no-op” 存储引擎,它可能被用来查找与存储引擎 自身不相关的性能瓶颈。 3.5.7 CSV 存储引擎: CSV 存储引擎实际上操作的就是一个标准的 CSV 文件,他不支持索引。起主要用途就是 大家有些时候可能会需要通过数据库中的数据导出成一份报表文件,而 CSV 文件是很多软件 都支持的一种较为标准的格式,所以我们可以通过先在数据库中建立一张 CVS 表,然后将生 成的报表信息插入到该表,即可得到一份 CSV 报表文件了。 3.6 小结 多存储引擎是 MySQL 有别于其他数据库管理软件的最大特色,不同的存储引擎有不同 的特点,可以应对不同的应用场景,这让我们在实际的应用中可以根据不同的应用特点来选 择最有利的存储引擎,给了我们足够的灵活性。通过这一章对 MySQL 各个存储引擎的初步 了解,我想各位读者朋友应该已经对 MySQL 的主要存储引擎有了一定的认识,在后续的章 节中对于一些常用的存储引擎还会有更为深入的介绍。
31. 第 4 章 MySQL 安全管理 前言 对于任何一个企业来说,其数据库系统中所保存数据的安全性无疑是非常重要的,尤其 是公司的有些商业数据,可能数据就是公司的根本,失去了数据的安全性,可能就是失去了 公司的一切。本章将针对 MySQL 的安全相关内容进行较为详细的介绍。 4.1 数据库系统安全相关因素 一、外围网络: MySQL 的大部分应用场景都是基于网络环境的,而网络本身是一个充满各种入侵危险 的环境,所以要保护他的安全,在条件允许的情况下,就应该从最外围的网络环境开始“布 防”,因为这一层防线可以从最大范围内阻止可能存在的威胁。 在网络环境中,任意两点之间都可能存在无穷无尽的“道路”可以抵达,是一个真正“条 条道路通罗马”的环境。在那许许多多的道路中,只要有一条道路不够安全,就可能被入侵 者利用。当然,由于所处的环境不同,潜在威胁的来源也会不一样。有些 MySQL 所处环境是 暴露在整个广域网中,可以说是完全“裸露”在任何可以接入网络环境的潜在威胁者面前。 而有些 MySQL 是在一个环境相对小一些的局域网之内,相对来说,潜在威胁者也会少很多。 处在局域网之内的 MySQL,由于有局域网出入口的网络设备的基本保护,相对于暴露在广域 网中要安全不少,主要威胁对象基本上控制在了可以接入局域网的内部潜在威胁者,和极少 数能够突破最外围防线(局域网出入口的安全设备)的入侵者。所以,尽可能的让我们的 MySQL 处在一个有保护的局域网之中,是非常必要的。 二、主机: 有了网络设备的保护,我们的 MySQL 就足够安全了么?我想大家都会给出否定的回答。 因为即使我们局域网出入口的安全设备足够的强大,可以拦截住外围试图入侵的所有威胁 者,但如果威胁来自局域网内部呢?比如局域网中可能存在被控制的设备,某些被控制的有 权限接入局域网的设备,以及内部入侵者等都仍然是威胁者。所以说,吉使在第一层防线之 内,我们仍然存在安全风险,局域网内部仍然会有不少的潜在威胁存在。 这个时候就需要我们部署第二道防线“主机层防线”了 。“主机层防线”主要拦截网络 (包括局域网内)或者直连的未授权用户试图入侵主机的行为。因为一个恶意入侵者在登录 到主机之后,可能通过某些软件程序窃取到那些自身安全设置不够健壮的数据库系统的登入 口令,从而达到窃取或者破坏数据的目的。如一个主机用户可以通过一个未删除且未设置密 码的无用户名本地帐户轻易登入数据库,也可以通过 MySQL 初始安装好之后就存在的无密码 的“root@localhost”用户登录数据库并获得数据库最高控制权限。
32. 非法用户除了通过登入数据库获取(或者破坏)数据之外,还可能通过主机上面相关权 限设置的漏洞,跳过数据库而直接获取 MySQL 数据(或者日志)文件达到窃取数据的目的, 或者直接删除数据(或者日志)文件达到破坏数据的目的。 三、数据库: 通过第二道防线“主机层防线”的把守,我们又可以挡住很大一部分安全威胁者。但仍 然可能有极少数突破防线的入侵者。而且即使没有任何“漏网之鱼”,那些有主机登入权限 的使用者呢?是否真的就是完全可信任对象?No,我们不能轻易冒这个潜在风险。对于一个 有足够安全意识的管理员来说,是不会轻易放任任何一个潜在风险存在的。 这个时候,我们的第三道防线, “数据库防线”就需要发挥他的作用了。“数据库防线” 也就是 MySQL 数据库系统自身的访问控制授权管理相关模块。这道防线基本上可以说是 MySQL 的最后一道防线了,也是最核心最重要的防线。他首先需要能够抵挡住在之前的两层 防线都没有能够阻拦住的所有入侵威胁,同时还要能够限制住拥有之前二层防线自由出入但 不具备数据库访问权限的潜在威胁者,以确保数据库自身的安全以及所保存数据的安全。 之前的二层防线对于所有数据库系统来说基本上区别不大,都存在着基本相同的各种威 胁,不论是 Oracle 还是 MySQL,以及任何其他的数据库管理系统,都需要基本一致的“布 防”策略。但是这第三层防线,也就是各自自身的“数据库防线”对于每个数据库系统来说 都存在较大的差异,因为每种数据库都有各自不太一样的专门负责访问授权相关功能的模 块。不论是权限划分还是实现方式都可能不太一样。 对于 MySQL 来说,其访问授权相关模块主要是由两部分组成。一个是基本的用户管理模 块,另一个是访问授权控制模块。用户管理模块的功能相对简单一些,主要是负责用户登录 连接相关的基本权限控制,但其在安全控制方面的作用却不比任何环节小。他就像 MySQL 的一个“大门门卫”一样,通过校验每一位敲门者所给的进门“暗号”(登入口令),决定是 否给敲门者开门。而访问授权控制模块则是随时随地检查已经进门的访问者,校验他们是否 有访问所发出请求需要访问的数据的权限。通过校验者可顺利拿到数据,而未通过校验的访 问者,只能收到“访问越权了”的相关反馈。 上面的三道防线组成了如图 4-1 所示的三道坚固的安全保护壁垒,就像三道坚固的城墙 一样保护这 MySQL 数据库中的数据。只要保障足够,基本很难有人能够攻破这三道防线。
33. 图 4-1 四、代码: 1、SQL 语句相关安全因素: “SQL 注入攻击”这个术语我想大部分读者朋友都听说过了?指的就是攻击者根据数据 库的 SQL 语句解析器的原理,利用程序中对客户端所提交数据的校验漏洞,从而通过程序动 态提交数据接口提交非法数据,达到攻击者的入侵目的。 “SQL 注入攻击”的破坏性非常的大,轻者造成数据被窃取,重者数据遭到破坏,甚至 可能丢失全部的数据。如果读者朋友还不是太清楚何为“SQL 注入攻击”,建议通过互联网 搜索一下,可以得到非常多非常详细的介绍及案例分析,这里有不做详细介绍了。 2、程序代码相关安全因素: 程序代码如果权限校验不够仔细而存在安全漏洞,则同样可能会被入侵者利用,达到窃 取数据等目的。比如,一个存在安全漏洞的信息管理系统,很容易就可能窃取到其他一些系 统的登入口令。之后,就能堂而皇之的轻松登录相关系统达到窃取相关数据的目的。甚至还 可能通过应用系统中保存不善的数据库系统连接登录口令,从而带来更大的损失。 4.2 MySQL 权限系统介绍
34. 4.2.1 权限系统简介 MySQL 的权限系统在实现上比较简单,相关权限信息主要存储在几个被称为 grant tables 的系 统 表 中 , 即 : mysql.User, mysql.db, mysql.Host, mysql.table_priv 和 mysql.column_priv。由于权限信息数据量比较小,而且访问又非常频繁,所以 Mysql 在启 动的时候,就会将所有的权限信息都 Load 到内存中保存在几个特定的结构中。所以才有我 们每次手工修改了权限相关的表之后,都需要执行“FLUSH PRIVILEGES”命令重新加载 MySQL 的权限信息。当然,如果我们通过 GRANT,REVOKE 或者 DROP USER 命令来修改相关权限,则 不需要手工执行 FLUSH PRIVILEGES 命令,因为通过 GRANT,REVOKE 或者 DROP USER 命令所 做的权限修改在修改系统表的同时也会更新内存结构中的权限信息。在 MySQL5.0.2 或更高 版本的时候,MySQL 还增加了 CREATE USER 命令,以此创建无任何特别权限(仅拥有初始 USAGE 权限)的用户,通过 CREATE USER 命令创建新了新用户之后,新用户的信息也会自动更新到 内存结构中。所以,建议读者一般情况下尽量使用 GRANT,REVOKE,CREATE USER 以及 DROP USER 命令来进行用户和权限的变更操作,尽量减少直接修改 grant tables 来实现用户和权 限变更的操作。 4.2.2 权限授予与去除 要为某个用户授权,可以使用 GRANT 命令,要去除某个用户已有的权限则使用 REVOKE 命令。当然,出了这两者之外还有一种比较暴力的办法,那就是直接更新 grant tables 系 统表。当给某个用户授权的时候,不仅需要指定用户名,同时还要指定来访主机。如果在授 权的时候仅指定用户名,则 MySQL 会自动认为是对'username'@'%'授权。要去除某个用户的 的权限同样也需要指定来访主机。 可能有些时候我们还会需要查看某个用户目前拥有的权限,这可以通过两个方式实现, 首先是通过执行“SHOW GRANTS FOR 'username'@'hostname'” 命令来获取之前该用户身上 的所有授权。另一种方法是查询 grant tables 里面的权限信息。 4.2.3 权限级别 MySQL 中的权限分为五个级别,分别如下: 1、Global Level: Global Level 的权限控制又称为全局权限控制,所有权限信息都保存在 mysql.user 表 中。Global Level 的所有权限都是针对整个 mysqld 的,对所有的数据库下的所有表及所有 字段都有效。如果一个权限是以 Global Level 来授予的,则会覆盖其他所有级别的相同权 限设置。比如我们首先给 abc 用户授权可以 UPDATE 指定数据库如 test 的 t 表,然后又在 全局级别 REVOKE 掉了 abc 用户对所有数据库的所有表的 UPDATE 权限。则这时候的 abc 用户 将不再拥有用对 test.t 表的更新权限。Global Level 主要有如下这些权限(见表 4-1):
35. 表 4-1 名称 版本支持 限制信息 ALTER ALL 表结构更改权限 ALTER ROUTINE 5.0.3+ procedure,function 和 trigger 等的 变更权限 CREATE ALL 数据库,表和索引的创建权限 CREATE ROUTINE 5.0.3+ procedure,function 和 trigger 等的 变更权限 CREATE TABLES 4.0.2+ 临时表的创建权限 CREATE USER 5.0.3+ 创建用户的权限 CREATE VIEW 5.0.1+ 创建视图的权限 DELETE All 删除表数据的权限 DROP All 删除数据库对象的权限 EXECUTE 5.0.3+ procedure,function 和 trigger 等的 执行权限 FILE All 执行 LOAD DATA INFILE 和 SELECT ... INTO FILE 的权限 INDEX All 在已有表上创建索引的权限 INSERT All 数据插入权限 LOCK TABLES 4.0.2+ 执行 LOCK TABLES 命令显示给表加锁的 权限 PROCESS All 执行 SHOW PROCESSLIST 命令的权限 RELOAD All 执行 FLUSH 等让数据库重新 Load 某些对 象或者数据的命令的权限 REPLICATION CLIENT 4.0.2+ 执 行 SHOW MASTER STATUS 和 SHOW SLAVE STATUS 命令的权限 REPLICATION SLAVE 4.0.2+ 复制环境中 Slave 连接用户所需要的复 制权限 SELECT All 数据查询权限 SHOW DATABASES 4.0.2+ 执行 SHOW DATABASES 命令的权限 SHOW VIEW 5.0.1+ 执 行 SHOW CREATE VIEW 命 令 查 看 view 创建语句的权限 SHUTDOWN All MySQL Server 的 shut down 权 限( 如 通 过 mysqladmin 执行 shutdown 命令所使 用的连接用户) SUPER 4.0.2+ 执 行 kill 线 程 , CHANGE MASTER, PURGE MASTER LOGS, and SET GLOBAL 等命令的权限 UPDATE All 更新数据的权限 USAGE All 新创建用户后不授任何权限的时候所拥 有的最小权限 TEMPORARY
36. 要授予 Global Level 的权限,则只需要在执行 GRANT 命令的时候,用“*.*”来指定适 用范围是 Global 的即可,当有多个权限需要授予的时候,也并不需要多次重复执行 GRANT 命令,只需要一次将所有需要的权限名称通过逗号(“,”)分隔开即可,如下: root@localhost : mysql 05:14:35> GRANT SELECT,UPDATE,DELETE,INSERT ON *.* TO 'def'@'localhost'; Query OK, 0 rows affected (0.00 sec) 2、Database Level Database Level 是在 Global Level 之下,其他三个 Level 之上的权限级别,其作用域 即为所指定整个数据库中的所有对象。与 Global Level 的权限相比,Database Level 主要 少了以下几个权限:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER 和 USAGE 这几个权限,没有增加任何权限。之 前我们说过 Global Level 的权限会覆盖底下其他四层的相同权限,Database Level 也一样 , 虽然他自己可能会被 Global Level 的权限设置所覆盖,但同时他也能覆盖比他更下层的 Table,Column 和 Routine 这三层的权限。 如果要授予 Database Level 的权限,则可以有两种实现方式: 1、在执行 GRANT 命令的时候,通过“database.*”来限定权限作用域为 database 整个 数据库,如下: root@localhost : mysql 06:06:26> GRANT ALTER ON test.* TO 'def'@'localhost'; Query OK, 0 rows affected (0.00 sec) root@localhost : test 06:12:45> SHOW GRANTS FOR def@localhost; +------------------------------------------------------------------+ Grants for def@localhost +------------------------------------------------------------------+ GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' GRANT ALTER ON `test`.* TO 'def'@'localhost' +------------------------------------------------------------------+ 2、先通过 USE 命令选定需要授权的数据库,然后通过“*”来限定作用域,这样授权的 作用域实际上就是当前选定的整个数据库。 root@localhost : mysql 06:14:05> USE test; Database changed root@localhost : test 06:13:10> GRANT DROP ON * TO 'def'@'localhost'; Query OK, 0 rows affected (0.00 sec) root@localhost : test 06:15:26> SHOW GRANTS FOR def@localhost; +------------------------------------------------------------------+ Grants for def@localhost +------------------------------------------------------------------+ GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' GRANT DROP, ALTER ON `test`.* TO 'def'@'localhost'
37. +------------------------------------------------------------------+ 在授予权限的时候,如果有相同的权限需要授予多个用户,我们也可以在授权语句中一 次写上多个用户信息,通过逗号(,)分隔开就可以了,如下: root@localhost : mysql 05:22:32> grant create on perf.* to 'abc'@'localhost','def'@'localhost'; Query OK, 0 rows affected (0.00 sec) root@localhost : mysql 05:22:46> SHOW GRANTS FOR def@localhost; +------------------------------------------------------------------+ Grants for def@localhost +------------------------------------------------------------------+ GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'def'@'localhost' GRANT DROP, ALTER ON `test`.* TO 'def'@'localhost' GRANT CREATE ON `perf`.* TO 'def'@'localhost' +-----------------------------------------------------------------+ 3 rows in set (0.00 sec) root@localhost : mysql 05:23:13> SHOW GRANTS FOR abc@localhost; +------------------------------------------------------------------+ Grants for abc@localhost +------------------------------------------------------------------+ GRANT CREATE ON `perf`.* TO 'abc'@'localhost' GRANT SELECT ON `test`.* TO 'abc'@'localhost' +------------------------------------------------------------------+ 3 rows in set (0.00 sec) 3、Table Level Database Level 之下就是 Table Level 的权限了,Table Level 的权限可以被 Global Level 和 Database Level 的权限所覆盖,同时也能覆盖 Column Level 和 Routine Level 的 权限。 Table Level 的权限作用范围是授权语句中所指定数据库的指定表。如可以通过如下语 句给 test 数据库的 t1 表授权: root@localhost : test 12:02:15> GRANT INDEX ON test.t1 TO 'abc'@'%.jianzhaoyang.com'; Query OK, 0 rows affected, 1 warning (0.00 sec) root@localhost : test 12:02:53> SHOW GRANTS FOR 'abc'@'%.jianzhaoyang.com'; +----------------------------------------------------------+ Grants for abc@*.jianzhaoyang.com +----------------------------------------------------------+ GRANT USAGE ON *.* TO 'abc'@'%.jianzhaoyang.com'
38. GRANT INDEX ON `test`.`t1` TO 'abc'@'%.jianzhaoyang.com' +----------------------------------------------------------+ 上面的授权语句在测试给 test 数据库的 t1 表授予 Table Level 的权限的同时,还测试 了将权限授予含有通配符“%”的所有“.jianzhaoyang.com”主机。其中的 USAGE 权限是每 个用户都有的最基本权限。 Table Level 的权限由于其作用域仅限于某个特定的表,所以权限种类也比较少,仅有 ALTER,CREATE,DELETE,DROP,INDEX,INSERT,SELECT UPDATE 这八种权限。 4、Column Level Column Level 的权限作用范围就更小了,仅仅是某个表的指定的某个(活某些)列。 由于权限的覆盖原则,Column Level 的权限同样可以被 Global,Database,Table 这三个 级别的权限中的相同级别所覆盖,而且由于 Column Level 所针对的权限和 Routine Level 的权限作用域没有重合部分,所以不会有覆盖与被覆盖的关系。针对 Column Level 级别的 权限仅有 INSERT,SELECT 和 UPDATE 这 三 种 。Column Level 的权限授权语句语法基本和 Table Level 差不多,只是需要在权限名称后面将需要授权的列名列表通过括号括起来,如下: root@localhost : test 12:14:46> GRANT SELECT(id,value) ON test.t2 TO 'abc'@'%.jianzhaoyang.com'; Query OK, 0 rows affected(0.00 sec) root@localhost : test 12:16:49> SHOW GRANTS FOR 'abc'@'%.jianzhaoyang.com'; +-----------------------------------------------------------------------+ Grants for abc@*.jianzhaoyang.com +-----------------------------------------------------------------------+ GRANT USAGE ON *.* TO 'abc'@'%.jianzhaoyang.com' GRANT SELECT (value, id) ON `test`.`t2` TO 'abc'@'%.jianzhaoyang.com' GRANT INDEX ON `test`.`t1` TO 'abc'@'%.jianzhaoyang.com' +-----------------------------------------------------------------------+ 注意:当某个用户在向某个表插入(INSERT)数据的时候,如果该用户在该表中某列上 面没有 INSERT 权限,则该列的数据将以默认值填充。这一点和很多其他的数据库都有一些 区别,是 MySQL 自己在 SQL 上面所做的扩展。 5、Routine Level Routine Level 的权限主要只有 EXECUTE 和 ALTER ROUTINE 两种,主要针对的对象是 procedure 和 function 这两种对象,在授予 Routine Level 权限的时候,需要指定数据库 和相关对象,如: root@localhost : test 04:03:26> GRANT EXECUTE ON test.p1 to 'abc'@'localhost'; Query OK, 0 rows affected (0.00 sec) 除了上面几类权限之外,还有一个非常特殊的权限 GRANT,拥有 GRANT 权限的用户可以
39. 将自身所拥有的任何权限全部授予其他任何用户,所以 GRANT 权限是一个非常特殊也非常重 要 的权 限。GRANT 权限的授予方式也和其他任何权限都不太一样,通常都是通过在执行 GRANT 授权语句的时候在最后添加 WITH GRANT OPTION 子句达到授予 GRANT 权限的目的。 此外,我们还可以通过 GRANT ALL 语句授予某个 Level 的所有可用权限给某个用户, 如: root@localhost : test 04:15:48> grant all on test.t5 to 'abc'; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:27:39> grant all on perf.* to 'abc'; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:27:52> show grants for 'abc'; +--------------------------------------------------+ Grants for abc@% +--------------------------------------------------+ GRANT USAGE ON *.* TO 'abc'@'%' GRANT ALL PRIVILEGES ON `perf`.* TO 'abc'@'%' GRANT ALL PRIVILEGES ON `test`.`t5` TO 'abc'@'%' +--------------------------------------------------+ 在以上五个 Level 的权限中,Table、Column 和 Routine 三者在授权中所依赖(或者引 用)的对象必须是已经存在的,而不像 Database Level 的权限授予,可以在当前不存在该 数据库的时候就完成授权。 4.2.4 MySQL 访问控制实现原理 MySQL 访问控制实际上由两个功能模块共同组成,从第一篇的第二章架构组成中可以看 到,一个是负责“看守 MySQL 大门”的用户管理模块,另一个就是负责监控来访者每一个动 作的访问控制模块。用户管理模块决定造访客人能否进门,而访问控制模块则决定每个客人 进门能拿什么不能拿什么。下面是一张 MySQL 中实现访问控制的简单流程图(见图 4-2):
40. 图 4-2
41. 1、 用户管理 我们先看看用户管理模块是如何工作的。在 MySQL 中,用户访问控制部分的实现比较简 单,所有授权用户都存放在一个系统表中:mysql.user,当然这个表不仅仅存放了授权用户 的基本信息,还存放有部分细化的权限信息。用户管理模块需要使用的信息很少,主要就是 Host,User,Password 这三项,都在 mysql.user 表中,如下: sky@localhost : (none) 12:35:04> USE mysql; Database changed sky@localhost : mysql 12:35:08> DESC user; +---------------+--------------------+------+-----+---------+-------+ Field Type Null Key Default Extra +---------------+--------------------+------+-----+---------+-------+ Host char(60) NO PRI User char(16) NO PRI Password char(41) NO ... ... +---------------+--------------------+------+-----+---------+-------+ 一个用户要想访问 MySQL,至少需要提供上面列出的这三项数据,MySQL 才能判断是否 该让他“进门”。这三项实际上由量部分组成:访问者来源的主机名(或者主机 IP 地址信息 ) 和访问者的来访“暗号”(登录用户名和登录密码),这两部分中的任何一个没有能够匹配上 都无法让看守大门的用户管理模块乖乖开门。其中 Host 信息存放的是 MySQL 允许所对应的 User 的信任主机,可以是某个具体的主机名(如:mytest)或域名(如:www.domain.com), 也可以是以“%”来充当通配符的某个域名集合(如:%.domain.com);也可以是一个具体的 IP 地址(如:1.2.3.4),同样也可以是存在通配符的域名集合(如:1.2.3.%);还可以用“%” 来代表任何主机,就是不对访问者的主机做任何限制。如以下设置: root@localhost : mysql 01:18:12> SELECT host,user,password FROM user ORDER BY user; +--------------------+------+-------------------------------------------+ host user password +--------------------+------+-------------------------------------------+ % abc *.jianzhaoyang.com abc localhost abc *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 1.2.3.4 abc *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 1.2.3.* def *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 % def *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 localhost def *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 ... ... +--------------------+------+-------------------------------------------+ 但是这里有一个比较特殊的访问限制,如果要通过 localhost 访问的话,必须要有一条 专门针对 localhost 的授权信息,即使不对任何主机做限制也不行。如下例所示,存在 def@% 的用户设置,但是如果不使用-h 参数来访问,则登录会被拒绝,因为 mysql 在默认情况下
42. 会连接 localhost: sky@sky:~$'>sky:~$'>sky:~$'>sky:~$ mysql -u def -p Enter password:'>password:'>password:'>password:'>password:'>password:'>password:'>password: ERROR 1045 (28000): Access denied for password:'>password:'>password:'>password:'>password:'>password:'>password:'>password: YES) user 'def'@'localhost' (using 但是当通过-h 参数,明确指定了访问的主机地址之后就没问题了,如下: sky@sky:~$'>sky:~$'>sky:~$'>sky:~$ mysql -u def -p -h 127.0.0.1 Enter password:'>password:'>password:'>password:'>password:'>password:'>password:'>password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 17 Server version:'>version: 5.0.51a-log Source distribution Type 'help;' or '\h' for help. Type '\c' to clear the buffer. def@127.0.0.1 : (none) 01:26:04> 如果我们有一条 localhost 的访问授权则可以不使用-h 参数来指定登录 host 而连接默 认的 localhost: sky@sky:~$'>sky:~$'>sky:~$'>sky:~$ mysql -u abc -p Enter password:'>password:'>password:'>password:'>password:'>password:'>password:'>password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 18 Server version:'>version: 5.0.51a-log Source distribution Type 'help;' or '\h' for help. Type '\c' to clear the buffer. abc@localhost : (none) 01:27:19> exit Bye 如果 MySQL 正在运行之中的时候,我们对系统做了权限调整,那调整之后的权限什么时 候会生效呢? 我们先了解何时 MySQL 存放于内存结构中的权限信息被更新:FLUSH PRIVILEGES 会强 行让 MySQL 更新 Load 到内存中的权限信息;GRANT、REVOKE 或者 CREATE USER 和 DROP USER 操作会直接更新内存中俄权限信息;重启 MySQL 会让 MySQL 完全从 grant tables 中读取权 限信息。 那内存结构中的权限信息更新之后对已经连接上的用户何时生效呢? 对于 Global Level 的权限信息的修改,仅仅只有更改之后新建连接才会用到,对于已 经连接上的 session 并不会受到影响。而对于 Database Level 的权限信息的修改,只有当 客户端请求执行了“USE database_name”命令之后,才会在重新校验中使用到新的权限信 息。 所以有些时候如果在做了比较紧急的 Global 和 Database 这两个 Level 的权限变更之后 , 可能需要通过“KILL”命令将已经连接在 MySQL 中的 session 杀掉强迫他们重新连接以使 用更新后的权限。对于 Table Level 和 Column Level 的权限,则会在下一次需要使用到该 权限的 Query 被请求的时候生效,也就是说,对于应用来讲,这两个 Level 的权限,更新之 后立刻就生效了,而不会需要执行“KILL”命令。
43. 2、 访问控制 当客户端连接通过用户管理模块的验证,可连接上 MySQL Server 之后,就会发送各种 Query 和 Command 给 MySQL Server,以实现客户端应用的各种功能。当 MySQL 接收到客户 端的请求之后,访问控制模块是需要校验该用户是否满足提交的请求所需要的权限。权限校 验过程是从最大范围的权限往最小范围的权限开始依次校验所涉及到的每个对象的每个权 限。 在验证所有所需权限的时候,MySQL 首先会查找存储在内存结构中的权限数据,首先查 找 Global Level 权限,如果所需权限在 Global Level 都有定义(GRANT 或者 REVOKE), 则完成权限校验(通过或者拒绝),如果没有找到所有权限的定义,则会继续往后查找 Database Level 权限,进行 Global Level 未定义的所需权限的校验,如果仍然没有能够 找到所有所需权限的定义,MySQL 会继续往更小范围的权限定义域查找,也就是 Table Level,最后则是 Column Level 或者 Routine Level。 下面我们就以客户端通过 abc@localhost 连接后请求如下 Query 我为例: SELECT id,name FROM test.t4 where status = 'deleted';
44. 图 4-3 在前面我们了解到 MySQL 的 grant tables 有 mysql.user,mysql.db,mysql.host, mysql.table_priv 和 mysql.column_priv 这五个,我想出了 mysql.host 之外的四个都是非
45. 常容易理解的,每一个表针对 MySQL 中的一种逻辑对象,存放某一特定 Level 的权限,唯独 mysql.host 稍有区别。我们现在就来看看 mysql.host 权限表到底在 MySQL 的访问控制中充 当了一个什么样的角色呢? mysql.host 在 MySQL 访问控制模块中所实现的功能比较特殊, 和其他几个 grant tables 不太一样。首先是 mysql.host 中的权限数据不是(也不能)通过 GRANT 或者 REVOKE 来授予 或者去除,必须通过手工通过 INSERT、UPDATE 和 DELETE 命令来修改其中的数据。其次是 其中的权限数据无法单独生效,必须通过和 mysql.db 权限表的数据一起才能生效。而且仅 当 mysql.db 中存在不完整(某些场景下的特殊设置)的时候,才会促使访问控制模块再结 合 mysql.host 中查找是否有相应的补充权限数据实现以达到权限校验的目的,就比如上图 中所示。在 mysql.db 中无法找到满足权限校验的所有条件的数据(db.User = 'abc' AND db.host = 'localhost' AND db.Database_name = 'test'),则说明在 mysql.db 中无法完 成权限校验,所以也不会直接就校验 db.Select_priv 的值是否为'Y'。但是 mysql.db 中有 db.User = 'abc' AND db.Database_name = 'test' AND db.host = '' 这样一条权限信息 存在,大家可能注意到了这条权限信息中的 db.host 中是空值,注意是空值而不是'%'这个 通配符哦。当 MySQL 注意到有这样一条权限信息存在的时候,就该是 mysql.host 中所存放 的权限信息出场的时候了。这时候,MySQL 会检测 mysql.host 中是否存在满足如下条件的 权限信息:host.Host = 'localhost' AND host.Db = 'test'。如果存在,则开始进行 Select_priv 权限的校验。由于权限信息存在于 mysql.db 和 mysql.host 两者之中,而且是 两者信息合并才能满足要求,所以 Select_priv 的校验也需要两表都为'Y'才能满足要求, 通过校验。 我们已经清楚,MySQL 的权限是授予“username@hostname”的,也就是说,至少需要 用户名和主机名二者才能确定一个访问者的权限。又由于 hostname 可以是一个含有通配符 的域名,也可以是一个含有通配符的 IP 地址段。那么如果同一个用户有两条权限信息,一 条是针对特定域名的,另外一个是含有通配符的域名,而且前者属于后者包含。这时候 MySQL 如何来确定权限信息呢?实际上 MySQL 永远优先考虑更精确范围的权限。在 MySQL 内部会按 照 username 和 hostname 作一个排序,对于相同 username 的权限,其 host 信息越接近访问 者的来源 host,则排序位置越靠前,则越早被校验使用到。而且,MySQL 在权限校验过程中 , 只要找到匹配的权限之后,就不会再继续往后查找是否还有匹配的权限信息,而直接完成校 验过程。 大家应该也看到了在 mysql.user 这个权限表中有 max_questions,max_updates, max_connections,max_user_connections 这四列,前面三列是从 MySQL4.0.2 版本才开始 有的,其功能是对访问用户进行每小时所使用资源的限制,而最后的 max_user_connections 则是从 MySQL5.0.3 版本才开始有的,他和 max_connections 的区别是限制耽搁用户的连接 总次数,而不是每小时的连接次数。而要使这四项限制生效,需要在创建用户或者给用户授 权的时候加上以下四种子句: max_questions : WITH MAX_QUERIES_PER_HOUR n; max_updates : WITH MAX_UPDATES_PER_HOUR n; max_connections : WITH MAX_CONNECTIONS_PER_HOUR n; max_user_connections: MAX_USER_CONNECTIONS。 四个子句可以同时使用,如:
46. “ WITH MAX_QUERIES_PER_HOUR MAX_USER_CONNECTIONS 10000”。 5000 MAX_CONNECTIONS_PER_HOUR 10 4.3 MySQL 访问授权策略 在我们了解了影响数据库系统安全的相关因素以及 MySQL 权限系统的工作原理之后,就 需要为我们的系统设计一个安全合理的授权策略。我想,每个人心里都清楚,要想授权最简 单最简单方便,维护工作量最少,那自然是将所有权限都授予所有的用户来的最简单方便了 。 但是,我们大家肯定也都知道,一个用户所用有的权限越大,那么他给我们的系统所带来的 潜在威胁也就越大。所以,从安全方面来考虑的话,权限自然是授予的越小越好。一个有足 够安全意识的管理员在授权的时候,都会只授予必要的权限,而不会授予任何多余的权限。 既然我们这一章是专门讨论安全的,那么我们现在也就从安全的角度来考虑如何设计一个更 为安全合理的授权策略。 首先,需要了解来访主机。 由于 MySQL 数据库登录验证用户的时候是出了用户名和密码之外,还要验证来源主机。 所以我们还需要了解每个用户可能从哪些主机发起连接。当然,我们也可以通过授权的时候 直接通过“%”通配符来给所有主机都有访问的权限,但是这样作就违背了我们安全策略的 原则,带来了潜在风险,所以并不可取。尤其是在没有局域网的防火墙保护的情况下,更是 不能轻易允许可以从任何主机登录的用户存在。能通过具体主机名或者 IP 地址指定的尽量 通过使用具体的主机名和 IP 地址来限定来访主机,不能用具体的主机名或者 IP 地址限定的 也需要用尽可能小的通配范围来限定。 其次,了解用户需求。 既然是要做到仅授予必要的权限,那么我们必须了解每个用户所担当的角色,也就是说 , 我们需要充分了解每个用户需要连接到数据库上完成什么工作。了解该用户是一个只读应用 的用户,还是一个读写都有的帐户;是一个备份作业的用户还是一个日常管理的帐户;是只 需要访问特定的某个(或者某几个)数据库(Schema),还是需要访问所有的数据库。只有 了解了需要做什么,才能准确的了解需要授予什么样的权限。因为如果权限过低,会造成工 作无法正常完成,而权限过高,则存在潜在的安全风险。 再次,要为工作分类。 为了做到各司其职,我们需要将需要做的工作分门别类,不同类别的工作使用不同的用 户,做好用户分离。虽然这样可能会带来管理成本方面的部分工作量增加,但是基于安全方 面的考虑,这部分管理工作量的增加是非常值得的。而且我们所需要做的用户分离也只是一 个适度的分离。比如将执行备份工作、复制工作、常规应用访问、只读应用访问和日常管理 工作分别分理出单独的特定帐户来授予各自所需权限。这样,既可以让安全风险尽量降低, 也可以让同类同级别的相似权限合并在一起,不互相交织在一起。对于 PROCESS,FILE 和 SUPER 这样的特殊权限,仅仅只有管理类帐号才需要,不应该授予其他非管理帐号。 最后,确保只有绝对必要者拥有 GRANT OPTION 权限。 之前在权限系统介绍的时候我们已经了解到 GRANT OPTION 权限的特殊性,和拥有该权
47. 限之后的潜在风险,所以在这里也就不再累述了。总之,为了安全考虑,拥有 GRANT OPTION 权限的用户越少越好,尽可能只让拥有超级权限的用户才拥有 GRANT OPTION 权限。 4.4 安全设置注意事项 在前面我们了解了影响数据库系统安全的几个因素,也了解了 MySQL 权限系统的相关原 理和实现,这一节我们将针对这些因素进行一些基本的安全设置讨论,了解一些必要的注意 事项。 首先,自然是最外围第一层防线的网络方面的安全。 我们首先要确定我们所维护的 MySQL 环境是否真的需要提供网络服务?是否可以使我 们的 MySQL 仅仅提供本地访问,而禁止网络服务?如果可以,那么我们可以在启动 MySQL 的时候通过使用“--skip-networking”参数选项,让 MySQL 不通过 TCP/IP 监听网络请求, 而仅仅通过命名管道或共享内存(在 Windows 中)或 Unix 套接字文件(在 Unix 中)来和客户端 连接交互。 当然,在本章最开始的时候,我们就已经讨论过,由于 MySQL 数据库在大部分应用场景 中都是在网络环境下,通过网络连接提供服务。所以我们只有少部分应用能通过禁用网络监 听来断绝网络访问以保持安全,剩下的大部分还是需要通过其他方案来解决网络方面存在的 潜在安全威胁。 使用私有局域网络。我们可以通过使用私有局域网络,通过网络设备,统一私有局域网 的出口,并通过网络防火墙设备控制出口的安全。 使用 SSL 加密通道。如果我们的数据对保密要求非常严格,可以启用 MySQL 提供的 SSL 访问接口,将传输数据进行加密。使网络传输的数据即使被截获,也无法轻易使用。 访问授权限定来访主机信息。在之前的权限系统介绍中我们已经了解到 MySQL 的权限信 息是针对用户和来访主机二者结合定位的。所以我们可以在授权的时候,通过指定主机的主 机名、域名或者 IP 地址信息来限定来访主机的范围。 其次,在第二层防线主机上面也有以下一些需要注意的地方。 OS 安全方面。关闭 MySQL Server 主机上面任何不需要的服务,这不仅能从安全方面减 少潜在隐患,还能减轻主机的部分负担,尽可能提高性能。使用网络扫描工具(如 nmap 等) 扫描主机端口,检查除了 MySQL 需要监听的端口 3306(或者自定义更改后的某个端口)之 外,还有哪些端口是打开正在监听的,并去掉不必要的端口。严格控制 OS 帐号的管理,以 防止帐号信息外泄,尤其是 root 和 mysql 帐号。对 root 和 mysql 等对 mysql 的相关文件有 特殊操作权限的 OS 帐号登录后做出比较显眼的提示,并在 Terminal 的提示信息中输出当前 用户信息,以防止操作的时候经过多次用户切换后出现人为误操作。 用非 root 用户运行 MySQL。这在 MySQL 官方文档中也有非常明显的提示,提醒用户不 要使用 root 用户来运行 MySQL。因为如果使用 root 用户运行 MySQL,那么 mysqld 的进程就 会拥有 root 用户所拥有的权限,任何具有 FILE 权限的 MySQL 用户就可以在 MySQL 中向系统 中的任何位置写入文件。当然,由于 MySQL 不接受操作系统层面的认证,所以任何操作系统
48. 层级的帐号都不能直接登录 MySQL,这一点和 Oracle 的权限认证有些区别,所以在这一方 面我们可以减少一些安全方面的顾虑。 文件和进程安全。合理设置文件的权限属性,MySQL 相关的数据和日志文件和所在的文 件夹属主和所属组都设置为 mysql,且禁用其他所有用户(除了拥有超级权限的用户,如 root)的读写权限。以防止数据或者日志文件被窃取或破坏。因为如果一个用户对 MySQL 的数据文件有读取权限的话,可以很容易将数据复制。binlog 文件也很容易还原整个数据 库。而如果有写权限的话就更糟了,因为有了写权限,数据或者日志文件就有被破坏或者删 除的风险存在。保护好 socket 文件的安全,尽量不要使用默认的位置(如/tmp/mysql.sock), 以防止被有意或无意的删除。 确保 MySQL Server 所在的主机上所必要运行的其他应用或者服务足够安全,避免因为 其他应用或者服务存在安全漏洞而被入侵者攻破防线。 在 OS 层面还有很多关于安全方面的其他设置和需要注意的地方,但考虑到篇幅问题, 这里就不做进一步分析了,有兴趣的读者可以参考各种不同 OS 在安全方面的专业书籍。 再次,就是最后第三道防线 MySQL 自身方面的安全设置注意事项。 到了最后这道防线上,我们有更多需要注意的地方。 用户设置。我们必须确保任何可以访问数据库的用户都有一个比较复杂的内容作为密 码,而不是非常简单或者比较有规律的字符,以防止被使用字典破解程序攻破。在 MySQL 初始安装完成之后,系统中可能存在一个不需要任何密码的 root 用户,有些版本安装完成 之后还会存在一个可以通过 localhost 登录的没有用户名和密码的帐号。这些帐号会给系统 带来极大的安全隐患,所以我们必须在正式启用之前尽早删除,或者设置一个比较安全的密 码。对于密码数据的存放,也不要存放在简单的文本文件之中,而应该使用专业密码管理软 件来管理(如 KeePass)。同时,就像之前在网络安全注意事项部分讲到的那样,尽可能为 每一个帐户限定一定范围的可访问主机。尤其是拥有超级权限的 MySQL root 帐号,尽量确 保只能通过 localhost 访问。 安全参数。在 MySQL 官方参考手册中也有说明,不论是从安全方面考虑还是从性能以及 功能稳定性方面考虑,不需要使用的功能模块尽量都不要启用。例如,如果不需要使用用户 自定义函数,就不要在启动的时候使用“--allow-suspicious-udfs”参数选项,以防止被 别有居心的潜在威胁者利用此功能而对 MySQL 的安全造成威胁;不需要从本地文件中 Load 数据到数据库中,就使用“--local-infile=0”禁用掉可以从客户端机器上 Load 文件到数 据库中;使用新的密码规则和校验规则(不要使用“--old-passwords”启动数据库),这项 功能是为了兼容旧版本的密码校验方式的,如无额数必要,不要使用该功能,旧版本的密码 加密方式要比新的方式在安全方面弱很多。 除了以上这三道防线,我们还应该让连接 MySQL 数据库的应用程序足够安全,以防止入 侵者通过应用程序中的漏洞而入侵到应用服务器,最终通过应用程序中的数据库相关关配置 而获取数据库的登录口令。
49. 4.5 小结 安全无小事,一旦安全出了问题一切都完了。数据的安全是一个企业安全方面最核心最 重要的内容,只有保障的数据的安全,企业才有可能真正“安全”。希望这一章 MySQL 安全 方面的内容能够对各位读者在构筑安全的企业级 MySQL 数据库系统中带来一点帮助。 第 5 章 MySQL 备份与恢复 前言 数据库的备份与恢复一直都是 DBA 工作中最为重要的部分之一,也是基本工作之一。 任何正式环境的数据库都必须有完整的备份计划和恢复测试,本章内容将主要介绍 MySQL 数据库的备份与恢复相关内容。 5.1 数据库备份使用场景 你真的明白了自己所做的数据库备份是要面对什么样的场景的吗? 我想任何一位维护过数据库的人都知道数据库是需要备份的,也知道备份数据库是数据 库维护必不可少的一件事情。那么是否每一个人都知道自己所做的备份到底是为了应对哪些 场景的呢?抑或者说我们每个人是否都很清楚的知道,为什么一个数据库需要作备份呢?读 到这里,我想很多读者朋友都会嗤之以鼻,“备份的作用不就是为了防止原数据丢失吗,这 谁不知道?”。确实,数据库的备份很大程度上的作用,就是当我们的数据库因为某些原因 而造成部分或者全部数据丢失后,方便找回丢失的数据。但是,不同类型的数据库备份,所 能应付情况是不一样的,而且,数据库的备份同时也还具有其他很多的作用。而且我想,每 个人对数据库备份的作用的理解可能都会有部分区别。 下面我就列举一下我个人理解的我们能够需要用到数据库备份的一些比较常见的情况 吧。 一、数据丢失应用场景 1、人为操作失误造成某些数据被误操作; 2、软件 BUG 造成数据部分或者全部丢失; 3、硬件故障造成数据库数据部分或全部丢失; 4、安全漏洞被入侵数据被恶意破坏; 二、非数据丢失应用场景
50. 5、特殊应用场景下基于时间点的数据恢复; 6、开发测试环境数据库搭建; 7、相同数据库的新环境搭建; 8、数据库或者数据迁移; 上面所列出的只是一些常见的应用场景而已,除了上面这几种场景外,数据库备份还会 有很多其他应用场景,这里就不一一列举了。那么各位读者过曾经或是现在所做的数据库备 份到底是为了应对以上哪一种(或者几种)场景?或者说,我们所做的数据库备份能够应对 以上哪几种应用场景?不知道这个问题大家是否有考虑过。 我们必须承认,没有哪一种数据库备份能够解决所有以上列举的几种常见应用场景,即 使仅仅只是数据丢失的各种场景都无法通过某一种数据库备份完美的解决,当然也就更不用 说能够解决所有的备份应用场景了。 比如当我们遇到磁盘故障,丢失了整个数据库的所有数据,并且无法从已经出现故障的 硬盘上面恢复出来的时候,我们可能必须通过一个实时或者有短暂时间差的复制备份数据库 存在。当然如果没有这样的一个数据库,就必须要有最近时间的整个数据库的物理或者逻辑 备份数据,并且有该备份之后的所有物理或者逻辑增量备份,以期望尽可能将数据恢复到出 现故障之前最近的时间点。而当我们遇到认为操作失误造成数据被误操作之后,我们需要有 一个能恢复到错误操作时间点之前的瞬间的备份存在,当然这个备份可能是整个数据库的备 份,也可以仅仅只是被误操作的表的备份。而当我们要做跨平台的数据库迁移的时候,我们 所需要的又只能是一个逻辑的数据库备份,因为平台的差异可能使物理备份的文件格式在两 个平台上无法兼容。 既然没有哪一种很多中数据库备份能够完美的解决所有的应用场景,而每个数据库环境 所需要面对的数据库备份应用场景又可能各不一样,可能只是需要面对很多种场景中的某一 种或几种,那么我们就非常有必要指定一个合适的备份方案和备份策略,通过最简单的技术 和最低廉的成本,来满足我们的需求。 5.2 逻辑备份与恢复测试 5.2.1 什么样的备份是数据库逻辑备份呢? 大家都知道,数据库在返回数据给我们使用的时候都是按照我们最初所设计期望的具有 一定逻辑关联格式的形式一条一条数据来展现的,具有一定的商业逻辑属性,而在物理存储 的层面上数据库软件却是按照数据库软件所设计的某种特定格式经过一定的处理后存放。 数据库逻辑备份就是备份软件按照我们最初所设计的逻辑关系,以数据库的逻辑结构对 象为单位,将数据库中的数据按照预定义的逻辑关联格式一条一条生成相关的文本文件,以 达到备份的目的。
51. 5.2.2 常用的逻辑备份 逻辑备份可以说是最简单,也是目前中小型系统最常使用的备份方式。在 MySQL 中我们 常用的逻辑备份主要就是两种,一种是将数据生成可以完全重现当前数据库中数据的 INSERT 语句,另外一种就是将数据通过逻辑备份软件,将我们数据库表数据以特定分隔符 进行分隔后记录在文本文件中。 1、生成 INSERT 语句备份 两种逻辑备份各有优劣,所针对的使用场景也会稍有差别,我们先来看一下生成 INSERT 语句的逻辑备份。 在 MySQL 数据库中,我们一般都是通过 MySQL 数据库软件自带工具程序中的 mysqldump 来实现声称 INSERT 语句的逻辑备份文件。其使用方法基本如下: Dumping definition and data mysql database or table Usage: mysqldump [OPTIONS] database [tables] OR mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...] OR mysqldump [OPTIONS] --all-databases [OPTIONS] 由于 mysqldump 的使用方法比较简单,大部分需要的信息都可以通过运行“mysqldump -help”而获得。这里我只想结合 MySQL 数据库的一些概念原理和大家探讨一下当我们使用 mysqldump 来做数据库逻辑备份的时候有些什么技巧以及需要注意一些什么内容。 我们都知道,对于大多数使用数据库的软件或者网站来说,都希望自己数据库能够提供 尽可能高的可用性,而不是时不时的就需要停机停止提供服务。因为一旦数据库无法提供服 务,系统就无法再通过存取数据来提供一些动态功能。所以对于大多数系统来说如果要让每 次备份都停机来做可能都是不可接受的,可是 mysqldump 程序的实现原理是通过我们给的参 数信息加上数据库中的系统表信息来一个表一个表获取数据然后生成 INSERT 语句再写入备 份文件中的。这样就出现了一个问题,在系统正常运行过程中,很可能会不断有数据变更的 请求正在执行,这样就可能造成在 mysqldump 备份出来的数据不一致。也就是说备份数据很 可能不是同一个时间点的数据,而且甚至可能都没办法满足完整性约束。这样的备份集对于 有些系统来说可能并没有太大问题,但是对于有些对数据的一致性和完整性要求比较严格系 统来说问题就大了,就是一个完全无效的备份集。 对于如此场景,我们该如何做?我们知道,想数据库中的数据一致,那么只有两种情况 下可以做到。 第一、同一时刻取出所有数据; 第二、数据库中的数据处于静止状态。 对于第一种情况,大家肯定会想,这可能吗?不管如何,只要有两个以上的表,就算我 们如何写程序,都不可能昨晚完全一致的取数时间点啊。是的,我们确实无法通过常规方法 让取数的时间点完全一致,但是大家不要忘记,在同一个事务中,数据库是可以做到所读取 的数据是处于同一个时间点的。所以,对于事务支持的存储引擎,如 Innodb 或者 BDB 等 ,
52. 我们就可以通过控制将整个备份过程控制在同一个事务中,来达到备份数据的一致性和完整 性,而且 mysqldump 程序也给我们提供了相关的参数选项来支持该功能,就是通过 “-single-transaction”选项,可以不影响数据库的任何正常服务。 对于第二种情况我想大家首先想到的肯定是将需要备份的表锁定,只允许读取而不允许 写入。是的,我们确实只能这么做。我们只能通过一个折衷的处理方式,让数据库在备份过 程中仅提供数据的查询服务,锁定写入的服务,来使数据暂时处于一个一致的不会被修改的 状态,等 mysqldump 完成备份后再取消写入锁定,重新开始提供完整的服务。mysqldump 程 序自己也提供了相关选项如“--lock-tables”和“--lock-all-tables”,在执行之前会锁 定表,执行结束后自动释放锁定。这里有一点需要注意的就是, “--lock-tables”并不是一 次性将需要 dump 的所有表锁定,而是每次仅仅锁定一个数据库的表,如果你需要 dump 的表 分别在多个不同的数据库中,一定要使用“--lock-all-tables”才能确保数据的一致完整 性。 当通过 mysqldump 生成 INSERT 语句的逻辑备份文件的时候,有一个非常有用的选项可 以供我们使用,那就是“--master-data[=value]”。当添加了“--master-data=1”的时候, mysqldump 会将当前 MySQL 使用到 binlog 日志的名称和位置记录到 dump 文件中,并且是被 以 CHANGE_MASTER 语句的形式记录,如果仅仅只是使用“--master-data”或者“--masterdata=2”,则 CHANGE_MASTER 语句会以注释的形式存在。这个选项在实施 slave 的在线搭建 的时候是非常有用的,即使不是进行在线搭建 slave,也可以在某些情况下做恢复的过程中 通过备份的 binlog 做进一步恢复操作。 在某些场景下,我们可能只是为了将某些特殊的数据导出到其他数据库中,而又不希望 通过先建临时表的方式来实现,我们还可以在通过 mysqldump 程序的“—where='wherecondition'”来实现,但只能在仅 dump 一个表的情况下使用。 其实除了以上一些使用诀窍之外,mysqldump 还提供了其他很多有用的选项供大家在不 同的场景下只用,如通过“--no-data”仅仅 dump 数据库结构创建脚本,通过“--no-createinfo”去掉 dump 文件中创建表结构的命令等等,感兴趣的读者朋友可以详细阅读 mysqldump 程序的使用介绍再自行测试。 2、生成特定格式的纯文本备份数据文件备份 除了通过生成 INSERT 命令来做逻辑备份之外,我们还可以通过另外一种方式将数据库 中的数据以特定分隔字符将数据分隔记录在文本文件中,以达到逻辑备份的效果。这样的备 份数据与 INSERT 命令文件相比,所需要使用的存储空间更小,数据格式更加清晰明确,编 辑方便。但是缺点是在同一个备份文件中不能存在多个表的备份数据,没有数据库结构的重 建命令。对于备份集需要多个文件,对我们产生的影响无非就是文件多了维护和恢复成本增 加,但这些基本上都可以通过编写一些简单的脚本来实现 那我们一般可以使用什么方法来生成这样的备份集文件呢,其实 MySQL 也已经给我们实 现的相应的功能。 在 MySQL 中一般都使用以下两种方法来获得可以自定义分隔符的纯文本备份文件。 1、通过执行 SELECT ... TO OUTFILE FROM ...命令来实现
53. 在 MySQL 中提供了一种 SELECT 语法,专供用户通过 SQL 语句将某些特定数据以指定格 式输出到文本文件中,同时也提供了实用工具和相关的命令可以方便的将导出文件原样再导 入到数据库中。正不正是我们做备份所需要的么? 该命令有几个需要注意的参数如下: 实现字符转义功能的“FIELDS ESCAPED BY ['name']” 将 SQL 语句中需要转义的字符 进行转义; 可以将字段的内容“包装”起来的“FIELDS [OPTIONALLY] ENCLOSED BY 'name'”,如 果不使用“OPTIONALLY”则包括数字类型的所有类型数据都会被“包装”,使 用“OPTIONALLY” 之后,则数字类型的数据不会被指定字符“包装”。 通过"FIELDS TERMINATED BY"可以设定每两个字段之间的分隔符; 而通过“LINES TERMINATED BY”则会告诉 MySQL 输出文件在每条记录结束的时候需要 添加什么字符。 如以下示例: root@localhost : test 10:02:02> SELECT * INTO OUTFILE '/tmp/dump.text' -> FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' -> LINES TERMINATED BY '\n' -> FROM test_outfile limit 100; Query OK, 100 rows affected (0.00 sec) root@localhost : test 10:02:11> exit Bye root@sky:/tmp# cat dump.text 350021,21,"A","abcd" 350022,22,"B","abcd" 350023,23,"C","abcd" 350024,24,"D","abcd" 350025,25,"A","abcd" ... ... 2、通过 mysqldump 导出 可能我们都知道 mysqldump 可以将数据库中的数据以 INSERT 语句的形式生成相关备份 文件,其实除了生成 INSERT 语句之外,mysqldump 还同样能实现上面 “SELECT ... TO OUTFILE FROM ...”所实现的功能,而且同时还会生成一个相关数据库结构对应的创建脚本 。 如以下示例: root@sky:~#'>sky:~#'>sky:~#'>sky:~# ls -l /tmp/mysqldump total 0 root@sky:~#'>sky:~#'>sky:~#'>sky:~# mysqldump -uroot -T/tmp/mysqldump test test_outfile --fieldsenclosed-by=\" --fields-terminated-by=, root@sky:~#'>sky:~#'>sky:~#'>sky:~# ls -l /tmp/mysqldump total 8 -rw-r--r-- 1 root root 1346 2008-10-14 22:18 test_outfile.sql -rw-rw-rw- 1 mysql mysql 2521 2008-10-14 22:18 test_outfile.txt
54. root@sky:~#'>sky:~# cat /tmp/mysqldump/test_outfile.txt 350021,21,"A","abcd" 350022,22,"B","abcd" 350023,23,"C","abcd" 350024,24,"D","abcd" 350025,25,"A","abcd" ... ... root@sky:~#'>sky:~# cat /tmp/mysqldump/test_outfile.sql -- MySQL dump 10.11 --- Host: localhost Database: test -- ------------------------------------------------------- Server version 5.0.51a-log /*!40101 /*!40101 /*!40101 /*!40101 /*!40103 /*!40103 /*!40101 /*!40111 SET SET SET SET SET SET SET SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; NAMES utf8 */; @OLD_TIME_ZONE=@@TIME_ZONE */; TIME_ZONE='+00:00' */; @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='' */; @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; --- Table structure for table `test_outfile` -DROP TABLE IF EXISTS `test_outfile`; SET @saved_cs_client = @@character_set_client; SET character_set_client = utf8; CREATE TABLE `test_outfile` ( `id` int(11) NOT NULL default '0', `t_id` int(11) default NULL, `a` char(1) default NULL, `mid` varchar(32) default NULL ) ENGINE=MyISAM DEFAULT CHARSET=utf8; SET character_set_client = @saved_cs_client; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 /*!40101 /*!40101 /*!40101 SET SET SET SET SQL_MODE=@OLD_SQL_MODE */; CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
55. /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2008-10-14 14:18:23 这样的输出结构对我们做为备份来使用是非常合适的,当然如果一次有多个表需要被 dump,就会针对每个表都会生成两个相对应的文件。 5.2.3 逻辑备份恢复方法 仅仅有了备份还是不够啊,我们得知道如何去使用这些备份,现在我们就看看上面所做 的逻辑备份的恢复方法: 由于所有的备份数据都是以我们最初数据库结构的设计相关的形式所存储,所以逻辑备 份的恢复也相对比较简单。当然,针对两种不同的逻辑备份形式,恢复方法也稍有区别。下 面我们就分别针对这两种逻辑备份文件的恢复方法做一个简单的介绍。 1、INSERT 语句文件的恢复: 对于 INSERT 语句形式的备份文件的恢复是最简单的,我们仅仅只需要运行该备份文件 中的所有(或者部分)SQL 命令即可。首先,如果需要做完全恢复,那么我们可以通过使用 “mysql < backup.sql”直接调用备份文件执行其中的所有命令,将数据完全恢复到备份时 候的状态。如果已经使用 mysql 连接上了 MySQL,那么也可以通过在 mysql 中执行“source /path/backup.sql”或者“\. /path/backup.sql”来进行恢复。 2、纯数据文本备份的恢复: 如果是上面第二中形式的逻辑备份,恢复起来会稍微麻烦一点,需要一个表一个表通过 相关命令来进行恢复,当然如果通过脚本来实现自动多表恢复也是比较方便的。恢复方法也 有两个,一是通过 MySQL 的“LOAD DATA INFILE”命令来实现,另一种方法就是通过 MySQL 提供的使用工具 mysqlimport 来进行恢复。 逻辑备份能做什么?不能做什么? 在清楚了如何使用逻辑备份进行相应的恢复之后,我们需要知道我们可以利用这些逻辑 备份做些什么。 1、通过逻辑备份,我们可以通过执行相关 SQL 或者命令将数据库中的相关数据完全恢 复到备份时候所处的状态,而不影响不相关的数据; 2、通过全库的逻辑备份,我们可以在新的 MySQL 环境下完全重建出一个于备份时候完 全一样的数据库,并且不受 MySQL 所处的平台类型限制; 3、通过特定条件的逻辑备份,我们可以将某些特定数据轻松迁移(或者同步)到其他 的 MySQL 或者另外的数据库环境; 4、通过逻辑备份,我们可以仅仅恢复备份集中的部分数据而不需要全部恢复。 在知道了逻辑备份能做什么之后,我们必须还要清楚他不能做什么,这样我们自己才能 清楚的知道这样的一个备份能否满足自己的预期,是否确实是自己想要的。 1、逻辑备份无法让数据恢复到备份时刻以外的任何一个时刻;
56. 2、逻辑备份无法 5.2.4 逻辑备份恢复测试 时有听到某某的数据库出现问题,而当其信心十足的准备拿之前所做好的数据库进行恢 复的时候才发现自己的备份集不可用,或者并不能达到自己做备份时候所预期的恢复效果。 遇到这种情景的时候,恐怕每个人都会郁闷至极的。数据库备份最重要最关键的一个用途就 是当我们的数据库出现某些异常状况,需要对数据进行恢复的时候使用的。作为一个维护人 员,我们是绝对不应该出现此类低级错误的。那我们到底该如何避免此类问题呢?只有一个 办法,那就是周期性的进行模拟恢复测试,校验我们的备份集是否真的有效,是否确实能够 按照我们的备份预期进行相应的恢复。 到这里可能有人会问,恢复测试又该如何做呢,我们总不能真的将线上环境的数据进行 恢复啊?是的,线上环境的数据确实不能被恢复,但是我们为什么不能在测试环境或者其他 的地方做呢?做恢复测试只是为了验证我们的备份是否有效,是否能达到我们的预期。所以 在做恢复测试之前我们一定要先清楚的知道我们所做的备份到底是为了应用于什么样的场 景的。就比如我们做了一个全库的逻辑备份,目的可能是为了当数据库出现逻辑或者物理异 常的时候能够恢复整个数据库的数据到备份时刻,那么我们恶的恢复测试就只需要将整个逻 辑备份进行全库恢复,看是否能够成功的重建一个完整的数据库。至于恢复的数据是否和备 份时刻一致,就只能依靠我们自己来人工判断比较。此外我们可能还希望当某一个数据库对 象,比如某个表出现问题之后能够尽快的恢复该表数据到备份时刻。那么我们就可以针对单 个指定表进行抽样恢复测试。 下面我们就假想数据库主机崩溃,硬件损坏,造成数据库数据全部丢失,来做一次全库 恢复的测试示例: 当我们的数据库出现硬件故障,数据全部丢失之后,我们必须尽快找到一台新的主机以 顶替损坏的主机来恢复相应的服务。在恢复服务之前,我们首先需要重建损坏的数据库。假 设我们已经拿到了一台新的主机,MySQL 软件也已经安装就位,相关设置也都已经调整好, 就等着恢复数据库了。 我们需要取回离崩溃时间最近的一次全库逻辑备份文件,复制到准备的新主机上,启动 已经安装好的 MySQL。 由于我们有两种逻辑备份格式,每种格式的恢复方法并不一样,所以这里将对两种格式 的逻辑备份的恢复都进行示例。 1、如果是 INSERT 语句的逻辑备份 a、准备好备份文件,copy 到某特定目录,如“/tmp”下; b、通过执行如下命令执行备份集中的相关命令: mysql -uusername -p < backup.sql 或者先通过 mysql 登录到数据库中,然后再执行如下命令: root@localhost : (none) 09:59:40> source /tmp/backup.sql c、再到数据库中检查相应的数据库对象,看是否已经齐全;
57. d、抽查几个表中的数据进行人工校验,并通知开启应用内部测试校验,当所有校验都 通过之后,即可对外提供服务了。 当然上面所说的步骤都是在默认每一步都正常的前提下进行的,如果发现某一步有问 题。假若在 b 步骤出现异常,无法继续进行下去,我们首先需要根据出现的错误来排查是否 是我们恢复命令有错?是否我们的环境有问题等?等等。如果我们确认是备份文件的问题, 那么说明我们的这个备份是无效的,说明测试失败了。如果我们恢复过程很正常,但是在校 验的时候发现缺少数据库对象,或者某些对象中的数据不正确,或者根本没有数据。同样说 明我们的备份级无法满足预期,备份失败。当然,如果我们是在实际工作的恢复过程中遇到 类似情况的时候,如果还有更早的备份集,我们必须退一步使用更早的备份集做相同的恢复 操作。虽然更早的备份集中的数据可能会有些失真,但是至少可以部分恢复,而不至于丢失 所有数据。 2、如果我们是备份的以特殊分隔符分隔的纯数据文本文件 a、第一步和 INSERT 备份文件没有区别,就是将最接近崩溃时刻的备份文件准备好; b、通过特定工具或者命令将数据导入如到数据库中: 由于数据库结构创建脚本和纯文本数据备份文件分开存放,所以我们首先需要执行数据 库结构创建脚本,然后再导入数据。结构创建脚本的方法和上面第一种备份的恢复测试中的 b 步骤完全一样。 有了数据库结构之后,我们就可以导入备份数据了,如下: mysqlimport --user=name --password=pwd test --fields-enclosed-by=\" -fields-terminated-by=, /tmp/test_outfile.txt 或者 LOAD DATA INFILE '/tmp/test_outfile.txt' INTO TABLE test_outfile FIELDS TERMINATED BY '"' ENCLOSED BY ','; 后面的步骤就和备份文件为 INSERT 语句备份的恢复完全一样了,这里就不再累述。 5.3 物理备份与恢复测试 前面一节我们了解了如何使用 MySQL 的逻辑备份,并做了一个简单的逻辑备份恢复示 例,在这一节我们再一起了解一些 MySQL 的物理备份。 5.3.1 什么样的备份是数据库物理课备份 在了解 MySQL 的物理备份之前,我们需要先了解一下,什么是数据库物理备份?既然是 物理备份,那么肯定是和数据库的物理对象相对应的。就如同逻辑备份根据由我们根据业务 逻辑所设计的数据库逻辑对象所做的备份一样,数据库的物理备份就是对数据库的物理对象 所做的备份。
58. 数据库的物理对象主要由数据库的物理数据文件、日志文件以及配置文件等组成。在 MySQL 数据库中,除了 MySQL 系统共有的一些日志文件和系统表的数据文件之外,每一种存 储引擎自己还会有不太一样的物理对象,在之前第一篇的“MySQL 物理文件组成”中我们已 经有了一个基本的介绍,在下面我们将详细列出几种常用的存储引擎各自所对应的物理对象 (物理文件) ,以便在后面大家能够清楚的知道各种存储引擎在做物理备份的时候到底哪些 文件是需要备份的哪些又是不需要备份的。 5.3.2 MySQL 物理备份所需文件 MyISAM 存储引擎 MyISAM 存储引擎的所有数据都存放在 MySQL 配置中所设定的“datadir”目录下。实际 上不管我们使用的是 MyISAM 存储引擎还是其他任何存储引擎,每一个数据库都会在 “datadir”目录下有一个文件夹(包括系统信息的数据库 mysql 也是一样)。在各个数据库 中每一个 MyISAM 存储引擎表都会有三个文件存在,分别为记录表结构元数据的“.frm”文 件,存储表数据的“.MYD”文件,以及存储索引数据的“.MYI”文件。由于 MyISAM 属于非 事务性存储引擎,所以他没有自己的日志文件。所以 MyISAM 存储引擎的物理备份,除了备 份 MySQL 系统的共有物理文件之外,就只需要备份上面的三种文件即可。 Innodb 存储引擎 Innodb 存储引擎属于事务性存储引擎,而且存放数据的位置也可能与 MyISAM 存储引擎 有所不同,这主要取决于我们对 Innodb 的“”相关配置所决定。决定 Innodb 存放数据位置 的 配 置 为 “ innodb_data_home_dir ” 、 “ innodb_data_file_path ” 和 “innodb_log_group_home_dir”这三个目录位置指定参数,以及另外一个决定 Innodb 的表 空间存储方式的参数“innodb_file_per_table”。前面三个参数指定了数据和日志文件的存 放位置,最后一个参数决定 Innodb 是以共享表空间存放数据还是以独享表空间方式存储数 据。这几个参数的相关使用说明我们已经在第一篇的“MySQL 存储引擎介绍”中做了相应的 解释,在 MySQL 的官方手册中也有较为详细的说明,所以这里就不再累述了。 如 果 我 们 使 用 了 共 享 表 空 间 的 存 储 方 式 , 那 么 Innodb 需 要 备 份 备 份 “innodb_data_home_dir”和“innodb_data_file_path”参数所设定的所有数据文件, “datadir”中相应数据库目录下的所有 Innodb 存储引擎表的“.frm”文件; 而如果我们使用了独享表空间,那么我们除了备份上面共享表空间方式所需要备份的所 有文件之外,我们还需要备份“datadir”中相应数据库目录下的所有“.idb”文件,该文 件中存放的才是独享表空间方式下 Innodb 存储引擎表的数据。可能在这里有人文,既然是 使用独享表空间,那我们为什么还要备份共享表空间“才使用到”的数据文件呢?其实这是 很多人的一个共性误区,以为使用独享表空间的时候 Innodb 的所有信息就都存放在 “datadir”所设定数据库目录下的“.ibd”文件中。实际上并不是这样的, “.ibd”文件中 所存放的仅仅只是我们的表数据而已,大家都很清楚,Innodb 是事务性存储引擎,他是需 要 undo 和 redo 信息的,而不管 Innodb 使用的是共享还是独享表空间的方式来存储数据, 与事务相关的 undo 信息以及其他的一些元数据信息,都是存放在“innodb_data_home_dir”
59. 和“innodb_data_file_path”这两个参数所设定的数据文件中的。所以要想 Innodb 的物理 备 份 有 效 ,“innodb_data_home_dir”和“innodb_data_file_path”参数所设定的数据文件 不管在什么情况下我们都必须备份。 此外,除了上面所说的数据文件之外,Innodb 还有自己存放 redo 信息和相关事务信息 的日志文件在“innodb_log_group_home_dir”参数所设定的位置。所以要想 Innodb 物理备 份能够有效使用,我们还比需要备份“innodb_log_group_home_dir”参数所设定的位置的 所有日志文件。 NDB Cluster 存储引擎 NDB Cluster 存储引擎(其实也可以说是 MySQL Cluster)的物理备份需要备份的文 件主要有一下三类: 1、 元数据(Metadata):包含所有的数据库以及表的定义信息; 2、 表数据(Table Records):保存实际数据的文件; 3、 事务日志数据(Transaction Log):维持事务一致性和完整性,以及恢复过程中所 需要的事务信息。 不论是通过停机冷备份,还是通过 NDB Cluster 自行提供的在线联机备份工具,或者 是第三方备份软件来进行备份,都需要备份以上三种物理文件才能构成一个完整有效的备份 集。当然,相关的配置文件,尤其是管理节点上面的配置信息,同样也需要备份。 5.3.3 各存储引擎常用物理备份方法 由于不同存储引擎所需要备份的物理对象(文件)并不一样,且每个存储引擎对数据文 件的一致性要求也不一样所以各个存储引擎在进行物理备份的时候所使用的备份方法也有 区别。当然,如果我们是要做冷备份(停掉数据库之后的备份) ,我们所需要做的事情都很 简单,那就是直接 copy 所有数据文件和日志文件到备份集需要存放的位置即可,不管是何 种存储引擎都可以这样做。由于冷备份方法简单,实现容易,所以这里就不详细说明了。 在我们的实际应用环境中,是很少有能够让我们可以停机做日常备份的情况的,我们只 能在数据库提供服务的情况下来完成数据库备份。这也就是我们俗称的热物理备份了。下面 我们就针对各个存储引擎单独说明各自最常用的在线(热)物理备份方法。 MyISAM 存储引擎 上面我们介绍了 MyISAM 存储引擎文件的物理文件比较集中,而且不支持事务没有 redo 和 undo 日志,对数据一致性的要求也并不是特别的高,所以 MyISAM 存储引擎表的物理备份 也比较简单,只要将 MyISAM 的物理文件 copy 出来即可。但是,虽然 MyISAM 存储引擎没有 事务支持,对数据文件的一致性要求没有 Innodb 之类的存储引擎那么严格,但是 MyISAM 存储引擎的同一个表的数据文件和索引文件之间是有一致性要求的。当 MyISAM 存储引擎发 现某个表的数据文件和索引文件不一致的时候,会标记该表处于不可用状态,并要求你进行 修复动作,当然,一般情况下的修复都会比较容易。但是,即使数据库存储引擎本身对数据 文件的一致性要求并不是很苛刻,我们的应用也允许数据不一致吗?我想答案肯定是否定
60. 的,所以我们自己必须至少保证数据库在备份时候的数据是处于某一个时间点的,这样就要 求我们必须做到在备份 MyISAM 数据库的物理文件的时候让 MyISAM 存储引擎停止写操作,仅 仅提供读服务,其根本实质就是给数据库表加锁来阻止写操作。 MySQL 自己提供了一个使用程序 mysqlhotcopy,这个程序就是专门用来备份 MyISAM 存 储引擎的。不过如果你有除了 MyISAM 之外的其他非事务性存储引擎,也可以通过合适的参 数设置,或者微调该备份脚本,也都能通过 mysqlhotcopy 程序来完成相应的备份任务,基 本用法如下: mysqlhotcopy db_name[./table_regex/] [new_db_name directory] 从上面的基本使用方法我们可以看到,mysqlhotcopy 出了可以备份整个数据库,指定 的某个表,还可以通过正则表达式来匹配某些表名来针对性的备份某些表。备份结果就是指 定数据库的文件夹下包括所有指定的表的相应物理文件。 mysqlhotcopy 是一个用 perl 编写的使用程序,其主要实现原理实际上就是通过先 LOCK 住表,然后执行 FLUSH TABLES 动作,该正常关闭的表正常关闭,将该 fsync 的数据都 fsync, 然后通过执行 OS 级别的复制(cp 等)命令,将需要备份的表或者数据库的所有物理文件都 复制到指定的备份集位置。 此外,我们也可以通过登录数据库中手工加锁,然后再通过操作系统的命令来复制相关 文件执行热物理备份,且在完成文件 copy 之前,不能退出加锁的 session(因为退出会自 动解锁),如下: root@localhost : test 08:36:35> FLUSH TABLES WITH READ LOCK; Query OK, 0 rows affected (0.00 sec) 不退出 mysql,在新的终端下做如下备份: mysql@sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$ cp -R test /tmp/backup/test mysql@sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$ ls -l /tmp/backup/ total 4 drwxr-xr-x 2 mysql mysql 4096 2008-10-19 21:57 test mysql@sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$'>sky:/data/mysql/mydata$ ls -l /tmp/backup/test total 39268 -rw-r----- 1 mysql mysql 8658 2008-10-19 21:57 hotcopy_his.frm -rw-r----- 1 mysql mysql 36 2008-10-19 21:57 hotcopy_his.MYD -rw-r----- 1 mysql mysql 1024 2008-10-19 21:57 hotcopy_his.MYI -rw-r----- 1 mysql mysql 8586 2008-10-19 21:57 memo_test.frm ... ... -rw-rw---- 1 mysql mysql 8554 2008-10-19 22:01 test_csv.frm -rw-rw---- 1 mysql mysql 0 2008-10-19 22:01 test_csv.MYD -rw-rw---- 1 mysql mysql 1024 2008-10-19 22:01 test_csv.MYI -rw-r----- 1 mysql mysql 8638 2008-10-19 21:57 test_myisam.frm
61. -rw-r----- 1 -rw-r----- 1 -rw-r----- 1 -rw-r----- 1 -rw-r----- 1 ... ... mysql mysql mysql mysql mysql mysql 20999600 2008-10-19 21:57 test_myisam.MYD mysql 10792960 2008-10-19 21:57 test_myisam.MYI mysql 8638 2008-10-19 21:57 test_outfile.frm mysql 2400 2008-10-19 21:57 test_outfile.MYD mysql 1024 2008-10-19 21:57 test_outfile.MYI 然后再在之前的执行锁定命令的 session 中解锁 root@localhost : test 10:00:57> unlock tables; Query OK, 0 rows affected (0.00 sec) 这样就完成了一次物理备份,而且大家也从文件列表中看到了,备份中还有 CSV 存储引 擎的表。 Innodb 存储引擎 Innodb 存储引擎由于是事务性存储引擎,有 redo 日志和相关的 undo 信息,而且对数 据的一致性和完整性的要求也比 MyISAM 要严格很多,所以 Innodb 的在线(热)物理备份要 比 MyISAM 复杂很多,一般很难简单的通过几个手工命令来完成,大都是通过专门的 Innodb 在线物理备份软件来完成。 Innodb 存储引擎的开发者(Innobase 公司)开发了一款名为 ibbackup 的商业备份软件 , 专门实现 Innodb 存储引擎数据的在线物理备份功能。该软件可以在 MySQL 在线运行的状态 下,对数据库中使用 Innodb 存储引擎的表进行备份,不过仅限于使用 Innodb 存储引擎的 表。 由于这款软件并不是开源免费的产品,我个人也很少使用,主要也是下载的试用版试用 而已,所以这里就不详细介绍了,各位读者朋友可以通过 Innobase 公司官方网站获取详细 的使用手册进行试用 NDB Cluster 存储引擎 NDB Cluster 存储引擎也是一款事务性存储引擎,和 Innodb 一样也有 redo 日志。NDB Cluter 存储引擎自己提供了备份功能,可以通过相关的命令实现。当然,停机冷备的方法 也是有效的。 在线联机备份步骤如下: 1、 连接上管理服务器; 2、 在管理节点上面执行 “START BACKUP” 命令; 3、 在管理节点上发出备份指令之后,管理节点会通知所有数据节点开始进行备份,并 反馈通知结果。 4、 管理节点在通知发出备份指令之前会生成一个备份号来唯一定位这次备份所产生 的备份集。当各数据节点收到备份指令之后,就会开始进行备份操作。 5、 当所有数据节点都完成备份之后,管理节点才会反馈“备份完成”的信息给客户端 。
62. 由于 NDB Cluster 的备份,备份指令是从管理节点发起,且并不会等待备份完成就会 返回,所以也没办法直接通过 “Ctrl + c” 或者其他方式来中断备份进程,所以 NDB Cluster 提供了相应的命令来中断当前正在进行的备份操作,如下: 1、 登录管理节点 2、 执行 “ABORT BACKUP backup_id”,命令中的 backup_id 即之前发起备份命令的 时候所产生的备份号。 3、 管理结带你上会用消息“放弃指示的备份 backup_id”确认放弃请求,注意,则时 候其实并没有收到数据节点对请求的实际回应。 4、 然后管理节点才会将中断备份的指令发送到所有数据节点上面,然后当各个数据节 点都中断备份并删除了当前产生的备份文件之后,才会返回“备份 backup_id 因* **而放弃”。至此,中断备份操作完成。 通过 NDB Cluster 存储引擎自己的备份命令来进行备份之后,会将前面所提到的三种 文件存放在参与备份的节点上面,且被存放在三个不同的文件中,类似如下: BACKUP-backup_id.node_id.ctl,内容包含相关的控制信息和元数据的控制文件。每个 节点均会将相同的表定义(对于 Cluster 中的所有表)保存在自己的该文件中。 BACKUP-backup_id-n.node_id.data,数据备份文件,被分成多个不同的片段来保存, 在备份过程中,不同的节点将保存不同的备份数据所产生的片段,每个节点保存的文件都会 有信息指明数据所属表的部分,且在备份片段文件最后还包含了最后的校验信息,以确保备 份能够正确恢复。 BACKUP-backup_id.node_id.log,事务日志备份文件中仅包含已提交事务的相关信息, 且仅保存已在备份中保存的表上的事务,各个阶段所保存的日志信息也不一样,因为仅仅针 对各节点所包含的数据记录相关的日志信息。 上面的备份文件命名规则中,backup_id 是指备份号,不同的备份集会针对有一个不 同的备份号,node_id 则是指明该备份文件属于哪个数据节点,而在数据文件的备份文件中 的 n 则是指明片段号。 5.3.4 各存储引擎常用物理备份恢复方法 和之前逻辑备份一样,光有备份是没有意义的,还需要能够将备份有效的恢复才行。物 理备份和逻辑备份相比最大的优势就是恢复速度快,因为主要是物理文件的拷贝,将备份文 件拷贝到需要恢复的位置,然后进行简单的才做即可。 MyISAM 存储引擎 MyISAM 存储引擎由于其特性,物理备份的恢复也比较简单。 如果是通过停机冷备份或者是在运行状态通过锁定写入操作后的备份集来恢复,仅仅只 需要将该备份集直接通过操作系统的拷贝命令将相应的数据文件复制到对应位置来覆盖现 有文件即可。
63. 如果是通过 mysqlhotcopy 软件来进行的在线热备份,而且相关的备份信息也记录进入 了数据库中相应的表,其恢复操作可能会需要结合备份表信息来进行恢复。 Innodb 存储引擎 对于冷备份,Innodb 存储引擎进行恢复所需要的操作和其他存储引擎没有什么差别, 同样是备份集文件(包括数据文件和日志文件)复制到相应的目录即可。但是对于通过其他 备份软件所进行的备份,就需要根据备份软件本身的要求来进行了。比如通过 ibbackup 来 进行的备份,同样也需要通过他来进行恢复才可以,具体的恢复方法请通过该软件的使用手 册来进行,这里就不详细介绍了。 NDB Cluster 存储引擎 对于停机冷备,恢复方法和其他存储引擎也没有太多区别,只不过有一点需要特别注意 的就是恢复的时候必须要将备份集中文件恢复到对应的数据节点之少,否则无法正确完成恢 复过程。 而通过 NDB Cluster 所提供的备份命令来生成的备份集,需要使用专用的备份恢复软 件 ndb_restore 来进行。ndb_restore 软件将从备份集中读取出备份相关的控制信息,而 且 ndb_restore 软件必须在单独的数据节点上面分别进行。所以当初备份进行过程中有多 少数据节点,现在就需要运行多少次 ndb_restore。而且,首次通过 ndb_restore 来进行 恢复的话,还必须恢复元数据,也就是会重建所有的数据库和表。 5.5 备份策略的设计思路 备份是否完整,能否满足要求,关键还是需要看所设计的备份策略是否合理,以及备份 操作是否确实按照所设计的备份策略进行了。 针对于不同的用途,所需要的备份类型是不一样的,所以需要的备份策略有各有不同。 如为了应对本章最开始所描述的在线应用的数据丢失的问题,我们的备份就需要快速恢复, 而且最好是仅仅需要增量恢复就能找回所需数据。对于这类需求,最好是有在线的,且部分 延迟恢复的备用数据库。因为这样可以在最短时间内找回所需要的数据。甚至在某些硬件设 备出现故障的时候,将备用库直接开发对外提供服务都可以。当然,在资源缺乏的情况下, 可能难以找到足够的备用硬件设备来承担这个备份责任的时候,我们也可以通过物理备份来 解决,毕竟物理备份的恢复速度要比逻辑备份的快很多。 而对于那些非数据丢失的应用场景,大多数时候恢复时间的要求并不是太高,只要可以 恢复出一个完整可用的数据库就可以了。所以不论是物理备份还是逻辑备份,影响都不大。 从我个人经验来看,可以根据不同的需求不同的级别通过如下的几个思路来设计出合理 的备份策略: 1、 对于较为核心的在线应用系统,比需要有在线备用主机通过 MySQL 的复制进行相
64. 应的备份,复制线程可以一直开启,恢复线程可以每天恢复一次,尽量让备机的数 据延后主机在一定的时间段之内。这个延后的时间多长合适主要是根据实际需求决 定,一般来说延后一天是一个比较常规的做法。 2、 对于重要级别稍微低一些的应用,恢复时间要求不是太高的话,为了节约硬件成本 , 不必要使用在线的备份主机来单独运行备用 MySQL,而是通过每一定的时间周期内 进行一次物理全备份,同时每小时(或者其他合适的时间段)内将产生的二进制日 志进行备份。这样虽然没有第一种备份方法恢复快,但是数据的丢失会比较少。恢 复所需要的时间由全备周期长短所决定。 3、 而对于恢复基本没有太多时间要求,但是不希望太多数据丢失的应用场景,则可以 通过每一定时间周期内进行一次逻辑全备份,同时也备份相应的二进制日志。使用 逻辑备份而不使用物理备份的原因是因为逻辑备份实现简单,可以完全在线联机完 成,备份过程不会影响应用提供服务。 4、 对于一些搭建临时数据库的备份应用场景,则仅仅只需要通过一个逻辑全备份即可 满足需求,都不需要用二进制日志来进行恢复,因为这样的需求对数据并没有太苛 刻的要求。 上面的四种备份策略都还比较较粗糙,甚至不能算是一个备份策略。目的只是希望能给 大家一个指定备份策略的思路。各位读者朋友可以根据这个思路根据实际的应用场景,指定 出各种不同的备份策略。 5.6 小结 总的来说,MySQL 的备份与恢复都不是太复杂,方法也比较单一。姑且不说逻辑备份, 对于物理备份来说,确实是还不够完善。缺少一个开源的比较好的在线热物理备份软件,一 直是 MySQL 一个比较大的遗憾,也是所有 MySQL 使用者比较郁闷的事情。 当然,没有开源的备份软件使用,非开源的商业软件也还是有的,如比较著名的 Zmanda 备份恢复软件,功能就比较全面,使用也不太复杂,在商业的 MySQL 备份恢复软件市场上 有较高的占有率。而且,Zmanda 同时还提供社区版本的免费下载使用。 不过,稍微让人有所安慰的是 MySQL 在实际应用场景中大多是有一台或者多台 Slave 机器来作为热备的。在需要进行备份的时候通过 Slave 来进行备份也不是太难,而且通过 暂时停止 Slave 上面的 SQL 线程,即可让 Slave 机器停止所有数据写入操作,然后就可 以进行在线进行备份操作了。所以即使买不起商用软件或者不太想买关系也不是太大。
65. 第 6 章 影响 MySQL Server 性能的相关因素 前言: 大部分人都一致认为一个数据库应用系统(这里的数据库应用系统概指所有使用数据库的系统)的 性能瓶颈最容易出现在数据的操作方面,而数据库应用系统的大部分数据操作都是通过数据库管理软件 所提供的相关接口来完成的。所以数据库管理软件也就很自然的成为了数据库应用系统的性能瓶颈所 在,这是当前业界比较普遍的一个看法。但我们的应用系统的性能瓶颈真的完全是因为数据库管理软件 和数据库主机自身造成的吗?我们将通过本章的内容来进行一个较为深入的分析,让大家了解到一个数 据库应用系统的性能到底与哪些地方有关,让大家寻找出各自应用系统的出现性能问题的根本原因,而 尽可能清楚的知道该如何去优化自己的应用系统。 考虑到本书的数据库对象是 MySQL,而 MySQL 最多的使用场景是 WEB 应用,那么我们就以一个 WEB 应 用系统为例,逐个分析其系统构成,结合笔者在大型互联网公司从事 DBA 工作多年的经验总结,分析出 数据库应用系统中各个环境对性能的影响。 6.1 商业需求对性能的影响 应用系统中的每一个功能在设计初衷肯定都是出于为用户提供某种服务,或者满足用户的某种需 求,但是,并不是每一个功能在最后都能很成功,甚至有些功能的推出可能在整个系统中是画蛇添足。 不仅没有为用户提高任何体验度,也没有为用户改进多少功能易用性,反而在整个系统中成为一个累 赘,带来资源的浪费。 不合理需求造成资源投入产出比过低 需求是否合理很多时候可能并不是很容易界定,尤其是作为技术人员来说,可能更难以确定一个需 求的合理性。即使指出,也不一定会被产品经历们认可。那作为技术人员的我们怎么来证明一个需求是 否合理呢? 第一、每次产品经理们提出新的项目(或者功能需求)的时候,应该要求他们同时给出该项目的预 期收益的量化指标,以备项目上先后统计评估投入产出比率; 第二、在每次项目进行过程中,应该详细记录所有的资源投入,包括人力投入,硬件设施的投入, 以及其他任何项目相关的资源投入; 第三、项目(或者功能需求)上线之后应该及时通过手机相关数据统计出项目的实际收益值,以便 计算投入产出比率的时候使用; 第四、技术部门应该尽可能推动设计出一个项目(或者功能需求)的投入产出比率的计算规则。在 项目上线一段时间之后,通过项目实际收益的统计数据和项目的投入资源量,计算出整个项目的实际投 入产出值,并公布给所有参与项目的部门知晓,同时存放以备后查。 有了实际的投入产出比率,我们就可以和项目立项之初产品经理们的预期投入产出比率做出比较, 判定出这个项目做的是否值得。而且当积累了较多的项目投入产出比率之后,我们可以根据历史数据分
66. 析出一个项目合理的投入产出比率应该是多少。这样,在项目立项之初,我们就可以判定出产品经理们 的预期投入产出比率是否合理,项目是否真的有进行的必要。 有了实际的投入产出比率之后,我们还可以拿出数据给老板们看,让他知道功能并不是越多越好, 让他知道有些功能是应该撤下来的,即使撤下该功能可能需要投入不少资源。 实际上,一般来说,在产品开发及运营部门内部都会做上面所说的这些事情的。但很多时候可能更 多只是一种形式化的过程。在有些比较规范的公司可能也完成了上面的大部分流程,但是要么数据不公 开,要么公开给其他部门的数据存在一定的偏差,不具备真实性。 为什么会这样?其实就一个原因,就是部门之间的利益冲突及业绩冲突问题。产品经理们总是希望 尽可能的让用户觉得自己设计的产品功能齐全,让老板觉得自己做了很多事情。但是从来都不会去关心 因为做一个功能所带来的成本投入,或者说是不会特别的关心这一点。而且很多时候他们也并不能太理 解技术方面带来的复杂度给产品本身带来的负面影响。 这里我们就拿一个看上去很简单的功能来分析一下。 需求:一个论坛帖子总量的统计 附加要求:实时更新 在很多人看来,这个功能非常容易实现,不就是执行一条 SELECT COUNT(*)的 Query 就可以得到结果 了么?是的,确实只需要如此简单的一个 Query 就可以得到结果。但是,如果我们采用不是 MyISAM 存储 引擎,而是使用的 Innodb 的存储引擎,那么大家可以试想一下,如果存放帖子的表中已经有上千万的帖 子的时候,执行这条 Query 语句需要多少成本?恐怕再好的硬件设备,恐怕都不可能在 10 秒之内完成一 次查询吧。如果我们的访问量再大一点,还有人觉得这是一件简单的事情么? 既然这样查询不行,那我们是不是该专门为这个功能建一个表,就只有一个字段,一条记录,就存 放这个统计量,每次有新的帖子产生的时候,都将这个值增加 1,这样我们每次都只需要查询这个表就可 以得到结果了,这个效率肯定能够满足要求了。确实,查询效率肯定能够满足要求,可是如果我们的系 统帖子产生很快,在高峰时期可能每秒就有几十甚至上百个帖子新增操作的时候,恐怕这个统计表又要 成为大家的噩梦了。要么因为并发的问题造成统计结果的不准确,要么因为锁资源争用严重造成整体性 能的大幅度下降。 其实这里问题的焦点不应该是实现这个功能的技术细节,而是在于这个功能的附加要求 “实时更 新”上面。当一个论坛的帖子数量很大了之后,到底有多少人会关注这个统计数据是否是实时变化的? 有多少人在乎这个数据在短时间内的不精确性?我想恐怕不会有人会傻傻的盯着这个统计数字并追究当 自己发了一个帖子然后回头刷新页面发现这个统计数字没有加 1 吧?即使明明白白的告诉用户这个统计 数据是每过多长时间段更新一次,那有怎样?难道会有很多用户就此很不爽么? 只要去掉了这个“实时更新”的附加条件,我们就可以非常容易的实现这个功能了。就像之前所提 到的那样,通过创建一个统计表,然后通过一个定时任务每隔一定时间段去更新一次里面的统计值,这 样既可以解决统计值查询的效率问题,又可以保证不影响新发贴的效率,一举两得。 实际上,在我们应用的系统中还有很多很多类似的功能点可以优化。如某些场合的列表页面参与列 表的数据量达到一个数量级之后,完全可以不用准确的显示这个列表总共有多少条信息,总共分了多少
67. 页,而只需要一个大概的估计值或者一个时间段之前的统计值。这样就省略了我们的分页程序需要在分 以前实时 COUNT 出满足条件的记录数。 其实,在很多应用系统中,实时和准实时,精确与基本准确,在很多地方所带来的性能消耗可能是 几个性能的差别。在系统性能优化中,应该尽量分析出那些可以不实时和不完全精确的地方,作出一些 相应的调整,可能会给大家带来意想不到的巨大性能提升。 无用功能堆积使系统过度复杂影响整体性能 很多时候,为系统增加某个功能可能并不需要花费太多的成本,而要想将一个已经运行了一段时间 的功能从原有系统中撤下来却是非常困难的。 首先,对于开发部门,可能要重新整理很多的代码,找出可能存在与增加该功能所编写的代码有交 集的其他功能点,删除没有关联的代码,修改有关联的代码; 其次,对于测试部门,由于功能的变动,必须要回归测试所有相关的功能点是否正常。可能由于界 定困难,不得不将回归范围扩展到很大,测试工作量也很大。 最后,所有与撤除下线某个功能相关的工作参与者来说,又无法带来任何实质性的收益,而恰恰相 反是,带来的只可能是风险。 由于上面的这几个因素,可能很少有公司能够有很完善的项目(或者功能)下线机制,也很少有公 司能做到及时将系统中某些不合适的功能下线。所以,我们所面对的应用系统可能总是越来越复杂,越 来越庞大,短期内的复杂可能并无太大问题,但是随着时间的积累,我们所面对的系统就会变得极其臃 肿。不仅维护困难,性能也会越来越差。尤其是有些并不合理的功能,在设计之初或者是刚上线的时候 由于数据量较小,带来不了多少性能损耗。可随着时间的推移,数据库中的数据量越来越大,数据检索 越来越困难,对真个系统带来的资源消耗也就越来越大。 而且,由于系统复杂度的不断增加,给后续其他功能的开发带来实现的复杂度,可能很多本来很简 单的功能,因为系统的复杂而不得不增加很多的逻辑判断,造成系统应用程序的计算量不断增加,本身 性能就会受到影响。而如果这些逻辑判断还需要与数据库交互通过持久化的数据来完成的话,所带来的 性能损失就更大,对整个系统的性能影响也就更大了。 6.2 系统架构及实现对性能的影响 一个 WEB 应用系统,自然离不开 Web 应用程序(Web App)和应用程序服务器(App Server)。App Server 我们能控制的内容不多,大多都是使用已经久经考验的成熟产品,大家能做的也就只是通过一些 简单的参数设置调整来进行调优,不做细究。而 Web App 大部分都是各自公司根据业务需求自行开发, 可控性要好很多。所以我们从 Web 应用程序着手分析一个应用程序架构的不同设计对整个系统性能的影 响将会更合适。 上一节中商业需求告诉了我们一个系统应该有什么不应该有什么,系统架构则则决定了我们系统的 构建环境。就像修建一栋房子一样,在清楚了这栋房子的用途之后,会先有建筑设计师来画出一章基本
68. 的造型图,然后还需要结构设计师为我们设计出结构图。系统架构设计的过程就和结构工程好似设计结 构图一样,需要为整个系统搭建出一个尽可能最优的框架,让整个系统能够有一个稳定高效的结构体系 让我们实现各种商业需求。 谈到应用系统架构的设计,可能有人的心里会开始嘀咕,一个 DBA 有什么资格谈论人家架构师(或 者程序员)所设计的架构?其实大家完全没有必要这样去考虑,我们谈论架构只是分析各种情形下的性 能消耗区别,仅仅是根据自己的专业特长来针对相应架构给出我们的建议及意见,并不是要批判架构整 体的好坏,更不是为了推翻某个架构。而且我们所考虑的架构大多数时候也只是数据层面相关的架构。 我们数据库中存放的数据都是适合在数据库中存放的吗? 对于有些开发人员来说,数据库就是一个操作最方便的万能存储中心,希望什么数据都存放在数据 库中,不论是需要持久化的数据,还是临时存放的过程数据,不论是普通的纯文本格式的字符数据,还 是多媒体的二进制数据,都喜欢全部塞如数据库中。因为对于应用服务器来说,数据库很多时候都是一 个集中式的存储环境,不像应用服务器那样可能有很多台;而且数据库有专门的 DBA 去帮忙维护,而不 像应用服务器很多时候还需要开发人员去做一些维护;还有一点很关键的就是数据库的操作非常简单统 一,不像文件操作或者其他类型的存储方式那么复杂。 其实我个人认为,现在的很多数据库为我们提供了太多的功能,反而让很多并不是太了解数据库的 人错误的使用了数据库的很多并不是太擅长或者对性能影响很大的功能,最后却全部怪罪到数据库身 上。 实际上,以下几类数据都是不适合在数据库中存放的: 1. 二进制多媒体数据 将二进制多媒体数据存放在数据库中,一个问题是数据库空间资源耗用非常严重,另一个问题 是这些数据的存储很消耗数据库主机的 CPU 资源。这种数据主要包括图片,音频、视频和其他一些 相关的二进制文件。这些数据的处理本不是数据的优势,如果我们硬要将他们塞入数据库,肯定会 造成数据库的处理资源消耗严重。 2. 流水队列数据 我们都知道,数据库为了保证事务的安全性(支持事务的存储引擎)以及可恢复性,都是需要 记录所有变更的日志信息的。而流水队列数据的用途就决定了存放这种数据的表中的数据会不断的 被 INSERT,UPDATE 和 DELETE,而每一个操作都会生成与之对应的日志信息。在 MySQL 中,如果是支 持事务的存储引擎,这个日志的产生量更是要翻倍。而如果我们通过一些成熟的第三方队列软件来 实现这个 Queue 数据的处理功能,性能将会成倍的提升。 3. 超大文本数据 对于 5.0.3 之前的 MySQL 版本,VARCHAR 类型的数据最长只能存放 255 个字节,如果需要存储更 长的文本数据到一个字段,我们就必须使用 TEXT 类型(最大可存放 64KB)的字段,甚至是更大的 LONGTEXT 类型(最大 4GB)。而 TEXT 类型数据的处理性能要远比 VARCHAR 类型数据的处理性能低下 很多。从 5.0.3 版本开始,VARCHAR 类型的最大长度被调整到 64KB 了,但是当实际数据小于 255 Bytes 的时候,实际存储空间和实际的数据长度一样,可一旦长度超过 255 Bytes 之后,所占用的存 储空间就是实际数据长度的两倍。 所以,超大文本数据存放在数据库中不仅会带来性能低下的问题,还会带来空间占用的浪费问 题。
69. 是否合理的利用了应用层 Cache 机制? 对于 Web 应用,活跃数据的数据量总是不会特别的大,有些活跃数据更是很少变化。对于这类数 据,我们是否有必要每次需要的时候都到数据库中去查询呢?如果我们能够将变化相对较少的部分活跃 数据通过应用层的 Cache 机制 Cache 到内存中,对性能的提升肯定是成数量级的,而且由于是活跃数据, 对系统整体的性能影响也会很大。 当然,通过 Cache 机制成功的案例数不胜数,但是失败的案例也同样并不少见。如何合理的通过 Cache 技术让系统性能得到较大的提升也不是通过寥寥几笔就能说明的清楚,这里我仅根据以往的经验列 举一下什么样的数据适合通过 Cache 技术来提高系统性能: 1. 系统各种配置及规则数据; 由于这些配置信息变动的频率非常低,访问概率又很高,所以非常适合存使用 Cache; 2. 活跃用户的基本信息数据; 虽然我们经常会听到某某网站的用户量达到成百上千万,但是很少有系统的活跃用户量能够都 达到这个数量级。也很少有用户每天没事干去将自己的基本信息改来改去。更为重要的一点是 用户的基本信息在应用系统中的访问频率极其频繁。所以用户基本信息的 Cache,很容易让整个 应用系统的性能出现一个质的提升。 3. 活跃用户的个性化定制信息数据; 虽然用户个性化定制的数据从访问频率来看,可能并没有用户的基本信息那么的频繁,但相对 于系统整体来说,也占了很大的比例,而且变更皮律一样不会太多。从 Ebay 的 PayPal 通过 MySQL 的 Memory 存储引擎实现用户个性化定制数据的成功案例我们就能看出对这部分信息进行 Cache 的价值了。虽然通过 MySQL 的 Memory 存储引擎并不像我们传统意义层面的 Cache 机制, 但正是对 Cache 技术的合理利用和扩充造就了项目整体的成功。 4. 准实时的统计信息数据; 所谓准实时的统计数据,实际上就是基于时间段的统计数据。这种数据不会实时更新,也很少 需要增量更新,只有当达到重新 Build 该统计数据的时候需要做一次全量更新操作。虽然这种 数据即使通过数据库来读取效率可能也会比较高,但是执行频率很高之后,同样会消耗不少资 源。既然数据库服务器的资源非常珍贵,我们为什么不能放在应用相关的内存 Cache 中呢? 5. 其他一些访问频繁但变更较少的数据; 出了上面这四种数据之外,在我们面对的各种系统环境中肯定还会有各种各样的变更较少但是 访问很频繁的数据。只要合适,我们都可以将对他们的访问从数据库移到 Cache 中。 我们的数据层实现都是最精简的吗? 从以往的经验来看,一个合理的数据存取实现和一个拙劣的实现相比,在性能方面的差异经常会超 出一个甚至几个数量级。我们先来分析一个非常简单且经常会遇到类似情况的示例: 在我们的示例网站系统中,现在要实现每个用户查看各自相册列表(假设每个列表显示 10 张相片) 的时候,能够在相片名称后面显示该相片的留言数量。这个需求大家认为应该如何实现呢?我想 90%的开 发开发工程师会通过如下两步来实现该需求: 1、通过“SELECT id,subject,url FROM photo WHERE user_id = ? limit 10” 得到第一页的相片
70. 相关信息; 2、通过第 1 步结果集中的 10 个相片 id 循环运行十次 “SELECT COUNT(*) FROM photo_comment WHERE photh_id = ?” 来得到每张相册的回复数量然后再瓶装展现对象。 此外可能还有部分人想到了如下的方案: 1、和上面完全一样的操作步骤; 2、通过程序拼装上面得到的 10 个 photo 的 id,再通过 in 查询“SELECT photo_id,count(*) FROM photo_comment WHERE photo_id in (?) GROUP BY photo_id” 一次得到 10 个 photo 的所有回复数量, 再组装两个结果集得到展现对象。 我们来对以上两个方案做一下简单的比较: 1、从 MySQL 执行的 SQL 数量来看 ,第一种解决方案为 11(1+10=11)条 SQL 语句,第二种解决方案 为 2 条 SQL 语句(1+1); 2、从应用程序与数据库交互来看,第一种为 11 次,第二种为 2 次; 3、从数据库的 IO 操作来看,简单假设每次 SQL 为 1 个 IO,第一种最少 11 次 IO,第二种小于等于 11 次 IO,而且只有当数据非常之离散的情况下才会需要 11 次; 4、从数据库处理的查询复杂度来看,第一种为两类很简单的查询,第二种有一条 SQL 语句有 GROUP BY 操作,比第一种解决方案增加了了排序分组操作; 5、从应用程序结果集处理来看,第一种 11 次结果集的处理,第二中 2 次结果集的处理,但是第二种 解决方案中第二词结果处理数量是第一次的 10 倍; 6、从应用程序数据处理来看,第二种比第一种多了一个拼装 photo_id 的过程。 我们先从以上 6 点来做一个性能消耗的分析: 1、由于 MySQL 对客户端每次提交的 SQL 不管是相同还是不同,都需要进行完全解析,这个动作主要 消耗的资源是数据库主机的 CPU,那么这里第一种方案和第二种方案消耗 CPU 的比例是 11:2。SQL 语句的 解析动作在整个 SQL 语句执行过程中的整体消耗的 CPU 比例是较多的; 2、应用程序与数据库交互所消耗的资源基本上都在网络方面,同样也是 11:2; 3、数据库 IO 操作资源消耗为小于或者等于 1:1; 4、第二种解决方案需要比第一种多消耗内存资源进行排序分组操作,由于数据量不大,多出的消耗 在语句整体消耗中占用比例会比较小,大概不会超过 20%,大家可以针对性测试; 5、结果集处理次数也为 11:2,但是第二中解决方案第二次处理数量较大,整体来说两次的性能消 耗区别不大; 6、应用程序数据处理方面所多出的这个 photo_id 的拼装所消耗的资源是非常小的,甚至比应用程 序与 MySQL 做一次简单的交互所消耗的资源还要少。 综合上面的这 6 点比较,我们可以很容易得出结论,从整体资源消耗来看,第二中方案会远远优于 第一种解决方案。而在实际开发过程中,我们的程序员却很少选用。主要原因其实有两个,一个是第二 种方案在程序代码实现方面可能会比第一种方案略为复杂,尤其是在当前编程环境中面向对象思想的普 及,开发工程师可能会更习惯于以对象为中心的思考方式来解决问题。还有一个原因就是我们的程序员 可能对 SQL 语句的使用并不是特别的熟悉,并不一定能够想到第二条 SQL 语句所实现的功能。对于第一个 原因,我们可能只能通过加强开发工程师的性能优化意识来让大家能够自觉纠正,而第二个原因的解决 就正是需要我们出马的时候了。 SQL 语句正是我们的专长,定期对开发工程师进行一些相应的数据库知 识包括 SQL 语句方面的优化培训,可能会给大家带来意想不到的收获的。
71. 这里我们还仅仅只是通过一个很长见的简单示例来说明数据层架构实现的区别对整体性能的影响, 实际上可以简单的归结为过渡依赖嵌套循环的使用或者说是过渡弱化 SQL 语句的功能造成性能消耗过多 的实例。后面我将进一步分析一下更多的因为架构实现差异所带来的性能消耗差异。 过度依赖数据库 SQL 语句的功能造成数据库操作效率低下 前面的案例是开发工程师过渡弱化 SQL 语句的功能造成的资源浪费案例,而这里我们再来分析一个 完全相反的案例:在群组简介页面需要显示群名称和简介,每个群成员的 nick_name,以及群主的个人签 名信息。 需求中所需信息存放在以下四个表中:user,user_profile,groups,user_group 我们先看看最简单的实现方法,一条 SQL 语句搞定所有事情: SELECT name,description,user_type,nick_name,sign FROM groups,user_group,user ,user_profile WHERE groups.id = ? AND groups.id = user_group.group_id AND user_group.user_id = user.id AND user_profile.user_id = user.id 当然我们也可以通过如下稍微复杂一点的方法分两步搞定: 首先取得所有需要展示的 group 的相关信息和所有群组员的 nick_name 信息和组员类别: SELECT name,description,user_type,nick_name FROM groups,user_group,user WHERE groups.id = ? AND groups.id = user_group.group_id AND user_group.user_id = user.id 然后在程序中通过上面结果集中的 user_type 找到群主的 user_id 再到 user_profile 表中取得群主 的签名信息: SELECT sign FROM user_profile WHERE user_id = ? 大家应该能够看出两者的区别吧,两种解决方案最大的区别在于交互次数和 SQL 复杂度。而带来的 实际影响是第一种解决方案对 user_profile 表有不必要的访问(非群主的 profile 信息),造成 IO 访问 的直接增加在 20%左右。而大家都知道,IO 操作在数据库应用系统中是非常昂贵的资源。尤其是当这个 功能的 PV 较大的时候,第一种方案造成的 IO 损失是相当大的。 重复执行相同的 SQL 造成资源浪费 这个问题其实是每个人都非常清楚也完全认同的一个问题,但是在应用系统开发过程中,仍然会常 有这样的现象存在。究其原因,主要还是开发工程师思维中面向对象的概念太过深入,以及为了减少自 己代码开发的逻辑和对程序接口过度依赖所造成的。 我曾经在一个性能优化项目中遇到过一个案例,某个功能页面一侧是 “分组”列表,是一列“分
72. 组”的名字。页面主要内容则是该“分组”的所有“项目”列表。每个“项目”以名称(或者图标)显 示,同时还有一个 SEO 相关的需求就是每个“项目”名称的链接地址中是需要有“分组”的名称的。所 以在“项目”列表的每个 “项目”的展示内容中就需要得到该项目所属的组的名称。按照开发工程师开 发思路,非常容易产生取得所有“项目”结果集并映射成相应对象之后,再从对象集中获取“项目”所 属组的标识字段,然后循环到“分组”表中取得需要的”组名“。然后再将拼装成展示对象。 看到这里,我想大家应该已经知道这里存在的一个最大的问题就是多次重复执行了完全相同的 SQL 得到完全相同的内容。同时还犯了前面第一个案例中所犯的错误。或许大家看到之后会不相信有这样的 案例存在,我可以非常肯定的告诉大家,事实就是这样。同时也请大家如果有条件的话,好好 Review 自 己所在的系统的代码,非常有可能同样存在上面类似的情形。 还有部分解决方案要远优于上面的做法,那就是不循环去取了,而是通过 Join 一次完成,也就是解 决了第一个案例所描述的性能问题。但是又误入了类似于第二个案例所描述的陷阱中了,因为实际上他 只需要一次查询就可以得到所有“项目”所属的“分组”的名称(所有项目都是同一个组的)。 当然,也有部分解决方案也避免了第二个案例的问题,分为两条 SQL,两步完成了这个需求。这样在 性能上面基本上也将近是数量级的提升了。 但是这就是性能最优的解决方案了么?不是的,我们甚至可以连一次都不需要访问就获得所需要的 “分组”名称。首先,侧栏中的“分组”列表是需要有名称的,我们为什么不能直接利用到呢? 当然,可能有些系统的架构决定了侧栏和主要内容显示区来源于不同的模板(或者其他结构),那 么我们也完全可以通过在进入这个功能页面的链接请求中通过参数传入我们需要的 “分组”名称。这样 我们就可以完全不需要根据“项目”相关信息去数据库获取所属“分组”的信息,就可以完成相应需求 了。当然,是否需要通过请求参数来节省最后的这一次访问,可能会根据这个功能页面的 PV 来决定,如 果访问并不是非常频繁,那么这个节省可能并不是很明显,而应用系统的复杂度却有所增加,而且程序 看上去可能也会不够优雅,但是如果访问非常频繁的场景中,所节省的资源还是比较可观的。 上面还仅仅只是列举了我们平时比较常见的一些实现差异对性能所带来的影响,除了这些实现方面 所带来的问题之外,应用系统的整体架构实现设计对系统性能的影响可能会更严重。下面大概列举了一 些较为常见的架构设计实现不当带来的性能问题和资源浪费情况。 1、Cache 系统的不合理利用导致 Cache 命中率低下造成数据库访问量的增加,同时也浪费了 Cache 系统的硬件资源投入; 2、过度依赖面向对象思想,对系统 3、对可扩展性的过渡追求,促使系统设计的时候将对象拆得过于离散,造成系统中大量的复杂 Join 语句,而 MySQL Server 在各数据库系统中的主要优势在于处理简单逻辑的查询,这与其锁定的机制也有 较大关系; 4、对数据库的过渡依赖,将大量更适合存放于文件系统中的数据存入了数据库中,造成数据库资源 的浪费,影响到系统的整体性能,如各种日志信息; 5、过度理想化系统的用户体验,使大量非核心业务消耗过多的资源,如大量不需要实时更新的数据 做了实时统计计算。 以上仅仅是一些比较常见的症结,在各种不同的应用环境中肯定还会有很多不同的性能问题,可能
73. 需要大家通过仔细的数据分析和对系统的充分了解才能找到,但是一旦找到症结所在,通过相应的优化 措施,所带来的收益也是相当可观的。 6.3 Quer y 语句对系统性能的影响 前面一节我们介绍了应用系统的实现差异对数据库应用系统整体性能的影响,这一节我们将分析 SQL 语句的差异对系统性能的影响。 我想对于各位读者来说,肯定都清楚 SQL 语句的优劣是对性能有影响的,但是到底有多大影响可能 每个人都会有不同的体会,每个 SQL 语句在优化之前和优化之后的性能差异也是各不相同,所以对于性 能差异到底有多大这个问题我们我们这里就不做详细分析了。我们重点分析实现同样功能的不同 SQL 语 句在性能方面会产生较大的差异的根本原因,并通过一个较为典型的示例来对我们的分析做出相应的验 证。 为什么返回完全相同结果集的不同 SQL 语句,在执行性能方面存在差异呢?这里我们先从 SQL 语句在 数据库中执行并获取所需数据这个过程来做一个大概的分析了。 当 MySQL Server 的连接线程接收到 Client 端发送过来的 SQL 请求之后,会经过一系列的分解 Parse,进行相应的分析。然后,MySQL 会通过查询优化器模块(Optimizer)根据该 SQL 所设涉及到的数 据表的相关统计信息进行计算分析,然后再得出一个 MySQL 认为最合理最优化的数据访问方式,也就是 我们常说的“执行计划”,然后再根据所得到的执行计划通过调用存储引擎借口来获取相应数据。然后 再将存储引擎返回的数据进行相关处理,并以 Client 端所要求的格式作为结果集返回给 Client 端的应 用程序。 注:这里所说的统计数据,是我们通过 ANALYZE TABLE 命令通知 MySQL 对表的相关数据做分析之后所 获得到的一些数据统计量。这些统计数据对 MySQL 优化器而言是非常重要的,优化器所生成的执行计划 的好坏,主要就是由这些统计数据所决定的。实际上,在其他一些数据库管理软件中也有类似相应的统 计数据。 我们都知道,在数据库管理软件中,最大的性能瓶颈就是在于磁盘 IO,也就是数据的存取操作上 面。而对于同一份数据,当我们以不同方式去寻找其中的某一点内容的时候,所需要读取的数据量可能 会有天壤之别,所消耗的资源也自然是区别甚大。所以,当我们需要从数据库中查询某个数据的时候, 所消耗资源的多少主要就取决于数据库以一个什么样的数据读取方式来完成我们的查询请求,也就是取 决于 SQL 语句的执行计划。 对于唯一一个 SQL 语句来说,经过 MySQL Parse 之后分解的结构都是固定的,只要统计信息稳定,其 执行计划基本上都是比较固定的。而不同写法的 SQL 语句,经过 MySQL Parse 之后分解的结构结构就可能 完全不同,即使优化器使用完全一样的统计信息来进行优化,最后所得出的执行计划也可能完全不一 样。而执行计划又是决定一个 SQL 语句最终的资源消耗量的主要因素。所以,实现功能完全一样的 SQL 语 句,在性能上面可能会有差别巨大的性能消耗。当然,如果功能一样,而且经过 MySQL 的优化器优化之 后的执行计划也完全一致的不同 SQL 语句在资源消耗方面可能就相差很小了。当然这里所指的消耗主要 是 IO 资源的消耗,并不包括 CPU 的消耗。
74. 下面我们将通过一两个具体的示例来分析写法不一样而功能完全相同的两条 SQL 的在性能方面的差 异。 示例一 需求:取出某个 group(假设 id 为 100)下的用户编号(id),用户昵称(nick_name)、用户性别 ( sexuality ) 、 用 户 签 名 ( sign ) 和 用 户 生 日 ( birthday ) , 并 按 照 加 入 组 的 时 间 (user_group.gmt_create)来进行倒序排列,取出前 20 个。 解决方案一、 SELECT id,nick_name FROM user,user_group WHERE user_group.group_id = 1 and user_group.user_id = user.id limit 100,20; 解决方案二、 SELECT user.id,user.nick_name FROM ( SELECT user_id FROM user_group WHERE user_group.group_id = 1 ORDER BY gmt_create desc limit 100,20) t,user WHERE t.user_id = user.id; 我们先来看看执行计划: sky@localhost : example 10:32:13> explain -> SELECT id,nick_name -> FROM user,user_group -> WHERE user_group.group_id = 1 -> and user_group.user_id = user.id -> ORDER BY user_group.gmt_create desc -> limit 100,20\G *************************** 1. row *************************** id: 1 select_type:'>type: SIMPLE table: user_group type:'>type: ref possible_keys: user_group_uid_gid_ind,user_group_gid_ind key: user_group_gid_ind key_len: 4 ref: const rows: 31156
75. Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra: Using where; Using filesort *************************** 2. row *************************** id:'>id:'>id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table:'>table:'>table: user type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: eq_ref possible_keys:'>keys:'>keys:'>keys: PRIMARY key:'>key:'>key:'>key: PRIMARY key_len:'>len:'>len:'>len: 4 ref:'>ref:'>ref:'>ref: example.user_group.user_id rows:'>rows:'>rows:'>rows: 1 Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra: sky@localhost : example 10:32:20> explain -> SELECT user.id,user.nick_name -> FROM ( -> SELECT user_id -> FROM user_group -> WHERE user_group.group_id = 1 -> ORDER BY gmt_create desc -> limit 100,20) t,user -> WHERE t.user_id = user.id\G *************************** 1. row *************************** id:'>id:'>id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: PRIMARY table:'>table:'>table:'>table: type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ALL possible_keys:'>keys:'>keys:'>keys: NULL key:'>key:'>key:'>key: NULL key_len:'>len:'>len:'>len: NULL ref:'>ref:'>ref:'>ref: NULL rows:'>rows:'>rows:'>rows: 20 Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra: *************************** 2. row *************************** id:'>id:'>id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: PRIMARY table:'>table:'>table:'>table: user type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: eq_ref possible_keys:'>keys:'>keys:'>keys: PRIMARY key:'>key:'>key:'>key: PRIMARY key_len:'>len:'>len:'>len: 4 ref:'>ref:'>ref:'>ref: t.user_id rows:'>rows:'>rows:'>rows: 1 Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:'>Extra:
76. *************************** 3. row *************************** id: 2 select_type:'>type: DERIVED table: user_group type:'>type: ref possible_keys: user_group_gid_ind key:'>key:'>key:'>key: user_group_gid_ind key_len: 4 ref: const rows: 31156 Extra: Using filesort 执行计划对比分析: 解决方案一中的执行计划显示 MySQL 在对两个参与 Join 的表都利用到了索引,user_group 表利用了 user_group_gid_ind 索 引 ( key:'>key:'>key:'>key: user_group_gid_ind ) , user 表 利 用 到 了 主 键 索 引 ( key:'>key:'>key:'>key: PRIMARY),在参与 Join 前 MySQL 通过 Where 过滤后的结果集与 user 表进行 Join,最后通过排序取出 Join 后结果的“limit 100,20”条结果返回。 解决方案二的 SQL 语句利用到了子查询,所以执行计划会稍微复杂一些,首先可以看到两个表都和 解决方案 1 一样都利用到了索引(所使用的索引也完全一样),执行计划显示该子查询以 user_group 为 驱动,也就是先通过 user_group 进行过滤并马上进行这一论的结果集排序,也就取得了 SQL 中的 “limit 100,20”条结果,然后与 user 表进行 Join,得到相应的数据。这里可能有人会怀疑在自查询中 从 user_group 表所取得与 user 表参与 Join 的记录条数并不是 20 条,而是整个 group_id=1 的所有结果。 那么清大家看看该执行计划中的第一行,该行内容就充分说明了在外层查询中的所有的 20 条记录全部被 返回。 通过比较两个解决方案的执行计划,我们可以看到第一中解决方案中需要和 user 表参与 Join 的记录 数 MySQL 通过统计数据估算出来是 31156,也就是通过 user_group 表返回的所有满足 group_id=1 的记录 数(系统中的实际数据是 20000)。而第二种解决方案的执行计划中,user 表参与 Join 的数据就只有 20 条,两者相差很大,通过本节最初的分析,我们认为第二中解决方案应该明显优于第一种解决方案。 下面我们通过对比两个解决觉方案的 SQL 实际执行的 profile 详细信息,来验证我们上面的判断。由 于 SQL 语句执行所消耗的最大两部分资源就是 IO 和 CPU,所以这里为了节约篇幅,仅列出 BLOCK IO 和 CPU 两项 profile 信息(Query Profiler 的详细介绍将在后面章节中独立介绍): 先打开 profiling 功能,然后分别执行两个解决方案的 SQL 语句: sky@localhost : example 10:46:43> set profiling = 1; Query OK, 0 rows affected (0.00 sec) sky@localhost : example 10:46:50> SELECT id,nick_name -> FROM user,user_group -> WHERE user_group.group_id = 1 -> and user_group.user_id = user.id -> ORDER BY user_group.gmt_create desc
77. -> limit 100,20; +--------+-----------+ id nick_name +--------+-----------+ 990101 990101 990102 990102 990103 990103 990104 990104 990105 990105 990106 990106 990107 990107 990108 990108 990109 990109 990110 990110 990111 990111 990112 990112 990113 990113 990114 990114 990115 990115 990116 990116 990117 990117 990118 990118 990119 990119 990120 990120 +--------+-----------+ 20 rows in set (1.02 sec) sky@localhost : example 10:46:58> SELECT user.id,user.nick_name -> FROM ( -> SELECT user_id -> FROM user_group -> WHERE user_group.group_id = 1 -> ORDER BY gmt_create desc -> limit 100,20) t,user -> WHERE t.user_id = user.id; +--------+-----------+ id nick_name +--------+-----------+ 990101 990101 990102 990102 990103 990103 990104 990104 990105 990105 990106 990106
78. 990107 990107 990108 990108 990109 990109 990110 990110 990111 990111 990112 990112 990113 990113 990114 990114 990115 990115 990116 990116 990117 990117 990118 990118 990119 990119 990120 990120 +--------+-----------+ 20 rows in set (0.96 sec) 查看系统中的 profile 信息,刚刚执行的两个 SQL 语句的执行 profile 信息已经记录下来了: sky@localhost : example 10:47:07> show profiles\G *************************** 1. row *************************** Query_ID:'>ID: 1 Duration:'>Duration: 1.02367600 Query:'>Query: SELECT id,nick_name FROM user,user_group WHERE user_group.group_id = 1 and user_group.user_id = user.id ORDER BY user_group.gmt_create desc limit 100,20 *************************** 2. row *************************** Query_ID:'>ID: 2 Duration:'>Duration: 0.96327800 Query:'>Query: SELECT user.id,user.nick_name FROM ( SELECT user_id FROM user_group WHERE user_group.group_id = 1 ORDER BY gmt_create desc limit 100,20) t,user WHERE t.user_id = user.id 2 rows in set (0.00 sec) sky@localhost : example 10:47:34> SHOW profile CPU,BLOCK IO io FOR query 1;
79. +--------------------+----------+-----------+------------+--------------+---------------+ Status Duration CPU_user CPU_system Block_ops_in Block_ops_out +--------------------+----------+-----------+------------+--------------+---------------+ (initialization) 0.000068 0 0 0 0 Opening tables 0.000015 0 0 0 0 System lock 0.000006 0 0 0 0 Table lock 0.000009 0 0 0 0 init 0.000026 0 0 0 0 optimizing 0.000014 0 0 0 0 statistics 0.000068 0 0 0 0 preparing 0.000019 0 0 0 0 executing 0.000004 0 0 0 0 Sorting result 1.03614 0.5600349 0.428027 0 15632 Sending data 0.071047 0 0.004 88 0 end 0.000012 0 0 0 0 query end 0.000006 0 0 0 0 freeing items 0.000012 0 0 0 0 closing tables 0.000007 0 0 0 0 logging slow query 0.000003 0 0 0 0 +--------------------+----------+-----------+------------+--------------+---------------+ 16 rows in set (0.00 sec) sky@localhost : example 10:47:40> SHOW profile CPU,BLOCK IO io FOR query 2; +--------------------+----------+----------+------------+--------------+---------------+ Status Duration CPU_user CPU_system Block_ops_in Block_ops_out +--------------------+----------+----------+------------+--------------+---------------+ (initialization) 0.000087 0 0 0 0 Opening tables 0.000018 0 0 0 0 System lock 0.000007 0 0 0 0 Table lock 0.000059 0 0 0 0 optimizing 0.00001 0 0 0 0 statistics 0.000068 0 0 0 0 preparing 0.000017 0 0 0 0 executing 0.000004 0 0 0 0 Sorting result 0.928184 0.572035 0.352022 0 32 Sending data 0.000112 0 0 0 0 init 0.000025 0 0 0 0 optimizing 0.000012 0 0 0 0 statistics 0.000025 0 0 0 0 preparing 0.000013 0 0 0 0 executing 0.000004 0 0 0 0 Sending data 0.000241 0 0 0 0 end 0.000005 0 0 0 0 query end 0.000006 0 0 0 0
80. freeing items 0.000015 0 0 0 0 closing tables 0.000004 0 0 0 0 removing tmp table 0.000019 0 0 0 0 closing tables 0.000005 0 0 0 0 logging slow query 0.000004 0 0 0 0 +--------------------+----------+----------+------------+--------------+---------------+ 我们先看看两条 SQL 执行中的 IO 消耗,两者区别就在于“Sorting result”,我们回 顾一下前面执行计划的对比,两个解决方案的排序过滤数据的时机不一样,排序后需要取 得的数据量一个是 20000,一个是 20,正好和这里的 profile 信息吻合,第一种解决方案的 “Sorting result”的 IO 值是第二种解决方案的将近 500 倍。 然后再来看看 CPU 消耗,所有消耗中,消耗最大的也是“Sorting result”这一项,第 一个消耗多出的缘由和上面 IO 消耗差异是一样的。 结论: 通过上面两条功能完全相同的 SQL 语句的执行计划分析,以及通过实际执行后的 profile 数据的验证,都证明了第二种解决方案优于第一种解决方案。同时通过后者的实际 验证,也再次证明了我们前面所做的执行计划基本决定了 SQL 语句性能。 6.4 Schema 设计对系统的性能影响 前面两节中,我们已经分析了在一个数据库应用系统的软环境中应用系统的架构实现和系统中与数 据库交互的 SQL 语句对系统性能的影响。在这一节我们再分析一下系统的数据模型设计实现对系统的性 能影响,更通俗一点就是数据库的 Schema 设计对系统性能的影响。 在很多人看来,数据库 Schema 设计是一件非常简单的事情,就大体按照系统设计时候的相关实体对 象对应成一个一个的表格基本上就可以了。然后为了在功能上做到尽可能容易扩展,再根据数据库范式 规则进行调整,做到第三范式或者第四范式,基本就算完事了。 数据库 Schema 设计真的有如上面所说的这么简单么?可以非常肯定的告诉大家,数据库 Schema 设计 所需要做的事情远远不止如此。如果您之前的数据库 Schema 设计一直都是这么做的,那么在该设计应用 于正式环境之后,很可能带来非常大的性能代价。 由于在后面的“MySQL 数据库应用系统设计”中的“系统架构最优化“这一节中会介较为详细的从 性能优化的角度来分析如何如何设计数据库 Schema,所以这里暂时先不介绍如何来设计性能优异的数据 库 Schema 结构,仅仅通过一个实际的示例来展示 Schema 结构的不一样在性能方面所带来的差异。 需求概述:一个简单的讨论区系统,需要有用户,用户组,组讨论区这三部分基本功能 简要分析:1、需要存放用户数据的表; 2、需要存放分组信息和存放用户与组关系的表
81. 3、需要存放讨论信息的表; 解决方案: 原始方案一:分别用用四个表来存放用户,分组,用户与组关系以及各组的讨论帖子的信息如 下: user 用户表: +-------------+---------------+------+-----+---------+-------+ Field Type Null Key Default Extra +-------------+---------------+------+-----+---------+-------+ id int(11) NO 0 nick_name varchar(32) NO NULL password char(64) YES NULL email varchar(32) NO NULL status varchar(16) NO NULL sexuality char(1) NO NULL msn varchar(32) YES NULL sign varchar(64) YES NULL birthday date YES NULL hobby varchar(64) YES NULL location varchar(64) YES NULL description varchar(1024) YES NULL +-------------+---------------+------+-----+---------+-------+ groups 分组表: +--------------+---------------+------+-----+---------+-------+ Field Type Null Key Default Extra +--------------+---------------+------+-----+---------+-------+ id int(11) NO NULL gmt_create datetime NO NULL gmt_modified datetime NO NULL name varchar(32) NO NULL status varchar(16) NO NULL description varchar(1024) YES NULL +--------------+---------------+------+-----+---------+-------+ user_group 关系表: +--------------+-------------+------+-----+---------+-------+ Field Type Null Key Default Extra +--------------+-------------+------+-----+---------+-------+ user_id int(11) NO MUL NULL group_id int(11) NO MUL NULL user_type int(11) NO NULL gmt_create datetime NO NULL gmt_modified datetime NO NULL status varchar(16) NO NULL
82. +--------------+-------------+------+-----+---------+-------+ group_message 讨论组帖子表: +--------------+--------------+------+-----+---------+-------+ Field Type Null Key Default Extra +--------------+--------------+------+-----+---------+-------+ id int(11) NO NULL gmt_create datetime NO NULL gmt_modified datetime NO NULL group_id int(11) NO NULL user_id int(11) NO NULL subject varchar(128) NO NULL content text YES NULL +--------------+--------------+------+-----+---------+-------+ 优化后方案二: user 用户表: +-------------+---------------+------+-----+---------+-------+ Field Type Null Key Default Extra +-------------+---------------+------+-----+---------+-------+ id int(11) NO 0 nick_name varchar(32) NO NULL password char(64) YES NULL email varchar(32) NO NULL status varchar(16) NO NULL +-------------+---------------+------+-----+---------+-------+ user_profile 用户属性表(记录与 user 一一对应): +-------------+---------------+------+-----+---------+-------+ Field Type Null Key Default Extra +-------------+---------------+------+-----+---------+-------+ sexuality char(1) NO NULL msn varchar(32) YES NULL sign varchar(64) YES NULL birthday date YES NULL hobby varchar(64) YES NULL location varchar(64) YES NULL description varchar(1024) YES NULL +-------------+---------------+------+-----+---------+-------+ groups 和 user_group 这两个表和方案一完全一样 group_message 讨论组帖子表: +--------------+--------------+------+-----+---------+-------+
83. Field Type Null Key Default Extra +--------------+--------------+------+-----+---------+-------+ id int(11) NO NULL gmt_create datetime NO NULL gmt_modified datetime NO NULL group_id int(11) NO NULL user_id int(11) NO NULL author varchar(32) NO NULL subject varchar(128) NO NULL +--------------+--------------+------+-----+---------+-------+ group_message_content 帖子内容表(记录与 group_message 一一对应): +--------------+---------+------+-----+---------+-------+ Field Type Null Key Default Extra +--------------+---------+------+-----+---------+-------+ group_msg_id int(11) NO NULL content text NO NULL +--------------+---------+------+-----+---------+-------+ 我们先来比较一下两个解决方案所设计的 Schema 的区别。区别主要体现在两点,一个区别是在 group_message 表中增加了 author 字段来存放发帖作者的昵称,与 user 表的 nick_name 相对应,另外一 个就是第二个解决方案将 user 表和 group_message 表都分拆成了两个表,关系分别都是一一对应。 方案二看上去比方案一要更复杂一些,首先是表的数量多了 2 个,然后是在 group_message 中冗余存 放了作者昵称。我们试想一下,一个讨论区系统,访问最多的页面会是什么?我想大家都会很清楚是帖 子标题列表页面。而帖子标题列表页面最主要的信息就是都是来自 group_message 表中,同时帖子标题 后面的作者一般都是通过用户名成(昵称)来展示。按照第一种解决方案来设计的 Schema,我们就需要 执行类似如下这样的 SQL 语句来得到数据: SELECT t.id, t.subject,user.id, u.nick_name FROM ( SELECT id, user_id, subject FROM group_message WHERE group_id = ? ORDER BY gmt_modified DESC LIMIT 20 ) t, user u WHERE t.user_id = u.id 但是第二中解决方案所需要执行的 SQL 就会简单很多,如下: SELECT t.id, t.subject, t.user_id, t.author FROM group_message WHERE group_id = ? ORDER BY gmt_modified DESC LIMIT 20 两个 SQL 相比较,大家都能很明显的看出谁优谁劣了,第一个是需要读取两个表的数据进行 Join,
84. 与第二个 SQL 相比性能差距很大,尤其是如果第一个再写的差一点,性能更是非常糟糕,两者所带来的 资源消耗就更相差玄虚了。 不仅仅如此,由于第一个方案中的 group_message 表中还包含一个大字段“content”,该字段所存 放的信息要占整个表的绝大部分存储空间,但在这条系统中执行最频繁的 SQL 之一中是完全不需要该字 段所存放信息的,但是由于这个 SQL 又没办法做到不访问 group_message 表的数据,所以第一条 SQL 在数 据读取过程中会需要读取大量没有任何意义的数据。 在系统中用户数据的读取也是比较频繁的,但是大多数地方所需要的用户数据都只是用户的几个基 本属性,如用户的 id,昵称,密码,状态,邮箱等,所以将用户表的这几个属性单独分离出来后,也会 让大量的 SQL 语句在运行的时候减少数据的检索量,从而提高性能。 可能有人会觉得,在我们将一个表分成两个表的时候,我们如果要访问被分拆出去的信息的时候, 性能不是就会变差了吗?是的,对于那些需要访问如 user 的 sign,msn 等原来只需要一个表就可以完成 的 SQL 来说,现在都需要两条 SQL 来完成,性能确实会 有所降低,但是由于两个表都是一对一的关联关 系,关联字段的过滤性也非常高,而且这样的查询需求在整个系统中所占有的比例也并不高,所以这里 所带来的性能损失实际上要远远小于在其他 SQL 上所节省出来的资源,所以完全不必为此担心 6.5 硬件环境对系统性能的影响 在本章之前的所有部分都是介绍的整个系统中的软件环境对系统性能的影响,这一节我们将从系统 硬件环境来分析对数据库系统的影响,并从数据库服务器主机的角度来做一些针对性的优化建议。 任何一个系统的硬件环境都会对起性能起到非常关键的作用,这一点我想每一位读者朋友都是非常 清楚的。而数据库应用系统环境中,由于数据库自身的特点和在系统中的角色决定了他在整个系统中是 最难以扩展的部分。所以在大多数环境下,数据库服务器主机(或者主机集群)的性能在很大程度上决 定了整个应用系统的性能。 既然我们的数据库主机资源如此重要,肯定很多读者朋友会希望知道,数据库服务器主机的各部分 硬件到底谁最重要,各部分对整体性能的影响各自所占的比例是多少,以便能够根据这些比例选取合适 的主机机型作为数据库主机。但是我只能很遗憾的告诉大家,没有任何一个定律或者法则可以很准确的 给出这个答案。 当然,大家也不必太沮丧。虽然没有哪个法则可以准确的知道我们到底该如何选配一个主机的各部 分硬件,但是根据应用类型的不同,总体上还是有一个可以大致遵循的原则可以参考的。 首先,数据库主机是存取数据的地方,那么其 IO 操作自然不会少,所以数据库主机的 IO 性能肯定是 需要最优先考虑的一个因素,这一点不管是什么类型的数据库应用都是适用的。不过,这里的 IO 性能并 不仅仅只是指物理的磁盘 IO,而是主机的整体 IO 性能,是主机整个 IO 系统的总体 IO 性能。而 IO 性能 本身又可以分为两类,一类是每秒可提供的 IO 访问次数,也就是我们常说的 IOPS 数量,还有一种就是每 秒的 IO 总流量,也就是我们常说的 IO 吞吐量。在主机中决定 IO 性能部件主要由磁盘和内存所决定,当 然也包括各种与 IO 相关的板卡。
85. 其次,由于数据库主机和普通的应用程序服务器相比,资源要相对集中很多,单台主机上所需要进 行的计算量自然也就比较多,所以数据库主机的 CPU 处理能力也不能忽视。 最后,由于数据库负责数据的存储,与各应用程序的交互中传递的数据量比其他各类服务器都要 多,所以数据库主机的网络设备的性能也可能会成为系统的瓶颈。 由于上面这三类部件是影响数据库主机性能的最主要因素,其他部件成为性能瓶颈的几率要小很 多,所以后面我们通过对各种类型的应用做一个简单的分析,再针对性的给出这三类部件的基本选型建 议。 1、典型 OLTP 应用系统 对于各种数据库系统环境中大家最常见的 OLTP 系统,其特点是并发量大,整体数据量比较多,但每 次访问的数据比较少,且访问的数据比较离散,活跃数据占总体数据的比例不是太大。对于这类系统的 数据库实际上是最难维护,最难以优化的,对主机整体性能要求也是最高的。因为他不仅访问量很高, 数据量也不小。 针对上面的这些特点和分析,我们可以对 OLTP 的得出一个大致的方向。 虽然系统总体数据量较大,但是系统活跃数据在数据总量中所占的比例不大,那么我们可以通过扩 大内存容量来尽可能多的将活跃数据 cache 到内存中; 虽然 IO 访问非常频繁,但是每次访问的数据量较少且很离散,那么我们对磁盘存储的要求是 IOPS 表 现要很好,吞吐量是次要因素; 并发量很高,CPU 每秒所要处理的请求自然也就很多,所以 CPU 处理能力需要比较强劲; 虽然与客户端的每次交互的数据量并不是特别大,但是网络交互非常频繁,所以主机与客户端交互 的网络设备对流量能力也要求不能太弱。 2、典型 OLAP 应用系统 用于数据分析的 OLAP 系统的主要特点就是数据量非常大,并发访问不多,但每次访问所需要检索的 数据量都比较多,而且数据访问相对较为集中,没有太明显的活跃数据概念。 基于 OLAP 系统的各种特点和相应的分析,针对 OLAP 系统硬件优化的大致策略如下: 数据量非常大,所以磁盘存储系统的单位容量需要尽量大一些; 单次访问数据量较大,而且访问数据比较集中,那么对 IO 系统的性能要求是需要有尽可能大的每秒 IO 吞吐量,所以应该选用每秒吞吐量尽可能大的磁盘; 虽然 IO 性能要求也比较高,但是并发请求较少,所以 CPU 处理能力较难成为性能瓶颈,所以 CPU 处 理能力没有太苛刻的要求; 虽然每次请求的访问量很大,但是执行过程中的数据大都不会返回给客户端,最终返回给客户端的 数据量都较小,所以和客户端交互的网络设备要求并不是太高; 此外,由于 OLAP 系统由于其每次运算过程较长,可以很好的并行化,所以一般的 OLAP 系统都是由多 台主机构成的一个集群,而集群中主机与主机之间的数据交互量一般来说都是非常大的,所以在集群中 主机之间的网络设备要求很高。 3、除了以上两个典型应用之外,还有一类比较特殊的应用系统,他们的数据量不是特别大,但是访 问请求及其频繁,而且大部分是读请求。可能每秒需要提供上万甚至几万次请求,每次请求都非常简
86. 单,可能大部分都只有一条或者几条比较小的记录返回,就比如基于数据库的 DNS 服务就是这样类型的 服务。 虽然数据量小,但是访问极其频繁,所以可以通过较大的内存来 cache 住大部分的数据,这能够保 证非常高的命中率,磁盘 IO 量比较小,所以磁盘也不需要特别高性能的; 并发请求非常频繁,比需要较强的 CPU 处理能力才能处理; 虽然应用与数据库交互量非常大,但是每次交互数据较少,总体流量虽然也会较大,但是一般来说 普通的千兆网卡已经足够了。 在很多人看来,性能的根本决定因素是硬件性能的好坏。但实际上,硬件性能只能在某些阶段对系 统性能产生根本性影响。当我们的 CPU 处理能力足够的多,IO 系统的处理能力足够强的时候,如果我们 的应用架构和业务实现不够优化,一个本来很简单的实现非得绕很多个弯子来回交互多次,那再强的硬 件也没有用,因为来回的交互总是需要消耗时间。尤其是有些业务逻辑设计不是特别合理的应用,数据 库 Schema 设计的不够合理,一个任务在系统中又被分拆成很多个步骤,每个步骤都使用了非常复杂的 Query 语句。笔者曾经就遇到过这样一个系统,该系统是购买的某知名厂商的一个项目管理软件。该系统 最初运行在一台 Dell2950 的 PC Server 上面,使用者一直抱怨系统响应很慢,但我从服务器上面的状态 来看系统并繁忙(系统并发不是太大)。后来使用者强烈要求通过更换硬件设施来提升系统性能,虽然 我一直反对,但最后在管理层的要求下,更换成了一台 Sun 的 S880 小型机,主机 CPU 的处理能力至少是 原来机器的 3 倍以上,存储系统也从原来使用本地磁盘换成使用 EMC 的中断存储 CX300。可在试用阶段, 发现系统整体性能没有任何的提升,最终还是取消了更换硬件的计划。 所以,在应用系统的硬件配置方面,我们应该要以一个理性的眼光来看待,只有合适的才是最好 的。并不是说硬件资源越好,系统性能就一定会越好。而且,硬件系统本身总是有一个扩展极限的,如 果我们一味的希望通过升级硬件性能来解决系统的性能问题,那么总有一天将会遇到无法逾越的瓶颈。 到那时候,就算有再多的钱去砸也无济于事了。 6.6 小结 虽然本章是以影响 MySQL Server 性能的相关因素来展开分析,但实际上很多内容都对于大多数数据 库应用系统适用。数据库管理软件仅仅是实现了数据库应用系统中的数据存取操作,和数据的持久化。 数据库应用系统的优化真正能带来最大收益的就是商业需求和系统架构及业务实现的优化,然后是数据 库 Schema 设计的优化,然后才是 Query 语句的优化,最后才是数据库管理软件自身的一些优化。通过笔 者的经验,在整个系统的性能优化中,如果按照百分比来划分上面几个层面的优化带来的性能收益,可 以得出大概如下的数据: 需求和架构及业务实现优化:55% Query 语句的优化:30% 数据库自身的优化:15% 很多时候,大家看到数据库应用系统中性能瓶颈出现在数据库方面,就希望通过数据库的优化来解 决问题,但不管 DBA 对数据库多们了解,对 Query 语句的优化多么精通,最终还是很难解决整个系统的性 能问题。原因就在于并没有真正找到根本的症结所在。 所以,数据库应用系统的优化,实际上是一个需要多方面配合,多方面优化的才能产生根本性改善 的事情。简单来说,可以通过下面三句话来简单的概括数据库应用系统的性能优化:商业需求合理化, 系统架构最优化,逻辑实现精简化,硬件设施理性化。
87. 第 7 章 MySQL 数据库锁定机制 前言: 为了保证数据的一致完整性,任何一个数据库都存在锁定机制。锁定机制的优劣直接应想到一个数 据库系统的并发处理能力和性能,所以锁定机制的实现也就成为了各种数据库的核心技术之一。本章将 对 MySQL 中两种使用最为频繁的存储引擎 MyISAM 和 Innodb 各自的锁定机制进行较为详细的分析。 7.1 MySQL 锁定机制简介 数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变 得有序所设计的一种规则。对于任何一种数据库来说都需要有相应的锁定机制,所以 MySQL 自然也不能 例外。MySQL 数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特 点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定 场景而优化设计,所以各存储引擎的锁定机制也有较大区别。 总的来说,MySQL 各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁 定。下面我们先分析一下 MySQL 这三种锁定的特点和各自的优劣所在。 ● 行级锁定(row-level) 行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒 度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的 并发处理能力而提高一些需要高并发应用系统的整体性能。 虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源 的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行 级锁定也最容易发生死锁。 ● 表级锁定(table-level) 和行级锁定相反,表级别的锁定是 MySQL 各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的
88. 特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一 次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。 当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大 打折扣。 ● 页级锁定(page-level) 页级锁定是 MySQL 中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁 定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的 并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗 的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,应用程序的 访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。 在 MySQL 数据库中,使用表级锁定的主要是 MyISAM,Memory,CSV 等一些非事务性存储引擎,而使用 行级锁定的主要是 Innodb 存储引擎和 NDB Cluster 存储引擎,页级锁定主要是 BerkeleyDB 存储引擎的锁 定方式。 MySQL 的如此的锁定机制主要是由于其最初的历史所决定的。在最初,MySQL 希望设计一种完全独立 于各种存储引擎的锁定机制,而且在早期的 MySQL 数据库中,MySQL 的存储引擎(MyISAM 和 Momery)的 设计是建立在“任何表在同一时刻都只允许单个线程对其访问(包括读)”这样的假设之上。但是,随 着 MySQL 的不断完善,系统的不断改进,在 MySQL3.23 版本开发的时候,MySQL 开发人员不得不修正之前 的假设。因为他们发现一个线程正在读某个表的时候,另一个线程是可以对该表进行 insert 操作的,只 不过只能 INSERT 到数据文件的最尾部。这也就是从 MySQL 从 3.23 版本开始提供的我们所说的 Concurrent Insert。 当出现 Concurrent Insert 之后,MySQL 的开发人员不得不修改之前系统中的锁定实现功能,但是仅 仅只是增加了对 Concurrent Insert 的支持,并没有改动整体架构。可是在不久之后,随着 BerkeleyDB 存储引擎的引入,之前的锁定机制遇到了更大的挑战。因为 BerkeleyDB 存储引擎并没有 MyISAM 和 Memory 存储引擎同一时刻只允许单一线程访问某一个表的限制,而是将这个单线程访问限制的颗粒度缩小到了 单个 page,这又一次迫使 MySQL 开发人员不得不再一次修改锁定机制的实现。 由于新的存储引擎的引入,导致锁定机制不能满足要求,让 MySQL 的人意识到已经不可能实现一种 完全独立的满足各种存储引擎要求的锁定实现机制。如果因为锁定机制的拙劣实现而导致存储引擎的整 体性能的下降,肯定会严重打击存储引擎提供者的积极性,这是 MySQL 公司非常不愿意看到的,因为这 完全不符合 MySQL 的战略发展思路。所以工程师们不得不放弃了最初的设计初衷,在锁定实现机制中作 出修改,允许存储引擎自己改变 MySQL 通过接口传入的锁定类型而自行决定该怎样锁定数据。 7.2 各种锁定机制分析
89. 在整体了解了 MySQL 锁定机制之后,这一节我们将详细分析 MySQL 自身提供的表锁定机制和其他储引 擎实自身实现的行锁定机制,并通过 MyISAM 存储引擎和 Innodb 存储引擎实例演示。 表级锁定 MySQL 的表级锁定主要分为两种类型,一种是读锁定,另一种是写锁定。在 MySQL 中,主要通过四个 队列来维护这两种锁定:两个存放当前正在锁定中的读和写锁定信息,另外两个存放等待中的读写锁定 信息,如下: • Current read-lock queue (lock->read) • Pending read-lock queue (lock->read_wait) • Current write-lock queue (lock->write) • Pending write-lock queue (lock->write_wait) 当前持有读锁的所有线程的相关信息都能够在 Current read-lock queue 中找到,队列中的信息按 照获取到锁的时间依序存放。而正在等待锁定资源的信息则存放在 Pending read-lock queue 里面,另 外两个存放写锁信息的队列也按照上面相同规则来存放信息。 虽然对于我们这些使用者来说 MySQL 展现出来的锁定(表锁定)只有读锁定和写锁定这两种类型, 但是在 MySQL 内部实现中却有多达 11 种锁定类型,由系统中一个枚举量(thr_lock_type)定义,各值描 述如下: 锁定类型 IGNORE UNLOCK READ WRITE READ_WITH_SHARED_LOCKS READ_HIGH_PRIORITY READ_NO_INSERT WRITE_ALLOW_WRITE WRITE_ALLOW_READ WRITE_CONCURRENT_INSERT WRITE_DELAYED WRITE_LOW_PRIORITY WRITE_ONLY 说明 当发生锁请求的时候内部交互使用,在锁定结构和队列 中并不会有任何信息存储 释放锁定请求的交互用所类型 普通读锁定 普通写锁定 在 Innodb 中使用到,由如下方式产生 如:SELECT ... LOCK IN SHARE MODE 高优先级读锁定 不允许 Concurent Insert 的锁定 这个类型实际上就是当由存储引擎自行处理锁定的时 候,mysqld 允许其他的线程再获取读或者写锁定,因为 即使资源冲突,存储引擎自己也会知道怎么来处理 这种锁定发生在对表做 DDL(ALTER TABLE ...)的时 候,MySQL 可以允许其他线程获取读锁定,因为 MySQL 是 通过重建整个表然后再 RENAME 而实现的该功能,所在整 个过程原表仍然可以提供读服务 正在进行 Concurent Insert 时候所使用的锁定方式,该 锁定进行的时候,除了 READ_NO_INSERT 之外的其他任何 读锁定请求都不会被阻塞 在使用 INSERT DELAYED 时候的锁定类型 显 示 声 明 的 低 级 别 锁 定 方 式 , 通 过 设 置 LOW_PRIORITY_UPDAT = 1 而产生 当在操作过程中某个锁定异常中断之后系统内部需要进 行 CLOSE TABLE 操作,在这个过程中出现的锁定类型就 是 WRITE_ONLY
90. 读锁定 一个新的客户端请求在申请获取读锁定资源的时候,需要满足两个条件: 1、 请求锁定的资源当前没有被写锁定; 2、 写锁定等待队列(Pending write-lock queue)中没有更高优先级的写锁定等待; 如果满足了上面两个条件之后,该请求会被立即通过,并将相关的信息存入 Current read-lock queue 中,而如果上面两个条件中任何一个没有满足,都会被迫进入等待队列 Pending read-lock queue 中等待资源的释放。 写锁定 当客户端请求写锁定的时候,MySQL 首先检查在 Current write-lock queue 是否已经有锁定相同资 源的信息存在。 如果 Current write-lock queue 没有 , 则 再 检 查 Pending write-lock queue ,如 果 在 Pending write-lock queue 中找到了,自己也需要进入等待队列并暂停自身线程等待锁定资源。反之,如果 Pending write-lock queue 为空,则再检测 Current read-lock queue,如果有锁定存在,则同样需要 进入 Pending write-lock queue 等待。当然,也可能遇到以下这两种特殊情况: 1. 请求锁定的类型为 WRITE_DELAYED; 2. 请 求 锁 定 的 类 型 为 WRITE_CONCURRENT_INSERT 或 者 是 TL_WRITE_ALLOW_WRITE , 同 时 Current read lock 是 READ_NO_INSERT 的锁定类型。 当遇到这两种特殊情况的时候,写锁定会立即获得而进入 Current write-lock queue 中 如果刚开始第一次检测就 Current write-lock queue 中已经存在了锁定相同资源的写锁定存在,那 么就只能进入等待队列等待相应资源锁定的释放了。 读请求和写等待队列中的写锁请求的优先级规则主要为以下规则决定: 1. 除了 READ_HIGH_PRIORITY 的读锁定之外,Pending write-lock queue 中的 WRITE 写锁定能够阻 塞所有其他的读锁定; 2. READ_HIGH_PRIORITY 读锁定的请求能够阻塞所有 Pending write-lock queue 中的写锁定; 3. 除了 WRITE 写锁定之外,Pending write-lock queue 中的其他任何写锁定都比读锁定的优先级 低。 写锁定出现在 Current write-lock queue 之后,会阻塞除了以下情况下的所有其他锁定的请求: 1. 在某些存储引擎的允许下,可以允许一个 WRITE_CONCURRENT_INSERT 写锁定请求 2. 写锁定为 WRITE_ALLOW_WRITE 的时候,允许除了 WRITE_ONLY 之外的所有读和写锁定请求 3. 写锁定为 WRITE_ALLOW_READ 的时候,允许除了 READ_NO_INSERT 之外的所有读锁定请求 4. 写锁定为 WRITE_DELAYED 的时候,允许除了 READ_NO_INSERT 之外的所有读锁定请求 5. 写锁定为 WRITE_CONCURRENT_INSERT 的时候,允许除了 READ_NO_INSERT 之外的所有读锁定请求 随着 MySQL 存储引擎的不断发展,目前 MySQL 自身提供的锁定机制已经没有办法满足需求了,很多存 储引擎都在 MySQL 所提供的锁定机制之上做了存储引擎自己的扩展和改造。 MyISAM 存储引擎基本上可以说是对 MySQL 所提供的锁定机制所实现的表级锁定依赖最大的一种存储 引擎了,虽然 MyISAM 存储引擎自己并没有在自身增加其他的锁定机制,但是为了更好的支持相关特性,
91. MySQL 在原有锁定机制的基础上为了支持其 Concurrent Insert 的特性而进行了相应的实现改造。 而其他几种支持事务的存储存储引擎,如 Innodb,NDB Cluster 以及 Berkeley DB 存储引擎则是让 MySQL 将锁定的处理直接交给存储引擎自己来处理,在 MySQL 中仅持有 WRITE_ALLOW_WRITE 类型的锁定。 由于 MyISAM 存储引擎使用的锁定机制完全是由 MySQL 提供的表级锁定实现,所以下面我们将以 MyISAM 存储引擎作为示例存储引擎,来实例演示表级锁定的一些基本特性。由于,为了让示例更加直 观,我将使用显示给表加锁来演示: 时 刻 1 2 3 4 Session a READ sky@localhost : example 11:21:08> lock table test_table_lock read; Query OK, 0 rows affected (0.00 sec) 显示给 test_table_lock 加读锁定 sky@localhost : example 11:21:10> select * from test_table_lock limit 1; +------+------+ a b +------+------+ 1 1 +------+------+ 1 row in set (0.01 sec) 自己的读操作未被阻塞 sky@localhost : example 11:21:15> update test_table_lock set b = a limit 1; ERROR 1099 (HY000): Table 'test_table_lock' was locked with a READ lock and can't be updated sky@localhost : example 11:21:09> unlock tables; Query OK, 0 rows affected (0.00 sec) 解除读锁 5 sky@localhost : example 11:48:19> l Session b sky@localhost : example 11:21:13> select * from test_table_lock limit 1; +------+------+ a b +------+------+ 1 1 +------+------+ 1 row in set (0.01 sec) 其他线程的读也未被阻塞 sky@localhost : example 11:21:20> update test_table_lock set b = a limit 1; 写一下试试看?被阻塞了 sky@localhost : example 11:21:20> update test_table_lock set b = a limit 1; Query OK, 0 rows affected (1 min 15.52 sec) Rows matched: 1 Changed: 0 Warnings: 0 在 session a 释放锁定资源之后,session b 获得了资源,更新成功 sky@localhost : example 11:48:20> ins
92. ock table test_table_lock read local; Query OK, 0 rows affected (0.00 sec) 获取读锁定的时候增加 local 选项 6 7 8 9 10 ert into test_table_lock values(1,'s','c'); Query OK, 1 row affected (0.00 sec) 其他 session 的 insert 未被阻塞 sky@localhost : example 11:48:23> update test_table_lock set a = 1 limit 1; 其他 session 的更新操作被阻塞 WRITE 这次加写锁试试看: sky@localhost : example 11:27:01> lock table test_table_lock write; Query OK, 0 rows affected (0.00 sec) sky@localhost : example 11:27:10> sky@localhost : example 11:27:16> select * from test_table_lock limit select * from test_table_lock limit 1; 1; 其他 session 被阻塞 +------+------+ a b +------+------+ 1 1 +------+------+ 1 row in set (0.01 sec) 自己 session 可以继续读 sky@localhost : example 11:27:02> unlock tables; Query OK, 0 rows affected (0.00 sec) 释放锁定资源 sky@localhost : example 11:27:16> select * from test_table_lock limit 1; +------+------+ a b +------+------+ 1 1 +------+------+ 1 row in set (36.36 sec) 其他 session 获取的资源 WRITE_ALLOW_READ sky@localhost : example 11:42:24> sky@localhost : example 11:42:25> alter table test_table_lock add(c select * from test_table_lock limit 1; varchar(16)); +------+------+ Query OK, 5242880 rows affected a b (7.06 sec) +------+------+ Records: 5242880 Duplicates: 0 1 1 Warnings: 0 +------+------+ 1 row in set (0.01 sec) 通过执行 DDL(ALTER TABLE),获取 W 其他 session 的读未被阻塞
93. RITE_ALLOW_READ 类型的写锁定 行级锁定 行级锁定不是 MySQL 自己实现的锁定方式,而是由其他存储引擎自己所实现的,如广为大家所知的 Innodb 存储引擎,以及 MySQL 的分布式存储引擎 NDB Cluster 等都是实现了行级锁定。 Innodb 锁定模式及实现机制 考虑到行级锁定君由各个存储引擎自行实现,而且具体实现也各有差别,而 Innodb 是目前事务型存 储引擎中使用最为广泛的存储引擎,所以这里我们就主要分析一下 Innodb 的锁定特性。 总的来说,Innodb 的锁定机制和 Oracle 数据库有不少相似之处。Innodb 的行级锁定同样分为两种类 型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存, Innodb 也同样使用 了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。 当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源 的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一 个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。 而意向 锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时 候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上 面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面 添加一个排他锁的话,则先在表 上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。所 以,可以说 Innodb 的锁定模式实际上可以分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和 意向排他锁(IX),我们可以通过以下表格来总结上面这四种所的共存逻辑关系: 共享锁 排他锁 意向共享锁 意向排他锁 (S) (X) (IS) (IX) 共享锁(S) 排他锁(X) 意 向 共 享 锁 (IS) 意 向 排 他 锁 (IX) 兼容 冲突 兼容 冲突 冲突 冲突 兼容 冲突 兼容 冲突 冲突 兼容 冲突 冲突 兼容 兼容 虽然 Innodb 的锁定机制和 Oracle 有不少相近的地方,但是两者的实现确是截然不同的。总的来说就 是 Oracle 锁定数据是通过需要锁定的某行记录所在的物理 block 上的事务槽上表级锁定信息,而 Innodb 的锁定则是通过在指向数据记录的第一个索引键之前和最后一个索引键之后的空域空间上标记锁定信息 而实现的。Innodb 的这种锁定实现方式被称为“NEXT-KEY locking”(间隙锁),因为 Query 执行过程 中通过过范围查找的华,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。 间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜 的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成 很大的危害。而 Innodb 给出的解释是为了组织幻读的出现,所以他们选择的间隙锁来实现锁定。 除了间隙锁给 Innodb 带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性
94. 能隐患: ● 当 Query 无法利用索引的时候,Innodb 会放弃使用行级别锁定而改用表级别的锁定,造成并发 性能的降低; ● 当 Quuery 使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所只想的数据可 能有部分并不属于该 Query 的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范 围,而不是具体的索引键; ● 当 Query 在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索 引只是过滤条件的一部分),一样会被锁定 Innodb 各事务隔离级别下锁定及死锁 Innodb 实现的在 ISO/ANSI SQL92 规范中所定义的 Read UnCommited,Read Commited,Repeatable Read 和 Serializable 这四种事务隔离级别。同时,为了保证数据在事务中的一致性,实现了多版本数据 访问。 之前在第一节中我们已经介绍过,行级锁定肯定会带来死锁问题, Innodb 也不可能例外。至于死锁 的产生过程我们就不在这里详细描述了,在后面的锁定示例中会通过一个实际的例子为大家爱展示死锁 的产生过程。这里我们主要介绍一下,在 Innodb 中当系检测到死锁产生之后是如何来处理的。 在 Innodb 的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间 内就检测到该死锁的存在。当 Innodb 检测到系统中产生了死锁之后,Innodb 会通过相应的判断来选这产 生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。那 Innodb 是以什么来为标 准判定事务的大小的呢?MySQL 官方手册中也提到了这个问题,实际上在 Innodb 发现死锁之后,会计算 出两个事务各自插入、更新或者删除的数据量来判定两个事务的大小。也就是说哪个事务所改变的记录 条数越多,在死锁中就越不会被回滚掉。但是有一点需要注意的就是,当产生死锁的场景中涉及到不止 Innodb 存储引擎的时候,Innodb 是没办法检测到该死锁的,这时候就只能通过锁定超时限制来解决该死 锁了。另外,死锁的产生过程的示例将在本节最后的 Innodb 锁定示例中演示。 Innodb 锁定机制示例 mysql> create table test_innodb_lock (a int(11),b varchar(16)) engine=innodb; Query OK, 0 rows affected (0.02 sec) mysql> create index test_innodb_a_ind on test_innodb_lock(a); Query OK, 0 rows affected (0.05 sec) Records:'>Records: 0 Duplicates:'>Duplicates: 0 Warnings:'>Warnings: 0 mysql> create index test_innodb_lock_b_ind on test_innodb_lock(b); Query OK, 11 rows affected (0.01 sec) Records:'>Records: 11 Duplicates:'>Duplicates: 0 Warnings:'>Warnings: 0 时刻 Session a Session b 行锁定基本演示 1 mysql> set autocommit=0; mysql> set autocommit=0; Query OK, 0 rows affected (0.00 Query OK, 0 rows affected (0.00 sec) sec)
95. mysql> update test_innodb_lock set b = 'b1' where a = 1; Query OK, 1 row affected (0.00 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 1 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 更新,但是不提交 2 3 mysql> update test_innodb_lock set b = 'b1' where a = 1; 被阻塞,等待 mysql> commit; Query OK, 0 rows affected (0.05 sec) 提交 4 5 mysql> update test_innodb_lock set b = 'b1' where a = 1; Query OK, 0 rows affected (36.14 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 0 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 解除阻塞,更新正常进行 无索引升级为表锁演示 mysql> update test_innodb_lock set b = '2' where b = 2000; Query OK, 1 row affected (0.02 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 1 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 6 7 mysql> update test_innodb_lock set b = '3' where b = 3000; 被阻塞,等待 mysql> commit; Query OK, 0 rows affected (0.10 sec) 8 9 mysql> update test_innodb_lock set b = '3' where b = 3000; Query OK, 1 row affected (1 min 3.41 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 1 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 阻塞解除,完成更新 间隙锁带来的插入问题演示 mysql> select * test_innodb_lock; +------+------+ a b from
96. +------+------+ 1 b2 3 3 4 4000 5 5000 6 6000 7 7000 8 8000 9 9000 1 b1 +------+------+ 9 rows in set (0.00 sec) mysql> update test_innodb_lock set b = a * 100 where a < 4 and a > 1; Query OK, 1 row affected (0.02 sec) Rows matched:'>matched: 1 Changed:'>Changed: 1 Warnings:'>Warnings: 0 10 11 mysql> insert into test_innodb_lock values(2,'200'); 被阻塞,等待 mysql> commit; Query OK, 0 rows affected (0.02 sec) 12 13 mysql> insert into test_innodb_lock values(2,'200'); Query OK, 1 row affected (38.68 sec) 阻塞解除,完成插入 使用共同索引不同数据的阻塞示例 mysql> update test_innodb_lock set b = 'bbbbb' where a = 1 and b = 'b2'; Query OK, 1 row affected (0.00 sec) Rows matched:'>matched: 1 Changed:'>Changed: 1 Warnings:'>Warnings: 0 14 15 mysql> update test_innodb_lock set b = 'bbbbb' where a = 1 and b = 'b1'; 被阻塞 mysql> commit; Query OK, 0 rows affected (0.02 sec)
97. 16 17 mysql> update test_innodb_lock set b = 'bbbbb' where a = 1 and b = 'b1'; Query OK, 1 row affected (42.89 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 1 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 session 提交事务,阻塞去除,更新完成 死锁示例 mysql> update t1 set id = 110 where id = 11; Query OK, 0 rows affected (0.00 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 0 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 0 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 18 mysql> update t2 set id = 210 where id = 21; Query OK, 1 row affected (0.00 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 1 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 1 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 19 mysql> update t2 set id = 2100 where id = 21; 等待 session b 释放资源,被阻塞 20 mysql> update t1 set id = 1100 where id = 11; Query OK, 0 rows affected (0.39 sec) Rows matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched:'>matched: 0 Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed:'>Changed: 0 Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings:'>Warnings: 0 等待 session a 释放资源,被阻塞 两个 session 互相等等待对方的资源释放之后才能释放自己的资源,造成了死锁 7.3 合理利用锁机制优化 MySQL MyISAM 表锁优化建议 对于 MyISAM 存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或者页级锁所带来的 附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较到,所以造成锁定资源 的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。 所以,在优化 MyISAM 存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别 是不可能改变的了,所以我们首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可 能的并发。
98. 1、 缩短锁定时间 缩短锁定时间,短短几个字,说起来确实听容易的,但实际做起来恐怕就并不那么简单了。如何让 锁定时间尽可能的短呢?唯一的办法就是让我们的 Query 执行时间尽可能的短。 a) 尽两减少大的复杂 Query,将复杂 Query 分拆成几个小的 Query 分布进行; b) 尽可能的建立足够高效的索引,让数据检索更迅速; c) 尽量让 MyISAM 存储引擎的表只存放必要的信息,控制字段类型; d) 利用合适的机会优化 MyISAM 表数据文件; 2、 分离能并行的操作 说到 MyISAM 的表锁,而且是读写互相阻塞的表锁,可能有些人会认为在 MyISAM 存储引擎的表上就只 能是完全的串行化,没办法再并行了。大家不要忘记了,MyISAM 的存储引擎还有一个非常有用的特性, 那就是 Concurrent Insert(并发插入)的特性。 MyISAM 存储引擎有一个控制是否打开 Concurrent Insert 功能的参数选项:concurrent_insert,可 以设置为 0,1 或者 2。三个值的具体说明如下: a) concurrent_insert=2,无论 MyISAM 存储引擎的表数据文件的中间部分是否存在因为删除数据 而留下的空闲空间,都允许在数据文件尾部进行 Concurrent Insert; b) concurrent_insert=1,当 MyISAM 存储引擎表数据文件中间不存在空闲空间的时候,可以从文 件尾部进行 Concurrent Insert; c) concurrent_insert=0,无论 MyISAM 存储引擎的表数据文件的中间部分是否存在因为删除数据 而留下的空闲空间,都不允许 Concurrent Insert。 3、合理利用读写优先级 在本章各种锁定分析一节中我们了解到了 MySQL 的表级锁定对于读和写是有不同优先级设定的,默 认情况下是写优先级要大于读优先级。所以,如果我们可以根据各自系统环境的差异决定读与写的优先 级。如果我们的系统是一个以读为主,而且要优先保证查询性能的话,我们可以通过设置系统参数选项 low_priority_updates=1,将写的优先级设置为比读的优先级低,即可让告诉 MySQL 尽量先处理读请 求。当然,如果我们的系统需要有限保证数据写入的性能的话,则可以不用设置 low_priority_updates 参数了。 这里我们完全可以利用这个特性,将 concurrent_insert 参数设置为 1,甚至如果数据被删除的可能 性很小的时候,如果对暂时性的浪费少量空间并不是特别的在乎的话,将 concurrent_insert 参数设置 为 2 都可以尝试。当然,数据文件中间留有空域空间,在浪费空间的时候,还会造成在查询的时候需要 读取更多的数据,所以如果删除量不是很小的话,还是建议将 concurrent_insert 设置为 1 更为合适。 Innodb 行锁优化建议 Innodb 存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁 定会要更高一些,但是在整体并发处理能力方面要远远优于 MyISAM 的表级锁定的。当系统并发量较高的 时候,Innodb 的整体性能和 MyISAM 相比就会有比较明显的优势了。但是,Innodb 的行级锁定同样也有其 脆弱的一面,当我们使用不当的时候,可能会让 Innodb 的整体性能表现不仅不能比 MyISAM 高,甚至可能 会更差。 要想合理利用 Innodb 的行级锁定,做到扬长避短,我们必须做好以下工作: a) 尽可能让所有的数据检索都通过索引来完成,从而避免 Innodb 因为无法通过索引键加锁而升级 为表级锁定;
99. b) 合理设计索引,让 Innodb 在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免 造成不必要的锁定而影响其他 Query 的执行; c) 尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定 的记录; d) 尽量控制事务的大小,减少锁定的资源量和锁定时间长度; e) 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少 MySQL 因为实现事务隔离级 别所带来的附加成本; 由于 Innodb 的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率 的的小建议,读者朋友可以根据各自的业务特点针对性的尝试: a) 类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁; b) 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; c) 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁 产生的概率; 系统锁定争用情况查询 对于两种锁定级别,MySQL 内部有两组专门的状态变量记录系统内部锁资源争用情况,我们先看看 MySQL 实现的表级锁定的争用状态变量: mysql> show status like 'table%'; +-----------------------+-------+ Variable_name Value +-----------------------+-------+ Table_locks_immediate 100 Table_locks_waited 0 +-----------------------+-------+ 这里有两个状态变量记录 MySQL 内部表级锁定的情况,两个变量说明如下: ● Table_locks_immediate:产生表级锁定的次数; ● Table_locks_waited:出现表级锁定争用而发生等待的次数; 两个 状 态 值 都 是 从 系 统 启 动 后 开 始 记 录 , 没 出 现 一 次 对 应 的 事 件 则 数 量 加 1 。如 果 这 里 的 Table_locks_waited 状态值比较高,那么说明系统中表级锁定争用现象比较严重,就需要进一步分析为 什么会有较多的锁定资源争用了。 对于 Innodb 所使用的行级锁定,系统中是通过另外一组更为详细的状态变量来记录的,如下: mysql> show status like 'innodb_row_lock%'; +-------------------------------+--------+ Variable_name Value +-------------------------------+--------+ Innodb_row_lock_current_waits 0 Innodb_row_lock_time 490578 Innodb_row_lock_time_avg 37736 Innodb_row_lock_time_max 121411 Innodb_row_lock_waits 13 +-------------------------------+--------+
100. Innodb 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及 最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。对各个状态量的说明如 下: ● Innodb_row_lock_current_waits:当前正在等待锁定的数量; ● Innodb_row_lock_time:从系统启动到现在锁定总时间长度; ● Innodb_row_lock_time_avg:每次等待所花平均时间; ● Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间; ● Innodb_row_lock_waits:系统启动后到现在总共等待的次数; 对 于 这 5 个 状 态 变 量 , 比 较 重 要 的 主 要 是 Innodb_row_lock_time_avg( 等 待 平 均 时 长 ) , Innodb_row_lock_waits(等待总次数)以及 Innodb_row_lock_time(等待总时长)这三项。尤其是当等 待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后 根据分析结果着手指定优化计划。 此外,Innodb 出了提供这五个系统状态变量之外,还提供的其他更为丰富的即时状态信息供我们分 析使用。可以通过如下方法查看: 1. 通过创建 Innodb Monitor 表来打开 Innodb 的 monitor 功能: mysql> create table innodb_monitor(a int) engine=innodb; Query OK, 0 rows affected (0.07 sec) 2. 然后通过使用“SHOW INNODB STATUS”查看细节信息(由于输出内容太多就不在此记录了); 可能会有读者朋友问为什么要先创建一个叫 innodb_monitor 的表呢?因为创建该表实际上就是告诉 Innodb 我们开始要监控他的细节状态了,然后 Innodb 就会将比较详细的事务以及锁定信息记录进入 MySQL 的 error log 中,以便我们后面做进一步分析使用。 7.4 小结 本章以 MySQL Server 中的锁定简介开始,分析了当前 MySQL 中使用最为广泛的锁定方式表级锁定和 行级锁定的基本实现机制,并通过 MyISAM 和 Innodb 这两大典型的存储引擎作为示例存储引擎所使用的表 级锁定和行级锁定做了较为详细的分析和演示。然后,再通过分析两种锁定方式的特性,给出相应的优 化建议和策略。最后了解了一下在 MySQL Server 中如何获得系统当前各种锁定的资源争用状况。希望本 章内容能够对各位读者朋友在理解 MySQL 锁定机制方面有一定的帮助。 第 8 章 MySQL 数据库 Query 的优化
101. 前言: 在之前“影响 MySQL 应用系统性能的相关因素”一章中我们就已经分析过了 Query 语句对数据库性 能的影响非常大,所以本章将专门针对 MySQL 的 Query 语句的优化进行相应的分析。 8.1 理解 MySQL 的 Quer y Opt i mi zer 8.1.1 MySQL Query Optimizer 是什么? 在“MySQL 架构组成”一章中的 “MySQL 逻辑组成”一节中我们已经了解到,在 MySQL 中有一个专 门负责优化 SELECT 语句的优化器模块,这就是我们本节将要重点分析的 MySQL Optimizer,其主要的功 能就是通过计算分析系统中收集的各种统计信息,为客户端请求的 Query 给出他认为最优的执行计划, 也就是他认为最优的数据检索方式。 当 MySQL Optimizer 接收到从 Query Parser (解析器)送过来的 Query 之后,会根据 MySQL Query 语句的相应语法对该 Query 进行分解分析的同时,还会做很多其他的计算转化工作。如常量转 化,无效内容删除,常量计算等等。所有这些工作都只为了 Optimizer 工作的唯一目的,分析出最优的 数据检索方式,也就是我们常说的执行计划。 8.1.2 MySQL Query Optimizer 基本工作原理 在分析 MySQL Optimizer 的工作原理之前,先了解一下 MySQL 的 Query Tree。MySQL 的 Query Tree 是通过优化实现 DBXP 的经典数据结构和 Tree 构造器而生成的一个指导完成一个 Query 语句的请求所 需要处理的工作步骤,我们可以简单的认为就是一个的数据处理流程规划,只不过是以一个 Tree 的数据 结构存放而已。通过 Query Tree 我们可以很清楚的知道一个 Query 的完成需要经过哪些步骤的处理, 每一步的数据来源在哪里,处理方式是怎样的。在整个 DBXP 的 Query Tree 生成过程中,MySQL 使用了 LEX 和 YACC 这两个功能非常强大的语法(词法)分析工具。MySQL Query Optimizer 的所有工作都是基 于这个 Query Tree 所进行的。各位读者朋友如果对 MySQL Query Tree 实现生成的详细信息比较感兴 趣,可以参考 Chales A. Bell 的《Expert MySQL》这本书,里面有比较详细的介绍。 MySQL Query Optimizer 并不是一个纯粹的 CBO(Cost Base Optimizer),而是在 CBO 的基础上增 加了一个被称为 Heuristic Optimize(启发式优化)的功能。也就是说,MySQL Query Optimizer 在优 化一个 Query 选择出他认为的最优执行计划的时候,并不一定完全按照系数据库的元信息和系统统计信 息,而是在此基础上增加了某些特定的规则。其实我个人的理解就是在 CBO 的实现中增加了部分 RBO(Rule Base Optimizer)的功能,以确保在某些特别的场景下控制 Query 按照预定的方式生成执行 计划。 当客户端向 MySQL 请求一条 Query ,到命令解析器模块完成请求分类区别出是 SELECT 并转发给 Query Optimizer 之后,Query Optimizer 首先会对整条 Query 进行,优化处理掉一些常量表达式的预 算,直接换算成常量值。并对 Query 中的查询条件进行简化和转换,如去掉一些无用或者显而易见的条
102. 件,结构调整等等。然后则是分析 Query 中的 Hint 信息(如果有),看显示 Hint 信息是否可以完全 确定该 Query 的执行计划。如果没有 Hint 或者 Hint 信息还不足以完全确定执行计划,则会读取所涉 及对象的统计信息,根据 Query 进行写相应的计算分析,然后再得出最后的执行计划。 Query Optimizer 是一个数据库软件非常核心的功能,虽然在这里说起来只是简单的几句话,但是 在 MySQL 内部,Query Optimizer 实际上是经过了很多复杂的运算分析,才得出最后的执行计划。对于 MySQL Query Optimizer 更多的信息,各位读者可以通过 MySQL Internal 文档进行更为全面的了解。 8.2 Quer y 语句优化基本思路和原则 在分析如何优化 MySQL Query 之前,我们需要先了解一下 Query 语句优化的基本思路和原则。一 般来说,Query 语句的优化思路和原则主要提现在以下几个方面: 1. 优化更需要优化的 Query; 2. 定位优化对象的性能瓶颈; 3. 明确的优化目标; 4. 从 Explain 入手; 5. 多使用 profile 6. 永远用小结果集驱动大的结果集; 7. 尽可能在索引中完成排序; 8. 只取出自己需要的 Columns; 9. 仅仅使用最有效的过滤条件; 10. 尽可能避免复杂的 Join 和子查询; 上面所列的几点信息,前面 4 点可以理解为 Query 优化的一个基本思路,后面部分则是我们优化中 的基本原则。 下面我们先针对 Query 优化的基本思路做一些简单的分析,理解为什么我们的 Query 优化到底该 如何进行。 优化更需要优化的 Query 为什么我们需要优化更需要优化的 Query?这个地球人都知道的“并不能成为问题的问题”我想就 并不需要我过多解释吧,哈哈。 那什么样的 Query 是更需要优化呢?对于这个问题我们需要从对整个系统的影响来考虑。什么 Query 的优化能给系统整体带来更大的收益,就更需要优化。一般来说,高并发低消耗(相对)的 Query 对整个系统的影响远比低并发高消耗的 Query 大。我们可以通过以下一个非常简单的案例分析来 充分说明问题。 假设有一个 Query 每小时执行 10000 次,每次需要 20 个 IO。另外一个 Query 每小时执行 10 次, 每次需要 20000 个 IO。
103. 我们先通过 IO 消耗方面来分析。可以看出,两个 Query 每小时所消耗的 IO 总数目是一样的,都是 200000 IO/小时。假设我们优化第一个 Query,从 20 个 IO 降低到 18 个 IO,也就是仅仅降低了 2 个 IO, 则我们节省了 2 * 10000 = 20000 (IO/小时)。而如果希望通过优化第二个 Query 达到相同的效果, 我们必须要让每个 Query 减少 20000 / 10 = 2000 IO。我想大家都会相信让第一个 Query 节省 2 个 IO 远比第二个 Query 节省 2000 个 IO 来的容易。 其次,如果通过 CPU 方面消耗的比较,原理和上面的完全一样。只要让第一个 Query 稍微节省一 小块资源,就可以让整个系统节省出一大块资源,尤其是在排序,分组这些对 CPU 消耗比较多的操作中 尤其突出。 最后,我们从对整个系统的影响来分析。一个频繁执行的高并发 Query 的危险性比一个低并发的 Query 要大很多。当一个低并发的 Query 走错执行计划,所带来的影响主要只是该 Query 的请求者的 体验会变差,对整体系统的影响并不会特别的突出,之少还属于可控范围。但是,如果我们一个高并发 的 Query 走错了执行计划,那所带来的后可很可能就是灾难性的,很多时候可能连自救的机会都不给你 就会让整个系统 Crash 掉。曾经我就遇到这样一个案例,系统中一个并发度较高的 Query 语句走错执 行计划,系统顷刻间 Crash,甚至我都还没有反应过来是怎么回事。当重新启动数据库提供服务后,系 统负载立刻直线飙升,甚至都来不及登录数据库查看当时有哪些 Active 的线程在执行哪些 Query。如 果是遇到一个并发并不太高的 Query 走错执行计划,至少我们还可以控制整个系统不至于系统被直接压 跨,甚至连问题根源都难以抓到。 定位优化对象的性能瓶颈 当我们拿到一条需要优化的 Query 之后,第一件事情是什么?是反问自己,这条 Query 有什么问 题?我为什么要优化他?只有明白了这些问题,我们才知道我们需要做什么,才能够找到问题的关键。 而不能就只是觉得某个 Query 好像有点慢,需要优化一下,然后就开始一个一个优化方法去轮番尝试。 这样很可能整个优化过程会消耗大量的人力和时间成本,甚至可能到最后还是得不到一个好的优化结 果。这就像看病一样,医生必须要清楚的知道我们病的根源才能对症下药。如果只是知道我们什么地方 不舒服,然后就开始通过各种药物尝试治疗,那这样所带来的后果可能就非常严重了。 所以,在拿到一条需要优化的 Query 之后,我们首先要判断出这个 Query 的瓶颈到底是 IO 还是 CPU。到底是因为在数据访问消耗了太多的时间,还是在数据的运算(如分组排序等)方面花费了太多资 源? 一般来说,在 MySQL 5.0 系列版本中,我们可以通过系统自带的 PROFILING 功能很清楚的找出一个 Query 的瓶颈所在。当然,如果读者朋友为了使用 MySQL 的某些在 5.1 版本中才有的新特性(如 Partition,EVENT 等)亦或者是比较喜欢尝试新事务而早早使用的 MySQL 5.1 的预发布版本,可能就没 办法使用这个功能了,因为该功能在 MySQL5.1 系列刚开始的版本中并不支持,不过让人非常兴奋的是该 功能在最新出来的 MySQL 5.1 正式版(5.1.30)又已经提供了。而如果读者朋友正在使用的 MySQL 是 4.x 版本,那可能就只能通过自行分析 Query 的各个执行步骤,找到性能损失最大的地方。 明确的优化目标 当我们定为到了一条 Query 的性能瓶颈之后,就需要通过分析该 Query 所完成的功能和 Query 对 系统的整体影响制订出一个明确的优化目标。没有一个明确的目标,优化过程将是一个漫无目的而且低
104. 效的过程,也很难达收到一个理想的效果。尤其是对于一些实现应用中较为重要功能点的 Query 更是如 此。 如何设定优化目标?这可能是很多人都非常头疼的问题,对于我自己也一样。要设定一个合理的优 化目标,不能过于理想也不能放任自由,确实是一件非常头疼的事情。一般来说,我们首先需要清楚的 了解数据库目前的整体状态,同时也要清楚的知道数据库中与该 Query 相关的数据库对象的各种信息, 而且还要了解该 Query 在整个应用系统中所实现的功能。了解了数据库整体状态,我们就能知道数据库 所能承受的最大压力,也就清楚了我们能够接受的最悲观情况。把握了该 Query 相关数据库对象的信 息,我们就应该知道实现该 Query 的消耗最理想情况下需要消耗多少资源,最糟糕又需要消耗多少资 源。最后,通过该 Query 所实现的功能点在整个应用系统中的重要地位,我们可以大概的分析出该 Query 可以占用的系统资源比例,而且我们也能够知道该 Query 的效率给客户带来的体验影响到底有多 大。 当我们清楚了这些信息之后,我们基本可以得出该 Query 应该满足的一个性能范围是怎样的,这也 就是我们的优化目标范围,然后就是通过寻找相应的优化手段来解决问题了。如果该 Query 实现的应用 系统功能比较重要,我们就必须让目标更偏向于理想值一些,即使在其他某些方面作出一些让步与牺 牲,比如调整 schema 设计,调整索引组成等,可能都是需要的。而如果该 Query 所实现的是一些并不 是太关键的功能,那我们可以让目标更偏向悲观值一些,而尽量保证其他更重要的 Query 的性能。这种 时候,即使需要调整商业需求,减少功能实现,也不得不应该作出让步。 从 Explain 入手 现在,优化目标也已经明确了,自然是奥开始动手的时候了。我们的优化到底该从何处入手呢?答 案只有一个,从 Explain 开始入手。为什么?因为只有 Explain 才能告诉你,这个 Query 在数据库中是 以一个什么样的执行计划来实现的。 但是,有一点我们必须清楚,Explain 只是用来获取一个 Query 在当前状态的数据库中的执行计 划,在优化动手之前,我们比需要根据优化目标在自己头脑中有一个清晰的目标执行计划。只有这样, 优化的目标才有意义。一个优秀的 SQL 调优人员(或者成为 SQL Performance Tuner),在优化任何一 个 SQL 语句之前,都应该在自己头脑中已经先有一个预定的执行计划,然后通过不断的调整尝试,再借 助 Explain 来验证调整的结果是否满足自己预定的执行计划。对于不符合预期的执行计划需要不断分析 Query 的写法和数据库对象的信息,继续调整尝试,直至得到预期的结果。 当然,人无完人,并不一定每次自己预设的执行计划都肯定是最优的,在不断调整测试的过程中, 如果发现 MySQL Optimizer 所选择的执行计划的实际执行效果确实比自己预设的要好,我们当然还是应 该选择使用 MySQL optimizer 所生成的执行计划。 上面的这个优化思路,只是给大家指了一个优化的基本方向,实际操作还需要读者朋友不断的结合 具体应用场景不断的测试实践来体会。当然也并不一定所有的情况都非要严格遵循这样一个思路,规则 是死的,人是活的,只有更合理的方法,没有最合理的规则。 在了解了上面这些优化的基本思路之后,我们再来看看优化的几个基本原则。 永远用小结果集驱动大的结果集
105. 很多人喜欢在优化 SQL 的时候说用小表驱动大表,个人认为这样的说法不太严谨。为什么?因 为大表经过 WHERE 条件过滤之后所返回的结果集并不一定就比小表所返回的结果集大,可能反而更小。 在这种情况下如果仍然采用小表驱动大表,就会得到相反的性能效果。 其实这样的结果也非常容易理解,在 MySQL 中的 Join,只有 Nested Loop 一种 Join 方式,也就是 MySQL 的 Join 都是通过嵌套循环来实现的。驱动结果集越大,所需要循环的此时就越多,那么被驱动表 的访问次数自然也就越多,而每次访问被驱动表,即使需要的逻辑 IO 很少,循环次数多了,总量自然也 不可能很小,而且每次循环都不能避免的需要消耗 CPU ,所以 CPU 运算量也会跟着增加。所以,如果 我们仅仅以表的大小来作为驱动表的判断依据,假若小表过滤后所剩下的结果集比大表多很多,结果就 是需要的嵌套循环中带来更多的循环次数,反之,所需要的循环次数就会更少,总体 IO 量和 CPU 运算 量也会少。而且,就算是非 Nested Loop 的 Join 算法,如 Oracle 中的 Hash Join,同样是小结果集 驱动大的结果集是最优的选择。 所以,在优化 Join Query 的时候,最基本的原则就是“小结果集驱动大结果集”,通过这个原则 来减少嵌套循环中的循环次数,达到减少 IO 总量以及 CPU 运算的次数。 尽可能在索引中完成排序 只取出自己需要的 Columns 任何时候在 Query 中都只取出自己需要的 Columns,尤其是在需要排序的 Query 中。为什么? 对于任何 Query,返回的数据都是需要通过网络数据包传回给客户端,如果取出的 Column 越多, 需要传输的数据量自然会越大,不论是从网络带宽方面考虑还是从网络传输的缓冲区来看,都是一个浪 费。 如果是需要排序的 Query 来说,影响就更大了。在 MySQL 中存在两种排序算法,一种是在 MySQL4.1 之前的老算法,实现方式是先将需要排序的字段和可以直接定位到相关行数据的指针信息取 出,然后在我们所设定的排序区(通过参数 sort_buffer_size 设定)中进行排序,完成排序之后再次 通过行指针信息取出所需要的 Columns,也就是说这种算法需要访问两次数据。第二种排序算法是从 MySQL4.1 版本开始使用的改进算法,一次性将所需要的 Columns 全部取出,在排序区中进行排序后直 接将数据返回给请求客户端。改行算法只需要访问一次数据,减少了大量的随机 IO,极大的提高了带有 排序的 Query 语句的效率。但是,这种改进后的排序算法需要一次性取出并缓存的数据比第一种算法 要多很多,如果我们将并不需要的 Columns 也取出来,就会极大的浪费排序过程所需要的内存。在 MySQL4.1 之后的版本中,我们可以通过设置 max_length_for_sort_data 参数大小来控制 MySQL 选择 第一种排序算法还是第二种排序算法。当所取出的 Columns 的单条记录总大小 max_length_for_sort_data 设置的大小的时候,MySQL 就会选择使用第一种排序算法,反之,则会选 择第二种优化后的算法。为了尽可能提高排序性能,我们自然是更希望使用第二种排序算法,所以在 Query 中仅仅取出我们所需要的 Columns 是非常有必要的。 仅仅使用最有效的过滤条件 很多人在优化 Query 语句的时候很容易进入一个误区,那就是觉得 WHERE 子句中的过滤条件越多 越好,实际上这并不是一个非常正确的选择。其实我们分析 Query 语句的性能优劣最关键的就是要让他
106. 选择一条最佳的数据访问路径,如何做到通过访问最少的数据量完成自己的任务。 为什么说过滤条件多不一定是好事呢?请看下面示例: 需求: 查找某个用户在所有 group 中所发的讨论 message 基本信息。 场景: 1、知道用户 ID 和用户 nick_name 2、信息所在表为 group_message 3、group_message 中存在用户 ID(user_id)和 nick_name(author)两个索引 方案一:将用户 ID 和用户 nick_name 两者都作为过滤条件放在 WHERE 子句中来查询,Query 的执行计 划如下: sky@localhost : example 11:29:37> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 1 AND author='1111111111'\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ref possible_keys:'>keys: group_message_author_ind,group_message_uid_ind key:'>key: group_message_author_ind key_len:'>len: 98 ref:'>ref: const rows:'>rows: 1 Extra:'>Extra: Using where 1 row in set (0.00 sec) 方案二:仅仅将用户 ID 作为过滤条件放在 WHERE 子句中来查询,Query 的执行计划如下: sky@localhost : example 11:30:45> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 1\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ref possible_keys:'>keys: group_message_uid_ind key:'>key: group_message_uid_ind key_len:'>len: 4 ref:'>ref: const rows:'>rows: 1 Extra:'>Extra: 1 row in set (0.00 sec) 方案二:仅将用户 nick_name 作为过滤条件放在 WHERE 子句中来查询,Query 的执行计划如下:
107. sky@localhost : example 11:38:45> EXPLAIN SELECT * FROM group_message -> WHERE author = '1111111111'\G *************************** 1. row *************************** id: 1 select_type:'>type: SIMPLE table: group_message type:'>type: ref possible_keys: group_message_author_ind key: group_message_author_ind key_len:'>len:'>len:'>len: 98 ref: const rows: 1 Extra: Using where 1 row in set (0.00 sec) 初略一看三个执行计划好像都挺好的啊,每一个 Query 的执行类型都利用到了索引,而且都是 “ref”类型。可是仔细一分析,就会发现,group_message_uid_ind 索引的索引键长度为 4(key_len:'>len:'>len:'>len: 4),由于 user_id 字段类型为 int,所以我们可以判定出 Query Optimizer 给出的这个索引键长度是 完全准确的。而 group_message_author_ind 索引的索引键长度为 98(key_len:'>len:'>len:'>len: 98),因为 author 字 段定义为 varchar(32) ,而所使用的字符集是 utf8,32 * 3 + 2 = 98。而且,由于 user_id 与 author(来源于 nick_name)全部都是一一对应的,所以同一个 user_id 有哪些记录,那么所对应的 author 也会有完全相同的记录。所以,同样的数据在 group_message_author_ind 索引中所占用的存储 空间要远远大于 group_message_uid_ind 索引所占用的空间。占用空间更大,代表我们访问该索引所需 要读取的数据量就会更多。所以,选择 group_message_uid_ind 的执行计划才是最有的执行计划。也就 是说,上面的方案二才是最有方案,而使用了更多的 WHERE 条件的方案一反而没有仅仅使用 user_id 一个过滤条件的方案一优。 可能有些人会说,那如果将 user_id 和 author 两者建立联合索引呢?告诉你,效果可能比没有这 个索引的时候更差,因为这个联合索引的索引键更长,索引占用的空间将会更大。 这个示例并不一定能代表所有场景,仅仅是希望让大家明白,并不是任何时候都是使用的过滤条件 越多性能会越好。在实际应用场景中,肯定会存在更多更复杂的情形,怎样使我们的 Query 有一个更优 化的执行计划,更高效的性能,还需要靠大家仔细分析各种执行计划的具体差别,才能选择出更优化的 Query。 尽可能避免复杂的 Join 和子查询 我们都知道,MySQL 在并发这一块做的并不是太好,当并发量太高的时候,系统整体性能可能会急 剧下降,尤其是遇到一些较为复杂的 Query 的时候更是如此。这主要与 MySQL 内部资源的争用锁定控 制有关,如读写相斥等等。对于 Innodb 存储引擎由于实现了行级锁定可能还要稍微好一些,如果使用 的 MyISAM 存储引擎,并发一旦较高的时候,性能下降非常明显。所以,我们的 Query 语句所涉及到的 表越多,所需要锁定的资源就越多。也就是说,越复杂的 Join 语句,所需要锁定的资源也就越多,所 阻塞的其他线程也就越多。相反,如果我们将比较复杂的 Query 语句分拆成多个较为简单的 Query 语
108. 句分步执行,每次锁定的资源也就会少很多,所阻塞的其他线程也要少一些。 可能很多读者会有疑问,将复杂 Join 语句分拆成多个简单的 Query 语句之后,那不是我们的网络 交互就会更多了吗?网络延时方面的总体消耗也就更大了啊,完成整个查询的时间不是反而更长了吗? 是的,这种情况是可能存在,但也并不是肯定就会如此。我们可以再分析一下,一个复杂的 Join Query 语句在执行的时候,所需要锁定的资源比较多,可能被别人阻塞的概率也就更大,如果是一个简单的 Query,由于需要锁定的资源较少,被阻塞的概率也会小很多。所以 较为复杂的 Join Query 也有可能 在执行之前被阻塞而浪费更多的时间。而且,我们的数据库所服务的并不是单单这一个 Query 请求,还 有很多很多其他的请求,在高并发的系统中,牺牲单个 Query 的短暂响应时间而提高整体处理能力也是 非常值得的。优化本身就是一门平衡与取舍的艺术,只有懂得取舍,平衡整体,才能让系统更优。 对于子查询,可能不需要我多说很多人就明白为什么会不被推荐使用。在 MySQL 中,子查询的实现 目前还比较差,很难得到一个很好的执行计划,很多时候明明有索引可以利用,可 Query Optimizer 就 是不用。从 MySQL 官方给出的信息说,这一问题将在 MySQL6.0 中得到较好的解决,将会引入 SemiJoin 的执行计划,可 MySQL6.0 离我们投入生产环境使用恐怕还有很遥远的一段时间。所以,在 Query 优化的过程中,能不用子查询的时候就尽量不要使用子查询。 上面这些仅仅只是一些常用的优化原则,并不是说在 Query 优化中就只需要做到这些原则就可以, 更不是说 Query 优化只能通过这些原则来优化。在实际优化过程中,我们还可能会遇到很多带有较为复 杂商业逻辑的场景,具体的优化方法就只能根据不同的应用场景来具体分析,逐步调整。其实,最有效 的优化,就是不要用,也就是不要实现这个商业需求。 8.3 充分利用 Expl ai n 和 Profi l i ng 8.3.1 Explain 的使用 说到 Explain,肯定很多读者之前都都已经用过了,MySQL Query Optimizer 通过我让们执行 EXPLAIN 命令来告诉我们他将使用一个什么样的执行计划来优化我们的 Query。所以,可以说 Explain 是在优化 Query 时最直接有效的验证我们想法的工具。在本章前面部分我就说过,一个好的 SQL Performance Tuner 在动手优化一个 Query 之前,头脑中就应该已经有一个好的执行计划,后面的优化 工作只是为实现该执行计划而作出各种调整。 在我们对某个 Query 优化过程中,需要不断的使用 Explain 来验证我们的各种调整是否有效。就 像本书之前的很多示例都会通过 Explain 来验证和展示结果一样,所有的 Query 优化都应该充分利用 他。 我们先看一下在 MySQL Explain 功能中给我们展示的各种信息的解释: ◆ ID:Query Optimizer 所选定的执行计划中查询的序列号; ◆ Select_type:所使用的查询类型,主要有以下这几种查询类型 ◇ DEPENDENT SUBQUERY:子查询中内层的第一个 SELECT,依赖于外部查询的结果集; ◇ DEPENDENT UNION:子查询中的 UNION,且为 UNION 中从第二个 SELECT 开始的后面所有
109. ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ SELECT,同样依赖于外部查询的结果集; ◇ PRIMARY:子查询中的最外层查询,注意并不是主键查询; ◇ SIMPLE:除子查询或者 UNION 之外的其他查询; ◇ SUBQUERY:子查询内层查询的第一个 SELECT,结果不依赖于外部查询结果集; ◇ UNCACHEABLE SUBQUERY:结果集无法缓存的子查询; ◇ UNION:UNION 语句中第二个 SELECT 开始的后面所有 SELECT,第一个 SELECT 为 PRIMARY ◇ UNION RESULT:UNION 中的合并结果; Table:显示这一步所访问的数据库中的表的名称; Type:告诉我们对表所使用的访问方式,主要包含如下集中类型; ◇ all:全表扫描 ◇ const:读常量,且最多只会有一条记录匹配,由于是常量,所以实际上只需要读一次; ◇ eq_ref:最多只会有一条匹配结果,一般是通过主键或者唯一键索引来访问; ◇ fulltext: ◇ index:全索引扫描; ◇ index_merge:查询中同时使用两个(或更多)索引,然后对索引结果进行 merge 之后再读 取表数据; ◇ index_subquery:子查询中的返回结果字段组合是一个索引(或索引组合),但不是一个 主键或者唯一索引; ◇ rang:索引范围扫描; ◇ ref:Join 语句中被驱动表索引引用查询; ◇ ref_or_null:与 ref 的唯一区别就是在使用索引引用查询之外再增加一个空值的查询; ◇ system:系统表,表中只有一行数据; ◇ unique_subquery:子查询中的返回结果字段组合是主键或者唯一约束; ◇ Possible_keys:该查询可以利用的索引. 如果没有任何索引可以使用,就会显示成 null,这一 项内容对于优化时候索引的调整非常重要; Key:MySQL Query Optimizer 从 possible_keys 中所选择使用的索引; Key_len:被选中使用索引的索引键长度; Ref:列出是通过常量(const),还是某个表的某个字段(如果是 join)来过滤(通过 key) 的; Rows:MySQL Query Optimizer 通过系统收集到的统计信息估算出来的结果集记录条数; Extra:查询中每一步实现的额外细节信息,主要可能会是以下内容: ◇ Distinct:查找 distinct 值,所以当 mysql 找到了第一条匹配的结果后,将停止该值的查 询而转为后面其他值的查询; ◇ Full scan on NULL key:子查询中的一种优化方式,主要在遇到无法通过索引访问 null 值的使用使用; ◇ Impossible WHERE noticed after reading const tables:MySQL Query Optimizer 通过 收集到的统计信息判断出不可能存在结果; ◇ No tables:Query 语句中使用 FROM DUAL 或者不包含任何 FROM 子句; ◇ Not exists:在某些左连接中 MySQL Query Optimizer 所通过改变原有 Query 的组成而 使用的优化方法,可以部分减少数据访问次数; ◇ Range checked for each record (index map: N):通过 MySQL 官方手册的描述,当 MySQL Query Optimizer 没有发现好的可以使用的索引的时候,如果发现如果来自前面的 表的列值已知,可能部分索引可以使用。对前面的表的每个行组合,MySQL 检查是否可以使
110. ◇ ◇ ◇ ◇ ◇ ◇ ◇ 用 range 或 index_merge 访问方法来索取行。 Select tables optimized away:当我们使用某些聚合函数来访问存在索引的某个字段的 时候,MySQL Query Optimizer 会通过索引而直接一次定位到所需的数据行完成整个查 询。当然,前提是在 Query 中不能有 GROUP BY 操作。如使用 MIN()或者 MAX()的时 候; Using filesort:当我们的 Query 中包含 ORDER BY 操作,而且无法利用索引完成排序操 作的时候,MySQL Query Optimizer 不得不选择相应的排序算法来实现。 Using index:所需要的数据只需要在 Index 即可全部获得而不需要再到表中取数据; Using index for group-by:数据访问和 Using index 一样,所需数据只需要读取索引即 可,而当 Query 中使用了 GROUP BY 或者 DISTINCT 子句的时候,如果分组字段也在索引 中,Extra 中的信息就会是 Using index for group-by; Using temporary:当 MySQL 在某些操作中必须使用临时表的时候,在 Extra 信息中就会 出现 Using temporary 。主要常见于 GROUP BY 和 ORDER BY 等操作中。 Using where:如果我们不是读取表的所有数据,或者不是仅仅通过索引就可以获取所有需 要的数据,则会出现 Using where 信息; Using where with pushed condition:这是一个仅仅在 NDBCluster 存储引擎中才会出现 的信息,而且还需要通过打开 Condition Pushdown 优化功能才可能会被使用。控制参数 为 engine_condition_pushdown 。 这里我们通过分析示例来看一下不同的 Query 语句通过 Explain 所显示的不同信息: 我们先看一个简单的单表 Query: sky@localhost : example 11:33:18> explain select count(*),max(id),min(id) -> from user\G *************************** 1. row *************************** id: 1 select_type:'>type: SIMPLE table: NULL type:'>type: NULL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: NULL Extra: Select tables optimized away 对 user 表的单表查询,查询类型为 SIMPLE,因为既没有 UNION 也不是子查询。聚合函数 MAX MIN 以及 COUNT 三者所需要的数据都可以通过索引就能够直接定位得到数据,所以整个实现的 Extra 信息 为 Select tables optimized away。 再来看一个稍微复杂一点的 Query,一个子查询: sky@localhost : example 11:38:48> explain select name from groups -> where id in ( select group_id from user_group where user_id = 1)\G *************************** 1. row ***************************
111. id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: PRIMARY table:'>table: groups type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ALL possible_keys:'>keys: NULL key:'>key: NULL key_len:'>len: NULL ref:'>ref: NULL rows:'>rows: 50000 Extra:'>Extra: Using where *************************** 2. row *************************** id:'>id: 2 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: DEPENDENT SUBQUERY table:'>table: user_group type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ref possible_keys:'>keys: user_group_gid_ind,user_group_uid_ind key:'>key: user_group_uid_ind key_len:'>len: 4 ref:'>ref: const rows:'>rows: 1 Extra:'>Extra: Using where 通过 id 信息我们可以得知 MySQL Query Optimizer 给出的执行计划是首先对 groups 进行全表扫 描,然后第二步才访问 user_group 表,所使用的查询方式是 DEPENDENT SUBQUERY,对所需数据的访问 方式是索引扫描,由于过滤条件是一个整数,所以索引扫描的类型为 ref,过滤条件是 const。可以使 用的索引有两个,一个是基于 user_id,另一个则是基于 group_id 的。为什么基于 group_id 的索引 user_group_gid_ind 也被列为可选索引了呢?是因为与子查询的外层查询所关联的条件是基于 group_id 的。当然,最后 MySQL Query Optimizer 还是选择了使用基于 user_id 的索引 user_group_uid_ind。 由于篇幅关系,这里就不再继续举例了,大家可以通过自行通过 Explain 功能分析各自应用环境中 的各种 Query,了解他们在我们的 MySQL 中到底是怎么运行的。 8.3.2 Profiling 的使用 在本章第一节中我们还提到过通过 Query Profiler 来定位一条 Query 的性能瓶颈,这里我们再详 细介绍一下 Profiling 的用途及使用方法。 要想优化一条 Query,我们就需要清楚的知道这条 Query 的性能瓶颈到底在哪里,是消耗的 CPU 计算太多,还是需要的的 IO 操作太多?要想能够清楚的了解这些信息,在 MySQL 5.0 和 MySQL 5.1 正式版中已经可以非常容易做到了,那就是通过 Query Profiler 功能。 MySQL 的 Query Profiler 是一个使用非常方便的 Query 诊断分析工具,通过该工具可以获取一条 Query 在整个执行过程中多种资源的消耗情况,如 CPU,IO,IPC,SWAP 等,以及发生的 PAGE FAULTS,
112. CONTEXT SWITCHE 等等,同时还能得到该 Query 执行过程中 MySQL 所调用的各个函数在源文件中的位 置。下面我们看看 Query Profiler 的具体用法。 1、 开启 profiling 参数 root@localhost : (none) 10:53:11> set profiling=1; Query OK, 0 rows affected (0.00 sec) 通过执行 “set profiling”命令,可以开启关闭 Query Profiler 功能。 2、 执行 Query ... ... root@localhost : test 07:43:18> select status,count(*) -> from test_profiling group by status; +----------------+----------+ status count(*) +----------------+----------+ st_xxx1 27 st_xxx2 6666 st_xxx3 292887 st_xxx4 15 +----------------+----------+ 5 rows in set (1.11 sec) ... ... 在开启 Query Profiler 功能之后,MySQL 就会自动记录所有执行的 Query 的 profile 信息了。 3、获取系统中保存的所有 Query 的 profile 概要信息 root@localhost : test 07:47:35> show profiles; +----------+------------+------------------------------------------------------------+ Query_ID Duration Query +----------+------------+------------------------------------------------------------+ 1 0.00183100 show databases 2 0.00007000 SELECT DATABASE() 3 0.00099300 desc test 4 0.00048800 show tables 5 0.00430400 desc test_profiling 6 1.90115800 select status,count(*) from test_profiling group by status +----------+------------+------------------------------------------------------------+ 3 rows in set (0.00 sec) 通过执行 “SHOW PROFILE” 命令获取当前系统中保存的多个 Query 的 profile 的概要信息。 4、针对单个 Query 获取详细的 profile 信息。 在获取到概要信息之后,我们就可以根据概要信息中的 Query_ID 来获取某个 Query 在执行过程中
113. 详细的 profile 信息了,具体操作如下: root@localhost : test 07:49:24> show profile cpu, block io for query 6; +----------------------+----------+----------+------------+--------------+---------------+ Status Duration CPU_user CPU_system Block_ops_in Block_ops_out +----------------------+----------+----------+------------+--------------+---------------+ starting 0.000349 0.000000 0.000000 0 0 Opening tables 0.000012 0.000000 0.000000 0 0 System lock 0.000004 0.000000 0.000000 0 0 Table lock 0.000006 0.000000 0.000000 0 0 init 0.000023 0.000000 0.000000 0 0 optimizing 0.000002 0.000000 0.000000 0 0 statistics 0.000007 0.000000 0.000000 0 0 preparing 0.000007 0.000000 0.000000 0 0 Creating tmp table 0.000035 0.000999 0.000000 0 0 executing 0.000002 0.000000 0.000000 0 0 Copying to tmp table 1.900619 1.030844 0.197970 347 347 Sorting result 0.000027 0.000000 0.000000 0 0 Sending data 0.000017 0.000000 0.000000 0 0 end 0.000002 0.000000 0.000000 0 0 removing tmp table 0.000007 0.000000 0.000000 0 0 end 0.000002 0.000000 0.000000 0 0 query end 0.000003 0.000000 0.000000 0 0 freeing items 0.000029 0.000000 0.000000 0 0 logging slow query 0.000001 0.000000 0.000000 0 0 logging slow query 0.000002 0.000000 0.000000 0 0 cleaning up 0.000002 0.000000 0.000000 0 0 +----------------------+----------+----------+------------+--------------+---------------+ 上面的例子中是获取 CPU 和 Block IO 的消耗,非常清晰,对于定位性能瓶颈非常适用。希望得到 取其他的信息,都可以通过执行 “SHOW PROFILE *** FOR QUERY n” 来获取,各位读者朋友可以自行 测试熟悉。 8.4 合理设计并利用索引 索引,可以说是数据库相关优化尤其是在 Query 优化中最常用的优化手段之一了。但是很多人在大 部分时候都只是大概了解索引的用途,知道索引能够让 Query 执行的更快,而并不知道为什么会更快。 尤其是索引的实现原理,存储方式,以及不同索引之间的区别等就更不是太清楚了。正因为索引对我们 的 Query 性能影响很大,所以我们更应该深入理解 MySQL 中索引的基本实现,以及不同索引之间的区 别,才能分析出如何设计出最优的索引来最大幅度的提升 Query 的执行效率。 在 MySQL 中,主要有四种类型的索引,分别为:B-Tree 索引,Hash 索引,Fulltext 索引和 RTree 索引,下面针对这四种索引的基本实现方式及存储结构做一个大概的分析。
114. B-Tree 索引 B-Tree 索引是 MySQL 数据库中使用最为频繁的索引类型,除了 Archive 存储引擎之外的其他所有 的存储引擎都支持 B-Tree 索引。不仅仅在 MySQL 中是如此,实际上在其他的很多数据库管理系统中 B-Tree 索引也同样是作为最主要的索引类型,这主要是因为 B-Tree 索引的存储结构在数据库的数据检 索中有非常优异的表现。 一般来说,MySQL 中的 B-Tree 索引的物理文件大多都是以 Balance Tree 的结构来存储的,也就 是所有实际需要的数据都存放于 Tree 的 Leaf Node,而且到任何一个 Leaf Node 的最短路径的长度都 是完全相同的,所以我们大家都称之为 B-Tree 索引当然,可能各种数据库(或 MySQL 的各种存储引 擎)在存放自己的 B-Tree 索引的时候会对存储结构稍作改造。如 Innodb 存储引擎的 B-Tree 索引实 际使用的存储结构实际上是 B+Tree,也就是在 B-Tree 数据结构的基础上做了很小的改造,在每一个 Leaf Node 上面出了存放索引键的相关信息之外,还存储了指向与该 Leaf Node 相邻的后一个 Leaf Node 的指针信息,这主要是为了加快检索多个相邻 Leaf Node 的效率考虑。 在 Innodb 存储引擎中,存在两种不同形式的索引,一种是 Cluster 形式的主键索引(Primary Key),另外一种则是和其他存储引擎(如 MyISAM 存储引擎)存放形式基本相同的普通 B-Tree 索引, 这种索引在 Innodb 存储引擎中被称为 Secondary Index。下面我们通过图示来针对这两种索引的存放 形式做一个比较。 图示中左边为 Clustered 形式存放的 Primary Key,右侧则为普通的 B-Tree 索引。两种索引在 Root Node 和 Branch Nodes 方面都还是完全一样的。而 Leaf Nodes 就出现差异了。在 Primary Key 中,Leaf Nodes 存放的是表的实际数据,不仅仅包括主键字段的数据,还包括其他字段的数据,整个数 据以主键值有序的排列。而 Secondary Index 则和其他普通的 B-Tree 索引没有太大的差异,只是在 Leaf Nodes 出了存放索引键的相关信息外,还存放了 Innodb 的主键值。
115. 所以,在 Innodb 中如果通过主键来访问数据效率是非常高的,而如果是通过 Secondary Index 来 访问数据的话,Innodb 首先通过 Secondary Index 的相关信息,通过相应的索引键检索到 Leaf Node 之后,需要再通过 Leaf Node 中存放的主键值再通过主键索引来获取相应的数据行。 MyISAM 存储引擎的主键索引和非主键索引差别很小,只不过是主键索引的索引键是一个唯一且非空 的键而已。而且 MyISAM 存储引擎的索引和 Innodb 的 Secondary Index 的存储结构也基本相同,主要 的区别只是 MyISAM 存储引擎在 Leaf Nodes 上面出了存放索引键信息之外,再存放能直接定位到 MyISAM 数据文件中相应的数据行的信息(如 Row Number),但并不会存放主键的键值信息。 Hash 索引 Hash 索引在 MySQL 中使用的并不是很多,目前主要是 Memory 存储引擎使用,而且在 Memory 存 储引擎中将 Hash 索引作为默认的索引类型。所谓 Hash 索引,实际上就是通过一定的 Hash 算法,将 需要索引的键值进行 Hash 运算,然后将得到的 Hash 值存入一个 Hash 表中。然后每次需要检索的时 候,都会将检索条件进行相同算法的 Hash 运算,然后再和 Hash 表中的 Hash 值进行比较并得出相应 的信息。 在 Memory 存储引擎中,MySQL 还支持非唯一的 Hash 索引。可能很多人会比较惊讶,如果是非唯 一的 Hash 索引,那相同的值该如何处理呢?在 Memory 存储引擎的 Hash 索引中,如果遇到非唯一 值,存储引擎会将他们链接到同一个 hash 键值下以一个 链表的形式存在,然后在取得实际键值的时候 时候再过滤不符合的键。 由于 Hash 索引结构的特殊性,其检索效率非常的高,索引的检索可以一次定位,而不需要像 BTree 索引需要从根节点再到枝节点最后才能访问到页节点这样多次 IO 访问,所以 Hash 索引的效率要 远高于 B-Tree 索引。 可能很多人又会有疑问了,既然 Hash 索引的效率要比 B-Tree 高很多,为什么大家不都用 Hash 索引而还要使用 B-Tree 索引呢?任何事物都是有两面性的,,Hash 索引也一样,虽然 Hash 索引检 索效率非常之高,但是 Hash 索引本身由于其实的特殊性也带来了很多限制和弊端,主要有以下这些: 1. Hash 索引仅仅只能满足“=”,“IN”和“<=>”查询,不能使用范围查询; 由于 Hash 索引所比较的是进行 Hash 运算之后的 Hash 值,所以 Hash 索引只能用于等值的 过滤,而不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关 系,并不能保证还和 Hash 运算之前完全一样。 2. Hash 索引无法被利用来避免数据的排序操作; 由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且 Hash 值的大小关系并不一定 和 Hash 运算前的键值的完全一样,所以数据库无法利用索引的数据来避免任何和排序运算; 3. Hash 索引不能利用部分索引键查询; 对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并之后再一起计算 Hash 值, 而不是单独计算 Hash 值,所以当我们通过组合索引的前面一个或几个索引键进行查询的时 候,Hash 索引也无法被利用到; 4. Hash 索引在任何时候都不能避免表扫面; 前面我们已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash 运算结果的 Hash 值 和所对应的行指针信息存放于一个 Hash 表中,而且由于存在不同索引键存在相同 Hash 值的
116. 可能,所以即使我们仅仅取满足某个 Hash 键值的数据的记录条数,都无法直接从 Hash 索引 中直接完成查询,还是要通过访问表中的实际数据进行相应的比较而得到相应的结果。 5. Hash 索引遇到大量 Hash 值相等的情况后性能并不一定就会比 B-Tree 索引高; 对于选择性比较低的索引键,如果我们创建 Hash 索引,那么我们将会存在大量记录指针信息 存与同一个 Hash 值相关连。这样要定位某一条记录的时候就会非常的麻烦,可能会浪费非常 多次表数据的访问,而造成整体性能的地下。 Full-text 索引 Full-text 索引也就是我们常说的全文索引,目前在 MySQL 中仅有 MyISAM 存储引擎支持,而且也 并不是所有的数据类型都支持全文索引。目前来说,仅有 CHAR,VARCHAR 和 TEXT 这三种数据类型的列可 以建 Full-text 索引。 一般来说,Fulltext 索引主要用来替代效率低下的 LIKE '%***%' 操作。实际上,Full-text 索引 并不只是能简单的替代传统的全模糊 LIKE 操作,而且能通过多字段组合的 Full-text 索引一次全模糊 匹配多个字段。 Full-text 索引和普通的 B-Tree 索引的实现区别较大,虽然他同样是以 B-Tree 形式来存放索引 数据,但是他并不是通过字段内容的完整匹配,而是通过特定的算法,将字段数据进行分隔后再进行的 索引。一般来说 MySQL 系统会按照四个字节来分隔。在整个 Full-text 索引中,存储内容被分为两部 分,一部分是分隔前的索引字符串数据集合,另一部分是分隔后的词(或者词组)的索引信息。所以, Full-text 索引中,真正在 B-Tree 索引细细中的并不是我们表中的原始数据,而是分词之后的索引数 据。在 B-Tree 索引的节点信息中,存放了各个分隔后的词信息,以及指向包含该词的分隔前字符串信 息在索引数据集合中的位置信息。 Full-text 索引不仅仅能实现模糊匹配查找,在实现了基于自然语言的的匹配度查找。当然,这个 匹配读到底有多准确就需要读者朋友去自行验证了。Full-text 通过一些特定的语法信息,针对自然语 言做了各种相应规则的匹配,最后给出非负的匹配值。 此外,有一点是需要大家注意的,MySQL 目前的 Full-text 索引在中文支持方面还不太好,需要借 助第三方的补丁或者插件来完成。而且 Full-text 的创建所消耗的资源也是比较大的,所以在应用于实 际生产环境之前还是尽量做好评估。 关于 Full-text 的实际使用方法由于不是本书的重点,感兴趣的读者朋友可以自行参阅 MySQL 关 于 Full-text 相关的使用手册来了解更为详尽的信息。 R-Tree 索引 R-Tree 索引可能是我们在其他数据库中很少见到的一种索引类型,主要用来解决空间数据检索的问 题。 在 MySQL 中,支持一种用来存放空间信息的数据类型 GEOMETRY,且基于 OpenGIS 规范。在 MySQL5.0.16 之前的版本中,仅仅 MyISAM 存储引擎支持该数据类型,但是从 MySQL5.0.16 版本开始,
117. BDB,Innodb,NDBCluster 和 Archive 存储引擎也开始支持该数据类型。当然,虽然多种存储引擎都开 始支持 GEOMETRY 数据类型,但是仅仅之后 MyISAM 存储引擎支持 R-Tree 索引。 在 MySQL 中采用了具有二次分裂特性的 R-Tree 来索引空间数据信息,然后通过几何对象(MRB) 信息来创建索引。 虽然仅仅只有 MyISAM 存储引擎支持空间索引(R-Tree Index),但是如果我们是精确的等值匹 配,创建在空间数据上面的 B-Tree 索引同样可以起到优化检索的效果,空间索引的主要优势在于当我 们使用范围查找的时候,可以利用到 R-Tree 索引,而这时候,B-Tree 索引就无能为力了。 对于 R-Tree 索引的详细介绍和使用信息清参阅 MySQL 使用手册。 索引的利弊与如何判定是否需要索引 相信没一位读者朋友都知道索引能够极大的提高我们数据检索的效率,让我们的 Query 执行的更 快,但是可能并不是每一位朋友都清楚索引在极大提高检索效率的同时,也给我们的数据库带来了一些 负面的影响。下面我们就分别对 MySQL 中索引的利与弊做一个简单的分析。 索引的利处 索引能够给我们带来的最大益处可能读者朋友基本上都有一定的了解,但是我相信并不是每一位读 者朋友都能够了解的比较全面。很多朋友对数据库中的索引的认识可能主要还是只限于“能够提高数据 检索的效率,降低数据库的 IO 成本”。 确实,在数据库中个表的某个字段创建索引,所带来的最大益处就是将该字段作为检索条件的时候 可以极大的提高检索效率,加快检索时间,降低检索过程中所需要读取的数据量。但是索引所给我们带 来的收益只是提高表数据的检索效率吗?当然不是,索引还有一个非常重要的用途,那就是降低数据的 排序成本。 我们知道,每个索引中索引数据都是按照索引键键值进行排序后存放的,所以,当我们的 Query 语 句中包含排序分组操作的时候,如果我们的排序字段和索引键字段刚好一致,MySQL Query Optimizer 就会告诉 mysqld 在取得数据之后不用排序了,因为根据索引取得的数据已经是满足客户的排序要求。 那如果是分组操作呢?分组操作没办法直接利用索引完成。但是分组操作是需要先进行排序然后才 分组的,所以当我们的 Query 语句中包含分组操作,而且分组字段也刚好和索引键字段一致,那么 mysqld 同样可以利用到索引已经排好序的这个特性而省略掉分组中的排序操作。 排序分组操作主要消耗的是我们的内存和 CPU 资源,如果我们能够在进行排序分组操作中利用好索 引,将会极大的降低 CPU 资源的消耗。 索引的弊端 索引的益处我们都已经清楚了,但是我们不能光看到索引给我们带来的这么多益处之后就认为索引 是解决 Query 优化的圣经,只要发现 Query 运行不够快就将 WHERE 子句中的条件全部放在索引中。
118. 确实,索引能够极大的提高数据检索效率,也能够改善排序分组操作的性能,但是我们不能忽略的 一个问题就是索引是完全独立于基础数据之外的一部分数据。假设我们在 Table ta 中的 Column ca 创 建了索引 idx_ta_ca,那么任何更新 Column ca 的操作,MySQL 都需要在更新表中 Column ca 的同时, 也更新 Column ca 的索引数据,调整因为更新所带来键值变化后的索引信息。而如果我们没有对 Column ca 进行索引的话,MySQL 所需要做的仅仅只是更新表中 Column ca 的信息。这样,所带来的最 明显的资源消耗就是增加了更新所带来的 IO 量和调整索引所致的计算量。此外,Column ca 的索引 idx_ta_ca 是需要占用存储空间的,而且随着 Table ta 数据量的增长,idx_ta_ca 所占用的空间也会 不断增长。所以索引还会带来存储空间资源消耗的增长。 如何判定是否需要创建索引 在了解了索引的利与弊之后,我们知道了索引并不是越多越好,知道了索引也是会带来副作用的。 那我们到底该如何来判断某个索引是否应该创建呢? 实际上,并没有一个非常明确的定律可以清晰的定义出什么字段应该创建索引什么字段不该创建索 引。因为我们的应用场景实在是太复杂,存在太多的差异。当然,我们还是仍然能够找到几点基本的判 定策略来帮助我们分析是否需要创建索引。 ◆ 较频繁的作为查询条件的字段应该创建索引; 提高数据查询检索的效率最有效的办法就是减少需要访问的数据量,从上面所了解到的索引的 益处中我们知道了,索引正是我们减少通过索引键字段作为查询条件的 Query 的 IO 量的最有 效手段。所以一般来说我们应该为较为频繁的查询条件字段创建索引。 ◆ 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件; 唯一性太差的字段主要是指哪些呢?如状态字段,类型字段等等这些字段中存方的数据可能总 共就是那么几个几十个值重复使用,每个值都会存在于成千上万或是更多的记录中。对于这类 字段,我们完全没有必要创建单独的索引的。因为即使我们创建了索引,MySQL Query Optimizer 大多数时候也不会去选择使用,如果什么时候 MySQL Query Optimizer 抽了一下风 选择了这种索引,那么非常遗憾的告诉你,这可能会带来极大的性能问题。由于索引字段中每 个值都含有大量的记录,那么存储引擎在根据索引访问数据的时候会带来大量的随机 IO,甚至 有些时候可能还会出现大量的重复 IO。 这主要是由于数据基于索引扫描的特点所引起的。当我们通过索引访问表中的数据的时候, MySQL 会按照索引键的键值的顺序来依序进行访问。一般来说每个数据页中大都会存放多条记 录,但是这些记录可能大多数都不会是和你所使用的索引键的键值顺序一致。 假如有以下场景,我们通过索引查找键值为 A 和 B 的某些数据。当我们先通过 A 键值找到第一 条满足要求的记录后,我们会读取这条记录所在的 X 数据页,然后我们继续往下查找索引,发 现 A 键值所对应的另外一条记录也满足我们的要求,但是这条记录不在 X 数据页上面,而在 Y 数据页上面,这时候存储引擎就会丢弃 X 数据页,而读取 Y 数据页。如此继续一直到查找 完 A 键值所对应的所有记录。然后轮到 B 键值了,这时候发现正在查找的记录又在 X 数据页 上面,可之前读取的 X 数据页已经被丢弃了,只能再次读取 X 数据页。这时候,实际上已经 出现重复读取 X 数据页两次了。在继续往后的查找中,可能还会出现一次又一次的重复读取。
119. 这无疑极大的给存储引擎增大了 IO 访问量。 不仅如此,如果一个键值对应了太多的数据记录,也就是说通过该键值会返回占整个表比例很 大的记录的时候,由于根据索引扫描产生的都是随机 IO,其效率比进行全表扫描的顺序 IO 的 效率要差很多,即使不会出现重复 IO 的读取,同样会造成整体 IO 性能的下降。 很多比较有经验的 Query 调优专家经常说,当一条 Query 所返回的数据超过了全表的 15% 的 时候,就不应该再使用索引扫描来完成这个 Query 了。对于“15%”这个数字我们并不能判定 是否很准确,但是之少侧面证明了唯一性太差的字段并不适合创建索引。 ◆ 更新非常频繁的字段不适合创建索引; 上面在索引的弊端中我们已经分析过了,索引中的字段被更新的时候,不仅仅需要更新表中的 数据,同时还要更新索引数据,以确保索引信息是准确的。这个问题所带来的是 IO 访问量的较大 增加,不仅仅影响更新 Query 的响应时间,还会影响整个存储系统的资源消耗,加大整个存储系统 的负载。 当然,并不是存在更新的字段就比适合创建索引,从上面判定策略的用语上面也可以看出,是 “非常频繁”的字段。到底什么样的更新频率应该算是“非常频繁”呢?每秒,每分钟,还是每小 时呢?说实话,这个还真挺难定义的。很多时候还是通过比较同一时间段内被更新的次数和利用该 字段作为条件的查询次数来判断,如果通过该字段的查询并不是很多,可能几个小时或者是更长才 会执行一次,而更新反而比查询更频繁,那这样的字段肯定不适合创建索引。反之,如果我们通过 该字段的查询比较频繁,而且更新并不是特别多,比如查询十几二十次或是更多才可能会产生一次 更新,那我个人觉得更新所带来的附加成本也是可以接受的。 ◆ 不会出现在 WHERE 子句中的字段不该创建索引; 不会还有人会问为什么吧?自己也觉得这是废话了,哈哈! 单键索引还是组合索引 在大概了解了一下 MySQL 各种类型的索引以及索引本身的利弊与判断一个字段是否需要创建索引之 后,我们就需要着手创建索引来优化我们的 Query 了。在很多时候,我们的 WHERE 子句中的过滤条件 并不只是针对于单一的某个字段,而是经常会有多个字段一起作为查询过滤条件存在于 WHERE 子句中。 在这种时候,我们就必须要作出判断,是该仅仅为过滤性最好的字段建立索引还是该在所有字段(过滤 条件中的)上面建立一个组合索引呢? 对于这种问题,很难有一个绝对的定论,我们需要从多方面来分析考虑,平衡两种方案各自的优 劣,然后选择一种最佳的方案来解决。因为从上一节中我们了解到了索引在提高某些查询的性能的同 时,也会让某些更新的效率下降。而组合索引中因为有多个字段的存在,理论上被更新的可能性肯定比 单键索引要大很多,这样可能带来的附加成本也就比单键索引要高。但是,当我们的 WHERE 子句中的查 询条件含有多个字段的时候,通过这多个字段共同组成的组合索引的查询效率肯定比仅仅只用过滤条件 中的某一个字段创建的索引要高。因为通过单键索引所能过滤的数据并不完整,和通过组合索引相比, 存储引擎需要访问更多的记录数,自然就会访问更多的数据量,也就是说需要更高的 IO 成本。
120. 可能有些朋友会说,那我们可以通过创建多个单键索引啊。确实,我们可以将 WHERE 子句中的每一 个字段都创建一个单键索引。但是这样真的有效吗?在这样的情况下,MySQL Query Optimizer 大多数 时候都只会选择其中的一个索引,然后放弃其他的索引。即使他选择了同时利用两个或者更多的索引通 过 INDEX_MERGE 来优化查询,可能所收到的效果并不会比选择其中某一个单键索引更高效。因为如果选 择通过 INDEX_MERGE 来优化查询,就需要访问多个索引,同时还要将通过访问到的几个索引进行 merge 操作,所带来的成本可能反而会比选择其中一个最有效的索引来完成查询更高。 在一般的应用场景中,只要不是其中某个过滤字段在大多数场景下都能过滤出 90%以上的数据,而且 其他的过滤字段会存在频繁的更新,我一般更倾向于创建组合索引,尤其是在并发量较高的场景下更是 应该如此。因为当我们的并发量较高的时候,即使我们为每个 Query 节省很少的 IO 消耗,但因为执行 量非常大,所节省的资源总量仍然是非常可观的。 当然,我们创建组合索引并不是说就需要将查询条件中的所有字段都放在一个索引中,我们还应该 尽量让一个索引被多个 Query 语句所利用,尽量减少同一个表上面索引的数量,减少因为数据更新所带 来的索引更新成本,同时还可以减少因为索引所消耗的存储空间。 此外,MySQL 还为我们提供了一个减少优化索引自身的功能,那就是前缀索引。在 MySQL 中,我们 可以仅仅使用某个字段的前面部分内容做为索引键来索引该字段,来达到减小索引占用的存储空间和提 高索引访问的效率。当然,前缀索引的功能仅仅适用于字段前缀比较随机重复性很小的字段。如果我们 需要索引的字段的前缀内容有较多的重复,索引的过滤性自然也会随之降低,通过索引所访问的数据量 就会增加,这时候前缀索引虽然能够减少存储空间消耗,但是可能会造成 Query 访问效率的极大降低, 反而得不偿失。 Query 的索引选择 在有些场景下,我们的 Query 由于存在多个过滤条件,而这多个过滤条件可能会存在于两个或者更 多的索引中。在这种场景下,MySQL Query Optimizer 一般情况下都能够根据系统的统计信息选择出一 个针对该 Query 最优的索引完成查询,但是在有些情况下,可能是由于我们的系统统计信息的不够准确 完整,也可能是 MySQL Query Optimizer 自身功能的缺陷,会造成他并没有选择一个真正最优的索引而 选择了其他查询效率较低的索引。在这种时候,我们就不得不通过认为干预,在 Query 中增加 Hint 提 示 MySQL Query Optimizer 告诉他该使用哪个索引而不该使用哪个索引,或者通过调整查询条件来达到 相同的目的。 我们这里再次通过在本章第 2 节“Query 语句优化基本思路和原则”的“仅仅使用最有效的过滤条 件”中示例的基础上将 group_message 表的索引做部分调整,然后再进行分析。 在 group_message 上增加如下索引: create index group_message_author_subject on group_message(author,subject(16)); 调整后的索引信息如下(出于篇幅考虑省略了主键索引): sky@localhost : example 07:13:38> show indexes from group_message\G ...... *************************** 2. row *************************** Table: group_message
121. Non_unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique: 1 Key_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: group_message_author_subject Seq_in_index:'>index:'>index:'>index:'>index:'>index:'>index:'>index: 1 Column_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: author Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation: A Cardinality:'>Cardinality:'>Cardinality:'>Cardinality: NULL Sub_part:'>part:'>part:'>part: NULL Packed:'>Packed:'>Packed:'>Packed: NULL Null:'>Null:'>Null:'>Null: Index_type:'>type:'>type:'>type: BTREE Comment:'>Comment:'>Comment:'>Comment: *************************** 3. row *************************** Table:'>Table:'>Table:'>Table: group_message Non_unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique: 1 Key_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: group_message_author_subject Seq_in_index:'>index:'>index:'>index:'>index:'>index:'>index:'>index: 2 Column_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: subject Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation: A Cardinality:'>Cardinality:'>Cardinality:'>Cardinality: NULL Sub_part:'>part:'>part:'>part: 16 Packed:'>Packed:'>Packed:'>Packed: NULL Null:'>Null:'>Null:'>Null: Index_type:'>type:'>type:'>type: BTREE Comment:'>Comment:'>Comment:'>Comment: *************************** 4. row *************************** Table:'>Table:'>Table:'>Table: group_message Non_unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique: 1 Key_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: idx_group_message_uid Seq_in_index:'>index:'>index:'>index:'>index:'>index:'>index:'>index: 1 Column_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: user_id Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation: A Cardinality:'>Cardinality:'>Cardinality:'>Cardinality: NULL Sub_part:'>part:'>part:'>part: NULL Packed:'>Packed:'>Packed:'>Packed: NULL Null:'>Null:'>Null:'>Null: Index_type:'>type:'>type:'>type: BTREE Comment:'>Comment:'>Comment:'>Comment: *************************** 5. row *************************** Table:'>Table:'>Table:'>Table: group_message Non_unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique:'>unique: 1 Key_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: idx_group_message_author Seq_in_index:'>index:'>index:'>index:'>index:'>index:'>index:'>index: 1 Column_name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name:'>name: author Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation:'>Collation: A
122. Cardinality: Sub_part: Packed: Null: Index_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: Comment: NULL NULL NULL BTREE 从索引的 Sub_part 中,我们可以看到 subject 字段是取前 16 个字符的前缀作为索引键。下面假设 我们知道某个用户的 user_id ,nick_name 和 subject 字段的部分前缀信息(weiurazs),希望通过 这些条件查询出所有满足上面存在于 group_message 中的信息。我们知道存在三个索引可以被利用: idx_group_message_author , idx_group_message_uid 和 group_message_author_subject,而且也知 道每个 user_id 实际上都是和 一个 author 分别唯一对应的。所以实际上,无论是使用 user_id 和 author(nick_name)中的某一个来作为条件或者两个条件都使用,所得到的数据都是完全一样的。当 然,我们还需要 subject LIKE 'weiurazs%' 这个条件来过滤 subject 相关的信息。 根据三个索引的组成,和我们的查询条件,我们知道 group_message_author_subject 索引可以让我 们得到最高的检索效率,因为只有他索引了 subject 相关的信息,subject 是我们的查询必须包含的过 滤条件。下面我们分别看看使用 user_id ,author 和 两者共同使用时候的执行计划。 sky@localhost : example 07:48:45> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 3 AND subject LIKE 'weiurazs%'\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: ref possible_keys: idx_group_message_uid key: idx_group_message_uid key_len: 4 ref: const rows: 8 Extra: Using where 1 row in set (0.00 sec) 很明显,这不是我们所期望的执行计划,当然我们并不能责怪 MySQL,因为我们都没有使用 author 来进行过滤,Optimizer 当然不会选择 group_message_author_subject 这个索引,这是我们自己的 错。 sky@localhost : example 07:48:49> EXPLAIN SELECT * FROM group_message -> WHERE author = '3' AND subject LIKE 'weiurazs%'\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message
123. type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: range possible_keys:'>keys:'>keys:'>keys: group_message_author_subject,idx_group_message_author key:'>key:'>key:'>key: idx_group_message_author key_len:'>len:'>len:'>len: 98 ref:'>ref:'>ref:'>ref: NULL rows:'>rows:'>rows:'>rows: 8 Extra:'>Extra: Using where 1 row in set (0.00 sec) 这次我们改为使用 author 作为查询条件了,可 MySQL Query Optimizer 仍然没有选择 group_message_author_subject 这个索引,即使我们通过 analyze 分析也是同样的结果。 sky@localhost : example 07:48:57> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 3 AND author = '3' AND subject LIKE 'weiurazs%'\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: range possible_keys:'>keys:'>keys:'>keys: group_message_author_subject,idx_group_message_uid, idx_group_message_author key:'>key:'>key:'>key: idx_group_message_uid key_len:'>len:'>len:'>len: 98 ref:'>ref:'>ref:'>ref: NULL rows:'>rows:'>rows:'>rows: 8 Extra:'>Extra: Using where 1 row in set (0.00 sec) 同时使用 user_id 和 author 两者的时候,MySQL Query Optimizer 又再次选择了 idx_group_message_uid 这个索引,仍然不是我们期望的结果。 sky@localhost : example 07:51:11> EXPLAIN SELECT * FROM group_message -> FORCE INDEX(idx_group_message_author_subject) -> WHERE user_id = 3 AND author = '3' AND subject LIKE 'weiurazs%'\G *************************** 1. row *************************** id:'>id: 1 select_type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: SIMPLE table:'>table: group_message type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type:'>type: range possible_keys:'>keys:'>keys:'>keys: group_message_author_subject key:'>key:'>key:'>key: group_message_author_subject key_len:'>len:'>len:'>len: 148 ref:'>ref:'>ref:'>ref: NULL rows:'>rows:'>rows:'>rows: 8
124. Extra: Using where 在最后,我们不得不利用 MySQL 为我们提供的在优化 Query 时候所使用的高级功能,通过显式告 诉 MySQL Query Optimizer 我们要使用哪个索引的 Hint 功能。强制 MySQL 使用 group_message_author_subject 这个索引来完成查询,才达到我们所需要的效果。 或许有些读者会想,会不会是因为选择 group_message_author_subject 这个索引本身就不是一个 最有的选择呢?大家请看下面通过 mysqlslap 进行的实际执行各条 Query 的测试结果: sky@sky:~$'>sky:~$'>sky:~$'>sky:~$'>sky:~$'>sky:~$'>sky:~$'>sky:~$ mysqlslap --create-schema=example --query="SELECT * FROM group_message WHERE user_id = 3 AND subject LIKE 'weiurazs%'" --iterations=10000 Benchmark Average number of seconds to run all queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>queries:'>