在性能测试中遇到性能瓶颈最的多地方就是数据库这块,而数据库出问题很多都是索引使用不当导致,根据以往遇到的索引问题做个简单的总结:
一、索引的利弊
索引的好处:索引能够极大地提高数据检索的效率,让Query 执行得更快,也能够改善排序分组操作的性能,在进行排序分组操作中利用好索引,将会极大地降低CPU资源的消耗。
索引的弊端:
1、更新数据库时会更新索引,这样,最明显的资源消耗就是增加了更新所带来的 IO 量和调整索引所致的计算量。
测试代码:
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `time1` varchar(11) DEFAULT NULL, `time2` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURREN T_TIMESTAMP, `time3` int(11) DEFAULT NULL, `stats` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `NewIndex1` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=gbk |
a、为了保证测试的公平性,所以必须保证添加索引前后的数据是完全一致的,现在中保留表中id小于200万的数据。
mysql> DELETE FROM test.test WHERE id >2000000; Query OK, 231412 rows affected (0.92 sec) |
b、没有添加索引时添加200万数据用时1.81秒
mysql> INSERT INTO test.`test` (time1,time2,time3,stats) SELECT time1,time2,time3,stats FROM test.`test`; Query OK, 231412 rows affected (1.32 sec) Records: 231412 Duplicates: 0 Warnings: 0 |
c、清除刚添加数据
mysql> DELETE FROM test.test WHERE id >2000000; Query OK, 231412 rows affected (1.00 sec) |
d、添加索引
mysql> ALTER TABLE `test`.`test` ADD INDEX `time1_2_3_stats` (`time1`, `time2`, `time3`, `stats`); Query OK, 0 rows affected (0.97 sec) Records: 0 Duplicates: 0 Warnings: 0 |
e、添加索引后增加200万数据用时4.88秒
mysql> INSERT INTO test.`test` (time1,time2,time3,stats) SELECT time1,time2,time3,stats FROM test.`test`; Query OK, 231412 rows affected (2.27 sec) Records: 231412 Duplicates: 0 Warnings: 0 |
2、索引也会占用一定的存储空间,有些时候索引所占的空间有可能超过数据所占的空间;
例如:下面举一个比较特殊的例子(如果字段大小设置不合理或者索引建的过多可能会导致一些问题),表结构和索引情况如下:
CREATE TABLE `friends` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `uid` bigint(20) unsigned DEFAULT ’0′, `fuid` bigint(20) unsigned DEFAULT ’0′, `fname` varchar(50) DEFAULT ”, `fpicture` varchar(150) DEFAULT ”, `fsex` tinyint(1) DEFAULT ’0′, `status` tinyint(1) DEFAULT ’0′, PRIMARY KEY (`id`), KEY `fuid` (`fuid`), KEY `fuid_fname` (`fuid`,`fname`), KEY `uid_stats` (`uid`,`status`) ) ENGINE=MyISAM AUTO_INCREMENT=262145 DEFAULT CHARSET=gbk |
新建10万条数据后,这个表的索引文件为4.4M而数据文件仅有3.9M:
[root@qa05v /usr/local/mysql/data/test]# du -sh friends.* 12K friends.frm 3.9M friends.MYD 4.4M friends.MYI |
这里有点需要注意的是对于varchar字段,索引的长度是其定义的长度。比如一行中`fname` varchar(50) DEFAULT ” 实际只存了3个byte数据,但是其索引长度是50,所以造成了索引有可能是比数据大。
二、索引使用原则
1、索引可以改善查询,但会减慢更新,索引不是越多越好,特别是在数据增、删、改比较频繁的表中,过多的索引反而会导致系统整体性能的下降,这一点已经在索引的弊端中介绍过,这里就不做太多介绍。
2、离散程度越小,不适合加索引,例如:不要给性别建索引 status这样字段建索引;
测试代码(stats字段是0~4随机生成的):
mysql> SELECT SQL_NO_CACHE count(id) FROM test.test WHERE stats=4; +———–+ | count(id) | +———–+ | 740591 | +———–+ 1 row in set (0.40 sec) mysql> ALTER TABLE `test`.`test` add INDEX `stats` (`stats`); Query OK, 3702592 rows affected (24.08 sec) Records: 3702592 Duplicates: 0 Warnings: 0 mysql> SELECT SQL_NO_CACHE count(id) FROM test.test WHERE stats=4; +———–+ | count(id) | +———–+ | 740591 | +———–+ 1 row in set (1.58 sec) |
3、在数据量较少且访问频率不高的情况下,假如只有一百行记录的表不需要建立索引。因为在数据量少的情况下,使用全表扫描效果比走索引更好,这就好比只有一本只有5页的书,如果我们想找其中一个章节,我们一般不会通过目录去寻找,而是直接去找了。
测试数据:
mysql> SELECT COUNT(*) FROM test.newtest; //该表数据为10000条 +———-+ | COUNT(*) | +———-+ | 10001 | +———-+ |
没有索引时执行时间大概接近于0秒
mysql> SELECT SQL_NO_CACHE count(time1) FROM test.`newtest` WHERE time1 >’20130517100591′; +————–+ | count(time1) | +————–+ | 9992 | +————–+ |
添加索引
mysql> ALTER TABLE `test`.`newtest` ADD INDEX `time1` (`time1`); Query OK, 10001 rows affected (0.04 sec) Records: 10001 Duplicates: 0 Warnings: 0 |
添加索引后执行时间为0.02sec
mysql> SELECT SQL_NO_CACHE count(time1) FROM test.`newtest` WHERE time1 >’20130517100591′; +————–+ | count(time1) | +————–+ | 9992 | +————–+ 1 row in set (0.02 sec) |
4、避免建立两个或以上功能相同索引。例如已经建立字段A、B两个字段的索引,应该避免再建立字段A的单独索引。两个索引之间,对相同的查询都会起到相同的作用。建立两个功能相同的索引,反而会容易引起数据库产生错误的查询计划,降低查询效率。
5、选择正确的组合索引字段顺序,最常用的查询字段和选择性、区分度较高的字段,应该作为索引的前导字段使用。假设存在组合索引it1c1c2(c1,c2),查询语句select * from t1 where c1=1 and c2=2能够使用该索引。查询语句select * from t1 where c1=1也能够使用该索引。但是,查询语句select * from t1 where c2=2不能够使用该索引,因为没有组合索引的引导列,即,要想使用c2列进行查找,必需出现c1等于某值,所以在在添加联合索引的时候尽量将常用的字段放到最前面。
例如:
ALTER TABLE `test`.`friends` ADD INDEX `fuid_fname` (`fuid`, `fname`);创建组合索引 mysql> EXPLAIN SELECT * FROM test.`friends` WHERE fname=’test5′; +—-+————-+———+——+—————+——+———+——+——–+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+———+——+—————+——+———+——+——–+————-+ | 1 | SIMPLE | friends | ALL | NULL | NULL | NULL | NULL | 100000 | Using where | +—-+————-+———+——+—————+——+———+——+——–+————-+ |
如果单独再创建一个fuid索引这样造成了索引的浪费。
mysql> EXPLAIN SELECT * FROM test.`friends` WHERE fuid=’364′; +—-+————-+———+——+—————+————+———+——-+——-+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+———+——+—————+————+———+——-+——-+————-+ | 1 | SIMPLE | friends | ref | fuid_fname | fuid_fname | 9 | const | 33712 | Using where | +—-+————-+———+——+—————+————+———+——-+——-+————-+ |
联合索引使用情况:
mysql> EXPLAIN SELECT * FROM test.`friends` WHERE fuid=’364′ and fname=’test5′; +—-+————-+———+——+—————+————+———+————-+——+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+———+——+—————+————+———+————-+——+————-+ | 1 | SIMPLE | friends | ref | fuid_fname | fuid_fname | 112 | const,const | 2 | Using where +—-+————-+———+——+—————+————+———+————-+——+————-+ |
Sql优化器会对SELECT * FROM test.`friends` WHEREfname=’test5′ and fuid=’364′;进行优化,优化后的效果可能为:ELECT * FROM test.`friends` WHERE fuid=’364′ and fname=’test5′,这点目前还没证实;
mysql> EXPLAIN SELECT * FROM test.`friends` WHERE fname=’test5′ and fuid=’364′; +—-+————-+———+——+—————+————+———+————-+——+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+———+——+—————+————+———+————-+——+————-+ | 1 | SIMPLE | friends | ref | fuid_fname | fuid_fname | 112 | const,const | 2 | Using where +—-+————-+———+——+—————+————+———+————-+——+————-+ |
6、合适的字段数,组合索引的字段数不适宜较多,较多的组合索引字段数会降低索引查询效率,除业务特点需要建立多字段的组合主键例外。
三、索引分析利器explain
在做性能测试的过程中经常遇到一些数据库的问题,通常使用慢查询日志可以找到执行效果比较差的sql,但是仅仅找到这些sql是不行的,我们需要协助开发人员分析问题所在,这就经常用到explain。
explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。
使用方法,在select语句前加上explain就可以了:
mysql> explain select * from test.index_test where time1=’20130517100552′; +—-+————-+————+——+—————+——-+———+——-+——+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+————+——+—————+——-+———+——-+——+————-+ | 1 | SIMPLE | index_test | ref | time1 | time1 | 253 | const | 1 | Using where | +—-+————-+————+——+—————+——-+———+——-+——+————-+ |
EXPLAIN列的解释:
id:SELECT识别符。这是SELECT的查询序列号,若没有子查询和联合查询,id则都是1,并且Mysql会按照id从大到小的顺序执行query,在id相同的情况下,则从上到下执行。
table:显示这一行的数据是关于哪张表的。
type:这是重要的列,显示连接使用了何种类型。
Explain的type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:
system>const>eq_ref> ref > range > index > ALL
possible_keys:显示可能应用这张表中的那个索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句。
key:实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MYSQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MYSQL忽略索引。
key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好。
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:MYSQL认为必须检查的用来返回请求数据的行数。
Extra:关于MYSQL如何解析查询的额外信息。
Extra列返回的描述
Distinct:一旦MYSQL找到了与行相联合匹配的行,就不再搜索了
Not exists:MYSQL优化了LEFT JOIN,一旦它找到了匹配LEFT JOIN标准的行,就不再搜索了
Range checked for each Record(index map:#):没有找到理想的索引,因此对于从前面表中来的每一个行组合,MYSQL检查使用哪个索引,并用它来从表中返回行。这是使用索引的最慢的连接之一
Using filesort:当看到这个的时候,查询就有可能需要优化了。MYSQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序全部行
Using index:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候
Using temporary 看到这个的时候,查询需要优化了。这里,MYSQL需要创建一个临时表来存储结果,这通常发生在对不同的列集进行ORDER BY上,而不是GROUP BY上
Using where:使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回给用户。如果不想返回表中的全部行,并且连接类型ALL或index,这就会发生,或者是查询有问题不同连接类型的解释。
四、引起索引失效的一些因素
1、like语句操作
一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%”不会使用索引而like “aaa%”可以使用索引。
例如:
mysql> EXPLAIN SELECT * FROM test.`test` WHERE time1 LIKE ‘%550′ LIMIT 0,10; +—-+————-+——-+——+—————+——+———+——+———+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——+—————+——+———+——+———+————-+ | 1 | SIMPLE | test | ALL | NULL | NULL | NULL | NULL | 3922757 | Using where | +—-+————-+——-+——+—————+——+———+——+———+————-+ mysql> EXPLAIN SELECT * FROM test.`test` WHERE time1 LIKE ’2013%’ LIMIT 0,10; +—-+————-+——-+——-+—————+——-+———+——+———+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——-+—————+——-+———+——+———+————-+ | 1 | SIMPLE | test | range | time1 | time1 | 85 | NULL | 3922626 | Using where | +—-+————-+——-+——-+—————+——-+———+——+———+————-+ |
2、在索引列上使用函数,或者对索引列进行运算,运算包括(+,-,*,/,! 等)会导致索引失效
例如:
mysql> EXPLAIN SELECT * FROM test.test WHERE id-1=153743; +—-+————-+——-+——+—————+——+———+——+———+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——+—————+——+———+——+———+————-+ | 1 | SIMPLE | test | ALL | NULL | NULL | NULL | NULL | 3922757 | Using where | +—-+————-+——-+——+—————+——+———+——+———+————-+ mysql> EXPLAIN SELECT * FROM test.test WHERE id=153744; +—-+————-+——-+——-+—————+———+———+——-+——+——-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——-+—————+———+———+——-+——+——-+ | 1 | SIMPLE | test | const | PRIMARY | PRIMARY | 4 | const | 1 | | +—-+————-+——-+——-+—————+———+———+——-+——+——-+ |
3、查询的数量是表的大部分,比如30%以上,这只是估算值看。
例如:
mysql> select count(*) from test.test4; //查看一下数据量 +———-+ | count(*) | +———-+ | 200000 | +———-+ mysql> ALTER TABLE `test`.`test4` ADD INDEX `id_num` (`id_num`); //添加索引 Query OK, 0 rows affected (0.46 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select * from test.test4 where id_num<60000; //满足条件的数据为6万条的情况 +—-+————-+——-+——+—————+——+———+——+——–+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——+—————+——+———+——+——–+————-+ | 1 | SIMPLE | test4 | ALL | id_num | NULL | NULL | NULL | 200307 | Using where | +—-+————-+——-+——+—————+——+———+——+——–+————-+ mysql> explain select * from test.test4 where id_num<10000;//满足条件为1万条的情况 +—-+————-+——-+——-+—————+——–+———+——+——+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——-+—————+——–+———+——+——+————-+ | 1 | SIMPLE | test4 | range | id_num | id_num | 5 | NULL | 9998 | Using where | +—-+————-+——-+——-+—————+——–+———+——+——+————-+ 1 row in set (0.00 sec) |
4、字符型字段为数字时在where条件里不添加引号
例如:
被测试数据库的表结构如下:
mysql>desc test; +——-+————+——+—–+——————-+—————————–+ | Field | Type | Null | Key | Default | Extra | +——-+————+——+—–+——————-+—————————–+ | id | int(11) | NO | PRI | NULL | auto_increment | | time1 | char(42) | YES | MUL | NULL | | | time2 | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | time3 | int(11) | YES | | NULL | | | stats | tinyint(1) | YES | MUL | NULL | | +——-+————+——+—–+——————-+—————————–+ |
这是添加了引号的sql语句的执行计划:
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM test.test WHERE time1 =’20130517160342′; +—-+————-+——-+——+—————+——-+———+——-+——+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——+—————+——-+———+——-+——+————-+ | 1 | SIMPLE | test | ref | time1 | time1 | 85 | const | 4281 | Using where | +—-+————-+——-+——+—————+——-+———+——-+——+————-+ |
这是没有添加引号的sql语句的执行计划:
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM test.test WHERE time1 =20130517160342; +—-+————-+——-+——+—————+——+———+——+———+————-+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +—-+————-+——-+——+—————+——+———+——+———+————-+ | 1 | SIMPLE | test | ALL | time1 | NULL | NULL | NULL | 3922757 | Using where | +—-+————-+——-+——+—————+——+———+——+———+————-+ |