确实有必要为所有选定的列建立索引以便MySQL选择使用索引吗?
这是一个非常棘手的问题,因为有一些因素可以确定索引是否值得使用。
因素#1
对于任何给定的指数,关键人群是什么?换句话说,索引中记录的所有元组的基数(区别计数)是多少?
因素#2
您正在使用什么存储引擎?是否可以从索引访问所有需要的列?
下一步是什么 ???
让我们举一个简单的例子:一个包含两个值(男性和女性)的表
让我们创建一个测试索引使用情况的表
USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
id int not null auto_increment,
gender char(1),
primary key (id),
key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';
测试InnoDB
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql>
测试MyISAM
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | mf | ALL | gender | NULL | NULL | NULL | 40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>
InnoDB分析
当数据作为InnoDB加载时,请注意所有四个EXPLAIN
计划都使用了gender
索引。第三和第四EXPLAIN
计划使用gender
索引,即使请求的数据是id
。为什么?因为id
在中,PRIMARY KEY
并且所有二级索引都有指向的引用指针PRIMARY KEY
(通过gen_clust_index)。
MyISAM分析
当数据作为MyISAM加载时,请注意,前三个EXPLAIN
计划使用了gender
索引。在第四个EXPLAIN
计划中,查询优化器决定根本不使用索引。它选择了全表扫描。为什么?
不论DBMS如何,查询优化器都基于一个非常简单的经验法则:如果正在将索引筛选为用于执行查找的候选者,并且查询优化器计算出它必须查找的总数超过5%。表中的行:
- 如果所有需要检索的列都在所选索引中,则将进行全索引扫描
- 全表扫描,否则
结论
如果没有适当的覆盖索引,或者任何给定元组的键总数超过表的5%,则必须发生六件事:
- 意识到必须分析查询
- 从这些查询中找到所有
WHERE
,GROUP BY
和ORDER BY`子句
- 按此顺序编制索引
WHERE
具有静态值的子句列
GROUP BY
列
ORDER BY
列
- 避免全表扫描(查询缺少明智的
WHERE
条款)
- 避免使用错误密钥填充(或至少缓存那些错误密钥填充)
- 为表选择最佳的MySQL存储引擎(InnoDB或MyISAM)
我过去曾写过有关5%经验法则的文章:
更新2012-11-14 13:05 EDT
我回头看看您的问题以及原始的SO帖子。然后,我想到了我Analysis for InnoDB
之前提到的。它与person
桌子重合。为什么?
对于两个表mf
和person
- 存储引擎是InnoDB
- 主键是
id
- 通过二级索引访问表
- 如果表是MyISAM,我们将看到一个完全不同的
EXPLAIN
计划
现在,查看SO问题中的查询:select * from person order by age\G
。由于没有WHERE
子句,因此您明确要求进行全表扫描。该表的默认排序顺序为id
(PRIMARY KEY),因为其为auto_increment,而gen_clust_index(又称为聚集索引)由内部rowid排序。当您按索引排序时,请记住,InnoDB二级索引具有附加到每个索引条目的行ID。这就产生了每次都需要全行访问的内部需求。
ORDER BY
如果您忽略有关InnoDB索引组织方式的这些事实,那么在InnoDB表上进行设置可能是一项艰巨的任务。
回到该SO查询,因为您明确要求进行全表扫描,所以恕我直言,MySQL Query Optimizer做的正确(或至少选择了阻力最小的路径)。当涉及到InnoDB和SO查询时,执行一次全表扫描然后进行一次全表扫描要容易filesort
得多,而不是通过gen_clust_index对每个二级索引条目进行全索引扫描和行查找。
我不主张使用索引提示,因为它忽略了EXPLAIN计划。尽管如此,如果您真的比InnoDB更了解您的数据,则您将不得不求助于索引提示,尤其是对于没有WHERE
子句的查询。
更新2012-11-14 14:21 EDT
根据《了解MySQL内部原理》一书
页面202第7段说:
数据存储在称为聚簇索引的特殊结构中,聚簇索引是一个B树,其中主键充当键值,并且在数据部分中包含实际记录(而不是指针)。因此,每个InnoDB表必须具有一个主键。如果未提供,则添加一个特殊的行ID列,该列通常对用户不可见,以用作主键。辅助键将存储用于标识记录的主键的值。B树代码可在innobase / btr / btr0btr.c中找到。
这就是我早些时候说过的原因:与通过gen_clust_index为每个辅助索引条目进行完整的索引扫描和行查找相比,执行全表扫描然后进行某种文件排序要容易得多。InnoDB每次都会进行双索引查找。这听起来有些残酷,但这只是事实。同样,请考虑缺少WHERE
子句。这本身就是对MySQL查询优化器进行全表扫描的提示。
FOR ORDER BY
(这是此问题中的特殊情况)。问题确实指出在这种情况下存储引擎是InnoDB
(原始SO问题表明1万行在8个项目中相当均匀地分布,基数在这里也不应该成为问题)。可悲的是,我认为这不能回答问题。