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.25 SET 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.25 SET OPTION句法。
你可以用HIGH_PRIORITY属性指明一个特定SELECT是很重要的。见7.12 SELECT句法。
如果你有关于INSERT结合SELECT的问题,切换到使用新的MyISAM表,因为它们支持并发的SELECT和INSERT。
如果你主要混合INSERT和SELECT语句,DELAYED属性的INSERT将可能解决你的问题。见7.14 INSERT句法。
如果你有关于SELECT和DELETE的问题,LIMIT选项的DELETE可以帮助你。见7.11 DELETE句法。
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的信息)。