高效使用索引的首要条件是知道什么样的查询会使用到索引,这个问题和B+Tree中的“最左前缀原理”有关,下面经过例子说明最左前缀原理。mysql
这里先说一下联合索引的概念。MySQL中的索引能够以必定顺序引用多个列,这种索引叫作复合(联合)索引,通常的,一个联合索引是一个有序元组<a1, a2, …, an>,其中各个元素均为数据表的一列,实际上要严格定义索引须要用到关系代数,可是这里我不想讨论太多关系代数的话题,由于那样会显得很枯燥,因此这里就再也不作严格定义。另外,单列索引能够当作联合索引元素数为1的特例。sql
以employees.titles表为例,下面先查看其上都有哪些索引:函数
SHOW INDEX FROM employees.titles; +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Index_type | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | titles | 0 | PRIMARY | 1 | emp_no | A | NULL | | BTREE | | titles | 0 | PRIMARY | 2 | title | A | NULL | | BTREE | | titles | 0 | PRIMARY | 3 | from_date | A | 443308 | | BTREE | | titles | 1 | emp_no | 1 | emp_no | A | 443308 | | BTREE | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
从结果中能够到titles表的主索引为<emp_no, title, from_date>,还有一个辅助索引<emp_no>。为了不多个索引使事情变复杂(MySQL的SQL优化器在多索引时行为比较复杂),这里咱们将辅助索引drop掉:性能
ALTER TABLE employees.titles DROP INDEX emp_no;
这样就能够专心分析索引PRIMARY的行为了。优化
示例说明以下:spa
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
很明显,当按照索引中全部列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,索引能够被用到。这里有一点须要注意,理论上索引对顺序是敏感的,可是因为MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引,例如咱们将where中的条件顺序颠倒:code
EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
效果是同样的。blog
示例说明以下:索引
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
当查询条件精确匹配索引的左边连续一个或几个列时,如<emp_no>或<emp_no, title>,因此能够被用到,可是只能用到一部分,即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARY索引,可是key_len为4,说明只用到了索引的第一列前缀。ci
示例说明以下:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
此时索引使用状况和状况二相同,由于title未提供,因此查询只用到了索引的第一列,然后面的from_date虽然也在索引中,可是因为title不存在而没法和左前缀链接,所以须要对结果进行扫描过滤from_date(这里因为emp_no惟一,因此不存在扫描)。若是想让from_date也使用索引而不是where过滤,能够增长一个辅助索引<emp_no, from_date>,此时上面的查询会使用这个索引。除此以外,还可使用一种称之为“隔离列”的优化方法,将emp_no与from_date之间的“坑”填上。
首先咱们看下title一共有几种不一样的值:
SELECT DISTINCT(title) FROM employees.titles; +--------------------+ | title | +--------------------+ | Senior Engineer | | Staff | | Engineer | | Senior Staff | | Assistant Engineer | | Technique Leader | | Manager | +--------------------+
只有7种。在这种成为“坑”的列值比较少的状况下,能够考虑用“IN”来填补这个“坑”从而造成最左前缀:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager') AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 7 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
此次key_len为59,说明索引被用全了,可是从type和rows看出IN实际上执行了一个range查询,这里检查了7个key。看下两种查询的性能比较:
SHOW PROFILES; +----------+------------+-------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+-------------------------------------------------------------------------------+ | 10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'| | 11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ... | +----------+------------+-------------------------------------------------------------------------------+
“填坑”后性能提高了一点。若是通过emp_no筛选后余下不少数据,则后者性能优点会更加明显。固然,若是title的值不少,用填坑就不合适了,必须创建辅助索引。
示例说明以下:
EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
因为不是最左前缀,索引这样的查询显然用不到索引。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 56 | NULL | 1 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
此时能够用到索引,可是若是通配符不是只出如今末尾,则没法使用索引。(原文表述有误,若是通配符%不出如今开头,则能够用到索引,但根据具体状况不一样可能只会用其中一个前缀)
对于范围条件查询,MYSQL没法再使用范围后面的其余索引列了。但对多个等值条件查询则没有这样的限制。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
范围列能够用到索引(必须是最左前缀),可是范围列后面的列没法用到索引。同时,索引最多用于一个范围列,所以若是查询条件中有两个范围列则没法全用到索引。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
能够看到索引对第二个范围索引无能为力。这里特别要说明MySQL一个有意思的地方,那就是仅用explain可能没法区分范围索引和多值匹配,由于在type中这二者都显示为range。同时,用了“between”并不意味着就是范围查询,例以下面的查询:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no BETWEEN '10001' AND '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
看起来是用了两个范围查询,但做用于emp_no上的“BETWEEN”实际上至关于“IN”,也就是说emp_no实际是多值精确匹配。能够看到这个查询用到了索引所有三个列。所以在MySQL中要谨慎地区分多值匹配和范围匹配,不然会对MySQL的行为产生困惑。
很不幸,若是查询条件中含有函数或表达式,则MySQL不会为这列使用索引(虽然某些在数学意义上可使用)。例如:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
虽然这个查询和状况五中功能相同,可是因为使用了函数left,则没法为title列应用索引,而状况五中用LIKE则能够。再如:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
显然这个查询等价于查询emp_no为10001的函数,可是因为查询条件是一个表达式,MySQL没法为其使用索引。看来MySQL尚未智能到自动优化常量表达式的程度,所以在写查询语句时尽可能避免表达式出如今查询中,而是先手工私下代数运算,转换为无表达式的查询语句。
既然索引能够加快查询速度,那么是否是只要是查询语句须要,就建上索引?答案是否认的。由于索引虽然加快了查询速度,但索引也是有代价的:索引文件自己要消耗存储空间,同时索引会加剧插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,所以索引并非越多越好。通常两种状况下不建议建索引。
第一种状况是表记录比较少,例如一两千条甚至只有几百条记录的表,不必建索引,让查询作全表扫描就行了。至于多少条记录才算多,这个我的有我的的见解,我我的的经验是以2000做为分界线,记录数不超过 2000能够考虑不建索引,超过2000条能够酌情考虑索引。
另外一种不建议建索引的状况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:
Index Selectivity = Cardinality / #T
显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,上文用到的employees.titles表,若是title字段常常被单独查询,是否须要建索引,咱们看一下它的选择性:
SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles; +-------------+ | Selectivity | +-------------+ | 0.0000 | +-------------+
title的选择性不足0.0001(精确值为0.00001579),因此实在没有什么必要为其单独建索引。
有一种与索引选择性有关的索引优化策略叫作前缀索引,就是用列的前缀代替整个列做为索引key,当前缀长度合适时,能够作到既使得前缀索引的选择性接近全列索引,同时由于索引key变短而减小了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使用。
从图12能够看到employees表只有一个索引<emp_no>,那么若是咱们想按名字搜索一我的,就只能全表扫描了:
EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido'; +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
若是频繁按名字搜索员工,这样显然效率很低,所以咱们能够考虑建索引。有两种选择,建<first_name>或<first_name, last_name>,看下两个索引的选择性:
SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.0042 | +-------------+ SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9313 | +-------------+
<first_name>显然选择性过低,<first_name, last_name>选择性很好,可是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?能够考虑用first_name和last_name的前几个字符创建索引,例如<first_name, left(last_name, 3)>,看看其选择性:
SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.7879 | +-------------+
选择性还不错,但离0.9313仍是有点距离,那么把last_name前缀加到4:
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9007 | +-------------+
这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,咱们把这个前缀索引 建上:
ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));
此时再执行一遍按名字查询,比较分析一下与建索引前的结果:
SHOW PROFILES; +----------+------------+---------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+---------------------------------------------------------------------------------+ | 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | | 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | +----------+------------+---------------------------------------------------------------------------------+
性能的提高是显著的,查询速度提升了120多倍。
前缀索引兼顾索引大小和查询速度,可是其缺点是不能用于ORDER BY和GROUP BY操做,也不能用于Covering index(即当索引自己包含查询所需所有数据时,再也不访问数据文件自己)。
----------------------------------------------------------------------------------------------
补充该节中的"范围查询"说明:
Mysql对于范围查询range分的优化为单字段优化和多元素优化:
单元素索引范围条件的定义以下:
· 对于BTREE和HASH索引,当使用=、<=>、
IN
、
IS
NULL
或者
IS
NOT
NULL
操做符时,关键元素与常量值的比较关系对应一个范围条件,即const范围。
· 对于BTREE索引,当使用>、<、>=、<=、
BETWEEN
、!=或者<>,或者
LIKE
'pattern'
(其中
'pattern'
不以通配符开始)操做符时,关键元素与常量值的比较关系对应一个范围条件。
· 对于全部类型的索引,多个范围条件结合
OR
或
AND
则产生一个范围条件。
前面描述的“常量值”系指:
· 查询字符串中的常量
· 同一联接中的const或system表中的列
· 无关联子查询的结果
· 彻底从前面类型的子表达式组成的表达式
|
多元素索引的范围条件:
1.
----------------
对于BTREE索引,区间能够对结合
AND
的条件有用,其中每一个条件用一个常量值经过=、<=>、
IS
NULL
、>、<、>=、<=、!=、<>、
BETWEEN
或者
LIKE
'pattern'
(其中
'pattern'
不以通配符开头)比较一个关键元素。区间能够足够长以肯定一个包含全部匹配条件(或若是使用<>或!=,为两个区间)的记录的单一的关键元组。例如,对于条件:
key_part1 =
'foo'
AND
key_part2 >= 10
AND
key_part3 > 10
2.
-----------
对于HASH索引,可使用包含相同值的每一个区间。
key_part1 cmp const1
AND
key_part2 cmp const2
AND
...
AND
key_partN cmp constN;
这里,const1,const2,...为常量,cmp是=、<=>或者
IS
NULL
比较操做符之一,条件包括全部索引部分。(也就是说,有N 个条件,每个对应N-元素索引的每一个部分)。
3.
----------
若是包含区间内的一系列记录的条件结合使用
OR
,则造成包括一系列包含在区间并集的记录的一个条件。若是条件结合使用了
AND
,则造成包括一系列包含在区间交集内的记录的一个条件。例如,对于两部分索引的条件:
(key_part1 = 1
AND
key_part2 < 2)
OR
(key_part1 > 5)
区间为:
(1, -inf) < (key_part1, key_part2) < (1, 2)
(5, -inf) < (key_part1, key_part2)
|
mysql> set profiling=1; 2 Query OK, 0 rows affected (0.00 sec) eg: mysql> select * from test_1; mysql> show profiles; +----------+------------+----------------------+ | Query_ID | Duration | Query | +----------+------------+----------------------+ | 1 | 0.84718100 | select * from test_1 | +----------+------------+----------------------+ 1 row in set (0.00 sec)