MySQL性能优化——索引

原文地址:http://blog.codinglabs.org/articles/theory-of-mysql-index.htmlhtml

InnoDB使用B+Tree做为索引结构

最左前缀原理与相关优化

 

以employees.titles表为例,下面先查看其上都有哪些索引:mysql

  1. SHOW INDEX FROM employees.titles;
  2. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
  3. | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Index_type |
  4. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
  5. | titles | 0 | PRIMARY | 1 | emp_no | A | NULL | | BTREE |
  6. | titles | 0 | PRIMARY | 2 | title | A | NULL | | BTREE |
  7. | titles | 0 | PRIMARY | 3 | from_date | A | 443308 | | BTREE |
  8. | titles | 1 | emp_no | 1 | emp_no | A | 443308 | | BTREE |
  9. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+

从结果中能够到titles表的主索引为<emp_no, title, from_date>,还有一个辅助索引<emp_no>。为了不多个索引使事情变复杂(MySQL的SQL优化器在多索引时行为比 较复杂),这里咱们将辅助索引drop掉:sql

  1. ALTER TABLE employees.titles DROP INDEX emp_no;

这样就能够专心分析索引PRIMARY的行为了。函数

状况一:全列匹配。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26';
  2. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  5. | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | |
  6. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+

很明显,当按照索引中全部列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,索引能够被用到。这里有一点须要注意,理论上索引对顺序是敏感 的,可是因为MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引,例如咱们将where中的条件顺序颠倒:性能

  1. EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer';
  2. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  5. | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | |
  6. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+

效果是同样的。优化

状况二:最左前缀匹配。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+

当查询条件精确匹配索引的左边连续一个或几个列时,如<emp_no>或<emp_no, title>,因此能够被用到,可是只能用到一部分,即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARY索引,可是 key_len为4,说明只用到了索引的第一列前缀。spa

状况三:查询条件用到了索引中列的精确匹配,可是中间某个条件未提供。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+

此时索引使用状况和状况二相同,由于title未提供,因此查询只用到了索引的第一列,然后面的from_date虽然也在索引中,可是因为 title不存在而没法和左前缀链接,所以须要对结果进行扫描过滤from_date(这里因为emp_no惟一,因此不存在扫描)。若是想让 from_date也使用索引而不是where过滤,能够增长一个辅助索引<emp_no, from_date>,此时上面的查询会使用这个索引。除此以外,还可使用一种称之为“隔离列”的优化方法,将emp_no与from_date 之间的“坑”填上。htm

首先咱们看下title一共有几种不一样的值:blog

  1. SELECT DISTINCT(title) FROM employees.titles;
  2. +--------------------+
  3. | title |
  4. +--------------------+
  5. | Senior Engineer |
  6. | Staff |
  7. | Engineer |
  8. | Senior Staff |
  9. | Assistant Engineer |
  10. | Technique Leader |
  11. | Manager |
  12. +--------------------+

只有7种。在这种成为“坑”的列值比较少的状况下,能够考虑用“IN”来填补这个“坑”从而造成最左前缀:索引

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no='10001'
  3. AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager')
  4. AND from_date='1986-06-26';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 7 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

此次key_len为59,说明索引被用全了,可是从type和rows看出IN实际上执行了一个range查询,这里检查了7个key。看下两种查询的性能比较:

  1. SHOW PROFILES;
  2. +----------+------------+-------------------------------------------------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+-------------------------------------------------------------------------------+
  5. | 10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'|
  6. | 11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ... |
  7. +----------+------------+-------------------------------------------------------------------------------+

“填坑”后性能提高了一点。若是通过emp_no筛选后余下不少数据,则后者性能优点会更加明显。固然,若是title的值不少,用填坑就不合适了,必须创建辅助索引。

状况四:查询条件没有指定索引第一列。

  1. EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26';
  2. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where |
  6. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

因为不是最左前缀,索引这样的查询显然用不到索引。

状况五:匹配某列的前缀字符串。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%';
  2. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  5. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 56 | NULL | 1 | Using where |
  6. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

此时能够用到索引,可是若是通配符不是只出如今末尾,则没法使用索引。(原文表述有误,若是通配符%不出如今开头,则能够用到索引,但根据具体状况不一样可能只会用其中一个前缀)

状况六:范围查询。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title='Senior Engineer';
  2. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  5. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where |
  6. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

范围列能够用到索引(必须是最左前缀),可是范围列后面的列没法用到索引。同时,索引最多用于一个范围列,所以若是查询条件中有两个范围列则没法全用到索引。

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no < '10010'
  3. AND title='Senior Engineer'
  4. AND from_date BETWEEN '1986-01-01' AND '1986-12-31';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

能够看到索引对第二个范围索引无能为力。这里特别要说明MySQL一个有意思的地方,那就是仅用explain可能没法区分范围索引和多值匹配,由于在type中这二者都显示为range。同时,用了“between”并不意味着就是范围查询,例以下面的查询:

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no BETWEEN '10001' AND '10010'
  3. AND title='Senior Engineer'
  4. AND from_date BETWEEN '1986-01-01' AND '1986-12-31';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 16 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

看起来是用了两个范围查询,但做用于emp_no上的“BETWEEN”实际上至关于“IN”,也就是说emp_no实际是多值精确匹配。能够看到这个查询用到了索引所有三个列。所以在MySQL中要谨慎地区分多值匹配和范围匹配,不然会对MySQL的行为产生困惑。

状况七:查询条件中含有函数或表达式。

很不幸,若是查询条件中含有函数或表达式,则MySQL不会为这列使用索引(虽然某些在数学意义上可使用)。例如:

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+

虽然这个查询和状况五中功能相同,可是因为使用了函数left,则没法为title列应用索引,而状况五中用LIKE则能够。再如:

 

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000';
  2. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where |
  6. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

显然这个查询等价于查询emp_no为10001的函数,可是因为查询条件是一个表达式,MySQL没法为其使用索引。看来MySQL尚未智能到 自动优化常量表达式的程度,所以在写查询语句时尽可能避免表达式出如今查询中,而是先手工私下代数运算,转换为无表达式的查询语句。

 

索引选择性与前缀索引

既然索引能够加快查询速度,那么是否是只要是查询语句须要,就建上索引?答案是否认的。由于索引虽然加快了查询速度,但索引也是有代价的:索引文件 自己要消耗存储空间,同时索引会加剧插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,所以索引并非越多越好。通常两种情 况下不建议建索引。

第一种状况是表记录比较少,例如一两千条甚至只有几百条记录的表,不必建索引,让查询作全表扫描就行了。至于多少条记录才算多,这个我的有我的的见解,我我的的经验是以2000做为分界线,记录数不超过 2000能够考虑不建索引,超过2000条能够酌情考虑索引。

另外一种不建议建索引的状况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:

Index Selectivity = Cardinality / #T

显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,上文用到的employees.titles表,若是title字段常常被单独查询,是否须要建索引,咱们看一下它的选择性:

  1. SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.0000 |
  6. +-------------+

title的选择性不足0.0001(精确值为0.00001579),因此实在没有什么必要为其单独建索引。

有一种与索引选择性有关的索引优化策略叫作前缀索引,就是用列的前缀代替整个列做为索引key,当前缀长度合适时,能够作到既使得前缀索引的选择性 接近全列索引,同时由于索引key变短而减小了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使 用。

从图12能够看到employees表只有一个索引<emp_no>,那么若是咱们想按名字搜索一我的,就只能全表扫描了:

  1. EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
  2. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
  3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
  4. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where |
  6. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+

若是频繁按名字搜索员工,这样显然效率很低,所以咱们能够考虑建索引。有两种选择,建<first_name>或<first_name, last_name>,看下两个索引的选择性:

  1. SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.0042 |
  6. +-------------+
  7. SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
  8. +-------------+
  9. | Selectivity |
  10. +-------------+
  11. | 0.9313 |
  12. +-------------+

<first_name>显然选择性过低,<first_name, last_name>选择性很好,可是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?能够考虑用 first_name和last_name的前几个字符创建索引,例如<first_name, left(last_name, 3)>,看看其选择性:

  1. SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.7879 |
  6. +-------------+

选择性还不错,但离0.9313仍是有点距离,那么把last_name前缀加到4:

  1. SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.9007 |
  6. +-------------+

这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,咱们把这个前缀索引 建上:

  1. ALTER TABLE employees.employees
  2. ADD INDEX `first_name_last_name4` (first_name, last_name(4));

此时再执行一遍按名字查询,比较分析一下与建索引前的结果:

  1. SHOW PROFILES;
  2. +----------+------------+---------------------------------------------------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+---------------------------------------------------------------------------------+
  5. | 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
  6. | 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
  7. +----------+------------+---------------------------------------------------------------------------------+

性能的提高是显著的,查询速度提升了120多倍。

前缀索引兼顾索引大小和查询速度,可是其缺点是不能用于ORDER BY和GROUP BY操做,也不能用于Covering index(即当索引自己包含查询所需所有数据时,再也不访问数据文件自己)。