[转]MySQL查询优化技术讲座

MySQL查询优化技术讲座

  
数据库管理系统实现了理论上的概念,但是这种在实际硬件设备上的实现受到了实际物理条件的约束。其结果是,查询需要花费一些时间--有时候需要很长的时间。本期专题的内容就是帮助你找到如何让自己的等待时间最短的方法。

【文章概述】

使用索引

  索引是提高查询速度的最重要的工具。当然还有其它的一些技术可供使用,但是一般来说引起最大性能差异的都是索引的正确使用。因此我首先讲述是什么索引以及索引是怎么样提高查询性能的。同时还要讨论在某些环境中索引可能降低性能,并选择提供了数据表的索引一些指导方针。

MySQL查询优化器

  了解一些优化器的知识,作为对如何建立索引的补充,对我们是有好处的,因为这样你才能更好地利用自己所建立的索引。某些编写查询的方法实际上让索引不起作用,在一般情况下你应该避免这种情形的发生。这一讲中将讨论MySQL查询优化器,我将试图找到执行具有最高效率查询的方法。

数据类型与效率

  在大多数情况下,你所关注的是SELECT查询的优化,因为SELECT查询是最常见的查询类型,而且如何优化它们又不是太简单。与此形成对比,把数据载入数据库的操作就相对直接了。然而,你仍然可以利用某些策略来改善数据载入操作的效率。

调度和锁定的问题

  前面的部分主要是聚焦于如何让单独的查询执行的速度更快。MySQL还允许你改变语句调度的优先级,它可以使来自多个客户端的查询更好地协作。改变优先级还可以确保特定类型的查询被处理得更快。这一部分讲解MySQL的默认的调度策略和可以用来影响这些策略的选项。它还谈到了并发性插入操作的使用和存储引擎锁定层次对客户端的并发性的影响。

管理员的优化措施

  前面的部分中讲解的优化措施都是没有特权的MySQL用户能够执行的。可以控制MySQL服务器或计算机的系统管理员能够执行额外的优化措施。

【文章正文】

MySQL查询优化技术系列讲座之:使用索引

索引是提高查询速度的最重要的工具。当然还有其它的一些技术可供使用,但是一般来说引起最大性能差异的都是索引的正确使用。在MySQL邮件列表中,人们经常询问那些让查询运行得更快的方法。在大多数情况下,我们应该怀疑数据表上有没有索引,并且通常在添加索引之后立即解决了问题。当然,并不总是这样简单就可以解决问题的,因为优化技术本来就并非总是简单的。然而,如果没有使用索引,在很多情况下,你试图使用其它的方法来提高性能都是在浪费时间。首先使用索引来获取最大的性能提高,接着再看其它的技术是否有用。

  这一部分讲述了索引是什么以及索引是怎么样提高查询性能的。它还讨论了在某些环境中索引可能降低性能,并为你明智地选择数据表的索引提供了一些指导方针。在下一部分中我们将讨论MySQL查询优化器,它试图找到执行查询的效率最高的方法。了解一些优化器的知识,作为对如何建立索引的补充,对我们是有好处的,因为这样你才能更好地利用自己所建立的索引。某些编写查询的方法实际上让索引不起作用,在一般情况下你应该避免这种情形的发生。

  索引的优点

  让我们开始了解索引是如何工作的,首先有一个不带索引的数据表。不带索引的表仅仅是一个无序的数据行集合。例如,图1显示的ad表就是不带索引的表,因此如果需要查找某个特定的公司,就必须检查表中的每个数据行看它是否与目标值相匹配。这会导致一次完全的数据表扫描,这个过程会很慢,如果这个表很大,但是只包含少量的符合条件的记录,那么效率会非常低。


图1:无索引的ad表

  图2是同样的一张数据表,但是增加了对ad表的company_num数据列的索引。这个索引包含了ad表中的每个数据行的条目,但是索引的条目是按照company_num值排序的。现在,我们不是逐行查看以搜寻匹配的数据项,而是使用索引。假设我们查找公司13的所有数据行。我们开始扫描索引并找到了该公司的三个值。接着我们碰到了公司14的索引值,它比我们正在搜寻的值大。索引值是排过序的,因此当我们读取了包含14的索引记录的时候,我们就知道再也不会有更多的匹配记录,可以结束查询操作了。因此使用索引获得的功效是:我们找到了匹配的数据行在哪儿终止,并能够忽略其它的数据行。另一个功效来自使用定位算法查找第一条匹配的条目,而不需要从索引头开始执行线性扫描(例如,二分搜索就比线性扫描要快一些)。通过使用这种方法,我们可以快速地定位第一个匹配的值,节省了大量的搜索时间。数据库使用了多种技术来快速地定位索引值,但是在本文中我们不关心这些技术。重点是它们能够实现,并且索引是个好东西。


图2:索引后的ad表

  你可能要问,我们为什么不对数据行进行排序从而省掉索引?这样不是也能实现同样的搜索速度的改善吗?是的,如果表只有一个索引,这样做也可能达到相同的效果。但是你可能添加第二个索引,那么就无法一次使用两种不同方法对数据行进行排序了(例如,你可能希望在顾客名称上建立一个索引,在顾客ID号或电话号码上建立另外一个索引)。把与数据行相分离的条目作为索引解决了这个问题,允许我们创建多个索引。此外,索引中的行一般也比数据行短一些。当你插入或删除新的值的时候,移动较短的索引值比移动较长数据行的排序次序更加容易。

  不同的MySQL存储引擎的索引实现的具体细节信息是不同的。例如,对于MyISAM数据表,该表的数据行保存在一个数据文件中,索引值保存在索引文件中。一个数据表上可能有多个索引,但是它们都被存储在同一个索引文件中。索引文件中的每个索引都包含一个排序的键记录(它用于快速地访问数据文件)数组。

  与此形成对照的是,BDB和InnoDB存储引擎没有使用这种方法来分离数据行和索引值,尽管它们也把索引作为排序后的值集合进行操作。在默认情况下,BDB引擎使用单个文件存储数据和索引值。InnoDB使用单个数据表空间(tablespace),在表空间中管理所有InnoDB表的数据和索引存储。我们可以把InnoDB配置为每个表都在自己的表空间中创建,但是即使是这样,数据表的数据和索引也存储在同一个表空间文件中。
前面的讨论描述了单个表查询环境下的索引的优点,在这种情况下,通过减少对整个表的扫描,使用索引明显地提高了搜索的速度。当你运行涉及多表联结(jion)查询的时候,索引的价值就更高了。在单表查询中,你需要在每个数据列上检查的值的数量是表中数据行的数量。在多表查询中,这个数量可能大幅度上升,因为这个数量是这些表中数据行的数量所产生的。

  假设你拥有三个未索引的表t1、t2和t3,每个表都分别包含数据列i1、i2和i3,并且每个表都包含了1000条数据行,其序号从1到1000。查找某些值匹配的数据行组合的查询可能如下所示:

SELECT t1.i1, t2.i2, t3.i3
FROM t1, t2, t3
WHERE t1.i1 = t2.i2 AND t2.i1 = t3.i3;


  这个查询的结果应该是1000行,每个数据行包含三个相等的值。如果在没有索引的情况下处理这个查询,那么如果我们不对这些表进行全部地扫描,我们是没有办法知道哪些数据行含有哪些值的。因此你必须尝试所有的组合来查找符合WHERE条件的记录。可能的组合的数量是1000 x 1000 x 1000(10亿!),它是匹配记录的数量的一百万倍。这就浪费了大量的工作。这个例子显示,如果没有使用索引,随着表的记录不断增长,处理这些表的联结所花费的时间增长得更快,导致性能很差。我们可以通过索引这些数据表来显著地提高速度,因为索引让查询采用如下所示的方式来处理:

  1.选择表t1中的第一行并查看该数据行的值。

  2.使用表t2上的索引,直接定位到与t1的值匹配的数据行。类似地,使用表t3上的索引,直接定位到与表t2的值匹配的数据行。

  3.处理表t1的下一行并重复前面的过程。执行这样的操作直到t1中的所有数据行都被检查过。

  在这种情况下,我们仍然对表t1执行了完整的扫描,但是我们可以在t2和t3上执行索引查找,从这些表中直接地获取数据行。理论上采用这种方式运行上面的查询会快一百万倍。当然这个例子是为了得出结论来人为建立的。然而,它解决的问题却是现实的,给没有索引的表添加索引通常会获得惊人的性能提高。

  MySQL有几种使用索引的方式:

  · 如上所述,索引被用于提高WHERE条件的数据行匹配或者执行联结操作时匹配其它表的数据行的搜索速度。

  · 对于使用了MIN()或MAX()函数的查询,索引数据列中最小或最大值可以很快地找到,不用检查每个数据行。

  · MySQL利用索引来快速地执行ORDER BY和GROUP BY语句的排序和分组操作。

  · 有时候MySQL会利用索引来读取查询得到的所有信息。假设你选择了MyISAM表中的被索引的数值列,那么就不需要从该数据表中选择其它的数据列。在这种情况下,MySQL从索引文件中读取索引值,它所得到的值与读取数据文件得到的值是相同的。没有必要两次读取相同的值,因此没有必要考虑数据文件。

  索引的代价

  一般来说,如果MySQL能够找到方法,利用索引来更快地处理查询,它就会这样做。这意味着,对于大多数情况,如果你没有对表进行索引,就会使性能受到损害。这就是我所描绘的索引优点的美景。但是它有缺点吗?有的,它在时间和空间上都有开销。在实践中,索引的优点的价值一般会超过这些缺点,但是你也应该知道到底有一些什么缺点。

  首先,索引加快了检索的速度,但是减慢了插入和删除的速度,同时还减慢了更新被索引的数据列中的值的速度。也就是说,索引减慢了大多数涉及写操作的速度。发生这种现象的原因在于写入一条记录的时候不但需要写入数据行,还需要改变所有的索引。数据表带有的索引越多,需要做出的修改就越多,平均性能的降低程度也就越大。在本文的"高效率载入数据"部分中,我们将更细致地了解这些现象并找出处理方法。

  其次,索引会花费磁盘空间,多个索引相应地花费更多的磁盘空间。这可能导致更快地到达数据表的大小限制:

  · 对于MyISAM表,频繁地索引可能引起索引文件比数据文件更快地达到最大限制。

  · 对于BDB表,它把数据和索引值一起存储在同一个文件中,添加索引引起这种表更快地达到最大文件限制。

  · 在InnoDB的共享表空间中分配的所有表都竞争使用相同的公共空间池,因此添加索引会更快地耗尽表空间中的存储。但是,与MyISAM和BDB表使用的文件不同,InnoDB共享表空间并不受操作系统的文件大小限制,因为我们可以把它配置成使用多个文件。只要有额外的磁盘空间,你就可以通过添加新组件来扩展表空间。

  使用单独表空间的InnoDB表与BDB表受到的约束是一样的,因为它的数据和索引值都存储在单个文件中。

  这些要素的实际含义是:如果你不需要使用特殊的索引帮助查询执行得更快,就不要建立索引。

  选择索引

  假设你已经知道了建立索引的语法,但是语法不会告诉你数据表应该如何索引。这要求我们考虑数据表的使用方式。这一部分指导你如何识别出用于索引的备选数据列,以及如何最好地建立索引:

  用于搜索、排序和分组的索引数据列并不仅仅是用于输出显示的。换句话说,用于索引的最好的备选数据列是那些出现在WHERE子句、join子句、ORDER BY或GROUP BY子句中的列。仅仅出现在SELECT关键字后面的输出数据列列表中的数据列不是很好的备选列:

SELECT
col_a <- 不是备选列
FROM
tbl1 LEFT JOIN tbl2
ON tbl1.col_b = tbl2.col_c <- 备选列
WHERE
col_d = expr; <- 备选列

  当然,显示的数据列与WHERE子句中使用的数据列也可能相同。我们的观点是输出列表中的数据列本质上不是用于索引的很好的备选列。

  Join子句或WHERE子句中类似col1 = col2形式的表达式中的数据列都是特别好的索引备选列。前面显示的查询中的col_b和col_c就是这样的例子。如果MySQL能够利用联结列来优化查询,它一定会通过减少整表扫描来大幅度减少潜在的表-行组合。

  考虑数据列的基数(cardinality)。基数是数据列所包含的不同值的数量。例如,某个数据列包含值1、3、7、4、7、3,那么它的基数就是4。索引的基数相对于数据表行数较高(也就是说,列中包含很多不同的值,重复的值很少)的时候,它的工作效果最好。如果某数据列含有很多不同的年龄,索引会很快地分辨数据行。如果某个数据列用于记录性别(只有"M"和"F"两种值),那么索引的用处就不大。如果值出现的几率几乎相等,那么无论搜索哪个值都可能得到一半的数据行。在这些情况下,最好根本不要使用索引,因为查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。惯用的百分比界线是"30%"。现在查询优化器更加复杂,把其它一些因素也考虑进去了,因此这个百分比并不是MySQL决定选择使用扫描还是索引的唯一因素。

  索引较短的值。尽可能地使用较小的数据类型。例如,如果MEDIUMINT足够保存你需要存储的值,就不要使用BIGINT数据列。如果你的值不会长于25个字符,就不要使用CHAR(100)。较小的值通过几个方面改善了索引的处理速度:

  · 较短的值可以更快地进行比较,因此索引的查找速度更快了。

  · 较小的值导致较小的索引,需要更少的磁盘I/O。

  · 使用较短的键值的时候,键缓存中的索引块(block)可以保存更多的键值。MySQL可以在内存中一次保持更多的键,在不需要从磁盘读取额外的索引块的情况下,提高键值定位的可能性。

  对于InnoDB和BDB等使用聚簇索引(clustered index)的存储引擎来说,保持主键(primary key)短小的优势更突出。聚簇索引中数据行和主键值存储在一起(聚簇在一起)。其它的索引都是次级索引;它们存储主键值和次级索引值。次级索引屈从主键值,它们被用于定位数据行。这暗示主键值都被复制到每个次级索引中,因此如果主键值很长,每个次级索引就需要更多的额外空间。

  索引字符串值的前缀(prefixe)。如果你需要索引一个字符串数据列,那么最好在任何适当的情况下都应该指定前缀长度。例如,如果有CHAR(200)数据列,如果前面10个或20个字符都不同,就不要索引整个数据列。索引前面10个或20个字符会节省大量的空间,并且可能使你的查询速度更快。通过索引较短的值,你可以获得那些与比较速度和磁盘I/O节省相关的好处。当然你也需要利用常识。仅仅索引某个数据列的第一个字符串可能用处不大,因为如果这样操作,那么在索引中不会有太多的唯一值。

  你可以索引CHAR、VARCHAR、BINARY、VARBINARY、BLOB和TEXT数据列的前缀。

  使用最左(leftmost)前缀。建立多列复合索引的时候,你实际上建立了MySQL可以使用的多个索引。复合索引可以作为多个索引使用,因为索引中最左边的列集合都可以用于匹配数据行。这种列集合被称为"最左前缀"(它与索引某个列的前缀不同,那种索引把某个列的前面几个字符作为索引值)。

  假设你在表的state、city和zip数据列上建立了复合索引。索引中的数据行按照state/city/zip次序排列,因此它们也会自动地按照state/city和state次序排列。这意味着,即使你在查询中只指定了state值,或者指定state和city值,MySQL也可以使用这个索引。因此,这个索引可以被用于搜索如下所示的数据列组合:

state, city, zip
state, city
state

  MySQL不能利用这个索引来搜索没有包含在最左前缀的内容。例如,如果你按照city或zip来搜索,就不会使用到这个索引。如果你搜索给定的state和具体的ZIP代码(索引的1和3列),该索引也是不能用于这种组合值的,尽管MySQL可以利用索引来查找匹配的state从而缩小搜索的范围。

  不要过多地索引。不要认为"索引越多,性能越高",不要对每个数据列都进行索引。我们在前面提到过,每个额外的索引都会花费更多的磁盘空间,并降低写操作的性能。当你修改表的内容的时候,索引就必须被更新,甚至可能重新整理。如果你的索引很少使用或永不使用,你就没有必要减小表的修改操作的速度。此外,为检索操作生成执行计划的时候,MySQL会考虑索引。建立额外的索引会给查询优化器增加更多的工作量。如果索引太多,有可能(未必)出现MySQL选择最优索引失败的情况。维护自己必须的索引可以帮助查询优化器来避免这类错误。

  如果你考虑给已经索引过的表添加索引,那么就要考虑你将增加的索引是否是已有的多列索引的最左前缀。如果是这样的,不用增加索引,因为已经有了(例如,如果你在state、city和zip上建立了索引,那么没有必要再增加state的索引)。

  让索引类型与你所执行的比较的类型相匹配。在你建立索引的时候,大多数存储引擎会选择它们将使用的索引实现。例如,InnoDB通常使用B树索引。MySQL也使用B树索引,它只在三维数据类型上使用R树索引。但是,MEMORY存储引擎支持散列索引和B树索引,并允许你选择使用哪种索引。为了选择索引类型,需要考虑在索引数据列上将执行的比较操作类型:

  · 对于散列(hash)索引,会在每个数据列值上应用散列函数。生成的结果散列值存储在索引中,并用于执行查询。散列函数实现的算法类似于为不同的输入值生成不同的散列值。使用散列值的好处是散列值比原始值的比较效率更高。散列索引用于执行=或<=>操作等精确匹配的时候速度非常快。但是对于查询一个值的范围效果就非常差了:

id < 30
weight BETWEEN 100 AND 150

  · B树索引可以用于高效率地执行精确的或者基于范围(使用操作<、<=、=、>=、>、<>、!=和BETWEEN)的比较。B树索引也可以用于LIKE模式匹配,前提是该模式以文字串而不是通配符开头。

  如果你使用的MEMORY数据表只进行精确值查询,散列索引是很好的选择。这是MEMORY表使用的默认的索引类型,因此你不需要特意指定。如果你希望在MEMORY表上执行基于范围的比较,应该使用B树索引。为了指定这种索引类型,需要给索引定义添加USING BTREE。例如:

CREATE TABLE lookup
(
id INT NOT NULL,
name CHAR(20),
PRIMARY KEY USING BTREE (id)
) ENGINE = MEMORY;

  如果你希望执行的语句的类型允许,单个MEMORY表可以同时拥有散列索引和B树索引,即使在同一个数据列上。

  有些类型的比较不能使用索引。如果你只是通过把值传递到函数(例如STRCMP())中来执行比较操作,那么对它进行索引就没有价值。服务器必须计算出每个数据行的函数值,它会排除数据列上索引的使用。

  使用慢查询(slow-query)日志来识别执行情况较差的查询。这个日志可以帮助你找出从索引中受益的查询。你可以直接查看日志(它是文本文件),或者使用mysqldumpslow工具来统计它的内容。如果某个给定的查询多次出现在"慢查询"日志中,这就是一个线索,某个查询可能没有优化编写。你可以重新编写它,使它运行得更快。你要记住,在评估"慢查询"日志的时候,"慢"是根据实际时间测定的,在负载较大的服务器上"慢查询"日志中出现的查询会多一些。

MySQL查询优化系列讲座之:查询优化器

当你提交一个查询的时候,MySQL会分析它,看是否可以做一些优化使处理该查询的速度更快。这一部分将介绍查询优化器是如何工作的。如果你想知道MySQL采用的优化手段,可以查看MySQL参考手册。

  当然,MySQL查询优化器也利用了索引,但是它也使用了其它一些信息。例如,如果你提交如下所示的查询,那么无论数据表有多大,MySQL执行它的速度都会非常快:

SELECT * FROM tbl_name WHERE 0;

  在这个例子中,MySQL查看WHERE子句,认识到没有符合查询条件的数据行,因此根本就不考虑搜索数据表。你可以通过提供一个EXPLAIN语句看到这种情况,这个语句让MySQL显示自己执行的但实际上没有真正地执行的SELECT查询的一些信息。如果要使用EXPLAIN,只需要在EXPLAIN单词放在SELECT语句的前面:

mysql> EXPLAIN SELECT * FROM tbl_name WHERE 0/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
Extra: Impossible WHERE

  通常情况下,EXPLAIN返回的信息比上面的信息要多一些,还包括用于扫描数据表的索引、使用的联结类型、每张数据表中估计需要检查的数据行数量等非空(NULL)信息。

   优化器是如何工作的

  MySQL查询优化器有几个目标,但是其中最主要的目标是尽可能地使用索引,并且使用最严格的索引来消除尽可能多的数据行。你的最终目标是提交SELECT语句查找数据行,而不是排除数据行。优化器试图排除数据行的原因在于它排除数据行的速度越快,那么找到与条件匹配的数据行也就越快。如果能够首先进行最严格的测试,查询就可以执行地更快。假设你的查询检验了两个数据列,每个列上都有索引:

SELECT col3 FROM mytable
WHERE col1 = ’some value’ AND col2 = ’some other value’;

  假设col1上的测试匹配了900个数据行,col2上的测试匹配了300个数据行,而同时进行的测试只得到了30个数据行。先测试Col1会有900个数据行,需要检查它们找到其中的30个与col2中的值匹配记录,其中就有870次是失败了。先测试col2会有300个数据行,需要检查它们找到其中的30个与col1中的值匹配的记录,只有270次是失败的,因此需要的计算和磁盘I/O更少。其结果是,优化器会先测试col2,因为这样做开销更小。

  你可以通过下面一个指导帮助优化器更好地利用索引:

  尽量比较数据类型相同的数据列。当你在比较操作中使用索引数据列的时候,请使用数据类型相同的列。相同的数据类型比不同类型的性能要高一些。例如,INT与BIGINT是不同的。CHAR(10)被认为是CHAR(10)或VARCHAR(10),但是与CHAR(12)或VARCHAR(12)不同。如果你所比较的数据列的类型不同,那么可以使用ALTER TABLE来修改其中一个,使它们的类型相匹配。

  尽可能地让索引列在比较表达式中独立。如果你在函数调用或者更复杂的算术表达式条件中使用了某个数据列,MySQL就不会使用索引,因为它必须计算出每个数据行的表达式值。有时候这种情况无法避免,但是很多情况下你可以重新编写一个查询让索引列独立地出现。

  下面的WHERE子句显示了这种情况。它们的功能相同,但是对于优化目标来说就有很大差异了:

WHERE mycol < 4 / 2
WHERE mycol * 2 < 4

  对于第一行,优化器把表达式4/2简化为2,接着使用mycol上的索引来快速地查找小于2的值。对于第二个表达式,MySQL必须检索出每个数据行的mycol值,乘以2,接着把结果与4进行比较。在这种情况下,不会使用索引。数据列中的每个值都必须被检索到,这样才能计算出比较表达式左边的值。

  我们看另外一个例子。假设你对date_col列进行了索引。如果你提交一条如下所示的查询,就不会使用这个索引:

SELECT * FROM mytbl WHERE YEAR(date_col) < 1990;

  这个表达式不会把1990与索引列进行比较;它会把1990与该数据列计算出来的值比较,而每个数据行都必须计算出这个值。其结果是,没有使用date_col上的索引,因为执行这样的查询需要全表扫描。怎么解决这个问题呢?只需要使用文本日期,接着就可以使用date_col上的索引来查找列中匹配的值了:

WHERE date_col < ’1990-01-01’

  但是,假设你没有特定的日期。你可能希望找到一些与今天相隔固定的几天的日期的记录。表达这种类型的比较有很多种方法--它们的效率并不同。下面就有三种:

WHERE TO_DAYS(date_col) - TO_DAYS(CURDATE()) < cutoff
WHERE TO_DAYS(date_col) < cutoff + TO_DAYS(CURDATE())
WHERE date_col < DATE_ADD(CURDATE(), INTERVAL cutoff DAY)

  对于第一行,不会用到索引,因为每个数据行都必须检索以计算出TO_DAYS(date_col)的值。第二行要好一些。Cutoff和TO_DAYS(CURDATE())都是常量,因此在处理查询之前,比较表达式的右边可以被优化器一次性计算出来,而不需要每个数据行都计算一次。但是date_col列仍然出现在函数调用中,它阻止了索引的使用。第三行是这几个中最好的。同样,在执行查询之前,比较表达式的右边可以作为常量一次性计算出来,但是现在它的值是一个日期。这个值可以直接与date_col值进行比较,再也不需要转换成天数了。在这种情况下,会使用索引。

  在LIKE模式的开头不要使用通配符。有些字符串搜索使用如下所示的WHERE子句:

WHERE col_name LIKE ’%string%’

  如果你希望找到那些出现在数据列的任何位置的字符串,这个语句就是对的。但是不要因为习惯而简单地把"%"放在字符串的两边。如果你在查找出现在数据列开头的字符串,就删掉前面的"%"。假设你要查找那些类似MacGregor或MacDougall等以"Mac"开头的名字。在这种情况下,WHERE子句如下所示:

WHERE last_name LIKE ’Mac%’

  优化器查看该模式中词首的文本,并使用索引找到那些与下面的表达式匹配的数据行。下面的表达式是使用last_name索引的另一种形式:

WHERE last_name >= ’Mac’ AND last_name < ’Mad’


  这种优化不能应用于使用了REGEXP操作符的模式匹配。REGEXP表达式永远不会被优化。

  帮助优化器更好的判断索引的效率。在默认情况下,当你把索引列的值与常量进行比较的时候,优化器会假设键值在索引内部是均匀分布的。在决定进行常量比较是否使用索引的时候,优化器会快速地检查索引,估计出会用到多少个实体(entry)。对应MyISAM、InnoDB和BDB数据表来说,你可以使用ANALYZE TABLE让服务器执行对键值的分析。它会为优化器提供更好的信息。

  使用EXPLAIN验证优化器的操作。EXPLAIN语句可以告诉你是否使用了索引。当你试图用另外的方式编写语句或检查添加索引是否会提高查询执行效率的时候,这些信息对你是有帮助的。

  在必要的时候给优化器一些提示。正常情况下,MySQL优化器自由地决定扫描数据表的次序来最快地检索数据行。在有些场合中优化器没有作出最佳选择。如果你察觉这种现象发生了,就可以使用STRAIGHT_JOIN关键字来重载优化器的选择。带有STRAIGHT_JOIN的联结类似于交叉联结,但是强迫数据表按照FROM子句中指定的次序来联结。

  在SELECT语句中有两个地方可以指定STRAIGHT_JOIN。你可以在SELECT关键字和选择列表之间的位置指定,这样会对语句中所有的交叉联结产生影响;你也可以在FROM子句中指定。下面的两个语句功能相同:

SELECT STRAIGHT_JOIN ... FROM t1, t2, t3 ... ;
SELECT ... FROM t1 STRAIGHT_JOIN t2 STRAIGHT_JOIN t3 ... ;

  分别在带有STRAIGHT_JOIN和不带STRAIGHT_JOIN的情况下运行这个查询;MySQL可能因为什么原因没有按照你认为最好的次序使用索引(你可以使用EXPLAIN来检查MySQL处理每个语句的执行计划)。

  你还可以使用FORCE INDEX、USE INDEX或IGNORE INDEX来指导服务器如何使用索引。

  利用优化器更加完善的区域。MySQL可以执行联结和子查询,但是子查询是最近才支持的,是在MySQL 4.1中添加的。因而在很多情况下,优化器对联结操作的调整比对子查询的调整要好一些。当你的子查询执行地很慢的时候,这就是一条实际的提示。有一些子查询可以使用逻辑上相等的联结来重新表达。在可行的情况下,你可以把子查询重新改写为联结,看是否执行地快一些。

  测试查询的备用形式,多次运行。当你测试查询的备用形式的时候(例如,子查询与等同的联结操作对比),每种方式都应该多次运行。如果两种形式都只运行了一次,那么你通常会发现第二个查询比第一个快,这是因为第一个查询得到的信息仍然保留在缓存中,以至于第二个查询没有真正地从磁盘上读取数据。你还应该在系统负载相对平稳的时候运行查询,以避免系统中其它的事务影响结果。

  避免过度地使用MySQL自动类型转换。MySQL会执行自动的类型转换,但是如果你能够避免这种转换操作,你得到的性能就更好了。例如,如果num_col是整型数据列,那么下面这些查询将返回相同的结果:

SELECT * FROM mytbl WHERE num_col = 4;
SELECT * FROM mytbl WHERE num_col = ’4’;

  但是第二个查询涉及到了类型转换。转换操作本身为了把整型和字符串型转换为双精度型进行比较,使性能恶化了。更严重的情况是,如果num_col是索引的,那么涉及到类型转换的比较操作不会使用索引。

  相反类型的比较操作(把字符串列与数值比较)也会阻止索引的使用。假设你编写了如下所示的查询:

SELECT * FROM mytbl WHERE str_col = 4;

  在这个例子中,不会使用str_col上的索引,因为在把str_col中的字符串值转换成数值的时候,可能有很多值等于4(例如’4’、’4.0’和’4th’)。分辨哪些值符合要求的唯一办法是读取每个数据行并执行比较操作。

  使用EXPLAIN来检查优化器的操作

  EXPLAIN对于了解优化器生成的、用于处理语句的执行计划的内部信息是很有帮助的。在这一部分中,我们将解释EXPLAIN的两种用途:

  · 查看采用不同的方式编写的查询是否影响了索引的使用。

  · 查看向数据表添加索引对优化器生成高效率执行计划的能力的影响。

  这一部分只讨论与示例相关的EXPLAIN输入字段。

  前面,在"优化器是如何工作的"部分中我们得出的观点是,你编写表达式的方式将决定优化器是否能使用可用的索引。特别是上面的讨论使用了下面三个逻辑相等的WHERE子句的例子,只有第三个允许使用索引:

WHERE TO_DAYS(date_col) - TO_DAYS(CURDATE()) < cutoff
WHERE TO_DAYS(date_col) < cutoff + TO_DAYS(CURDATE())
WHERE date_col < DATE_ADD(CURDATE(), INTERVAL cutoff DAY)

  EXPLAIN允许你查看编写表达式的某种方式是否比另外的方式好一些。为了看到结果,让我们分别用这三个WHERE子句搜索成员表中过期的数据列值,把cutoff值设为30天。为了看到索引的使用和表达式编写方式之间的关系,我们首先对expiration列进行索引:

mysql> ALTER TABLE member ADD INDEX (expiration);

  接着在每个表达式形式上使用EXPLAIN,看优化器生成了什么样的执行计划:

mysql> EXPLAIN SELECT * FROM MEMBER
-> WHERE TO_DAYS(expiration) - TO_DAYS(CURDATE()) < 30/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 102
Extra: Using where
mysql> EXPLAIN SELECT * FROM MEMBER
-> WHERE TO_DAYS(expiration) < 30 + TO_DAYS(CURDATE())/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 102
Extra: Using where
mysql> EXPLAIN SELECT * FROM MEMBER
-> WHERE expiration < DATE_ADD(CURDATE(), INTERVAL 30 DAY)/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: range
possible_keys: expiration
key: expiration
key_len: 4
ref: NULL
rows: 6
Extra: Using where

  上面的结果显示,前面两个语句没有使用索引。类型(type)值表明了将如何从数据表中读取信息。ALL意味着"将检查所有的记录"。也就是说,它会执行全表扫描,没有利用索引。每个与键相关的列都是NULL也表明没有使用索引。

  与此形成对比的是,第三个语句的结果显示,采用这种方式编写的WHERE子句,优化器可以使用expiration列上的索引:

  · 类型(type)值表明它可以使用索引来搜索特定范围的值(小于右边表达式给定的值)。

  · 可能键(possible_keys)和键(key)值显示expiration上的索引已经被考虑作为备选索引,并且它也是真正使用的索引。

  · 行数(rows)值显示优化器估计自己需要检查6个数据行来处理该查询。这比前面两个执行计划的102小很多。

  EXPLAIN的第二种用途是查看添加索引是否能帮助优化器更高效率地执行语句。我将使用两个未被索引的数据表。它足够显示建立索引的效率。相同的规则可以应用于涉及多表的更加复杂的联结操作。

  假设我们有两个数据表t1和t2,每个有1000行,包含的值从1到1000。下面的查询查找出两个表中值相同的数据行:

mysql> SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2;
+------+------+
| i1 | i2 |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
...

  两个表都没有索引的时候,EXPLAIN产生下面的结果:

mysql> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using where

  类型列中的ALL表明要进行检查所有数据行的全表扫描。可能键列中的NULL表明没有找到用于提高查询速度的备选索引(键、键长度和参考列都是NULL也是因为缺少合适的索引)。Using where表明使用WHERE子句中的信息来识别合格的数据行。

  这段信息告诉我们,优化器没有为提高执行查询的效率找到任何有用的信息:

  · 它将对t1表进行全表扫描。

  · 对于t1中的每一行,它将执行t2的全表扫描,使用WHERE子句中的信息识别出合格的行。

  行数值显示了优化器估计的每个阶段查询需要检查的行数。T1的估计值是1000,因为1000可以完成全表扫描。相似地,t2的估计值也是1000,但是这个值是对于t1的每一行的。换句话说,优化器所估计的处理该查询所需要检查的数据行组合的数量是1000×1000,也就是一百万。这会造成很大的浪费,因为实际上只有1000个组合符合WHERE子句的条件。

  为了使这个查询的效率更高,给其中一个联结列添加索引并重新执行EXPLAIN语句:

mysql> ALTER TABLE t2 ADD INDEX (i2);
mysql> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 10
Extra: Using where; Using index

  我们可以看到性能提高了。T1的输出没有改变(表明还是需要进行全表扫描),但是优化器处理t2的方式就有所不同了:

  · 类型从ALL改变为ref,意味着可以使用参考值(来自t1的值)来执行索引查找,定位t2中合格的数据行。

  · 参考值在参考(ref)字段中给出了:sampdb.t1.i1。

  · 行数值从1000降低到了10,显示出优化器相信对于t1中的每一行,它只需要检查t2中的10行(这是一个悲观的估计值。实际上,在t2中只有一行与t1中数据行匹配。我们在后面会看到如何帮助优化器改善这个估计值)。数据行组合的全部估计值使1000×10=10000。它比前面的没有索引的时候估计出来的一百万好多了。

  对t1进行索引有价值吗?实际上,对于这个特定的联结操作,扫描一张表是必要的,因此没有必要对t1建立索引。如果你想看到效果,可以索引t1.i1并再次运行EXPLAIN:

mysql> ALTER TABLE t1 ADD INDEX (i1);
mysql> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: index
possible_keys: i1
key: i1
key_len: 5
ref: NULL
rows: 1000
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 10
Extra: Using where; Using index

  上面的输出与前面的EXPLAIN的输出相似,但是添加索引对t1的输出有一些改变。类型从NULL改成了index,附加(Extra)从空的改成了Using index。这些改变表明,尽管对索引的值仍然需要执行全表扫描,但是优化器还是可以直接从索引文件中读取值,根据不需要使用数据文件。你可以从MyISAM表中看到这类结果,在这种情况下,优化器知道自己只询问索引文件就能够得到所有需要的信息。对于InnoDB 和BDB表也有这样的结果,在这种情况下优化器可以单独使用索引中的信息而不用搜索数据行。

  我们可以运行ANALYZE TABLE使优化器进一步优化估计值。这会引起服务器生成键值的静态分布。分析上面的表并再次运行EXPLAIN得到了更好的估计值:

mysql> ANALYZE TABLE t1, t2;
mysql> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2/G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: index
possible_keys: i1
key: i1
key_len: 5
ref: NULL
rows: 1000
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 1
Extra: Using where; Using index

  在这种情况下,优化器估计在t2中与t1的每个值匹配的数据行只有一个。

  重载优化过程

  这个过程听起来多余,但是有时候你还是希望去掉某些MySQL优化行为的:

  重载优化器的表联结次序。使用STRAIGHT_JOIN强迫优化器按照特定的次序使用数据表。在这样操作的时候,你必须对数据表进行排序,这样才能保证第一张表是被选择的行数最少的表。如果你不能确定被选择行数最少的是哪一张表,那么就把行数最多的放到第一的位置。换句话说,试着对表进行排序,使最有约束力的选择出现在最前面。你对可能的备选数据行缩小地越早,执行查询的性能就越好。请确保在带有STRAIGHT_JOIN和不带STRAIGHT_JOIN的时候分别执行该查询。有时候由于某些原因的存在,优化器没有按照你认定的方式联结数据表,STRAIGHT_JOIN也可能没有实际的帮助作用。

  另一个可能性是在联结的数据表列表中的某个表的后面使用FORCE INDEX、USE INDEX和IGNORE INDEX调节符来告诉MySQL如何使用索引。这在优化器没有做出正确选择的时候是有用处的。

  以最小的代价清空一张表。当需要完全地清空一张MyISAM数据表的时候,最快的方法是删除它并利用它的.frm文件中存储的脚本来重新建立它。使用TRUNCATE TABLE语句实现:

TRUNCATE TABLE tbl_name;

  通过重新建立MyISAM数据表来清空它的这种服务器优化措施使该操作非常快,因为不需要单独地逐行删除。

  但是TRUNCATE TABLE也带来了一些副作用,在某些环境中是不符合要求的:

  · TRUNCATE TABLE不一定能够计算出被删除的数据列的精确数量。如果你需要这个数值,请使用不带WHERE子句的DELETE语句:

DELETE FROM tbl_name;

  · 但是,通过重新建立来清空数据表,它可能会把序号的起始值设置为1。为了避免这种情况,请使用"不优化的"全表DELETE语句,它带有一个恒为真的WHERE子句:

DELETE FROM tbl_name WHERE 1;

  添加WHERE子句会强迫MySQL进行逐行删除,因为它必须计算出每一行的值来判断是否能够删除它。这个语句执行的速度很慢,但是它却保留了当前的AUTO_INCREMENT序号。

MySQL查询优化系列讲座之:数据类型与效率

这一部分提供了如何选择数据类型来帮助提高查询运行速度的一些指导:

  在可以使用短数据列的时候就不要用长的。如果你有一个固定长度的CHAR数据列,那么就不要让它的长度超出实际需要。如果你在数据列中存储的最长的值有40个字符,就不要定义成CHAR(255),而应该定义成CHAR(40)。如果你能够用MEDIUMINT代替BIGINT,那么你的数据表就小一些(磁盘I/O少一些),在计算过程中,值的处理速度也快一些。如果数据列被索引了,那么使用较短的值带来的性能提高更加显著。不仅索引可以提高查询速度,而且短的索引值也比长的索引值处理起来要快一些。

  如果你可以选择数据行的存储格式,那么应该使用最适合存储引擎的那种。对于MyISAM数据表,最好使用固定长度的数据列代替可变长度的数据列。例如,让所有的字符列用CHAR类型代替VARCHAR类型。权衡得失,我们会发现数据表使用了更多的磁盘空间,但是如果你能够提供额外的空间,那么固定长度的数据行被处理的速度比可变长度的数据行要快一些。对于那些被频繁修改的表来说,这一点尤其突出,因为在那些情况下,性能更容易受到磁盘碎片的影响。

  · 在使用可变长度的数据行的时候,由于记录长度不同,在多次执行删除和更新操作之后,数据表的碎片要多一些。你必须使用OPTIMIZE TABLE来定期维护其性能。固定长度的数据行没有这个问题。

  · 如果出现数据表崩溃的情况,那么数据行长度固定的表更容易重新构造。使用固定长度数据行的时候,每个记录的开始位置都可以被检测到,因为这些位置都是固定记录长度的倍数,但是使用可变长度数据行的时候就不一定了。这不是与查询处理的性能相关的问题,但是它一定能够加快数据表的修复速度。

  尽管把MyISAM数据表转换成使用固定长度的数据列可以提高性能,但是你首先需要考虑下面一些问题:

  · 固定长度的数据列速度较快,但是占用的空间也较大。CHAR(n)列的每个值(即使是空值)通常占n个字符,这是因为把它存储到数据表中的时候,会在值的后面添加空格。VARCHAR(n)列占有的空间较小,因为只需要分配必要的字符个数用于存储值,加上一两个字节来存储值的长度。因此,在CHAR和VARCHAR列之间进行选择的时候,实际上是时间与空间的对比。如果速度是主要的考虑因素,那么就使用CHAR数据列获取固定长度列的性能优势。如果空间很重要,那么就使用VARCHAR数据列。总而言之,你可以认为固定长度的数据行可以提高性能,虽然它占用了更大的空间。但是对于某些特殊的应用程序,你可能希望使用两种方式来实现某个数据表,然后运行测试来决定哪种情况符合应用程序的需求。

  · 即使愿意使用固定长度类型,有时候你也没有办法使用。例如,长于255个字符的字符串就无法使用固定长度类型。

  MEMORY数据表目前都使用固定长度的数据行存储,因此无论使用CHAR或VARCHAR列都没有关系。两者都是作为CHAR类型处理的。

  对于InnoDB数据表,内部的行存储格式没有区分固定长度和可变长度列(所有数据行都使用指向数据列值的头指针),因此在本质上,使用固定长度的CHAR列不一定比使用可变长度VARCHAR列简单。因而,主要的性能因素是数据行使用的存储总量。由于CHAR平均占用的空间多于VARCHAR,因此使用VARCHAR来最小化需要处理的数据行的存储总量和磁盘I/O是比较好的。

  对于BDB数据表,无论使用固定长度或可变长度的数据列,差别都不大。两种方法你都可用试一下,运行一些实验测试来检测是否存在明显的差别。

  把数据列定义成不能为空(NOT NULL)。这会使处理速度更快,需要的存储更少。它有时候还简化了查询,因为在某些情况下你不需要检查值的NULL属性。

  考虑使用ENUM数据列。如果你拥有的某个数据列的基数很低(包含的不同的值数量有限),那么可以考虑把它转换为ENUM列。ENUM值可以被更快地处理,因为它们在内部表现为数值。

  使用PROCEDURE ANALYSE()。运行PROCEDURE ANALYSE()可以看到数据表中列的情况:

SELECT * FROM tbl_name PROCEDURE ANALYSE();
SELECT * FROM tbl_name PROCEDURE ANALYSE(16,256);

  输出的每一列信息都会对数据表中的列的数据类型提出优化建议。第二个例子告诉PROCEDURE ANALYSE()不要为那些包含的值多于16个或者256字节的ENUM类型提出建议。如果没有这样的限制,输出信息可能很长;ENUM定义通常很难阅读。

  根据的PROCEDURE ANALYSE()输出信息,你可能发现,可以修改自己的数据表来利用那些效率更高的数据类型。如果你决定改变某个数据列的类型,需要使用ALTER TABLE语句。

  使用OPTIMIZE TABLE来优化那些受到碎片影响的数据表。被大量修改的数据表,特别是那些包含可变长度数据列的表,容易遭受碎片的影响。碎片很糟糕,因为它会导致用于存储数据表的磁盘块形成无用空间(空洞)。随着时间的推移,为了得到有效的数据行,你必须读取更多的块,性能就会降低。这会出现在任何可变长度的数据行上,但是对于BLOB或TEXT数据列尤其突出,因为它们的长度差异太大了。在正常情况下使用OPTIMIZE TABLE会防止数据表的性能降低。OPTIMIZE TABLE可以用于MyISAM和BDB数据表,但是defragments只能用于MyISAM数据表。任何存储引擎中的碎片整理方法都是用mysqldump来转储(dump)数据表,接着使用转储的文件删除并重新建立那些数据表:

% mysqldump --opt db_name tbl_name > dump.sql
% mysql db_name < dump.sql

  把数据打包放入BLOB或TEXT数据列。使用BLOB或TEXT数据列存储打包(pack)的数据,并在应用程序中进行解包(unpack),使你能够在一次检索操作中得到需要的任何信息,而不需要进行多次检索。它对那些很难用标准的数据表结构表现的数据值和频繁变化的数据值也是有帮助的。

  解决这个问题的另一种方法是让那些处理Web窗体的应用程序把数据打包成某种数据结构,然后把它插入到单个BLOB或TEXT数据列中。例如,你可以使用XML表示调查表回复,把那些XML字符串存储在TEXT数据列中。由于要对数据进行编码(从数据表中检索数据的时候还需要解码),它会增加客户端的开销,但是可以简化数据结构,而且它还消除了那些因为改变了调查表的内容而必须改变数据表结构的需求。

  另一方面,BLOB和TEXT值也会引起自己的一些问题,特别是执行了大量的删除或更新操作的时候。删除这种值会在数据表中留下很大的"空洞",以后填入这些"空洞"的记录可能长度不同(前面讨论的OPTIMIZE TABLE提出解决这个问题的一些建议)。

  使用合成的(synthetic)索引。合成的索引列在某些时候是有用的。一种办法是根据其它的列的内容建立一个散列值,并把这个值存储在单独的数据列中。接下来你就可以通过检索散列值找到数据行了。但是,我们要注意这种技术只能用于精确匹配的查询(散列值对于类似<或>=等范围搜索操作符是没有用处的)。我们可以使用MD5()函数生成散列值,也可以使用SHA1()或CRC32(),或者使用自己的应用程序逻辑来计算散列值。请记住数值型散列值可以很高效率地存储。同样,如果散列算法生成的字符串带有尾部空格,就不要把它们存储在CHAR或VARCHAR列中,它们会受到尾部空格去除的影响。

  合成的散列索引对于那些BLOB或TEXT数据列特别有用。用散列标识符值查找的速度比搜索BLOB列本身的速度快很多。

  在不必要的时候避免检索大型的BLOB或TEXT值。例如,SELECT *查询就不是很好的想法,除非你能够确定作为约束条件的WHERE子句只会找到所需要的数据行。否则,你可能毫无目的地在网络上传输大量的值。这也是BLOB或TEXT标识符信息存储在合成的索引列中对我们有所帮助的例子。你可以搜索索引列,决定那些需要的数据行,然后从合格的数据行中检索BLOB或TEXT值。

  把BLOB或TEXT列分离到单独的表中。在某些环境中,如果把这些数据列移动到第二张数据表中,可以让你把原数据表中的数据列转换为固定长度的数据行格式,那么它就是有意义的。这会减少主表中的碎片,使你得到固定长度数据行的性能优势。它还使你在主数据表上运行SELECT *查询的时候不会通过网络传输大量的BLOB或TEXT值。

  高效率地载入数据

  在大多数情况下,你所关注的是SELECT查询的优化,因为SELECT查询是最常见的查询类型,而且如何优化它们又不是太简单。与此形成对比,把数据载入数据库的操作就相对直接了。然而,你仍然可以利用某些策略来改善数据载入操作的效率。基本的原理如下所示:

  · 批量载入比单行载入的效率高,因为在每条记录被载入后,键缓存(key cache)不用刷新(flush);可以在这批记录的末尾刷新键缓存。键缓存刷新的频率减少得越多,数据载入的速度就越快。

  · 没有索引的数据表的载入速度比有索引的要快一些。如果存在索引,不但要把记录添加到数据文件中,还必须修改索引来反映新增的记录。

  · 较短的SQL语句比较长的SQL语句快,因为它们所涉及到服务器端分析过程较少,同时通过网络把它们从客户端发送到服务器上的速度也更快。

  其中有些因素看起来是次要的(尤其是最后一个),但是如果你载入的数据很多,那么即使很小的效率差异也会导致一定的性能差别。我们可以从前面的一般原理得出几条如何快速载入数据的实践结论:

  · LOAD DATA(所有形式的)比INSERT效率高,因为它是批量载入数据行的。服务器只需要分析和解释一条语句,而不是多条语句。同样,索引只需要在所有的数据行被处理过之后才刷新,而不是每行刷新一次。

  · 不带LOCAL的LOAD DATA比带有LOCAL的LOAD DATA的速度要快。不带LOCAL的时候,文件必须位于服务器上,而且你必须拥有FILE权限,但是服务器却可以直接从磁盘上读取文件。使用LOAD DATA LOCAL的时候,客户端读取文件并通过网络把它发送给服务器,速度慢一些。

  · 如果你必须使用INSERT,那么试着使用在一个语句中指定多个数据行的形式:

INSERT INTO tbl_name VALUES(...),(...),... ;

  在这个语句中指定的数据行越多,效果就越好。这会减少必要的语句数量,并最小化索引刷新的次数。这一条结论看起来与前面所讨论的"语句越短,执行速度越快"相矛盾,但是实际上并不矛盾。这儿所讨论的是同时插入多个数据行的一个INSERT语句所花费的开销比功能相同的多个单行INSERT语句的花费的开销要小一些,并且多行语句消耗的索引刷新开销也少一些。

  如果你使用mysqldump生成数据库备份文件,那么MySQL 4.1会默认地生成多行INSERT语句:它会激活--opt (优化)选项,而这个选项会激活--extended-insert选项,该选项生成多行INSERT语句,还存在其它一些选项也可以使数据被载入的时候,转储文件被处理的效率更高。对于MySQL 4.1以前的版本,你可以明确地指定--opt或--extended-insert选项。

  使用mysqldump的时候要避免使用--complete-insert选项;它生成的INSERT语句是每个数据行一条语句的,语句总共会很长,比多行语句需要的分析操作更多。

  · 如果你必须使用INSERT语句,那么在可能的情况下,对它们进行分组以减少索引的刷新。对于事务性的存储引擎,在单个事务中提交,而不是在自动提交(autocommit)模式下提交INSERT语句可以实现这样的功能:

START TRANSACTION;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
COMMIT;

  对于非事务性的存储引擎,获取数据表上的写入锁,它被锁定的时候提交INSERT语句:

LOCK TABLES tbl_name WRITE;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
UNLOCK TABLES;

  无论采用哪种方法,你得到的好处都是相同的:索引在所有的语句都被执行之后才刷新一次,而不是每个INSERT语句刷新一次索引。后面介绍了在自动提交模式下或数据表没有被锁定的时候发生的情况。

  · 对于MyISAM数据表,减少索引刷新的另外一个策略是使用DELAYED_KEY_WRITE表选项。使用这个选项的时候,数据行会像平常一样立即写入数据文件中,但是键缓存只是偶尔刷新一次,而不是在每次插入操作之后都需要刷新。如果要在服务器上全面地使用延迟索引刷新,那么就需要使用--delay-key-write选项来启动mysqld。在这种情况下,每个数据表的索引块写入操作都会被延迟,直到这些数据块必须为其它的索引值提供空间、或者执行了FLUSH TABLES命令、或者数据表被关闭的时候才执行操作。

  如果你选择了对MyISAM数据表使用延迟键写入,那么不正常的服务器关闭可能会引起索引值的丢失。这不是致命的问题,因为MyISAM索引可以依据数据行来进行修复,但是如果想让修复过程出现,你就必须使用--myisam-recover=FORCE选项来启动服务器。这个选项会使服务器在打开MyISAM数据表的时候检查它们,如果有必要就自动地修复它们。

  对于复制(replication)从属服务器,你可能希望使用--delay-key-write=ALL来延迟所有的MyISAM数据表索引的刷新,不管在主服务器上最初是如何建立它们的。

  · 使用压缩的客户端/服务器协议来减少网络上数据传输的数量。对于大多数MySQL客户端来说,我们都可以使用--compress命令行选项来指定它。通常,这个选项只是在较慢的网络上使用,这是因为压缩操作会花费大量的处理器时间。

  · 让MySQL替你插入默认值。也就是说,无论如何都不要给INSERT语句中那些可以赋予默认值的列指定值。平均起来,你的语句更短,减少了通过网络发送到服务器的字符数量。此外,由于语句包含的值较少,服务器执行的分析和值转换操作也较少。

  · 对于MyISAM数据表,如果你必须把大量的数据载入一个新表,最好建立不带索引的表,载入数据,然后建立索引,这样的工作次序的速度要快一些。一次性地建立索引比每行都更新索引的速度要快一些。对于已经带有索引的表,如果预先删除或禁止索引,后来再重新建立或者激活索引,那么数据载入的速度也要快一些。这些策略不能应用于InnoDB或BDB表,它们没有对分离的索引建立过程进行优化。

  如果你考虑使用删除或禁止索引的策略,把数据载入MyISAM数据表,那么在评估获得的优势的时候,就需要考虑整个环境。如果你把少量的数据载入大型的数据表中,那么在没有任何特殊准备工作的情况下,重新建立索引花费的时间可能比载入数据的时间还要长。

  要删除并且重新建立索引,需要使用DROP INDEX和CREATE INDEX,或者使用与索引相关的ALTER TABLE。禁止和激活索引有两种办法:

  · 你可用使用ALTER TABLE的DISABLE KEYS和ENABLE KEYS形式:

ALTER TABLE tbl_name DISABLE KEYS;
ALTER TABLE tbl_name ENABLE KEYS;

  这些语句关闭或打开表中非唯一(non-unique)索引的更新过程。

  ALTER TABLE的DISABLE KEYS和ENABLE KEYS子句是索引禁止和激活操作的推荐方法,因为服务器也是这样操作的(如果你使用LOAD DATA语句把数据载入空的MyISAM表中,服务器会自动地执行这样的优化操作)。

  · Myisamchk工具可以执行索引维护。它直接在数据表文件上进行操作,因此使用它的时候,你必须拥有数据表文件的写入权限。
使用myisamchk禁止MyISAM表的索引的方法是,首先你要确保已经告诉了服务器让该数据表独立出来,接着把它移动到适当的数据库目录中,并运行下面的命令:

% myisamchk --keys-used=0 tbl_name

  载入数据之后,重新激活索引:

% myisamchk --recover --quick --keys-used=n tbl_name

  其中的n是位掩码(bitmask),它指明了要激活的索引。Bit 0(第一个位)与索引1对应。例如,如果某张表拥有三个索引,那么n的值应该是7(二进制的111)。你也可以使用--description选项来检测索引的数量:

% myisamchk --description tbl_name

  前面的数据载入原则也可以应用于混合查询环境(客户端执行多种不同的操作)。例如,你应该避免在那些频繁被修改(写入)的数据表上运行长时间的SELECT查询。这会引发大量的争用(contention),导致写入操作的性能较差。一个可能的解决办法是,如果你的写入操作主要是INSERT操作,那么把新记录添加到辅助表中,接着周期性地把这些记录添加到主表中。如果你必须立即访问这些新记录,那么这个策略是不行的,但是如果你能够承担得起短期内不访问这些数据的代价,那么使用辅助表可以在两个方面带来好处。首先,它减少了主表上的SELECT查询争用的问题,因此它们执行得更快。其次,把辅助表中的批量数据载入主表中所花费的时间总和也比单独载入记录花费的时间总和要小一些;键缓存只需要在每次批量载入结束后刷新一次,而不用每个数据行载入后都刷新一次。

  使用这种策略的一个应用是把Web服务器的Web页面访问日志载入MySQL数据库的时候。在这种情况下,保证实体立即进入主表的优先级并不高(没有这个必要性)。

  如果你在MyISAM表上使用了混合的INSERT和SELECT语句,你就可以利用并发性插入操作的优点了。这个特性允许插入和检索操作同时进行,而不需要使用辅助表。你可以查看"使用并发性插入操作"部分。

MySQL查询优化系列讲座之:调度和锁定

前面的部分主要是聚焦于如何让单独的查询执行的速度更快。MySQL还允许你改变语句调度的优先级,它可以使来自多个客户端的查询更好地协作,这样单个客户端就不会由于锁定而等待很长时间。改变优先级还可以确保特定类型的查询被处理得更快。这一部分讲解MySQL的默认的调度策略和可以用来影响这些策略的选项。它还谈到了并发性插入操作的使用和存储引擎锁定层次对客户端的并发性的影响。为了讨论的方便,我们把执行检索(SELECT)的客户端称为"读取者",把执行修改操作(DELETE、INSERT、REPLACE或UPDATE)的客户端称为"写入者"。

  MySQL的默认的调度策略可用总结如下:

  · 写入操作优先于读取操作。

  · 对某张数据表的写入操作某一时刻只能发生一次,写入请求按照它们到达的次序来处理。

  · 对某张数据表的多个读取操作可以同时地进行。

  MyISAM和MEMORY存储引擎借助于数据表锁来实现这样的调度策略。当客户端访问某张表的时候,首先必须获取它的锁。当客户端完成对表的操作的时候,锁就会被解除。通过LOCK TABLES和UNLOCK TABLES语句来显式地获取或释放锁是可行的,但是在通常情况下,服务器的锁管理器会自动地在需要的时候获取锁,在不再需要的时候释放锁。获取的锁的类型依赖于客户端是写入还是读取操作。

  对某张表进行写入操作的客户端必须拥有独占的(排他的)访问权的锁。操作在进行的过程中,该数据表处于不一致的(inconsistent)状态,因为数据记录在删除、添加或修改的时候,数据表上的索引也可能需要更新以相互匹配。这个数据表在变化的过程中,如果允许其它的客户端访问,会出现问题。非常明显,允许两个客户端同时写入一张数据表是不利的,因为这样的操作会很快使数据表中的信息成为一堆无用的垃圾。但是允许客户端读取变化之中的数据表也不好,因为正在读取的位置中的数据可能正在变化(修改),读取的结果可能不是真实的。

  对某张表执行读取操作的客户端必须获取一个锁,防止在读取的过程中,其它的客户端写入或改变表。但是这个锁不需要独占的访问权。读取操作不会改变数据,因此没有理由让某个读取者阻止其它的读取者访问这张表。因此读取锁允许其它的客户端在同一时刻读取这张表。

  MySQL提供了几个语句调节符,允许你修改它的调度策略:

  · LOW_PRIORITY关键字应用于DELETE、INSERT、LOAD DATA、REPLACE和UPDATE。

  · HIGH_PRIORITY关键字应用于SELECT和INSERT语句。

  · DELAYED关键字应用于INSERT和REPLACE语句。

  LOW_PRIORITY和HIGH_PRIORITY调节符影响那些使用数据表锁的存储引擎(例如MyISAM和MEMORY)。DELAYED调节符作用于MyISAM和MEMORY数据表。

  改变语句调度的优先级

  LOW_PRIORITY关键字影响DELETE、INSERT、LOAD DATA、REPLACE和UPDATE语句的执行调度。通常情况下,某张数据表正在被读取的时候,如果有写入操作到达,那么写入者一直等待读取者完成操作(查询开始之后就不能中断,因此允许读取者完成操作)。如果写入者正在等待的时候,另一个读取操作到达了,该读取操作也会被阻塞(block),因为默认的调度策略是写入者优先于读取者。当第一个读取者完成操作的时候,写入者开始操作,并且直到该写入者完成操作,第二个读取者才开始操作。

  如果写入操作是一个LOW_PRIORITY(低优先级)请求,那么系统就不会认为它的优先级高于读取操作。在这种情况下,如果写入者在等待的时候,第二个读取者到达了,那么就允许第二个读取者插到写入者之前。只有在没有其它的读取者的时候,才允许写入者开始操作。理论上,这种调度修改暗示着,可能存在LOW_PRIORITY写入操作永远被阻塞的情况。如果前面的读取操作在进行的过程中一直有其它的读取操作到达,那么新的请求都会插入到LOW_PRIORITY写入操作之前。

  SELECT查询的HIGH_PRIORITY(高优先级)关键字也类似。它允许SELECT插入正在等待的写入操作之前,即使在正常情况下写入操作的优先级更高。另外一种影响是,高优先级的SELECT在正常的SELECT语句之前执行,因为这些语句会被写入操作阻塞。

  如果你希望所有支持LOW_PRIORITY选项的语句都默认地按照低优先级来处理,那么请使用--low-priority-updates选项来启动服务器。通过使用INSERT HIGH_PRIORITY来把INSERT语句提高到正常的写入优先级,可以消除该选项对单个INSERT语句的影响。

  使用延迟插入操作

  DELAYED调节符应用于INSERT和REPLACE语句。当DELAYED插入操作到达的时候,服务器把数据行放入一个队列中,并立即给客户端返回一个状态信息,这样客户端就可以在数据表被真正地插入记录之前继续进行操作了。如果读取者从该数据表中读取数据,队列中的数据就会被保持着,直到没有读取者为止。接着服务器开始插入延迟数据行(delayed-row)队列中的数据行。在插入操作的同时,服务器还要检查是否有新的读取请求到达和等待。如果有,延迟数据行队列就被挂起,允许读取者继续操作。当没有读取者的时候,服务器再次开始插入延迟的数据行。这个过程一直进行,直到队列空了为止。

  感觉上LOW_PRIORITY和DELAYED是相似的,两者都允许数据行插入操作被延迟,但是它们对客户端操作的影响却有很大的差异。LOW_ PRIORITY强迫客户端等待,直到那些数据行可以被插入数据表。DELAYED允许客户端继续操作,服务器在内存中缓冲那些数据行,直到自己有时间处理它们。

  如果其它的客户端可能运行很长的SELECT语句并且你不希望阻塞,等待插入操作完成的时候,INSERT DELAYED就非常有用处了。客户端提交INSERT DELAYED的时候可能处理得很快,因为服务器只是简单地把要插入的数据行排队。

  但是,你也必须知道正常的INSERT与INSERT DELAYED行为之间的一些其它的差异。如果INSERT DELAYED语句包含语法错误,客户端会得到一个错误,但是却无法得到其它一些在正常情况下可以使用的信息。例如,当语句返回的时候,你无法依赖(得到)AUTO_INCREMENT(自动增长)值。同样,你无法得到唯一索引的副本数量。发生这种情况的原因在于插入操作在真正地被执行之前已经返回了状态信息。另一种可能出现的情况是,由于INSERT DELAYED语句的数据行都在内存中排队,当服务器崩溃或者使用kill -9退出的时候,数据行可能丢失(正常情况下,kill -TERM终止命令不会导致这种情况,因为服务器在退出之前会把数据行插入表中)。

  使用并发的插入操作

  MyISAM存储引擎有一条例外的规则,它允许读取者阻塞写入者。这种现象发生在MyISAM数据表中间没有"空洞"(可能是删除或更新数据行的结果)的情况下。当数据表没有"空洞"的时候,任何INSERT语句必然在末尾而不是中部添加数据行。在这种情况下,MySQL允许其它客户端在读取数据的同时向数据表添加数据行。这就是"并发性插入操作",因为它们同时发生,检索并没有被阻塞。

  如果你希望使用并发性插入操作,请注意下面一些事项:

  · 在INSERT语句中不要使用LOW_PRIORITY调节符。它会引起INSERT经常被读取者阻塞,因此阻碍了并发性插入操作的执行。

  · 如果读取者需要显式地锁定数据表以执行并发性插入操作,就应该使用LOCK TABLES ... READ LOCAL,而不是LOCK TABLES ... READ。LOCAL关键字会获取一个锁,允许并发性操作继续进行,因为它只能应用于数据表中已有的数据行,不会阻塞那些添加到末尾的新数据行。

  · LOAD DATA操作应该使用CONCURRENT调节符,允许该数据表上的SELECT语句同时执行。

  · 中间包含了"空洞"的MyISAM数据表不能使用并发性插入操作。但是,你可以使用OPTIMIZE TABLE语句来整理该数据表的碎片。

  锁的层次和并发性

  前面讨论的调度调节符允许你改变默认的调度策略。其中的大部分内容都是介绍使用这些调节符来解决数据表层次(table-level)的锁引起的问题,这都是MyISAM和MEMORY存储引擎用来管理数据表争用的问题的。

  BDB和InnoDB存储引擎实现了不同层次的锁,所以其性能特征和对争用的管理是不同的。BDB引擎使用页面层次(page-level)的锁。InnoDB引擎使用数据行层次(row-level)的锁,但是只在必要的时候使用(在很多情况下,例如当读取操作都完成的时候,InnoDB可能根本就不使用锁)。

  存储引擎使用的锁的层次对客户端的并发操作有很大的影响。假设两个客户端都希望更新某个数据表中的一行。由于要执行更新,每个客户端都需要一个写入锁。对于MyISAM数据表,引擎会为第一个客户端分配一个锁,这会引起第二个客户端阻塞,直到第一个客户端完成操作。对于BDB数据表,它可以实现更大的并发性:两个更新操作会同步进行,除非两个数据行都位于同一个页面中。在InnoDB数据表中,并发性更高;只要两个客户端没有更新同一行,两个更新操作就能同时发生。

  一般的规则是,锁的层次越细微,并发性越好,因为只要客户端使用数据表的部分不同,那么使用表的客户端就可以更多。它实际暗示着不同的存储引擎适合于不同的语句混合(mixes):

  · MyISAM检索的速度非常快。但是使用表层次的锁可能成为混合的检索和更新环境中的问题,特别是检索倾向于长时间运行的时候。在这些条件下,更新可能需要等待很久才能进行。

  · 当更新操作很多的时候,BDB和InnoDB数据表可以提供更好的性能。由于锁在页面或数据行层次进行,表被锁定的范围较小。这会减少锁的争用,提高并发性。

  在防止死锁(deadlock)方面,表层次的锁比细微层次的锁更有优势。使用表层次的锁的时候,死锁不会发生。服务器可以通过查看语句来检测需要的数据表,并提前锁定它们。而InnoDB和BDB数据表会发生死锁,因为这些存储引擎没有在事务开始的时候分配所有必要的锁。作为代替,在事务处理的过程中,当检测到需要锁的时候才分配。这就可能出现两个语句获取了锁,接着试图进一步获取锁(需要多个锁),但是这些锁却被对方保持着,等待对方释放。其结果是每个客户端都拥有一个锁,同时还需要利用其它的客户端拥有的锁才能继续执行。这会导致死锁,服务器必须终止其中一个事务。

MySQL查询优化讲座之:管理员的优化措施

前面的部分中讲解的优化措施都是没有特权的MySQL用户能够执行的。可以控制MySQL服务器或计算机的系统管理员能够执行额外的优化措施。例如,有些服务器参数附属于查询处理过程,并且是可以调整的,而且某些硬件配置因素对查询处理速度有直接的影响。在很多情况下,这些优化措施提高了整个服务器的性能,因此可以让所有的MySQL用户都受益。

  一般来说,当你执行管理员优化的时候,应该紧记以下规则:

  · 访问内存中的数据快于访问磁盘上的数据。

  · 尽量把数据保存在内存中可以减少磁盘操作。

  · 保留索引中的信息比保留数据记录的内容更重要。

  我们在后面将讨论如何应用这些规则。

  增加服务器缓存的大小。服务器拥有很多参数(系统变量),你可以改变这些参数来影响服务器的操作。其中的几个参数直接地影响查询处理的速度。你可以改变的最重要的参数是数据表缓存的大小和存储引擎用于缓冲索引操作信息的缓存大小。如果你拥有可用的内存,就把它分配给服务器的缓存,以允许信息存储在内存中并减少磁盘操作。这会有很好的效果,因为访问内存中的信息比从磁盘读取信息的速度快得多。

  · 当服务器打开表文件的时候,它试图保持这些文件的打开状态,以减少打开文件操作的数量。为了实现这样的功能,它在表缓存中维护打开文件的信息。table_cache系统变量控制着这个缓存的大小。如果服务器访问了大量的表,表缓存就会被填满,并且服务器会关闭那些有一段时间没有使用的表,为打开新表留出空间。你可以通过检查Opened_tables状态指示器来访问表缓存的效果:

SHOW STATUS LIKE ’Opened_tables’;

  Opened_tables显示了某个数据表必须打开的次数(因为它还没有打开)。这个值也显示为mysqladmin状态命令的输出信息中的Opens值。如果这个数字是稳定的或缓慢增长,那么它的设置可能是正确的。如果这个数字增长得很快,就意味着这个缓存太小了,必须经常关闭数据表来为打开其它的数据表留出空间。如果你拥有文件描述信息,增加表缓存大小将减少数据表打开操作的数量。

  · MyISAM存储引擎使用键缓冲来保持与索引相关的操作的索引信息块。它的大小是由key_buffer_size系统变量控制的。这个值越大,MySQL就一次性在内存中保持更多的索引信息块,可以增加在内存中(而不用从磁盘上读取新的信息块)找到键值的可能性。键缓存的默认大小是8MB。如果你拥有很多的内存,这是一个很保守的值,你可以直接增加它的大小,并且会看到基于索引的检索、索引的建立和修改操作的性能有很大改善。

  在MySQL 4.1以上版本中,你可以为MyISAM数据表建立附加的键缓存,并指定某些表使用它们。这样可以帮助提高这些数据表上的查询处理速度。

  · InnoDB和BDB引擎拥有自己的用于缓冲数据和索引值的缓存。它们的大小是由innodb_buffer_pool_size和bdb_cache_size变量控制的。InnoDB引擎还维护了一个日志缓冲。innodb_log_buffer_size变量可以控制它的大小。

  · 另一个专用的缓存是查询缓存,我们在"使用查询缓存"部分中解释。

  当你改变这些参数值的时候,应该遵循下面一些原则:

  · 每次只改变一个参数。如果你一次改变多个相互独立的变量,那么就很难评估每种改变的效果了。

  · 逐渐地增加系统变量值。根据理论,数量越多,性能越好,但是如果你使某个变量变得太大了,有可能造成系统资源匮乏,导致逆向效果,降低速度。

  · 不要在运行业务MySQL数据库的服务器上做调整参数的实验,最好建立一个独立的测试服务器。

  · 为了大致了解哪种参数变量可能适合自己的系统,你可以查看MySQL发布文档中包含的my-small.cnf、my-medium.cnf、my-large.cnf和my-huge.cnf选项文件(在Unix系统上,你可以在源发布文件的支持文件目录和二进制发布文件的共享目录总找到这些文件。在Windows上,它们位于基本的安装目录中,其扩展名可能是.ini)。这些文件可能让你知道最好改变服务器上的那些参数以适应不同的使用层次,并且为这些参数提供了一些典型值。

  用于提高服务器的操作性能的其它一些策略还包括:

  禁止不需要的存储引擎。服务器不会为禁止的引擎分配任何内存,因此我们可以利用这一点。如果从源文件建立MySQL,那么在配置的时候,大多数存储引擎就可以被排除在服务器之外。对于那些包含在服务器中的引擎来说,使用适当的启动选项可以在运行时禁止其中的大多数。
保持授权表许可的简单性。尽管服务器在内存中缓存了授权表内容,但是如果你在tables_priv或columns_priv表中有一些数据行的话,服务器就必须为每个查询语句检查表层次和列层次的权限。如果这些表是空的,那么服务器就能优化自己的权限检查过程,略过这些层次。

  如果你从源文件建立MySQL,那么就把它配置为使用静态类库,而不要使用共享类库。使用共享类库的动态二进制文件节约磁盘空间,然而静态二进制文件速度更快。但是,如果你使用了用户自定义函数(UDF)机制,那么有些系统要求使用动态链接。在这类系统上,静态二进制文件不能工作。

  使用MyISAM键缓存

  当MySQL执行某个利用了MyISAM数据表索引的语句的时候,它会使用键缓存来保持索引值。这种缓存减少了磁盘I/O:如果在缓存中找到了某个数据表需要的键值,就不需要再次从磁盘中读取。不幸的是,这种键缓存是有限的,并且在默认情况下,它是所有的MyISAM数据表共享使用的。如果在键缓存中没有找到键值并且键缓存是满的,争用将会导致:必须丢弃缓存中的某些值,为新值留出空间。如果下次需要那些已经被丢弃的值,就必须再次从磁盘上读取。

  如果你很倚重MyISAM数据表,那么把它的键保存在内存中效果会很好,但是缓存中的争用却会导致相反的效果。从同一张表或不同的表读取数据都可能引起争用。你可以通过把键缓存设置成足以保存某个特定数据表的全部索引,从而避免同一张数据表的争用,但是其它数据表的键仍然需要争用缓存空间。

  MySQL 4.1以上版本为这个问题提供了一种解决方案:它支持我们建立多个键缓存,并允许我们把某张数据表的索引指定并且预先装入某个缓存。如果你的数据表使用得很频繁,并且你有足够的内存,能够把它的索引载入缓存中,那么这种操作就是有用的。这种能力允许你同时避免同一张表和不同的表的争用:建立一个足够大的缓存,让它保存数据表的全部索引,并且指定该缓存专门用于那张数据表。在键被载入缓存之后,不在需要磁盘I/O操作。同时,键值永远不会被丢弃,对数据表的键的查看操作可以在内存中完成。

  下面的例子显示了如何为sampdb数据库的member数据表建立一个键缓存,该缓存的名称是member_cache,大小为1MB。执行这些指令的时候,你必须有超级(SUPER)权限。

  1.建立一个足够容纳数据表索引的独立的缓存:

mysql> SET GLOBAL member_cache.key_buffer_size = 1024*1024;

  2.给数据表指定键缓存:

mysql> CACHE INDEX member IN member_cache;
+---------------+--------------------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------------+--------------------+----------+----------+
| sampdb.member | assign_to_keycache | status | OK |
+---------------+--------------------+----------+----------+

  3.把数据表索引预先读入它的键缓存中:

mysql> LOAD INDEX INTO CACHE member;
+---------------+--------------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------------+--------------+----------+----------+
| sampdb.member | preload_keys | status | OK |+---------------+--------------+----------+----------+

  如果你希望把其它的数据表载入同一个缓存中,或者为其它的数据表建立键缓存,上面的操作就足够了。

  使用查询缓存

  MySQL服务器可以使用查询缓存来提高那些重复执行的SELECT语句的处理速度。它对性能的提高通常都是惊人的。查询缓存的工作方式如下所示:

  · 第一次执行某条SELECT语句的时候,服务器记住该查询的文本内容和它返回的结果。

  · 服务器下一次碰到这个语句的时候,它不会再次执行该语句。作为代替,它直接从查询缓存中的得到结果并把结果返回给客户端。

  · 查询缓存是基于服务器所接收到的查询字符串的文本内容的。如果某些查询的文本完全相同,那些它就认为这些查询是相同的。如果某些查询的字符不同,或者来自那些使用了不同的字符集或通讯协议的客户端,那么它会认为这些查询是不同的。同样,如果某些查询采用其它的功能相当、但是实际上没有指向相同的数据表(例如引用了不同的数据库中的同名数据表),那么它们也是不同的。

  · 当数据表被更新了之后,涉及到该数据表的任何缓存查询都变成无效的,并且会被丢弃。这可以防止服务器返回过期的结果。

  在默认情况下,MySQL对查询缓存的支持是内建的。如果你不希望使用这种缓存,并且想避免它所导致的性能开销,可以使用--without-query-cache选项来运行配置脚本建立服务器。

  如果需要检测某个服务器是否支持查询缓存,可以检查它的have_query_cache系统变量:

mysql> SHOW VARIABLES LIKE ’have_query_cache’;
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| have_query_cache | YES |
+------------------+-------+

  对于那些支持查询缓存的服务器来说,缓存的操作是基于三个系统变量值的:

  · query_cache_type决定查询缓存的操作模式。下表显示了可以使用的模式值:

模式 含义
0 不要缓存查询结果或检索缓存的结果。
1 缓存查询,除非它们以SELECT SQL_NO_CACHE开头。
2 根据需要只缓存那些以SELECT SQL_CACHE开头的查询。

  · query_cache_size决定分配给缓存的内存数量,单位是字节。

  · query_cache_limit设置被缓存的最大结果集大小;比这个值大的查询结果不会被缓存。

  例如,为了激活查询缓存并为它分配16MB内存,在配置文件中使用下面的设置:

[mysqld]
query_cache_type=1
query_cache_size=16M

  即使query_cache_type的值设置为零,query_cache_size指定内存数量也会被分配。为了避免浪费内存,只有在希望激活缓存的时候才把大小设置成大于零。同时,即使query_cache_type不为零,查询缓存的大小设置为零也会禁用缓存。

  使用了查询缓存的独立客户端会在服务器的默认缓存模式状态下操作。客户端可以使用下面的语句改变自己的查询的默认缓存模式:

SET query_cache_type = val;

  其中的val可以是0、1或2,它的意义与设置服务器启动时的query_cache_type变量的意义是相同的。在SET语句中,OFF、ON和DEMAND这些符号值与0、1和2对应。

  客户端还可以通过在SELECT关键字后天添加调节符来控制个别查询的缓存操作。如果缓存模式是ON或DEMAND,那么SELECT SQL_CACHE语句会让查询结果被缓存。SELECT SQL_NO_CACHE语句会使查询结果不被缓存。

  如果某些查询从经常改变的数据表中检索信息,那么抑止对这些查询的缓存操作是有用的。在这种情况下,缓存未必有多大用处。假设你把Web服务器请求的日志存储在MySQL数据表中,同时周期性地运行该数据表上的一组统计查询。对于很繁忙的Web服务器来说,会频繁地出现新行插入该数据表的操作,因此该数据表的任何缓存了的查询结果很快就变成无效的了。其含义是,尽管你周期性地提交统计查询,但是查询缓存可能对这些查询没有什么价值。在这种情况下,最好使用SQL_NO_CACHE调节符告诉服务器不要缓存这些查询的结果。

  硬件问题

  本文前面的部分中讨论的帮助你提高服务器性能的技术是没有考虑硬件配置的。你当然可以通过使用更好的硬件来让服务器运行地更快。但是并非所有的与硬件相关的改变都有相同的价值。当我们评估哪些硬件提高了性能的时候,最重要的原则与调整服务器参数的原则是相同的:尽可能地把最多的信息放在最快的存储中,并让这些信息尽可能地保持在该存储中。

  你可以改变几种硬件配置来提升服务器的性能:

  在计算机上安装更多的内存。这可以让你把服务器的缓存和缓冲区大小值配置成更大的,从而使数据保存在内存中的时间更长,从磁盘上读取信息的需要更少。

  重新配置系统,如果你拥有足够的内存,能够在内存文件系统中执行全部的交换操作,那么就删除所有的磁盘交换设备。否则,即使你拥有足够的用于交换操作的RAM,某些系统仍然会跟磁盘进行交换操作。

  增加更快的磁盘以改善I/O等待时间。在这种情况下,寻道时间是有代表性的主要的性能决定因素。横向移动磁头的速度比较慢,在磁头定位以后,从磁道上读取信息块的速度相对较快。但是,如果需要选择是添加更多的内存还是更快的磁盘,那么最好选择添加更多的内存。内存总是比磁盘快,而且添加内存可以让你使用更大的缓存,从而减少磁盘活动。

  通过在物理设备上划分磁盘活动来获取并行操作的优势。如果你可以在多个物理设备上划分读操作和写操作,那么其速度就会比从同一个设备读写要快一些。例如,如果你把数据库存储在一个设备上,把日志存储在另一个设备上,那么同时向两个设备写入信息的速度就比数据库和日志共享同一个设备的速度要快。请注意,使用同一个物理设备上的不同分区不算是并行操作。这是没有好处的,因为它们仍然需要争用相同的物理资源(磁头)。

  在把数据重新部署到另外一个设备之前,你要确保自己知道系统的负载状况。如果在某个特定的物理设备上正在运行一些重要的业务,那么把数据库放在该设备上有可能使性能更差。例如,如果你正在处理大量的Web业务,同时把数据库移动到Web服务器文档目录所在的设备上,就可能感觉不到任何优势。

  使用RAID设备可以让你获取并行操作的优势。

  使用多处理器硬件。对于类似MySQL服务器的多线程应用程序来说,多处理器硬件可以同时执行多个线程

(完)


文章来源:
http://www.itlearner.com/article/2005/2372.shtml(本文转载来源)
http://soft.yesky.com/lesson/486/2127486.shtml(文章最终来源)

你可能感兴趣的:(mysql)