大约在两年前,我写了一篇关于MySQL索引的文章。最近有同窗在文章的评论中对文章的内容提出质疑,质疑主要集中在联合索引的使用方式上。在那篇文章中,我说明联合索引是将各个索引字段作字符串链接后做为key,使用时将总体作前缀匹配。html
而这名同窗在这个页面找到了以下一句话:index condition pushdown is usually useful with multi-column indexes: the first component(s) is what index access is done for, the subsequent have columns that we read and check conditions on。从而认为联合索引的使用方式与文中不符。mysql
实际上,这个页面所讲述的是在MariaDB 5.3.3(MySQL是在5.6)开始引入的一种叫作Index Condition Pushdown(如下简称ICP)的查询优化方式。因为自己不是一个层面的东西,前文中说的是Index Access,而这里是Query Optimization,因此并不构成对前文正确性的影响。在写前文时,MySQL尚未ICP,因此文中没有涉及相关内容,但考虑到新版本的MariaDB或MySQL中ICP的启用确实影响了一些查询行为的外在表现。因此决定写这篇文章详细讲述一下ICP的原理以及对索引使用方式的优化。sql
先从一个简单的实验开始直观认识ICP的做用。数据库
首先须要安装一个支持ICP的MariaDB或MySQL数据库。我使用的是MariaDB 5.5.34,若是是使用MySQL则须要5.6版本以上。bash
Mac环境下能够经过brew安装:性能
brew install mairadb
其它环境下的安装请参考MariaDB官网关于下载安装的文档。优化
与前文同样,咱们使用Employees Sample Database,做为示例数据库。完整示例数据库的下载地址为:https://launchpad.net/test-db/employees-db-1/1.0.6/+download/employees_db-full-1.0.6.tar.bz2。spa
将下载的压缩包解压后,会看到一系列的文件,其中employees.sql就是导入数据的命令文件。执行.net
mysql -h[host] -u[user] -p < employees.sql
就能够完成建库、建表和load数据等一系列操做。此时数据库中会多一个叫作employees的数据库。库中的表以下:code
MariaDB [employees]> SHOW TABLES; +---------------------+ | Tables_in_employees | +---------------------+ | departments | | dept_emp | | dept_manager | | employees | | salaries | | titles | +---------------------+ 6 rows in set (0.00 sec)
咱们将使用employees表作实验。
employees表包含雇员的基本信息,表结构以下:
MariaDB [employees]> DESC employees.employees; +------------+---------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+---------------+------+-----+---------+-------+ | emp_no | int(11) | NO | PRI | NULL | | | birth_date | date | NO | | NULL | | | first_name | varchar(14) | NO | | NULL | | | last_name | varchar(16) | NO | | NULL | | | gender | enum('M','F') | NO | | NULL | | | hire_date | date | NO | | NULL | | +------------+---------------+------+-----+---------+-------+ 6 rows in set (0.01 sec)
这个表默认只有一个主索引,由于ICP只能做用于二级索引,因此咱们创建一个二级索引:
ALTER TABLE employees.employees ADD INDEX first_name_last_name (first_name, last_name);
这样就创建了一个first_name和last_name的联合索引。
为了明确看到查询性能,咱们启用profiling并关闭query cache:
SET profiling = 1; SET query_cache_type = 0; SET GLOBAL query_cache_size = 0;
而后咱们看下面这个查询:
MariaDB [employees]> SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man'; +--------+------------+------------+-----------+--------+------------+ | emp_no | birth_date | first_name | last_name | gender | hire_date | +--------+------------+------------+-----------+--------+------------+ | 254642 | 1959-01-17 | Mary | Botman | M | 1989-11-24 | | 471495 | 1960-09-24 | Mary | Dymetman | M | 1988-06-09 | | 211941 | 1962-08-11 | Mary | Hofman | M | 1993-12-30 | | 217707 | 1962-09-05 | Mary | Lichtman | F | 1987-11-20 | | 486361 | 1957-10-15 | Mary | Oberman | M | 1988-09-06 | | 457469 | 1959-07-15 | Mary | Weedman | M | 1996-11-21 | +--------+------------+------------+-----------+--------+------------+
根据MySQL索引的前缀匹配原则,二者对索引的使用是一致的,即只有first_name采用索引,last_name因为使用了模糊前缀,无法使用索引进行匹配。我将查询联系执行三次,结果以下:
+----------+------------+---------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+---------------------------------------------------------------------------+ | 38 | 0.00084400 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | | 39 | 0.00071800 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | | 40 | 0.00089600 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | +----------+------------+---------------------------------------------------------------------------+
而后咱们关闭ICP:
SET optimizer_switch='index_condition_pushdown=off';
在运行三次相同的查询,结果以下:
+----------+------------+---------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+---------------------------------------------------------------------------+ | 42 | 0.00264400 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | | 43 | 0.01418900 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | | 44 | 0.00234200 | SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man' | +----------+------------+---------------------------------------------------------------------------+
有意思的事情发生了,关闭ICP后,一样的查询,耗时是以前的三倍以上。下面咱们用explain看看二者有什么区别:
MariaDB [employees]> EXPLAIN SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man'; +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-----------------------+ | 1 | SIMPLE | employees | ref | first_name_last_name | first_name_last_name | 44 | const | 224 | Using index condition | +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-----------------------+ 1 row in set (0.00 sec)
MariaDB [employees]> EXPLAIN SELECT * FROM employees WHERE first_name='Mary' AND last_name LIKE '%man'; +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-------------+ | 1 | SIMPLE | employees | ref | first_name_last_name | first_name_last_name | 44 | const | 224 | Using where | +------+-------------+-----------+------+----------------------+----------------------+---------+-------+------+-------------+ 1 row in set (0.00 sec)
前者是开启ICP,后者是关闭ICP。能够看到区别在于Extra,开启ICP时,用的是Using index condition;关闭ICP时,是Using where。
其中Using index condition就是ICP提升查询性能的关键。下一节说明ICP提升查询性能的原理。
ICP的原理简单说来就是将能够利用索引筛选的where条件在存储引擎一侧进行筛选,而不是将全部index access的结果取出放在server端进行where筛选。
以上面的查询为例,在没有ICP时,首先经过索引前缀从存储引擎中读出224条first_name为Mary的记录,而后在server段用where筛选last_name的like条件;而启用ICP后,因为last_name的like筛选能够经过索引字段进行,那么存储引擎内部经过索引与where条件的对比来筛选掉不符合where条件的记录,这个过程不须要读出整条记录,同时只返回给server筛选后的6条记录,所以提升了查询性能。
下面经过图两种查询的原理详细解释。
在不支持ICP的系统下,索引仅仅做为data access使用。
在ICP优化开启时,在存储引擎端首先用索引过滤能够过滤的where条件,而后再用索引作data access,被index condition过滤掉的数据没必要读取,也不会返回server端。
有几个关于ICP的事情要注意:
[1] https://mariadb.com/kb/en/index-condition-pushdown/
[2] http://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html