对Mysql索引类型的一次重新认识

3 分钟读完

今天下午突然收到报警邮件,投票系统的Mysql数据库有大量慢查询,导致前台站点访问出现异常。按图索骥开始进行分析,先看后端站点日志,发现出问题时一个没有启用验证码的投票被刷了,出问题的表是实现防刷功能的内存表,数据量大概有十几万行。先在后台开启验证码,恢复前台站点访问。

按道理内存表再慢也比MyISAM要快,除了数据不能有效落地之外,没有其它明显的缺点了,应用在防刷功能上最合适了,怎么会出现大面积锁表的情况呢?随着分析的深入,发现自己忽略了一个重要的问题,索引类型对内存表也有至关重要的影响,特别是在QPS比较高的情况下。

内存表使用的索引默认为HASH,而一般的MyISAM默认则为BTREE。

由于类型为HASH的索引是以hash以后的值作为键,hash之前的大小关系和hash之后的结果没有必然联系,所以HASH索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。简而言之就是与memcache一样的kv存储,不能通过范围查询也是理所当然的了。

进而可以想到,也有如下问题

  • 无法对排序进行优化
  • 不能利用部分索引查询
  • 不能避免全表扫描

建表测试

CREATE TABLE `memory_test` (
  `id` int(10) unsigned NOT NULL DEFAULT '0',
  `firstname` varchar(10) NOT NULL DEFAULT '',
  `lastname` varchar(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `index` (`firstname`,`lastname`)
) ENGINE=MEMORY;

CREATE TABLE `myisam_test` (
  `id` int(10) unsigned NOT NULL DEFAULT '0',
  `firstname` varchar(10) NOT NULL DEFAULT '',
  `lastname` varchar(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `index` (`firstname`,`lastname`)
) ENGINE=MyISAM;

范围查询测试

EXPLAIN SELECT * FROM `memory_test` WHERE `id` > 5;
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | memory_test | ALL  | PRIMARY       | NULL | NULL    | NULL |    6 | Using where |
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
#无法使用hash索引,对 memory_test 表进行了一次扫描行数为6的全表扫描

EXPLAIN SELECT * FROM `myisam_test` WHERE `id` > 5;
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table       | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | myisam_test | range | PRIMARY       | PRIMARY | 4       | NULL |    2 | Using where |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
#使用主键索引,对 myisam_test 表进行了一次扫描行数为2的范围查询

排序测试

EXPLAIN SELECT * FROM `memory_test` ORDER BY `id` DESC;
+----+-------------+-------------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+-------------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | memory_test | ALL  | NULL          | NULL | NULL    | NULL |    6 | Using filesort |
+----+-------------+-------------+------+---------------+------+---------+------+------+----------------+
#仍然无法使用hash索引,产生一次全表扫描

EXPLAIN SELECT * FROM `myisam_test` ORDER BY `id` DESC;
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table       | type  | possible_keys | key     | key_len | ref  | rows | Extra |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------+
|  1 | SIMPLE      | myisam_test | index | NULL          | PRIMARY | 4       | NULL |    6 |       |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------+
#使用主键索引

联合索引中左前缀测试

EXPLAIN SELECT * FROM `memory_test` WHERE `firstname` = 'firstname1';
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | memory_test | ALL  | index         | NULL | NULL    | NULL |    6 | Using where |
+----+-------------+-------------+------+---------------+------+---------+------+------+-------------+
#无法使用联合索引

EXPLAIN SELECT * FROM `myisam_test` WHERE `firstname` = 'firstname1';
+----+-------------+-------------+------+---------------+-------+---------+-------+------+-------------+
| id | select_type | table       | type | possible_keys | key   | key_len | ref   | rows | Extra       |
+----+-------------+-------------+------+---------------+-------+---------+-------+------+-------------+
|  1 | SIMPLE      | myisam_test | ref  | index         | index | 32      | const |    1 | Using where |
+----+-------------+-------------+------+---------------+-------+---------+-------+------+-------------+
#可以使用到 index 索引

如果非要对内存表进行范围查询怎么办?当然也不是没有解决办法,Mysql在建索引的时候是可以指定索引类型的。

对内存表建立BTREE索引

CREATE TABLE `memory_btree_test` (
  `id` int(10) unsigned NOT NULL DEFAULT '0',
  `firstname` varchar(10) NOT NULL DEFAULT '',
  `lastname` varchar(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `index` (`firstname`,`lastname`) USING BTREE
) ENGINE=MEMORY

联合索引左前缀

EXPLAIN SELECT * FROM `memory_btree_test` WHERE `firstname` = 'firstname1';
+----+-------------+-------------------+------+---------------+-------+---------+-------+------+-------------+
| id | select_type | table             | type | possible_keys | key   | key_len | ref   | rows | Extra       |
+----+-------------+-------------------+------+---------------+-------+---------+-------+------+-------------+
|  1 | SIMPLE      | memory_btree_test | ref  | index         | index | 32      | const |    1 | Using where |
+----+-------------+-------------------+------+---------------+-------+---------+-------+------+-------------+

总结:

Mysql的每个存储引擎(MyISAM、InnoDB、Memory)都有它的适用场景,索引类型也不例外,当然更不要说各种产品了(Memcache、Redis等等)。了解了每个产品的应用场景才能更好的帮我们解决实际困难,而不是时不时的给自己添堵。在这次故障上,还是体现了自己的无知,仍需要加强学习。

留下评论