mysql调优,MySQL调优实战(博园首发)

声明:本文属魏强(乘风归去)(http://www.cnblogs.com/cfgq)原创 本文首发于博客园
转载请注明出处:http://www.cnblogs.com/cfgq,谢谢合作。
PDF下载,请添加QQ群:174297097与我联系
MySQL调优实战(博园首发)mysql调优

引言

MySQL作为最著名的开源数据库,目前已经被大量用于网站的构建。而对于数据库的开发,数据库优化更是不可避免的问题,尤其是在程序开发的后期。文章通过实际例子的优化,介绍了几种MySQL优化的技巧。

问题程序简介

通过MySQL的SHOW CREATE TABLE语句,可以显示出建立数据表的语句,这样不仅可以分析表格建立可能存在的问题。也可以了解表的结构。显示如清单1所示。

清单 1. 问题程序

mysql> USE HELLOWORLD;
Database changed
mysql> SHOW CREATE TABLE BLOBUSER;
+----------+-------------------------------------------------------------------+
| Table | Create Table|
+----------+-------------------------------------------------------------------+
| BLOBUSER | CREATE TABLE `blobuser` (
`id` int(11) NOT NULL auto_increment,
`username` varchar(20) default NULL,
`password` varchar(20) default NULL,
`mark` int(11) default NULL,
`usertype` char(10) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312 |
+----------+-------------------------------------------------------------------+
1 row in set (0.14 sec)
mysql> SHOW CREATE TABLE ARTICLE;
+---------+--------------------------------------------------------------------+
| Table | Create Table|
+---------+--------------------------------------------------------------------+
| ARTICLE | CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`userid` int(11) default NULL,
`username` varchar(20) default NULL,
`title` varchar(20) default NULL,
`content` mediumtext,
`publishTime` datetime default NULL,
PRIMARY KEY (`id`),
KEY `useridindex` (`userid`),
KEY `indexofa` (`username`),
CONSTRAINT `title2user` FOREIGN KEY (`userid`) REFERENCES `blobuser` (`id`) _disibledevent=> DELETE CASCADE _disibledevent=>) ENGINE=InnoDB DEFAULT CHARSET=gb2312 |
+---------+--------------------------------------------------------------------+
1 row in set (0.23 sec)
从清单1可以很清楚的了解问题程序包含BLOBUSER和ARTICLE两个表,分表代表博客用户和用户发表的文章。

目录结构

本文按照如下列表的顺序依次讲述各个调优点。
l BENCHMARK的使用
l 触发器解决count(*)问题
l PROCEDURE ANALYSE的使用
l EXPLAIN的使用
l 不多取的Select
l 索引
l MySQL缓存
l ENUM的合理使用(ENUM和VARCHAR)

BENCHMARK的使用

假设:程序中存在一个函数(Function),我们怀疑它是延误时间的罪魁祸首,因为每次执行这个函数,程序就会“死“一段时间,我们希望测试这个函数,计算它的执行时间。这种情况是非常常见的。
解决方法:我们知道,函数的一次调用的随机性实际上是很大的,有可能你在测试的那一时刻机器处于忙碌状态,导致你的测试不够准确,我们希望的是执行多次取平均值,这就可以使用BENCHMARK函数了,它的功能是一次性执行N次的某函数,计算消耗的时间。
使用:问题函数的定义如清单2所示。

清单 2. 问题Function

create function getcount() returns int
begin
declare a int default 0;
select count(*) from BLOBUSER into a;
return a;
end;//
这个Function很简单,也是作为测试使用,目的是返回BLOBUSER表的数据量。接下来我们使用BENCHMARK计算这个FUNCTION的执行时间。执行如清单3所演示

清单 3. 执行BENCHMARK

mysql> select benchmark(1000, getcount());
+----------------------------+
| benchmark(1000, getcount()) |
+----------------------------+
| 0 |
+----------------------------+
1 row in set (1 min 35.88 sec)
从清单2中可以看到,执行1000次的getcount用时1分36秒,如果这个函数被频繁使用,或者是在大访问量的系统中,这是非常严重的问题。BENCHMARK的作用就是为了帮助我们找出问题。
建议:如果你在自己的电脑上测试,请注意BENCHMARK占用的系统资源比较大,当笔者在机器上运行清单3时,机器处于极其忙碌的状态。所以读者可以适当减小循环执行的次数。

触发器解决count(*)问题

既然发现了BENCHMARK部分出现的问题,就应该去解决它,函数中就一个执行语句,因此我们确定是count(*)语句出现了问题,count(*)用于计算表的数据量。像这种统计函数实际上是经常使用的,像一些网站会给出历史访问量、注册用户量等等,如果每个用户访问,我们都去执行count(*),那么就太失策了。
解决方法:count(*)的作用是计算表的数据量,而实际上我们完全可以设置新建一个表count,表count只有一个字段int,并且只有一条数据,用于存储BLOBUSER的用户数量。当BLOBUSER表数据增加一条,就自动给count的数据自增1,表数据被删除一条,则自减1,查询数据量的时候,就直接访问count表的数值即可,这个时间几乎是可以忽略不计的。对于自增自减就需要使用到触发器了。如果是在数据库后期想到这个问题,就需要将count的初始值设置为count(*)函数的当前值。
使用:触发器的使用如清单4所示。

清单 4. 触发器定义

create table countblob(id int primary key);
select count(*) from BLOBUSER into @tempcount;
insert into countblob values(@tempcount);
delimiter //
create trigger addOneUser after insert _disibledevent=>BEGIN
update countblob set id = id + 1 limit 1;
END;//
create trigger removeOneUser after delete _disibledevent=>BEGIN
update countblob set id = id - 1 limit 1;
END;//
delimiter ;
清单4中的前三句是为了初始化数据。
结果清单5显示了测试的结果。

清单 5. 测试结果

mysql> SELECT COUNT(*) FROM BLOBUSER;
+----------+
| COUNT(*) |
+----------+
| 200001 |
+----------+
1 row in set (0.22 sec)
mysql> INSERT INTO BLOBUSER(username, password, mark, usertype) VALUES ('myusern
ame', 'password', 2, 'VIP');
Query OK, 1 row affected (0.09 sec)
mysql> SELECT * FROM COUNTBLOB;
+--------+
| id |
+--------+
| 200002 |
+--------+
1 row in set (0.00 sec)
mysql> DELETE FROM BLOBUSER WHERE id = 10000;
Query OK, 1 row affected (0.08 sec)
mysql> SELECT * FROM countblob;
+--------+
| id |
+--------+
| 200001 |
+--------+
1 row in set (0.00 sec)
从清单5中可以看出countblob的数据是自增自减的,每次查看countblob的数据,需要的时间总是接近于0。这样就大大提高了效率。

PROCEDURE ANALYSE的使用

很多时候表的设计决定了一部分的效率问题,好的表设计为将来的需求变化、效率提高提供了先决条件。但是有时候,表的结构是由插入的数据决定的,也就是得等到大量数据被插入的时候,才会发现原来表结构并不合适。为了在大量数据存在的情况下寻找合适的数据类型,MySQL提供了PROCEDURE ANALYSE的功能,目的是通过分析select查询结果对现有的表的每一列给出优化的建议。
使用:PROCEDURE ANALYSE的语法结构是SELECT … PROCEDURE ANALYSE([max_elements,[max_memory]]);其中max_elements(默认值256)是analyze查找每一列不同值时所需关注的最大不同值的数量,analyze还用这个值来检查优化的数据类型是否该是ENUM,如果该列的不同值的数量超过了max_elements值ENUM就不作为建议优化的数据类型;max_memory(默认值8192)是analyze查找每一列所有不同值时可能分配的最大的内存数量样例程序。我们对BLOBUSER进行分析,分析的清单如6所示。

清单 6. PROCEDURE ANALYSE的分析

mysql> SELECT * FROM BLOBUSER PROCEDURE ANALYSE(1);
+------------------------------+------------+---------------+------------+------
------+------------------+-------+-------------------------+-------------+------
--------------------------+
| Field_name | Min_value | Max_value | Min_length | Max_l
ength | Empties_or_zeros | Nulls | Avg_value_or_avg_length | Std | Optim
al_fieldtype |
+------------------------------+------------+---------------+------------+------
------+------------------+-------+-------------------------+-------------+------
--------------------------+
| helloworld.BLOBUSER.id | 1 | 200001 | 1 |
6 | 0 | 0 | 100001.4500 | 116099.3199 | MEDIU
MINT(6) UNSIGNED NOT NULL |
| helloworld.BLOBUSER.username | myusername | username99999 | 9 |
14 | 0 | 0 | 13.4444 | NULL | CHAR(
14) NOT NULL |
| helloworld.BLOBUSER.password | password | password99999 | 8 |
14 | 0 | 0 | 13.4444 | NULL | CHAR(
14) NOT NULL |
| helloworld.BLOBUSER.mark | 2 | 20 | 1 |
2 | 0 | 0 | 19.9999 | 0.0402 | TINYI
NT(2) UNSIGNED NOT NULL |
| helloworld.BLOBUSER.usertype | NORMAL | VIP | 3 |
6 | 0 | 0 | 4.5000 | NULL | ENUM(
'NORMAL','VIP') NOT NULL |
+------------------------------+------------+---------------+------------+------
------+------------------+-------+-------------------------+-------------+------
--------------------------+
5 rows in set (0.53 sec)
从清单6可以看出,根据目前存在的BLOBUSER数据,可以分析出各个字段更合适的数据类型,比如对于usertype字段,由于就NORMAL或者VIP类型,因此建议用ENUM(’NORMAL’,’VIP’),对于mark,数据的范围在2-20,所以不必使用INT类型,只需要使用TINYINT类型。根据这些建议,开发者可以选择改变表结构以节省更多空间,提高效率。
建议:PROCEDURE ANALYSE给出的结果只是建议,不是强制性的需求,如果你已经预测到未来的数据可能超出了建议数据类型的范围,你就不需要修改,这是根据情况而定的。

EXPLAIN的使用

MySQL调优的重要部分包括SQL语句的调优,尤其对select语句的调优更是主要任务,对于某个SQL语句执行效率不高的问题,就需要对这个SQL语句进行分析,对于简单的SQL语句,我们可以直观的看出它的一些优缺点,但是如果对于复杂的、嵌套的SQL语句,则需要通过MySQL的工具帮助我们分析,这就需要用到EXPLAIN语句。EXPLAIN 关键字可以让你知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN 的查询结果还会告诉你,你的索引主键被如何利用,你的数据表是如何被搜索和排序的等等。
使用:为了说明问题,我们执行清单7中的测试代码:explain select * from article as a, blobuser as b where b.username= a.username;,目的是找出用户发布的所有文章。执行EXPLAIN结果也在清单7中显示。

清单 7. EXPLAIN的用法

mysql> explain select a.title from article as a, blobuser as b where b.username
= a.username
-> ;
+----+-------------+-------+------+---------------+------+---------+------+-----
---+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows
| Extra |
+----+-------------+-------+------+---------------+------+---------+------+-----
---+-------------+
| 1 | SIMPLE | b | ALL | NULL | NULL | NULL | NULL | 1991
43 | |
| 1 | SIMPLE | a | ALL | NULL | NULL | NULL | NULL | 2003
15 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+-----
---+-------------+
2 rows in set (0.00 sec)
根据MySQL参考手册的知识,我们知道当type是ALL的时候,效率是比较低的,所以对a和
b都需要进行优化,可以看到rows列的数据分别是199143和200315,这个代价是非常大的,也就是说这个操作要遍历203119*199143行的数据,这是非常可怕的操作,原因就在于它的type是ALL类型,key是NULL。
解决方法既然分析表明a和b表的查找没有key,所以我首先为b的username建立索引,并且再次执行explain查看结果如清单8所示。

清单 8. 为BLOBUSER建立索引后EXPLAIN

mysql> create index indexofb _disibledevent=>Query OK, 200000 rows affected (9.89 sec)
Records: 200000 Duplicates: 0 Warnings: 0
mysql> explain select a.title from article as a, blobuser as b where b.username
= a.username;
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref
| rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
| 1 | SIMPLE | a | ALL | NULL | NULL | NULL | NULL
| 200315 | |
| 1 | SIMPLE | b | ref | indexofb | indexofb | 43 | hellowo
rld.a.username | 1 | Using where; Using index |
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
2 rows in set (0.00 sec)
可以看到b的类型变为ref类型,这是一个比较好的链接类型,只需要遍历43行。a表需要遍历200315行,如果我们继续为a的username建立索引,并且再次查看,结果如清单9所示。

清单 9. 为ARTICLE建立索引后EXPLAIN

mysql> create index indexofa _disibledevent=>Query OK, 200000 rows affected (9.64 sec)
Records: 200000 Duplicates: 0 Warnings: 0
mysql> explain select a.title from article as a, blobuser as b where b.username
= a.username;
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref
| rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
| 1 | SIMPLE | a | ALL | indexofa | NULL | NULL | NULL
| 195656 | |
| 1 | SIMPLE | b | ref | indexofb | indexofb | 43 | hellowo
rld.a.username | 1 | Using where; Using index |
+----+-------------+-------+------+---------------+----------+---------+--------
---------------+--------+--------------------------+
2 rows in set (0.02 sec)
A表的行数变为195656,又优化了。可以看到EXPLAIN帮助我们快速的发现问题,提供了众多的参数供我们分析、理解MySQL是如何处理SQL语句。

不多取的Select

当一个SELECT语句的返回必然具有可数性时,应该加上LIMIT限制。比如用户登录,数据库中保存了ID唯一的一条用户数据。很多时候我们用SELECT COUNT(*) FROM USER WHERE USERNAME = ‘…’这样的语句进行用户登录。虽然我们知道COUNT(*)返回的肯定是0或者1,然而我们依旧鼓励在句末加上LIMIT 1的限制。其实原因很简单,如果用户不存在,加不加LIMIT的效率是一样的,但是如果用户存在,并且WHERE子句部分的字段不具有唯一性(也就是可能存在多个),那么LIMIT 1告诉MySQL找到一个后立刻返回,而不加LIMIT则会试图寻找所有的数据,虽然二者的结果是一样的,但是效率上却相差一点,对于这个的测试结果如清单10所示。

清单 10. 不多取的SELECT

mysql> SELECT * FROM BLOBUSER WHERE username = 'username10000';
+-------+---------------+---------------+------+----------+
| id | username | password | mark | usertype |
+-------+---------------+---------------+------+----------+
| 10001 | username10000 | password10000 | 20 | VIP |
+-------+---------------+---------------+------+----------+
1 row in set (0.06 sec)
mysql> SELECT * FROM BLOBUSER WHERE username = 'username10000' limit 1;
+-------+---------------+---------------+------+----------+
| id | username | password | mark | usertype |
+-------+---------------+---------------+------+----------+
| 10001 | username10000 | password10000 | 20 | VIP |
+-------+---------------+---------------+------+----------+
1 row in set (0.02 sec)

索引

建立索引可以说是提高搜索效率最有效、最直接的方法了,从EXPLAIN的例子也可以看出,建立索引可以提高一个数量级的效率。一般来说,WHERE或者JOIN子句以后使用到的变量需要建立索引,但是也有例外,因为MySQL只对<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE才会使用索引,而以通配符%和_开头作查询时,MySQL不会使用索引。
更好的判断是否增加索引的方法是使用EXPLAIN对SQL语句进行分析,根据EXPLAIN的连接类型判断是否为此列添加索引,连接类型的好坏依次是:system const eq_ref ref range index ALL。对于索引能提高搜索效率的例子,这里就不重复给出,在EXPLAIN部分已经展示了,这里介绍索引的类型,针对不同情况,创建不同的索引。同样是建立索引,合适的索引会增加更多的效率。为了更好的说明这个问题,假设对ARTICLE表的数据进行搜索。

普通索引

假设按照文章的title进行搜索,因为title不唯一,所以建立普通索引即可,普通索引的建立方式为CREATE INDEX titleIndex _disibledevent=>、ALTER ARTICLE ADD INDEX indexTitle _disibledevent=>、在建立表时增加INDEX indexTitle (title) 此行。

主键索引

当一个表存在主键时,PRIMARY KEY的作用就是为了建立主键索引。它的特点就是主键必须唯一,不允许是NULL。

唯一索引

主键索引是特殊的唯一索引,唯一索引的特点是列值唯一,但允许为NULL,也就是一些字段必须是唯一的,但是又不作为主键,就可以使用唯一索引。

组合索引

如果不仅仅根据title搜索,同时还可以根据日期、内容等进行更精确的搜索,就需要用到组合索引。它的建立方式是ALTER TABLE ARTICLE ADD INDEX title_content (title,content(100))。注意,content后面是索引关注的长度,对于CLOB类型的content是必须指定长度的。建立完组合索引后,搜索就可以是:SELECT * FROM ARTICLE WHREE title="..." AND content="沈阳%"。组合索引比单独为两列分别建索引效率来的高,并且节省了硬盘空间。

不要滥用索引:索引虽然好,但是不要滥用索引,只在需要的地方添加,因为索引会影响INSERT、UPDATE、DELETE的效率,在执行这些操作时还需要操作索引文件。另外索引会带来额外的硬盘开销。

MySQL缓存

MySQL中重要的三个缓存是key_buffer_size,query_cache_size,table_cache。下面会分别介绍这几个缓存。
key_buffer_size指定了索引缓冲区的大小,它决定了索引处理的速度,特别是对索引读取的速度。MySQL默认的是设置为16M,而对于大型应用,这个数字是远远不够的。可以通过检查状态值key_read_requests和key_reads了解key_buffer_size设置的是否合理。比例key_reads / key_read_requests应该尽可能的低,至少是1:100,如果是1:1000以下就更好(上述状态值可以使用SHOW STATUS LIKE ‘key_read%’获得)。
对于文中示例,我们检查了它的key_buffer_size以及相关参数的设置,结果如清单11所示。

清单 11. key_buffer_size的相关参数的查询

mysql> SHOW STATUS LIKE 'key_read%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Key_read_requests | 23445 |
| Key_reads | 3234 |
+-------------------+-------+
2 rows in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'key_buffer%';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| key_buffer_size | 16777216 |
+-----------------+----------+
1 row in set (0.00 sec)
从清单11中可以看出它的buffer_size是16M,但是key_reads / key_read_requests的比例太低,所以可以适当将key_buffer_size增加。
query_cache_size是查询缓存,从4.0.1版本开始,MySQL提供了查询缓冲机制。使用查询缓冲机制,将SELECT语句的查询结果存放在缓冲区中,以后对于同样的SELECT语句(区分大小写),将直接从缓冲区中读取结果。根据MySQL用户手册,使用查询缓冲最多可以达到238%的效率。可以通过Qcache inserts、Qcache hits、Qcache lowmem prunes、Qcache free blocks 、cache total blocks这几个参数知道query_cache_size设置得是否合理。
Qcache_lowmem_prunes的值特别大,则表明经常出现缓冲不够用的情况,同时Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,此时需要增加缓冲大小。Qcache_hits的值不大,则表明你的查询重复率很低,这种情况下使用查询缓冲反而会影响效率,那么可以考虑不用查询缓冲,将query_cache_type设置为OFF即可。此外,在SELECT语句中加入SQL_NO_CACHE也可以明确表示不使用查询缓冲。Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。

清单 12. query_cache的相关参数的查询

mysql> SHOW STATUS LIKE 'Qcache%';
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Qcache_free_blocks | 10643 |
| Qcache_free_memory | 11525568 |
| Qcache_hits | 23432123 |
| Qcache_inserts | 983475 |
| Qcache_lowmem_prunes | 17429 |
| Qcache_not_cached | 4 |
| Qcache_queries_in_cache | 18643 |
| Qcache_total_blocks | 23864 |
+-------------------------+----------+
8 rows in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'query_cache%';
+------------------------------+----------+
| Variable_name | Value |
+------------------------------+----------+
| query_cache_limit | 1048576 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 11534336 |
| query_cache_type | _disibledevent=>| query_cache_wlock_invalidate | OFF |
+------------------------------+----------+
5 rows in set (0.00 sec)
如果想关闭查询缓存,就可以将query_cache_type设为OFF,清单12的query_cache_size是11M,也是默认设置,但是它的hits结果太大、Qcache_lowmem_prunes的数值相对11M也是太大,应该适当增加查询缓存的大小。
table_cache指定表高速缓存的大小。每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就会被打开并放入其中,这样可以更快地访问表内容。通过检查峰值时间的状态值Open_tables和Opened_tables,可以决定是否需要增加table_cache的值。如果你发现open_tables等于table_cache,并且opened_tables在不断增长,那么你就需要增加table_cache的值了(上述状态值可以使用SHOW STATUS LIKE ‘Open%tables’获得)。注意,不能盲目地把table_cache设置成很大的值。如果设置得太高,可能会造成文件描述符不足,从而造成性能不稳定或者连接失败。
对于这种参数设置,具有很大的不确定性,也不容易设置,需要根据当前的硬件环境、网络访问状态进行考量,比如在不繁忙时进行检查可能缓存就被认为设置大了,到了访问量高峰时,缓存的命中会迅速增加,这又会被认为设置小了。所以这种设置需要有一定的经验,并且经过测验才能准确的定位参数的数值。

清单 13. table_cache的相关参数的查询

mysql> SHOW STATUS LIKE 'Open%tables';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables | 125 |
| Opened_tables | 6963 |
+---------------+-------+
2 rows in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'table_cache%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| table_cache | 256 |
+---------------+-------+
1 row in set (0.00 sec)
清单13中可以看出Open_tables的数量还是小于table_cache的值,因此问题不大。

ENUM的合理使用(ENUM和VARCHAR)

一些字段,它的取值范围就在几个字符串之间,比如客户类型有:NORMAL、VIP、SVIP等等,这我们就鼓励使用ENUM类型。在MySQL参考手册中明确表明固定长度的表的效率是高的,所以能确定长度范围的就使用固定长度。实际上我们想想也可以知道,在执行SQL过程中,如果比较两张表的字段数值时,对于ENUM它的取值是有限的、长度固定的,而对于VARCHAR则需要进行实际字符串长度的计算,如果这种计算频繁,那么就将显然降低效率。

小结

本文根据实际例子,介绍了MySQL调优的一些实践方法,实际上MySQL调优是一个非常复杂的过程,除了文章中介绍的,还有许多参数、方法可以用于改变MySQL性能,由于篇幅限制,无法一一列出。由于本人知识水平有限,文章中倘若有错误的地方,欢迎联系我批评指正。

参考资源

· 参考“51CTO中MySQL系列文章”,查看MySQL系列文章。
· 查看文章“MySQL 5.1 参考手册”,了解MySQL官方的调优方法。
· 查看文章“LAMP 系统性能调优,第3 部分: MySQL 服务器调优”,了解MySQL调优。
· 查看文章“IBM数据库调优专题”,了解IBM的调优方法。
Tags:  绿博园 济南园博园 深圳园博园 园博园 mysql调优

延伸阅读

最新评论

发表评论