MySQL中文参考手册---10 从MySQL得到最大的性能
优化是一项复杂的任务,因为它最终需要对整个系统的理解。当用你的系统/应用的小知识做一些局部优化是可能的时候,你越想让你的系统更优化,你必须知道它也越多。
因此,本章将试图解释并给出优化MySQL的不同方法的一些例子。但是记住总是有某些(逐渐变难)是系统更快的方法留着去做。
10.1 优化概述
为了使一个系统更快的最重要部分当然是基本设计。你也需要知道你的系统将做这样的事情,那就是你的瓶颈。
最常见的瓶颈是:
- 磁盘寻道。磁盘花时间找到一个数据,用在1999年的现代磁盘其平均时间通常小于10ms,因此理论上我们能大约一秒寻道 1000 次。这个时间用新磁盘提高很慢并且很难对一个表优化。优化它的方法是将数据散布在多个磁盘上。
- 当磁盘在我们需要读数据的正确位置时,磁盘读/写。用1999年的现代,一个磁盘传输类似10-20Mb/s。这必寻道更容易优化,因为你能从多个磁盘并行地读。
- CPU周期。当我们读数据进内存时,(或如果它已经在那里)我们需要处理它以达到我们的结果。当我们有相对内存较小的表时,这是最常见的限制因素,但是用小表速度通常不是问题。
- 内存带宽。当CPU需要超出适合cpu缓存的数据时,缓存带宽就成为内存的一个瓶颈。这是对大多数系统的一个不常见的瓶颈但是你应该知道它。
10.2 系统/编译时和启动参数的调节
我们以系统级的东西开始,因为这些决策的某一些很早就做好了。在其他情况下,快速浏览这部分可能就够了,因为它对大收获并不重要,但是有一个关于在这个层次上收获有多大的感觉总是好的。
使用的缺省OS确实重要!为了最大程度地使用多CPU,应该使用Solaris(因为线程工作得确实不错)或Linux(因为2.2本的核心又确实不错的SMP支持)。而且在32位的机器上,Linux缺省有2G的文件大小限制。当新的文件系统被释出时( XFS ),希望这不久被修正。
因为我们没在很多平台上运行生产MySQL,我们忠告你在可能选择它前,测试你打算运行的平台。
其他建议:
- 如果你有足够的RAM,你能删除所有交换设备。一些操作系统在某些情况下将使用一个SWAP设备,即使你有空闲的内存。
- 使用
--skip-locking
的MySQL选项避免外部锁定。注意这将不影响MySQL功能,只要它仅运行在一个服务器上。只要在你运行myisamchk
以前,记得要停掉服务器(或锁定相关部分)。在一些系统上这个开关是强制的,因为外部锁定不是在任何情况下都工作。当用MIT-pthreads编译时,--skip-locking
选项缺省为打开(on),因为flock()
没在所有的平台上被MIT-pthreads充分支持。唯一的情况是如果你对同一数据运行MySQL服务器(不是客户),你不能使用--skip-locking
之时,否则对没有先清掉(flushing)或先锁定mysqld
服务器的表上运行myisamchk
。你仍然能使用LOCK TABLES
/UNLOCK TABLES
,即使你正在使用--skip-locking
。
10.2.1 编译和链接怎样影响MySQL的速度
大多数下列测试在Linux上并用MySQL基准进行的,但是它们应该对其他操作系统和工作负载给出一些指示。
当你用-static
链接时,你得到最快的可执行文件。使用Unix套接字而非TCP/IP连接一个数据库也可给出好一些的性能。
在Linux上,当用pgcc
和-O6
编译时,你将得到最快的代码。为了用这些选项编译“sql_yacc.cc”,你需要大约200M内存,因为gcc/pgcc
需要很多内存使所有函数嵌入(inline)。在配置MySQL时,你也应该设定CXX=gcc
以避免包括libstdc++
库(它不需要)。
只通过使用一个较好的编译器或较好的编译器选项,在应用中你能得到一个10-30%的加速。如果你自己编译SQL服务器,这特别重要!
在Intel上,你应该例如使用pgcc或Cygnus CodeFusion编译器得到最大速度。我们已经测试了新的 Fujitsu编译器,但是它是还没足够不出错来优化编译MySQL。
这里是我们做过的一些测量表:
- 如果你以
-O6
使用pgcc
并且编译任何东西,mysqld
服务器是比用gcc
快11%(用字符串99的版本)。 - 如果你动态地链接(没有
-static
),结果慢了13%。注意你仍能使用一个动态连接的MySQL库。只有服务器对性能是关键的。 - 如果你使用TCP/IP而非Unix套接字,结果慢7.5%。
- 在一个Sun SPARCstation 10上,
gcc
2.7.3是比Sun Pro C++ 4.2快13%。 - 在Solaris 2.5.1上,在单个处理器上MIT-pthreads比带原生线程的Solaris慢8-12%。以更多的负载/cpus,差别应该变得更大。
由TcX提供的MySQL-Linux的分发用pgcc
编译并静态链接。
10.2.2 磁盘问题
- 正如前面所述,磁盘寻道是一个性能的大瓶颈。当数据开始增长以致缓存变得不可能时,这个问题变得越来越明显。对大数据库,在那你或多或少地要随机存取数据,你可以依靠你将至少需要一次磁盘寻道来读取并且几次磁盘寻道写入。为了使这个问题最小化,使用有低寻道时间的磁盘。
- 为了增加可用磁盘轴的数量(并且从而减少寻道开销),符号联接文件到不同磁盘或分割磁盘是可能的。
- 使用符号连接
- 这意味着你将索引/数据文件符号从正常的数据目录链接到其他磁盘(那也可以被分割的)。这使得寻道和读取时间更好(如果磁盘不用于其他事情)。见10.2.2.1 使用数据库和表的符号链接。
- 分割
- 分割意味着你有许多磁盘并把第一块放在第一个磁盘上,在第二块放在第二个磁盘上,并且第 n块在第(n mod number_of_disks)磁盘上,等等。这意味着,如果你的正常数据大小于分割大小(或完美地排列过),你将得到较好一些的性能。注意,分割是否很依赖于OS和分割大小。因此用不同的分割大小测试你的应用程序。见10.8 使用你自己的基准。注意对分割的速度差异很依赖于参数,取决于你如何分割参数和磁盘数量,你可以得出以数量级的不同。注意你必须选择为随机或顺序存取优化。
- 为了可靠,你可能想要使用袭击RAID 0+1(分割+镜像),但是在这种情况下,你将需要2*N个驱动器来保存N个驱动器的数据。如果你有钱,这可能是最好的选择!然而你也可能必须投资一些卷管理软件投资以高效地处理它。
- 一个好选择是让稍重要的数据(它能再生)上存在RAID 0磁盘上,而将确实重要的数据(像主机信息和日志文件)存在一个RAID 0+1或RAID N磁盘上。如果因为更新奇偶位你有许多写入,RAID N可能是一个问题。
- 你也可以对数据库使用的文件系统设置参数。一个容易的改变是以noatime选项挂装文件系统。这是它跳过更新在inode中的最后访问时间,而且这将避免一些磁盘寻道。
10.2.2.1 为数据库和表使用符号链接
你可以从数据库目录移动表和数据库到别处,并且用链接到新地点的符号代替它们。你可能想要这样做,例如,转移一个数据库到有更多空闲空间的一个文件系统。
如果MySQL注意到一个表是一个符号链接,它将解析符号链接并且使用其实际指向的表,它可工作在支持realpath()
调用的所有系统上(至少Linux和Solaris支持realpath()
)!在不支持realpath()
的系统上,你应该不同时通过真实路径和符号链接访问表!如果你这样做,表在任何更新后将不一致。
MySQL缺省不支持数据库链接。只要你不在数据库之间做一个符号链接,一切将工作正常。假定你在MySQL数据目录下有一个数据库db1
,并且做了一个符号链接db2
指向db1
:
shell> cd /path/to/datadirshell> ln -s db1 db2
现在,对在db1
中的任一表tbl_a
,在db2
种也好象有一个表tbl_a
。如果一个线程更新db1.tbl_a
并且另一个线程更新db2.tbl_a
,将有问题。
如果你确实需要这样,你必须改变下列在“mysys/mf_format.c”中的代码:
if (!lstat(to,&stat_buff)) /* Check if it's a symbolic link */ if (S_ISLNK(stat_buff.st_mode) && realpath(to,buff))
把代码改变为这样:
if (realpath(to,buff))
10.2.3 调节服务器参数
你能用这个命令得到mysqld
服务器缺省缓冲区大小:
shell> mysqld --help
这个命令生成一张所有mysqld
选项和可配置变量的表。输出包括缺省值并且看上去象这样一些东西:
Possible variables for option --set-variable (-O) are:back_log current value: 5connect_timeout current value: 5delayed_insert_timeout current value: 300delayed_insert_limit current value: 100delayed_queue_size current value: 1000flush_time current value: 0interactive_timeout current value: 28800join_buffer_size current value: 131072key_buffer_size current value: 1048540lower_case_table_names current value: 0long_query_time current value: 10max_allowed_packet current value: 1048576max_connections current value: 100max_connect_errors current value: 10max_delayed_threads current value: 20max_heap_table_size current value: 16777216max_join_size current value: 4294967295max_sort_length current value: 1024max_tmp_tables current value: 32max_write_lock_count current value: 4294967295net_buffer_length current value: 16384query_buffer_size current value: 0record_buffer current value: 131072sort_buffer current value: 2097116table_cache current value: 64thread_concurrency current value: 10tmp_table_size current value: 1048576thread_stack current value: 131072wait_timeout current value: 28800
如果有一个mysqld
服务器正在运行,通过执行这个命令,你可以看到它实际上使用的变量的值:
shell> mysqladmin variables
每个选项在下面描述。对于缓冲区大小、长度和栈大小的值以字节给出,你能用于个后缀“K”或“M” 指出以K字节或兆字节显示值。例如,16M
指出16兆字节。后缀字母的大小写没有关系;16M
和16m
是相同的。
你也可以用命令SHOW STATUS
自一个运行的服务器看见一些统计。见7.21 SHOW
语法(得到表、列的信息)。
back_log
- 要求MySQL能有的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用,然后主线程花些时间(尽管很短)检查连接并且启动一个新线程。
back_log
值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。只有如果期望在一个短时间内有很多连接,你需要增加它,换句话说,这值对到来的TCP/IP连接的侦听队列的大小。你的操作系统在这个队列大小上有它自己的限制。 Unixlisten(2)
系统调用的手册页应该有更多的细节。检查你的OS文档找出这个变量的最大值。试图设定back_log
高于你的操作系统的限制将是无效的。 connect_timeout
mysqld
服务器在用Bad handshake
(糟糕的握手)应答前正在等待一个连接报文的秒数。delayed_insert_timeout
- 一个
INSERT DELAYED
线程应该在终止之前等待INSERT
语句的时间。 delayed_insert_limit
- 在插入
delayed_insert_limit
行后,INSERT DELAYED
处理器将检查是否有任何SELECT
语句未执行。如果这样,在继续前执行允许这些语句。 delayed_queue_size
- 应该为处理
INSERT DELAYED
分配多大一个队列(以行数)。如果排队满了,任何进行INSERT DELAYED
的客户将等待直到队列又有空间了。 flush_time
- 如果这被设置为非零值,那么每
flush_time
秒所有表将被关闭(以释放资源和sync到磁盘)。 interactive_timeout
- 服务器在关上它前在一个交互连接上等待行动的秒数。一个交互的客户被定义为对
mysql_real_connect()
使用CLIENT_INTERACTIVE
选项的客户。也可见wait_timeout
。 join_buffer_size
- 用于全部联结(join)的缓冲区大小(不是用索引的联结)。缓冲区对2个表间的每个全部联结分配一次缓冲区,当增加索引不可能时,增加该值可得到一个更快的全部联结。(通常得到快速联结的最佳方法是增加索引。)
key_buffer_size
- 索引块是缓冲的并且被所有的线程共享。
key_buffer_size
是用于索引块的缓冲区大小,增加它可得到更好处理的索引(对所有读和多重写),到你能负担得起那样多。如果你使它太大,系统将开始换页并且真的变慢了。记住既然MySQL不缓存读取的数据,你将必须为OS文件系统缓存留下一些空间。为了在写入多个行时得到更多的速度,使用LOCK TABLES
。见7.24LOCK TABLES/UNLOCK TABLES
语法。 long_query_time
- 如果一个查询所用时间超过它(以秒计),
Slow_queries
记数器将被增加。 max_allowed_packet
- 一个包的最大尺寸。消息缓冲区被初始化为
net_buffer_length
字节,但是可在需要时增加到max_allowed_packet
个字节。缺省地,该值太小必能捕捉大的(可能错误)包。如果你正在使用大的BLOB
列,你必须增加该值。它应该象你想要使用的最大BLOB
的那么大。 max_connections
- 允许的同时客户的数量。增加该值增加
mysqld
要求的文件描述符的数量。见下面对文件描述符限制的注释。见18.2.4Too many connections
错误。 max_connect_errors
- 如果有多于该数量的从一台主机中断的连接,这台主机阻止进一步的连接。你可用
FLUSH HOSTS
命令疏通一台主机。 max_delayed_threads
- 不要启动多于的这个数字的线程来处理
INSERT DELAYED
语句。如果你试图在所有INSERT DELAYED
线程在用后向一张新表插入数据,行将被插入,就像DELAYED
属性没被指定那样。 max_join_size
- 可能将要读入多于
max_join_size
个记录的联结将返回一个错误。如果你的用户想要执行没有一个WHERE
子句、花很长时间并且返回百万行的联结,设置它。 max_sort_length
- 在排序
BLOB
或TEXT
值时使用的字节数(每个值仅头max_sort_length
个字节被使用;其余的被忽略)。 max_tmp_tables
- (该选择目前还不做任何事情)。一个客户能同时保持打开的临时表的最大数量。
net_buffer_length
- 通信缓冲区在查询之间被重置到该大小。通常这不应该被改变,但是如果你有很少的内存,你能将它设置为查询期望的大小。(即,客户发出的SQL语句期望的长度。如果语句超过这个长度,缓冲区自动地被扩大,直到
max_allowed_packet
个字节。) record_buffer
- 每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,你可能想要增加该值。
sort_buffer
- 每个需要进行排序的线程分配该大小的一个缓冲区。增加这值加速
ORDER BY
或GROUP BY
操作。见18.5 MySQL在哪儿存储临时文件。 table_cache
- 为所有线程打开表的数量。增加该值能增加
mysqld
要求的文件描述符的数量。MySQL对每个唯一打开的表需要2个文件描述符,见下面对文件描述符限制的注释。对于表缓存如何工作的信息,见10.2.4 MySQL怎样打开和关闭表。 tmp_table_size
- 如果一张临时表超出该大小,MySQL产生一个
The table tbl_name is full
形式的错误,如果你做很多高级GROUP BY
查询,增加tmp_table_size
值。 thread_stack
- 每个线程的栈大小。由
crash-me
测试检测到的许多限制依赖于该值。缺省队一般的操作是足够大了。见10.8 使用你自己的基准。 wait_timeout
- 服务器在关闭它之前在一个连接上等待行动的秒数。也可见
interactive_timeout
。
MySQL使用是很具伸缩性的算法,因此你通常能用很少的内存运行或给MySQL更多的被存以得到更好的性能。
如果你有很多内存和很多表并且有一个中等数量的客户,想要最大的性能,你应该一些象这样的东西:
shell> safe_mysqld -O key_buffer=16M -O table_cache=128 \ -O sort_buffer=4M -O record_buffer=1M &如果你有较少的内存和大量的连接,使用这样一些东西:
shell> safe_mysqld -O key_buffer=512k -O sort_buffer=100k \ -O record_buffer=100k &
或甚至:
shell> safe_mysqld -O key_buffer=512k -O sort_buffer=16k \ -O table_cache=32 -O record_buffer=8k -O net_buffer=1K &
如果有很多连接,“交换问题”可能发生,除非mysqld
已经被配置每个连接使用很少的内存。当然如果你对所有连接有足够的内存,mysqld
执行得更好。
注意,如果你改变mysqld
的一个选项,它实际上只对服务器的那个例子保持。
为了明白一个参数变化的效果,这样做:
shell> mysqld -O key_buffer=32m --help
保证--help
选项是最后一个;否则,命令行上在它之后列出的任何选项的效果将不在反映在输出中。
10.2.4 MySQL怎样打开和关闭数据库表
table_cache
, max_connections
和max_tmp_tables
影响服务器保持打开的文件的最大数量。如果你增加这些值的一个或两个,你可以遇到你的操作系统每个进程打开文件描述符的数量上强加的限制。然而,你可以能在许多系统上增加该限制。请教你的OS文档找出如何做这些,因为改变限制的方法各系统有很大的不同。
table_cache
与max_connections
有关。例如,对于200个打开的连接,你应该让一张表的缓冲至少有200 * n
,这里n
是一个联结(join)中表的最大数量。
打开表的缓存可以增加到一个table_cache
的最大值(缺省为64;这可以用mysqld
的-O table_cache=#
选项来改变)。一个表绝对不被关闭,除非当缓存满了并且另外一个线程试图打开一个表时或如果你使用mysqladmin refresh
或mysqladmin flush-tables
。
当表缓存满时,服务器使用下列过程找到一个缓存入口来使用:
- 不是当前使用的表被释放,以最近最少使用(LRU)顺序。
- 如果缓存满了并且没有表可以释放,但是一个新表需要打开,缓存必须临时被扩大。
- 如果缓存处于一个临时扩大状态并且一个表从在用变为不在用状态,它被关闭并从缓存中释放。
对每个并发存取打开一个表。这意味着,如果你让2个线程存取同一个表或在同一个查询中存取表两次(用AS
),表需要被打开两次。任何表的第一次打开占2个文件描述符;表的每一次额外使用仅占一个文件描述符。对于第一次打开的额外描述符用于索引文件;这个描述符在所有线程之间共享。
10.2.5 在同一个数据库中创建大量数据库表的缺点
如果你在一个目录中有许多文件,打开、关闭和创建操作将会很慢。如果你执行在许多不同表上的SELECT
语句,当表缓存满时,将有一点开销,因为对每个必须打开的表,另外一个必须被关闭。你可以通过使表缓冲更大些来减少这个开销。
10.2.6 为什么有这么多打开的表?
当你运行mysqladmin status
时,你将看见象这样的一些东西:
Uptime: 426 Running threads: 1 Questions: 11082 Reloads: 1 Open tables: 12
如果你仅有6个表,这可能有点令人困惑。
MySQL是多线程的,因此它可以同时在同一个表上有许多询问。为了是2个线程在同一个文件上有不同状态的问题减到最小,表由每个并发进程独立地打开。这为数据文件消耗一些内存和一个额外的文件描述符。索引文件描述符在所有线程之间共享。
10.2.7 MySQL怎样使用内存
下表指出mysqld
服务器使用存储器的一些方式。在应用的地方,给出与存储器使用相关的服务器变量的名字。
- 关键字缓冲区(变量
key_buffer_size
)由所有线程分享;当需要时,分配服务器使用的其他缓冲区。见10.2.3 调节服务器参数。 - 每个连接使用一些线程特定的空间;一个栈(缺省64K,变量
thread_stack
)、一个连接缓冲区(变量net_buffer_length
)和一个结果缓冲区(变量net_buffer_length
)。当需要时,连接缓冲区和结果缓冲区动态地被扩大到max_allowed_packet
。当一个查询正在运行当前查询的一个拷贝时,也分配字符串。 - 所有线程共享同一基存储器。
- 目前还没有什么是内存映射的(除了压缩表,但是那是另外一个的故事)。这是因为4GB的32位存储器空间对最大的数据库来所不是足够大的。当一个64位寻址空间的系统变得更普遍时,我们可以为内存映射增加全面的支持。
- 每个做顺序扫描的请求分配一个读缓冲区(变量
record_buffer
)。 - 所有联结均用一遍完成并且大多数联结可以甚至不用一张临时表来完成。最临时的表是基于内存的(HEAP)表。有较大记录长度(以所有列的长度之和计算)的临时表或包含
BLOB
列的表在磁盘上存储。在MySQL版本3.23.2前一个问题是如果一张HEAP表超过tmp_table_size
的大小,你得到错误The table tbl_name is full
。在更新的版本中,这通过必要时自动将在内存的(HEAP)表转变为一个基于磁盘(MyISAM)的表来处理。为了解决这个问题,你可以通过设置mysqld
的tmp_table_size
选项,或通过在客户程序中设置SQL的SQL_BIG_TABLES
选项增加临时表的大小。见7.25SET OPTION
句法。在MySQL 3.20中,临时表的最大尺寸是record_buffer*16
,因此如果你正在使用这个版本,你必须增加record_buffer
值。你也可以使用--big-tables
选项启动mysqld
以总将临时表存储在磁盘上,然而,这将影响许多复杂查询的速度。 - 大多数做排序的请求分配一个排序缓冲区和一个或二个临时文件。见18.5 MySQL在哪儿存储临时文件。
- 几乎所有的语法分析和计算都在一家本地存储器中完成。对小项目没有内存开销并且一般的较慢存储器分配和释放被避免。内存仅为出乎意料的大字符串分配(这用
malloc()
和free()
完成)。 - 每个索引文件只被打开一次,并且数据文件为每个并发运行的线程打开一次。对每个并发线程,分配一个表结构、对每列的列结构和大小为
3 * n
的一个缓冲区(这里n
是最大的行长度,不算BLOB
列)。一个BLOB
使用5 ~ 8个字节加上BLOB
数据。 - 对每个有
BLOB
列的表,一个缓冲区动态地被扩大以便读入更大的BLOB
值。如果你扫描一个表,分配与最大BLOB
值一样大的一个缓冲区。 - 对所有在用的表的表处理器被保存在一个缓存中并且作为一个FIFO管理。通常缓存有64个入口。如果一个表同时被2个运行的线程使用,缓存为此包含2个入口。见10.2.4 MySQL如何打开和关闭数据库表。
- 一个
mysqladmin flush-tables
命令关闭所有不在用的表并在当前执行的线程结束时,标记所有在用的表准备被关闭。这将有效地释放大多数在用的内存。
ps
和其他系统状态程序可以报导mysqld
使用很多内存。这可以是在不同的内存地址上的线程栈造成的。例如,Solaris版本的ps
将栈间未用的内存算作已用的内存。你可以通过用swap -s
检查可用交换区来验证它。我们用商业内存漏洞探查器测试了mysqld
,因此应该有没有内存漏洞。
10.2.8 MySQL怎样锁定数据库表
MySQL中所有锁定不会是死锁的。这通过总是在一个查询前立即请求所有必要的锁定并且总是以同样的顺序锁定表来管理。
对WRITE
,MySQL使用的锁定方法原理如下:
- 如果在表上没有锁,放一个锁在它上面。
- 否则,把锁定请求放在写锁定队列中。
对READ
,MySQL使用的锁定方法原理如下:
- 如果在表上没有写锁定,把一个读锁定放在它上面。
- 否则,把锁请求放在读锁定队列中。
当一个锁定被释放时,锁定可被写锁定队列中的线程得到,然后是读锁定队列中的线程。
这意味着,如果你在一个表上有许多更改,SELECT
语句将等待直到有没有更多的更改。
为了解决在一个表中进行很多INSERT
和SELECT
操作的情况,你可在一张临时表中插入行并且偶尔用来自临时表的记录更新真正的表。
这可用下列代码做到:
mysql> LOCK TABLES real_table WRITE, insert_table WRITE;mysql> insert into real_table select * from insert_table;mysql> delete from insert_table;mysql> UNLOCK TABLES;
如果你在一些特定的情况字下区分检索的优先次序,你可以使用LOW_PRIORITY
选项的INSERT
。见7.14 INSERT
句法。
你也能改变在“mysys/thr_lock.c”中的锁代码以使用一个单个队列。在这种情况下,写锁定和读锁定将有同样优先级,它可能帮助一些应用程序。
10.2.9 数据库表级锁定的问题
MySQL的表锁定代码是不会死锁的。
MySQL使用表级锁定(而不是行级锁定或列级锁定)以达到很高的锁定速度。对于大表,表级锁定对大多数应用程序来说比行级锁定好一些,但是当然有一些缺陷。
在MySQL3.23.7和更高版本中,一个人能把行插入到MyISAM
表同时其他线程正在读该表。注意,目前只有在表中内有删除的行时才工作。
表级锁定使很多线程能够同时读一个表,但是如果一个线程想要写一个表,它必须首先得到独占存取权。在更改期间,所有其他想要存取该特定表的线程将等到更改就绪。
因为数据库的更改通常被视为比SELECT
更重要,更新一个表的所有语句比从一个表中检索信息的语句有更高的优先级。这应该保证更改不被“饿死”,因为一个人针对一个特定表会发出很多繁重的查询。
从MySQL 3.23.7开始,一个人可以能使用max_write_lock_count
变量强制MySQL在一个表上一个特定数量的插入后发出一个SELECT
。
对此一个主要的问题如下:
- 一个客户发出一个花很长时间运行的
SELECT
。 - 然后其他客户在一个使用的表上发出一个
UPDATE
;这个客户将等待直到SELECT
完成。 - 另一个客户在同一个表上发出另一个
SELECT
语句;因为UPDATE
比SELECT
有更高的优先级,该SELECT
将等待UPDATE
的完成。它也将等待第一个SELECT
完成!
对这个问题的一些可能的解决方案是:
- 试着使
SELECT
语句运行得更快;你可能必须创建一些摘要(summary)表做到这点。 - 用
--low-priority-updates
启动mysqld
。这将给所有更新(修改)一个表的语句以比SELECT
语句低的优先级。在这种情况下,在先前情形的最后的SELECT
语句将在INSERT
语句前执行。 - 你可以用
LOW_PRIORITY
属性给与一个特定的INSERT
、UPDATE
或DELETE
语句较低优先级。 - 为max_write_lock_count指定一个低值来启动
mysqld
使得在一定数量的WRITE
锁定后给出READ
锁定。 - 通过使用SQL命令:
SET SQL_LOW_PRIORITY_UPDATES=1
,你可从一个特定线程指定所有的更改应该由用低优先级完成。见7.25SET OPTION
句法。 - 你可以用
HIGH_PRIORITY
属性指明一个特定SELECT
是很重要的。见7.12SELECT
句法。 - 如果你有关于
INSERT
结合SELECT
的问题,切换到使用新的MyISAM
表,因为它们支持并发的SELECT
和INSERT
。 - 如果你主要混合
INSERT
和SELECT
语句,DELAYED
属性的INSERT
将可能解决你的问题。见7.14INSERT
句法。 - 如果你有关于
SELECT
和DELETE
的问题,LIMIT
选项的DELETE
可以帮助你。见7.11DELETE
句法。
10.3 使你的数据尽可能小
最基本的优化之一是使你的数据(和索引)在磁盘上(并且在内存中)占据的空间尽可能小。这能给出巨大的改进,因为磁盘读入较快并且通常也用较少的主存储器。如果在更小的列上做索引,索引也占据较少的资源。
你能用下面的技术使表的性能更好并且使存储空间最小:
- 尽可能地使用最有效(最小)的类型。MySQL有很多节省磁盘空间和内存的专业化类型。
- 如果可能使表更小,使用较小的整数类型。例如,
MEDIUMINT
经常比INT
好一些。 - 如果可能,声明列为
NOT NULL
。它使任何事情更快而且你为每列节省一位。注意如果在你的应用程序中你确实需要NULL
,你应该毫无疑问使用它,只是避免缺省地在所有列上有它。 - 如果你没有任何变长列(
VARCHAR
、TEXT
或BLOB
列),使用固定尺寸的记录格式。这比较快但是不幸地可能会浪费一些空间。见10.6 选择一种表类型。 - 每张桌子应该有尽可能短的主索引。这使一行的辨认容易而有效。
- 对每个表,你必须决定使用哪种存储/索引方法。见9.4 MySQL表类型。也可参见10.6 选择一种表类型。
- 只创建你确实需要的索引。索引对检索有好处但是当你需要快速存储东西时就变得糟糕。如果你主要通过搜索列的组合来存取一个表,以它们做一个索引。第一个索引部分应该是最常用的列。如果你总是使用许多列,你应该首先以更多的副本使用列以获得更好的列索引压缩。
- 如果很可能一个索引在头几个字符上有唯一的前缀,仅仅索引该前缀比较好。MySQL支持在一个字符列的一部分上的索引。更短的索引更快,不仅因为他们占较少的磁盘空间而且因为他们将在索引缓存中给你更多的命中率并且因此有更少磁盘寻道。见10.2.3 调节服务器参数。
- 在一些情形下,分割一个经常被扫描进2个表的表是有益的。特别是如果它是一个动态格式的表并且它可能使一个能用来扫描后找出相关行的较小静态格式的表。
10.4 MySQL索引的使用
索引被用来快速找出在一个列上用一特定值的行。没有索引,MySQL不得不首先以第一条记录开始并然后读完整个表直到它找出相关的行。表越大,花费时间越多。如果表对于查询的列有一个索引,MySQL能快速到达一个位置去搜寻到数据文件的中间,没有必要考虑所有数据。如果一个表有1000行,这比顺序读取至少快100倍。注意你需要存取几乎所有1000行,它较快的顺序读取,因为此时我们避免磁盘寻道。
所有的MySQL索引(PRIMARY
、UNIQUE
和INDEX
)在B树中存储。字符串是自动地压缩前缀和结尾空间。见7.27 CREATE INDEX
句法。
索引用于:
- 快速找出匹配一个
WHERE
子句的行。 - 当执行联结时,从其他表检索行。
- 对特定的索引列找出
MAX()
或MIN()
值。 - 如果排序或分组在一个可用键的最左面前缀上进行(例如,
ORDER BY key_part_1,key_part_2
),排序或分组一个表。如果所有键值部分跟随DESC
,键以倒序被读取。 - 在一些情况中,一个查询能被优化来检索值,不用咨询数据文件。如果对某些表的所有使用的列是数字型的并且构成某些键的最左面前缀,为了更快,值可以从索引树被检索出来。
假定你发出下列SELECT
语句:
mysql> SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;
如果一个多列索引存在于col1
和col2
上,适当的行可以直接被取出。如果分开的单行列索引存在于col1
和col2
上,优化器试图通过决定哪个索引将找到更少的行并来找出更具限制性的索引并且使用该索引取行。
如果表有一个多列索引,任何最左面的索引前缀能被优化器使用以找出行。例如,如果你有一个3行列索引(col1,col2,col3)
,你已经索引了在(col1)
、(col1,col2)
和(col1,col2,col3)
上的搜索能力。
如果列不构成索引的最左面前缀,MySQL不能使用一个部分的索引。假定你下面显示的SELECT
语句:
mysql> SELECT * FROM tbl_name WHERE col1=val1;mysql> SELECT * FROM tbl_name WHERE col2=val2;mysql> SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
如果一个索引存在于(col1、col2、col3)
上,只有上面显示的第一个查询使用索引。第二个和第三个查询确实包含索引的列,但是(col2)
和(col2、col3)
不是(col1、col2、col3)
的最左面前缀。
如果LIKE
参数是一个不以一个通配符字符起始的一个常数字符串,MySQL也为LIKE
比较使用索引。例如,下列SELECT
语句使用索引:
mysql> select * from tbl_name where key_col LIKE "Patrick%";mysql> select * from tbl_name where key_col LIKE "Pat%_ck%";
在第一条语句中,只考虑有"Patrick" <= key_col < "Patricl"
的行。在第二条语句中,只考虑有"Pat" <= key_col < "Pau"
的行。
下列SELECT
语句将不使用索引:
mysql> select * from tbl_name where key_col LIKE "%Patrick%";mysql> select * from tbl_name where key_col LIKE other_col;
在第一条语句中,LIKE
值以一个通配符字符开始。在第二条语句中,LIKE
值不是一个常数。
如果 column_name 是一个索引,使用column_name IS NULL
的搜索将使用索引。
MySQL通常使用找出最少数量的行的索引。一个索引被用于你与下列操作符作比较的列:=
、>
、>=
、<
、<=
、BETWEEN
和一个有一个非通配符前缀象'something%'
的LIKE
的列。
任何不跨越的在WHERE
子句的所有AND
层次的索引不用来优化询问。
下列WHERE
子句使用索引:
... WHERE index_part1=1 AND index_part2=2... WHERE index=1 OR A=10 AND index=2 /* index = 1 OR index = 2 */... WHERE index_part1='hello' AND index_part_3=5 /* optimized like "index_part1='hello'" */
这些WHERE
子句不使用索引:
... WHERE index_part2=1 AND index_part3=2 /* index_part_1 is not used */... WHERE index=1 OR A=10 /* No index */... WHERE index_part1=1 OR index_part2=10 /* No index spans all rows */
10.5 存取或更新数据的查询速度
首先,一件事情影响所有的询问。你有的许可系统设置越复杂,你得到更多的开销。
如果你不让任何GRANT
语句执行,MySQL将稍微优化许可检查。因此如果你有很大量,值得花时间来避免授权,否则更多的许可检查有更大的开销。
如果你的问题是与一些明显的MySQL函数有关,你总能在MySQL客户中计算其时间:
mysql> select benchmark(1000000,1+1);+------------------------+ benchmark(1000000,1+1) +------------------------+ 0 +------------------------+1 row in set (0.32 sec)
上面显示MySQL能在PentiumII 400MHz
上以0.32秒执行1,000,000个+
表达式。
所有MySQL函数应该被高度优化,但是以可能有一些例外并且benchmark(loop_count,expression)
是找出是否你的查询有问题的一个极好工具。
10.5.1 估计查询性能
在大多数情况下,你能通过计算磁盘寻道估计性能。对小的表,你通常能在1次磁盘寻道中找到行(因为这个索引可能被缓冲)。对更大的表,你能估计它(使用 B++ 树索引),你将需要:log(row_count)/log(index_block_length/3*2/(index_length + data_pointer_length))+1
次寻道找到行。
在MySQL中,索引块通常是1024个字节且数据指针通常是4个字节,这对一个有一个索引长度为3(中等整数)的 500,000 行的表给你:log(500,000)/log(1024/3*2/(3+4)) + 1
= 4 次寻道。
象上面的索引将要求大约 500,000 * 7 * 3/2 = 5.2M,(假设索引缓冲区被充满到2/3(它是典型的)),你将可能在内存中有索引的大部分并且你将可能仅需要1-2调用从OS读数据来找出行。
然而对于写,你将需要 4 次寻道请求(如上)来找到在哪儿存放新索引并且通常需2次寻道更新这个索引并且写入行。
注意,上述不意味着你的应用程序将缓慢地以 N log N 退化!当表格变得更大时,只要一切被OS或SQL服务器缓冲,事情将仅仅或多或少地更慢。在数据变得太大不能被缓冲后,事情将开始变得更慢直到你的应用程序仅仅受磁盘寻道限制(它以N log N增加)。为了避免这个增加,索引缓冲随数据增加而增加。见10.2.3 调节服务器参数。
10.5.2 SELECT
查询的速度
总的来说,当你想要使一个较慢的SELECT ... WHERE
更快,检查的第一件事情是你是否能增加一个索引。见10.4 MySQL 索引的使用。在不同表之间的所有引用通常应该用索引完成。你可以使用EXPLAIN
来确定哪个索引用于一条SELECT
语句。见7.22 EXPLAIN
句法(得到关于一条SELECT
的信息)。
一些一般的建议:
- 为了帮助MySQL更好地优化查询,在它已经装载了相关数据后,在一个表上运行
myisamchk --analyze
。这为每一个更新一个值,指出有相同值地平均行数(当然,对唯一索引,这总是1。) - 为了根据一个索引排序一个索引和数据,使用
myisamchk --sort-index --sort-records=1
(如果你想要在索引1上排序)。如果你有一个唯一索引,你想要根据该索引地次序读取所有的记录,这是使它更快的一个好方法。然而注意,这个排序没有被最佳地编写,并且对一个大表将花很长时间!
10.5.3 MySQL怎样优化WHERE
子句
where优化被放在SELECT
中,因为他们最主要在那里使用里,但是同样的优化被用于DELETE
和UPDATE
语句。
也要注意,本节是不完全的。MySQL确实作了许多优化而我们没有时间全部记录他们。
由MySQL实施的一些优化列在下面:
- 删除不必要的括号:
((a AND b) AND c OR (((a AND b) AND (c AND d))))-> (a AND b AND c) OR (a AND b AND c AND d)
- 常数调入:
(a<b AND b=c) AND a=5-> b>5 AND b=c AND a=5
- 删除常数条件(因常数调入所需):
(B>=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)-> B=5 OR B=6
- 索引使用的常数表达式仅计算一次。
- 在一个单个表上的没有一个
WHERE
的COUNT(*)
直接从表中检索信息。当仅使用一个表时,对任何NOT NULL
表达式也这样做。 - 无效常数表达式的早期检测。MySQL快速检测某些
SELECT
语句是不可能的并且不返回行。 - 如果你不使用
GROUP BY
或分组函数(COUNT()
、MIN()
……),HAVING
与WHERE
合并。 - 为每个子联结(sub join),构造一个更简单的
WHERE
以得到一个更快的WHERE
计算并且也尽快跳过记录。 - 所有常数的表在查询中的任何其他表前被首先读出。一个常数的表是:
- 一个空表或一个有1行的表。
- 与在一个
UNIQUE
索引、或一个PRIMARY KEY
的WHERE
子句一起使用的表,这里所有的索引部分使用一个常数表达式并且索引部分被定义为NOT NULL
。
所有下列的表用作常数表:
mysql> SELECT * FROM t WHERE primary_key=1;mysql> SELECT * FROM t1,t2 WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
- 对联结表的最好联结组合是通过尝试所有可能性来找到:(。如果所有在
ORDER BY
和GROUP BY
的列来自同一个表,那么当廉洁时,该表首先被选中。 - 如果有一个
ORDER BY
子句和一个不同的GROUP BY
子句,或如果ORDER BY
或GROUP BY
包含不是来自联结队列中的第一个表的其他表的列,创建一个临时表。 - 如果你使用
SQL_SMALL_RESULT
,MySQL将使用一个在内存中的表。 - 因为
DISTINCT
被变换到在所有的列上的一个GROUP BY
,DISTINCT
与ORDER BY
结合也将在许多情况下需要一张临时表。 - 每个表的索引被查询并且使用跨越少于30% 的行的索引。如果这样的索引没能找到,使用一个快速的表扫描。
- 在一些情况下,MySQL能从索引中读出行,甚至不咨询数据文件。如果索引使用的所有列是数字的,那么只有索引树被用来解答查询。
- 在每个记录被输出前,那些不匹配
HAVING
子句的行被跳过。
下面是一些很快的查询例子:
mysql> SELECT COUNT(*) FROM tbl_name;mysql> SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;mysql> SELECT MAX(key_part2) FROM tbl_name WHERE key_part_1=constant;mysql> SELECT ... FROM tbl_name ORDER BY key_part1,key_part2,... LIMIT 10;mysql> SELECT ... FROM tbl_name ORDER BY key_part1 DESC,key_part2 DESC,... LIMIT 10;
下列查询仅使用索引树就可解决(假设索引列是数字的):
mysql> SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;mysql> SELECT COUNT(*) FROM tbl_name WHERE key_part1=val1 AND key_part2=val2;mysql> SELECT key_part2 FROM tbl_name GROUP BY key_part1;
下列查询使用索引以排序顺序检索,不用一次另外的排序:
mysql> SELECT ... FROM tbl_name ORDER BY key_part1,key_part2,...mysql> SELECT ... FROM tbl_name ORDER BY key_part1 DESC,key_part2 DESC,...
10.5.4 MySQL怎样优化LEFT JOIN
在MySQL中,A LEFT JOIN B
实现如下:
- 表
B
被设置为依赖于表A
。 - 表
A
被设置为依赖于所有用在LEFT JOIN
条件的表(除B
外)。 - 所有
LEFT JOIN
条件被移到WHERE
子句中。 - 进行所有标准的联结优化,除了一个表总是在所有它依赖的表之后被读取。如果有一个循环依赖,MySQL将发出一个错误。
- 进行所有标准的
WHERE
优化。 - 如果在
A
中有一行匹配WHERE
子句,但是在B
中没有任何行匹配LEFT JOIN
条件,那么在B
中生成所有列设置为NULL
的一行。 - 如果你使用
LEFT JOIN
来找出在某些表中不存在的行并且在WHERE
部分你有下列测试:column_name IS NULL
,这里column_name 被声明为NOT NULL
的列,那么MySQL
在它已经找到了匹配LEFT JOIN
条件的一行后,将停止在更多的行后寻找(对一特定的键组合)。
10.5.5 MySQL怎样优化LIMIT
在一些情况中,当你使用LIMIT #
而不使用HAVING
时,MySQL将以不同方式处理查询。
- 如果你用
LIMIT
只选择一些行,当MySQL一般比较喜欢做完整的表扫描时,它将在一些情况下使用索引。 - 如果你使用
LIMIT #
与ORDER BY
,MySQL一旦找到了第一个#
行,将结束排序而不是排序整个表。 - 当结合
LIMIT #
和DISTINCT
时,MySQL一旦找到#
个唯一的行,它将停止。 - 在一些情况下,一个
GROUP BY
能通过顺序读取键(或在键上做排序)来解决,并然后计算摘要直到键值改变。在这种情况下,LIMIT #
将不计算任何不必要的GROUP
。 - 只要
MySQL
已经发送了第一个#
行到客户,它将放弃查询。 LIMIT 0
将总是快速返回一个空集合。这对检查查询并且得到结果列的列类型是有用的。- 临时表的大小使用
LIMIT #
计算需要多少空间来解决查询。
10.5.6 INSERT
查询的速度
插入一个记录的时间由下列组成:
- 连接:(3)
- 发送查询给服务器:(2)
- 分析查询:(2)
- 插入记录:(1 x 记录大小)
- 插入索引:(1 x 索引)
- 关闭:(1)
这里的数字有点与总体时间成正比。这不考虑打开表的初始开销(它为每个并发运行的查询做一次)。
表的大小以N log N (B 树)的速度减慢索引的插入。
加快插入的一些方法:
- 如果你同时从同一客户插入很多行,使用多个值表的
INSERT
语句。这比使用分开INSERT
语句快(在一些情况中几倍)。 - 如果你从不同客户插入很多行,你能通过使用
INSERT DELAYED
语句得到更高的速度。见7.14INSERT
句法。 - 注意,用
MyISAM
,如果在表中没有删除的行,能在SELECT
:s正在运行的同时插入行。 - 当从一个文本文件装载一个表时,使用
LOAD DATA INFILE
。这通常比使用很多INSERT
语句快20倍。见7.16LOAD DATA INFILE
句法。 - 当表有很多索引时,有可能多做些工作使得
LOAD DATA INFILE
更快些。使用下列过程:- 有选择地用
CREATE TABLE
创建表。例如使用mysql
或Perl-DBI。 - 执行
FLUSH TABLES
,或外壳命令mysqladmin flush-tables
。 - 使用
myisamchk --keys-used=0 -rq /path/to/db/tbl_name
。这将从表中删除所有索引的使用。 - 用
LOAD DATA INFILE
把数据插入到表中,这将不更新任何索引,因此很快。 - 如果你有
myisampack
并且想要压缩表,在它上面运行myisampack
。见10.6.3 压缩表的特征。 - 用
myisamchk -r -q /path/to/db/tbl_name
再创建索引。这将在将它写入磁盘前在内存中创建索引树,并且它更快,因为避免大量磁盘寻道。结果索引树也被完美地平衡。 - 执行
FLUSH TABLES
,或外壳命令mysqladmin flush-tables
。
这个过程将被构造进在MySQL的某个未来版本的
LOAD DATA INFILE
。 - 有选择地用
- 你可以锁定你的表以加速插入。
mysql> LOCK TABLES a WRITE;mysql> INSERT INTO a VALUES (1,23),(2,34),(4,33);mysql> INSERT INTO a VALUES (8,26),(6,29);mysql> UNLOCK TABLES;
主要的速度差别是索引缓冲区仅被清洗到磁盘上一次,在所有
INSERT
语句完成后。一般有与有不同的INSERT
语句那样夺的索引缓冲区清洗。如果你能用一个单个语句插入所有的行,锁定就不需要。锁定也将降低多连接测试的整体时间,但是对某些线程最大等待时间将上升(因为他们等待锁)。例如:thread 1 does 1000 insertsthread 2, 3, and 4 does 1 insertthread 5 does 1000 inserts
如果你不使用锁定,2、3和4将在1和5前完成。如果你使用锁定,2、3和4将可能不在1或5前完成,但是整体时间应该快大约40%。因为
INSERT
,UPDATE
和DELETE
操作在MySQL中是很快的,通过为多于大约5次连续不断地插入或更新一行的东西加锁,你将获得更好的整体性能。如果你做很多一行的插入,你可以做一个LOCK TABLES
,偶尔随后做一个UNLOCK TABLES
(大约每1000行)以允许另外的线程存取表。这仍然将导致获得好的性能。当然,LOAD DATA INFILE
对装载数据仍然是更快的。
为了对LOAD DATA INFILE
和INSERT
得到一些更快的速度,扩大关键字缓冲区。见10.2.3 调节服务器参数。
10.5.7 UPDATE
查询的速度
更改查询被优化为有一个写开销的一个SELECT
查询。写速度依赖于被更新数据大小和被更新索引的数量。
使更改更快的另一个方法是推迟更改并且然后一行一行地做很多更改。如果你锁定表,做一行一行地很多更改比一次做一个快。
注意,动态记录格式的更改一个较长总长的记录,可能切开记录。因此如果你经常这样做,时不时地OPTIMIZE TABLE
是非常重要的。见7.9 OPTIMIZE TABLE
句法。
10.5.8 DELETE
查询的速度
删除一个记录的时间精确地与索引数量成正比。为了更快速地删除记录,你可以增加索引缓存的大小。见10.2.3 调节服务器参数。
从一个表删除所有行比删除行的一大部分也要得多。
10.6 选择一种表类型
用MySQL,当前(版本 3.23.5)你能从一个速度观点在4可用表的格式之间选择。
- 静态MyISAM
- 这种格式是最简单且最安全的格式,它也是在磁盘格式最快的。速度来自于数据能在磁盘上被找到的难易方式。当所定有一个索引和静态格式的东西时,它很简单,只是行长度乘以行数量。而且在扫描一张表时,用每次磁盘读取来读入常数个记录是很容易的。安全性来自于如果当写入一个静态MyISAM文件时,你的计算机崩溃,
myisamchk
能很容易指出每行在哪儿开始和结束,因此它通常能回收所有记录,除了部分被写入的那个。注意,在MySQL中,所有索引总能被重建。 - 动态MyISAM
- 这种格式有点复杂,因为每一行必须有一个头说明它有多长。当一个记录在更改时变长时,它也可以在多于一个位置上结束。你能使用
OPTIMIZE table
或myisamchk
整理一张表。如果你在同一个表中有象某些VARCHAR
或BLOB
列那样存取/改变的静态数据,将动态列移入另外一个表以避免碎片可能是一个好主意。 - 压缩MyISAM
- 这是一个只读类型,用可选的
myisampack
工具生成。 - 内存(HEAP 堆)
- 这种表格式对小型/中型查找表十分有用。对拷贝/创建一个常用的查找表(用联结)到一个(也许临时)HEAP表有可能加快多个表联结。假定我们想要做下列联结,用同样数据可能要几倍时间。
SELECT tab1.a, tab3.a FROM tab1, tab2, tab3 WHERE tab1.a = tab2.a and tab2.a = tab3.a and tab2.c != 0;
为了加速它,我们可用tab2和tab3的联结创建一张临时表,因为用相同列( tab1.a )查找。这里是创建该表和结果选择的命令。
CREATE TEMPORARY TABLE test TYPE=HEAP SELECT tab2.a as a2, tab3.a as a3 FROM tab2, tab3 WHERE tab2.a = tab3.a and c = 0;SELECT tab1.a, test.a3 from tab1, test where tab1.a = test.a1;SELECT tab1.b, test.a3 from tab1, test where tab1.a = test.a1 and something;
10.6.1 静态(定长)表的特点
- 这是缺省格式。它用在表不包含
VARCHAR
、BLOB
或TEXT
列时候。 - 所有的
CHAR
、NUMERIC
和DECIMAL
列充填到列宽度。 - 非常快。
- 容易缓冲。
- 容易在崩溃后重建,因为记录位于固定的位置。
- 不必被重新组织(用
myisamchk
),除非一个巨量的记录被删除并且你想要归还空闲磁盘空间给操作系统。 - 通常比动态表需要更多的磁盘空间。
10.6.2 动态表的特点
- 如果表包含任何
VARCHAR
、BLOB
或TEXT
列,使用该格式。 - 所有字符串列是动态的(除了那些长度不到4的列)。
- 每个记录前置一个位图,对字符串列指出哪个列是空的(
''
),或对数字列哪个是零(这不同于包含NULL
值的列)。如果字符串列在删除尾部空白后有零长度,或数字列有零值,它在位图中标记并且不保存到磁盘上。非空字符串存储为一个长度字节加字符串内容。 - 通常比定长表占更多的磁盘空间。
- 每个记录仅使用所需的空间。如果一个记录变得更大,它按需要被切开多段,这导致记录碎片。
- 如果你与超过行长度的信息更新行,行将被分段。在这种情况中,你可能必须时时运行
myisamchk -r
以使性能更好。使用myisamchk -ei tbl_name
做一些统计。 - 在崩溃后不容易重建,因为一个记录可以是分很多段并且一个连接(碎片)可以丢失。
- 对动态尺寸记录的期望行长度是:
3+ (number of columns + 7) / 8+ (number of char columns)+ packed size of numeric columns+ length of strings+ (number of NULL columns + 7) / 8
对每个连接有6个字节的惩罚。无论何时更改引起记录的增大,一个动态记录被链接。每个新链接将至少是20个字节,因此下一增大将可能在同一链连中。如果不是,将有另外一个链接。你可以用
myisamchk -ed
检查有多少链接。所有的链接可以用myisamchk -r
删除。
10.6.3 压缩表的特点
- 一张用
myisampack
实用程序制作的只读表。所有具有MySQL扩展电子邮件支持的客户可以为其内部使用保留一个myisampack
拷贝。 - 解压缩代码存在于所有MySQL分发,以便甚至没有
myisampack
的客户能读取用myisampack
压缩的表。 - 占据很小的磁盘空间,使磁盘使用量减到最小。
- 每个记录被单独压缩(很小的存取开销)。对一个记录的头是定长的(1-3 字节),取决于表中最大的记录。每列以不同方式被压缩。一些压缩类型是:
- 通常对每列有一张不同的哈夫曼表。
- 后缀空白压缩。
- 前缀空白压缩。
- 用值
0
的数字使用1位存储。 - 如果整数列的值有一个小范围,列使用最小的可能类型来存储。例如,如果所有的值在
0
到255
的范围,一个BIGINT
列(8个字节)可以作为一个TINYINT
列(1字节)存储。 - 如果列仅有可能值的一个小集合,列类型被变换到
ENUM
。 - 列可以使用上面的压缩方法的组合。
- 能处理定长或动态长度的记录,然而不能处理
BLOB
或TEXT
列。 - 能用
myisamchk
解压缩。
MySQL能支持不同的索引类型,但是一般的类型是ISAM。这是一个B树索引并且你能粗略地为索引文件计算大小为(key_length+4)*0.67
,在所有的键上的总和。(这是对最坏情况,当所有键以排序顺序被插入时。)
字符串索引是空白压缩的。如果第一个索引部分是一个字符串,它也将压缩前缀。如果字符串列有很多尾部空白或是一个总不能用到全长的VARCHAR
列,空白压缩使索引文件更小。如果很多字符串有相同的前缀,前缀压缩是有帮助的。
10.6.4 内存表的特点
堆桌子仅存在于内存中,因此如果mysqld
被关掉或崩溃,它们将丢失,但是因为它们是很快,不管怎样它们是有用的。
MySQL内部的HEAP表使用没有溢出区的100%动态哈希并且没有与删除有关的问题。
你只能通过使用在堆表中的一个索引的用等式存取东西(通常用=
操作符)。
堆表的缺点是:
- 你要为你想要同时使用的所有堆表需要足够的额外内存。
- 你不能在索引的一个部分上搜索。
- 你不能顺序搜索下一个条目(即使用这个索引做一个
ORDER BY
)。 - MySQL也不能算出在2个值之间大概有多少行。这被优化器使用来决定使用哪个索引,但是在另一方面甚至不需要磁盘寻道。
10.7 其他优化技巧
对加快系统的未分类的建议是:
- 使用持久的连接数据库以避免连接开销。
- 总是检查你的所有询问确实使用你已在表中创建了的索引。在MySQL中,你可以用
EXPLAIN
命令做到。见7.22EXPLAIN
句法(得到关于SELECT
的信息)。 - 尝试避免在被更改了很多的表上的复杂的
SELECT
查询。这避免与锁定表有关的问题。 - 在一些情况下,使得基于来自其他表的列的信息引入一个“ 哈希”的列有意义。如果该列较短并且有合理的唯一值,它可以比在许多列上的一个大索引快些。在MySQL中,很容易使用这个额外列:
SELECT * from table where hash='calculated hash on col1 and col2' and col_1='constant' and col_2='constant' and ..
。 - 对于有很多更改的表,你应该试着避免所有
VARCHAR
或BLOB
列。只要你使用单个VARCHAR
或BLOB
列,你将得到动态行长度。见9.4 MySQL表类型。 - 只是因为行太大,分割一张表为不同的表一般没有什么用处。为了存取行,最大的性能命冲击是磁盘寻道以找到行的第一个字节。在找到数据后,大多数新型磁盘对大多数应用程序来说足够快,能读入整个行。它确实有必要分割的唯一情形是如果其动态行尺寸的表(见上述)能变为固定的行大小,或如果你很频繁地需要扫描表格而不需要大多数列。见9.4 MySQL表类型。
- 如果你很经常地需要基于来自很多行的信息计算(如计数),引入一个新表并实时更新计数器可能更好一些。类型的更改
UPDATE table set count=count+1 where index_column=constant
是很快的!当你使用象MySQL那样的只有表级锁定(多重读/单个写)的数据库时,这确实重要。这也将给出大多数数据库较好的性能,因为锁定管理器在这种情况下有较少的事情要做。 11111111111111111111111 - 如果你需要从大的记录文件表中收集统计信息,使用总结性的表而不是扫描整个表。维护总结应该比尝试做“实时”统计要快些。当有变化而不是必须改变运行的应用时,从记录文件重新生成新的总结表(取决于业务决策)要快多了!
- 如果可能,应该将报告分类为“实时”或“统计”,这里统计报告所需的数据仅仅基于从实际数据产生的总结表中产生。
- 充分利用列有缺省值的事实。当被插入值不同于缺省值时,只是明确地插入值。这减少MySQL需要做的语法分析并且改进插入速度。
- 在一些情况下,包装并存储数据到一个BLOB中是很方便的。在这种情况下,你必须在你的应用中增加额外的代码来打包/解包BLOB中的东西,但是这种方法可以在某些阶段节省很多存取。当你有不符合静态的表结构的数据时,这很实用。
- 在一般情况下,你应该尝试以第三范式保存数据,但是如果你需要这些以获得更快的速度,你应该不用担心重复或创建总结表。
- 存储过程或UDF(用户定义函数)可能是获得更好性能的一个好方法,然而如果你使用某些不支持它的数据库,在这种情况中,你应该总是有零一个方法(较慢的)做这些。
- 你总是能通过在你的应用程序中缓冲查询/答案并尝试同时做很多插入/更新来获得一些好处。如果你的数据库支持锁定表(象MySQL和Oracle),这应该有助于确保索引缓冲在所有更新后只清空一次。
- 但你不知道何时写入你的数据时,使用
INSERT /*! DELAYED */
。这加快处理,因为很多记录可以用一次磁盘写入被写入。 - 当你想要让你的选择显得更重要时,使用
INSERT /*! LOW_PRIORITY */
。 - 使用
SELECT /*! HIGH_PRIORITY */
来取得塞入队列的选择,它是即使有人等待做一个写入也要完成的选择。 - 使用多行
INSERT
语句来存储很多有一条SQL命令的行(许多SQL服务器支持它)。 - 使用
LOAD DATA INFILE
装载较大数量的数据。这比一般的插入快并且当myisamchk
集成在mysqld
中时,甚至将更快。 - 使用
AUTO_INCREMENT
列构成唯一值。 - 当使用动态表格式时,偶尔使用
OPTIMIZE TABLE
以避免碎片。见7.9O PTIMIZE TABLE
句法。 - 可能时使用
HEAP
表以得到更快的速度。见9.4 MySQL表类型。 - 当使用一个正常Web服务器设置时,图象应该作为文件存储。这仅在数据库中存储的一本文件的引用。这样做的主要原因是是一个正常的Web服务器在缓冲文件比数据库内容要好得多,因此如果你正在使用文件,较容易得到一个较快的系统。
- 对经常存取的不重要数据(象有关对没有cookie用户最后显示标语的信息)使用内存表。
- 在不同表中具有相同信息的列应该被声明为相同的并有相同的名字。在版本 3.23 前,你只能靠较慢的联结。尝试使名字简单化(在客户表中使用
name
而不是customer_name
)。为了使你的名字能移植到其他SQL服务器,你应该使他们短于18 个字符。 - 如果你需要确实很高的速度,你应该研究一下不同SQL服务器支持的数据存储的底层接口!例如直接存取MySQL
MyISAM
,比起使用SQL 接口,你能得到2-5倍的速度提升。然而为了能做到它,数据必须是在与应用程序性在同一台机器的服务器上,并且通常它只应该被一个进程存取(因为外部文件锁定确实很慢)。通过在MySQL服务器中引进底层MyISAM
命令能消除以上问题(如果需要,这可能是获得更好性能的一个容易的方法)。借助精心设计的数据库接口,应该相当容易支持这类优化。 - 在许多情况下,从一个数据库存取数据(使用一个实时连接)比存取一个文本文件快些,只是因为数据库比文本文件更紧凑(如果你使用数字数据)并且这将涉及更少的磁盘存取。你也节省代码,因为你不须分析你的文本文件来找出行和列的边界。
- 你也能使用复制加速。见19.1 数据库复制。
10.8 使用你自己的基准测试
你决定应该测试你的应用程序和数据库,以发现瓶颈在哪儿。通过修正它(或通过用一个“哑模块”代替瓶颈),你能容易确定下一个瓶颈(等等)。即使对你的应用程序来说,整体性能“足够好”,你至少应该对每个瓶颈做一个“计划”,如果某人“确实需要修正它”,如何解决它。
对于一些可移植的基准程序的例子,参见MySQL基准套件。见11 MySQL 基准套件。你能利用这个套件的任何程序并且为你的需要修改它。通过这样做,你能尝试不同的你的问题的解决方案并测试哪一个对你是最快的解决方案。
在系统负载繁重时发生一些问题是很普遍的,并且我们有很多与我们联系的客户,他们在生产系统中有一个(测试)系统并且有负载问题。到目前为止,被一种这些的情况是与基本设计有关的问题(表扫描在高负载时表现不好)或OS/库问题。如果系统已经不在生产系统中,它们大多数将很容易修正。
为了避免这样的问题,你应该把一些力气放在在可能最坏的负载下测试你的整个应用!
10.9 设计选择
MySQL在分开的文件中存储行数据和索引数据。许多(几乎全部)其他数据库在同一个文件中混合行和索引数据。我们相信,MySQL的选择对非常广泛的现代系统的来说是较好的。
存储行数据的另一个方法是在一个分开的区域保存每列信息(例子是SDBM和Focus)。这将对每个存取多于一列的查询获得一个性能突破。因为在多于一列被存取时,这快速退化,我们相信这个模型对通用功能的数据库不是最好。
更常见的情形是索引和数据一起被存储(就象Oracle/Sybase)。在这种情况中,你将在索引的叶子页面上找到行信息。有这布局的好处是它在许多情况下(取决于这个索引被缓冲得怎样)节省一次磁盘读。有这布局的坏处是:
- 表扫描更慢,因为你必须读完索引以获得数据。
- 你损失很多空间,因为你必须重复来自节点的索引(因为你不能在节点上存储行)
- 删除将随时间变化恶化数据库表(因为节点中的索引在删除后通常不被更新)。
- 你不能仅使用索引表为一个查询检索数据。
- 索引数据很难缓冲。
10.10 MySQL设计局限/折衷
因为MySQL使用极快的表锁定(多次读/一次写),留下的最大问题是在同一个表中的一个插入的稳定数据流与慢速选择的一个混合。
我们相信,在其他情况下,对大多数系统,异常快速的性能使它成为一个赢家。这种情形通常也可能通过表的多个副本来解决,但是它要花更多的力气和硬件。
对一些常见的应用环境,我们也在开发一些扩展功能以解决此问题。
10.11 可移植性
因为所有SQL服务器实现了SQL的不同部分,要花功夫编写可移植的SQL应用程序。对很简单的选择/插入,它是很容易的,但是你需要越多,它越困难,而且如果你想要应用程序对很多数据库都快,它变得更难!
为了使一个复杂应用程序可移植,你需要选择它应该与之工作的很多SQL服务器。
当你能使用MySQL的crash-me 程序(http://www.mysql.com/crash-me-choose.htmy)来找出你能与之使用的数据库服务器的选择的函数、类型和限制。crash-me现在对任何可能的事情测试了很长时间,但是它仍然理解测试过的大约450件事情。
例如,如果你想要能使用Informix 或 DB2,你不应该有比18个字符更长的列名。
MySQL基准程序和crash-me是独立于数据库的。通过观察我们怎么处理它,你能得到一个感觉,你必须为编写你的独立于数据库的应用程序做什么。基准本身可在MySQL源代码分发的“sql-bench”目录下找到。他们用DBI数据库接口以Perl写成(它解决问题的存取部分)。
到http://www.mysql.com/benchmark.html看这个基准的结果。