这是《高性能 MySQL(第三版)》第五章的读书笔记。mysql
索引在 MySQL 中也叫键(Key),是存储引擎用于快速找到记录的一种数据结构。web
表的数据量增大时,索引对良好的性能很是关键。索引是优化查询性能的最有效的手段。sql
MySQL 中,存储引擎先在索引中找到对应值,而后根据匹配的索引记录找到对应的数据行。数据库
mysql> SELECT * FROM blog.user WHERE user_id = 5;
若是 user_id 列上建有索引,MySQL 将使用该索引找到 user_id 为 5 的行。MySQL 先在索引上按值进行查找,而后返回全部包含该值的数据行。缓存
索引能够包含一个或多个列的值。若是包含多个列,MySQL 只能高效使用最左侧的前缀列。bash
B-Tree 索引使用 B-Tree 数据结构来存储数据。服务器
MySQL 的默认索引类型,大多数存储引擎都支持,只是存储结构会有所差别。例如 InnoDB 使用 B+Tree。数据结构
存储引擎以不一样方式使用 B-Tree 索引,性能也不一样。MyISAM 使用前缀压缩技术,使索引更小,而 InnoDB 则按照原数据格式进行存储。MyISAM 索引经过数据的物理位置引用被索引的行,而 InnoDB 则根据主键引用被索引的行。svg
B-Tree 对索引列是顺序组织存储的,适合查找某个范围内的数据。对于基于文本域的索引树,索引按字母排序。函数
对于下面的数据表:
CREATE TABLE people ( last_name VARCHAR(50) NOT NULL, first_name VARCHAR(50) NOT NULL, dob date NOT NULL, gender ENUM('m', 'f') NOT NULL, key(last_name, first_name, dob) );
INSERT INTO people VALUES ('Allen', 'Cuba', '1960-01-01', 'f'), ('Akroyd', 'Debbie', '1990-03-01', 'f'), ('Akroyd', 'Kristin', '1978-03-01', 'f'), ('Allen', 'Kristin', '1990-03-01', 'f'), ('Allen', 'Kim', '1920-03-01', 'f'), ('Allen', 'Merry', '1930-03-01', 'f'), ('Barry', 'Julia', '1990-03-01', 'f'), ('Bssia', 'Vivew', '1990-03-01', 'f'), ('Bssia', 'Vivew', '1960-03-01', 'f') ;
表中每行数据的索引中包含了 last_name, first_name, dob 三列。对应的内存结构是:
B-Tree 索引适合的查询类型有:
SELECT * FROM people WHERE last_name = 'Allen' AND first_name = 'Cuba' AND dob = '1960-01-01';
SELECT * FROM people WHERE last_name = 'Allen';
SELECT * FROM people WHERE last_name LIKE 'A%';
SELECT * FROM people WHERE last_name BETWEEN 'Allen' AND 'Eine';
SELECT * FROM people WHERE last_name = 'Allen' AND first_name LIKE 'K%';
SELECT last_name FROM people WHERE last_name LIKE 'A%';
B-Tree 索引的限制:
SELECT * FROM people WHERE last_name LIKE '%en'; <----------没用到索引中最左列的前缀
SELECT * FROM people WHERE first_name LIKE 'K%'; <----------没用到索引中的最左列
SELECT * FROM people WHERE dob = '1960-01-01'; <----------没用到索引中的最左列
SELECT * FROM people WHERE last_name= 'Allen' and first_name LIKE 'K%' and dob = '1920-03-01';
哈希索引基于哈希表实现,只有精确匹配索引全部列的查询才会有效。对于每一行数据,存储引擎都会对全部的索引列计算一个哈希码。哈希索引将全部的哈希码存储在索引中,同时在哈希表中保存指向每一个数据行的指针。
MySQL 中只有 Memory 引擎显式支持哈希索引,且支持非惟一哈希索引。若是多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。
对于下面的数据表:
CREATE TABLE hash_test ( fname VARCHAR(50) NOT NULL, Lname VARCHAR(50) NOT NULL, KEY USING HASH(fname) ) ENGINE=MEMORY;
INSERT INTO hash_test VALUES ('Arjen', 'Lentz'), ('Baron', 'Shwora'), ('Peter', 'Zaier'), ('Vadim', 'Tkachen') ;
假设使用哈希函数 f(),其返回值以下:
f('Arjen') = 2323
f('Baron') = 7837
f('Peter') = 8784
f('Vadim') = 2458
对应的哈希索引的数据结构以下:
Slot | Value |
---|---|
2323 | 指向第 1 行的指针 |
2458 | 指向第 4 行的指针 |
7837 | 指向第 2 行的指针 |
8784 | 指向第 3 行的指针 |
其中每一个 Slot 的编号是顺序的,可是数据行不是。对于下面的查询:
SELECT * FROM hash_test WHERE fname = 'Peter';
MySQL 会首先计算 Peter 的哈希值,并用该值寻找对应的记录指针,最后对比字段数据。哈希值为 8784,从而找到指向第 3 行的指针,而后对比这一行的字段数据是不是 Peter。
哈希索引的限制:
=
、IN()
、<=>
。不支持范围查询。哈希索引只适用于一些特定的场合。例如星型 schema,须要关联不少查找表,哈希索引适合查找表的需求。
InnoDB 引擎有一个特殊功能“自适应哈希索引(adaptive hash index)。当 InnoDB 引擎注意到某些索引值被使用的很是频繁时,它会在内存中基于 B-Tree 索引之上再建立一个哈希索引。这个功能彻底自动,用户没法控制,可是能够关闭。
若是存储引擎不支持哈希索引,能够参考 InnoDB,在 B-Tree 基础上建立一个伪哈希索引。这个索引仍是使用 B-Tree 进行查找,可是使用哈希值而不是键自己进行索引查找。须要在查询的 WHERE 字句中手动指定使用哈希函数。
例如,须要对某个很长的 URL 字段进行索引。若是使用 B-Tree 来存储,须要消耗大量存储空间。可是插入、查询等语句简单:
SELECT * FROM url WHERE url = "http://www.baidu.com";
如今,删除原来 url 列上的索引,增长一个被索引的 url_crc 列,使用 CRC32 作哈希,查询语句以下:
SELECT * FROM url WHERE url = "http://www.baidu.com" AND url_crc = CRC32("http://www.baidu.com");
MySQL 优化器会使用这个选择性很高而体积很小的基于 url_crc 的索引来完成查询,高效。若是对完整的 URL 字符串作索引,会很是慢。
哈希值能够手动维护,也能够经过触发器维护。经过触发器在插入和更新时维护 url_crc 列的示例:
CREATE TABLE pseudo_hash ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, url VARCHAR(255) NOT NULL, url_crc INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY(id) );
建立触发器,先经过 DELIMITER 临时修改分隔符,这样就能够在触发器定义中使用分号:
DELIMITER //
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudo_hash FOR EACH ROW BEGIN SET NEW.url_crc = CRC32(NEW.url);
END;
//
CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudo_hash FOR EACH ROW BEGIN SET NEW.url_crc = CRC32(NEW.url);
END;
//
DELIMITER ;
验证触发器可否维护哈希索引:
mysql> INSERT INTO pseudo_hash (url) VALUES('http://www.mysql.com') ; Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM pseudo_hash; +----+----------------------+------------+
| id | url | url_crc | +----+----------------------+------------+
| 1 | http://www.mysql.com | 1560514994 | +----+----------------------+------------+
1 row in set (0.00 sec)
mysql> UPDATE pseudo_hash SET url='http://www.baidu.com' WHERE id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> SELECT * FROM pseudo_hash; +----+----------------------+------------+
| id | url | url_crc | +----+----------------------+------------+
| 1 | http://www.baidu.com | 3500265894 | +----+----------------------+------------+
1 row in set (0.00 sec)
注意,这里的哈希函数尽可能使用 CRC32(),若是使用 MD5() 或 SHA1(),尽管碰撞几率下降了,可是哈希值很是长,且查询速度慢。
mysql> SELECT CRC32('http://www.baidu.com'); +-------------------------------+
| CRC32('http://www.baidu.com') | +-------------------------------+
| 3500265894 | <---------------INT 类型,32 位 +-------------------------------+
1 row in set (0.00 sec)
mysql> SELECT MD5('http://www.baidu.com'); +----------------------------------+
| MD5('http://www.baidu.com') | +----------------------------------+
| bfa89e563d9509fbc5c6503dd50faf2e | <---------------String 类型,128 位 +----------------------------------+
1 row in set (0.00 sec)
mysql> SELECT SHA1('http://www.baidu.com'); +------------------------------------------+
| SHA1('http://www.baidu.com') | +------------------------------------------+
| 633a42441e296c9004a78abe0b2ee3b37559d32f | <---------------String 类型,160 位 +------------------------------------------+
1 row in set (0.00 sec)
若是数据量很是大,CRC32() 出现大量冲突时,能够本身实现返回 64 位整数的哈希函数。简单示例以下:
mysql> SELECT CONV(RIGHT(MD5('http://www.baidu.com'), 16), 16, 10) AS HASH64; +----------------------+
| HASH64 | +----------------------+
| 14251166297358315310 | +----------------------+
1 row in set (0.00 sec)
MD5() 函数返回 32 位 16 进制的哈希值,RIGHT() 函数返回哈希值最后的 16 位字符。CONV() 函数将字符串从 16 进制转为 10 进制。
使用哈希索引进行查询时,必须在 WHERE 子句中包含哈希值和对应列值,即便发生冲突也能够正常工做:
SELECT * FROM url WHERE url = "http://www.baidu.com" AND url_crc = CRC32("http://www.baidu.com");
CRC32() 返回的是 32 位整数,当索引有 93000 条记录时出现冲突的几率是 1%。将 /usr/share/dict/words
中的词导入数据表,会有 98569 行。WHERE 子句中只包含哈希值时,冲突时返回多行数据而不是一行数据。
用于地理数据存储。空间索引会从全部维度来索引数据,可使用任意维度来组合查询。MySQL 的 GIS 支持并不完善,可使用 PostgreSQL 的 PostGIS。
查找文本中的关键词,而不是直接比较索引中的值。相似于搜索引擎,而不是简单的 WHERE 条件匹配。
在同一个列上能够同时建立全文索引和基于值的 B-Tree 索引。全文索引适用于 MATCH AGAINST 操做,而不是普通的 WHERE 条件操做。
经过索引,服务器能够快速定位到表的位置。
对于 B-Tree 索引,按照顺序存储数据,MySQL 能够进行 ORDER BY 和 GROUP BY 操做。由于数据有序存储,因此 B-Tree 也会将相关的列值存储在一块儿。若是要查询的字段包含在索引中,则只使用索引就能完成查询。索引优势:
数据量小的表,一般全局扫描更高效。中大型表,使用索引更有效。对于特大型表,创建和使用索引的代价也随之增加,须要使用其余技术,例如分区。
查询语句中,若是列不是独立的,MySQL 就不会使用索引。列不是独立的,指的是列是表达式的一部分,或函数参数。例如:
SELECT ... WHERE id + 1 = 5;
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
在很长的字符列上使用索引时,会使索引大且慢。可使用前面的模拟哈希索引。
也能够只索引开始的部分字符。对于 BLOB、TEXT 或很长的 VARCHAR 类型的列,必须使用前缀索引。须要选择足够长的前缀以保证较高的选择性(防止重复),同时不能太长以节约空间。
前缀索引的优缺点:
在示例数据库 sakila 中没有合适的例子,须要从表 city 中生成一个示例表:
CREATE TABLE sakila.city_demo(city VARCHAR(50) NOT NULL);
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo; -- <---- 这一行重复屡次
UPDATE sakila.city_demo SET city = (SELECT city FROM sakila.city ORDER BY RAND() LIMIT 1);
例以下面的例子:
MariaDB [sakila]> CREATE TABLE sakila.city_demo(city VARCHAR(50) NOT NULL);
Query OK, 0 rows affected (0.196 sec)
MariaDB [sakila]> INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city;
Query OK, 600 rows affected (0.032 sec)
Records: 600 Duplicates: 0 Warnings: 0
MariaDB [sakila]> INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
Query OK, 600 rows affected (0.004 sec)
Records: 600 Duplicates: 0 Warnings: 0
MariaDB [sakila]> INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
Query OK, 1200 rows affected (0.007 sec)
Records: 1200 Duplicates: 0 Warnings: 0
...
MariaDB [sakila]> INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
Query OK, 76800 rows affected (0.277 sec)
Records: 76800 Duplicates: 0 Warnings: 0
MariaDB [sakila]> UPDATE sakila.city_demo SET city = (SELECT city FROM sakila.city ORDER BY RAND() LIMIT 1);
Query OK, 153358 rows affected (1 min 6.620 sec)
Rows matched: 153600 Changed: 153358 Warnings: 0
能够经过计算完整列的选择性来计算合适的前缀长度,使前缀的选择性接近于完整列的选择性。计算完整列的选择性:
MariaDB [sakila]> SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo; +-------------------------------+ | COUNT(DISTINCT city)/COUNT(*) |
+-------------------------------+
| 0.0039 |
+-------------------------------+
1 row in set (0.119 sec)
计算不一样前缀长度的选择性:
MariaDB [sakila]> SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3, -> COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
-> COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
-> COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6,
-> COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7
-> FROM sakila.city_demo; +--------+--------+--------+--------+--------+
| sel3 | sel4 | sel5 | sel6 | sel7 | +--------+--------+--------+--------+--------+
| 0.0030 | 0.0037 | 0.0038 | 0.0039 | 0.0039 | +--------+--------+--------+--------+--------+
1 row in set (0.312 sec)
前缀长度达到 7 时,选择性提高的幅度基本稳定。
MariaDB [sakila]> ALTER TABLE sakila.city_demo ADD KEY(city(7));
Query OK, 0 rows affected (3.350 sec)
Records: 0 Duplicates: 0 Warnings: 0
多列索引同时在多个列上创建一个索引,索引列的顺序很重要。对于 AND 条件致使的索引相交时,最好使用一个包含全部相关列的多列索引,而不是多个独立的单列索引。
最经常使用的规则:将选择性最高的列放到索引最前列。
聚簇索引是一种数据存储方式,而不是索引类型。InnoDB 的聚簇索引在同一个结构中保存了 B-Tree 索引和数据行。
表有聚簇索引时,数据行实际上存放在索引的叶子页中。“聚簇”表示将数据行和相邻的键值存储在一块儿。由于数据行只能存储在一个地方,因此一个表只能有一个聚簇索引。
聚簇索引的优势:
聚簇索引的缺点:
通常经过查询的 WHERE 条件来建立合适的索引。可是设计优秀的索引应该考虑整个查询。对于常用的某几个列,能够考虑添加覆盖索引。
覆盖索引:索引包含(覆盖)全部须要查询的字段的值。MySQL 只能用 B-Tree 索引作覆盖索引。
覆盖索引的优势:
MySQL 有两种方式生成有序结果:
扫描索引自己是很快的,只须要从一条索引记录移动到紧接着的下一条记录。但若是索引不能覆盖查询所需的所有列(例如 SELECT *
操做),就不得不每扫描一条索引记录就回表查询一次对应的行,这基本上都是随机 I/O。按索引顺序读数据一般比顺序地全表扫描速度慢,尤为是 I/O 密集型工做负载。
MySQL 的索引能够同时用于排序和查找行。
用索引(而不是排序操做)对结果排序的规则:
Sakila 示例数据库的 rental 表在列(rental_date
, inventory_id
, customer_id
)上面建立了索引。
CREATE TABLE `rental` ( `rental_id` INT(11) NOT NULL AUTO_INCREMENT, `rental_date` DATETIME NOT NULL, `inventory_id` MEDIUMINT(8) UNSIGNED NOT NULL, `customer_id` SMALLINT(5) UNSIGNED NOT NULL, `return_date` DATETIME NULL DEFAULT NULL, `staff_id` TINYINT(3) UNSIGNED NOT NULL, `last_update` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`rental_id`), UNIQUE INDEX `rental_date` (`rental_date`, `inventory_id`, `customer_id`), INDEX `idx_fk_inventory_id` (`inventory_id`), INDEX `idx_fk_customer_id` (`customer_id`), INDEX `idx_fk_staff_id` (`staff_id`), CONSTRAINT `fk_rental_customer` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ON UPDATE CASCADE, CONSTRAINT `fk_rental_inventory` FOREIGN KEY (`inventory_id`) REFERENCES `inventory` (`inventory_id`) ON UPDATE CASCADE, CONSTRAINT `fk_rental_staff` FOREIGN KEY (`staff_id`) REFERENCES `staff` (`staff_id`) ON UPDATE CASCADE ) COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=16050 ;
下面示例中,WHERE 子句将前导列指定为常量。经过 rental_date 索引为下面的查询进行排序,从 EXPLAIN 中能够看到没有出现文件排序(filesort)操做:
mysql> EXPLAIN SELECT * FROM rental WHERE rental_date='2005-05-25' ORDER BY inventory_id, customer_id; +----+-------------+--------+------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+-------------+---------+-------+------+-------------+
| 1 | SIMPLE | rental | ref | rental_date | rental_date | 8 | const | 1 | Using where | +----+-------------+--------+------+---------------+-------------+---------+-------+------+-------------+
1 row in set (0.00 sec)
MyISAM 使用前缀压缩来减少索引的大小,将更多索引放入内存中。默认只压缩字符串,能够开启对整数的压缩。
压缩后,磁盘空间占用变为以前的十分之一,但速度变慢。I/O 密集型应用能够考虑压缩,CPU 密集型应用就算了。
CREATE TABLE 时指定 PACK_KEYS 参数来控制索引压缩方式。
能够在同一个列上建立多个索引。MySQL 须要单独维护重复的索引,且优化器在优化查询的时候也须要逐个考虑,影响性能。
重复索引:在同一个列上按相同顺序建立相同类型的索引。这是错误的用法。
冗余索引:若是建立了索引(A,B),再建立索引(A)就是冗余索引,由于这只是前一个索引的前缀索引。索引(A,B)也能够当作索引(A)来使用(冗余只是针对 B-Tree 索引而言)。再建立索引(B,A)或(B)都不是冗余索引。另外,不一样类型的索引(例如哈希索引或全文索引)也不会是 B-Tree 索引的冗余索引,无论覆盖哪一个列。通常不须要冗余索引。
冗余索引一般发生在添加新索引时,例如:
查找方法:在 MariaDB 中打开 userstates 服务器变量(默认是关闭的),让服务器运行一段时间后,经过查询 INFORMATION_SCHEMA.INDEX_STATISTICS 能够查到每一个索引的使用频率。
另外,可使用 Perconna Tollkit 中的 pt-index-usage 工具,读取查询日志后对每条查询进行 EXPLAIN 操做,而后打印关于索引和查询的报告。
InnoDB 只有在访问行的时候才会对其加锁,而索引能够减小 InnoDB 访问的行数,从而减小锁的数量。索引可让查询锁定更少的行。
下面示例返回 2-4 行数据,可是会锁定 1-4 行的数据。由于 MySQL 为该查询选择的计划是索引范围扫描,存储引擎只接收了 WHERE 条件的第一部分,
mysql> SET AUTOCOMMIT=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE; +----------+
| actor_id | +----------+
| 2 |
| 3 |
| 4 | +----------+
3 rows in set (0.00 sec)
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE; +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | actor | range | PRIMARY | PRIMARY | 2 | NULL | 3 | Using where; Using index | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id < 5 FOR UPDATE; +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | actor | range | PRIMARY | PRIMARY | 2 | NULL | 4 | Using where; Using index | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
上面的例子中,Extra 列中出现了“Using WHERE”,表示 MySQL 服务器在存储引擎返回行后再应用 WHERE 过滤条件。怎么证实确实锁定了第一行数据呢,新开一个链接,访问这一行数据便可,会发现查询一直是挂起状态,直到上面窗口提交或回滚事务(留意总查询时间):
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id = 1 FOR UPDATE; +----------+
| actor_id | +----------+
| 1 | +----------+
1 row in set (45.64 sec)
网站用户信息表具备不少列,例如国家、城市、地区、性别,须要支持这些特征来搜索用户。同时,须要根据评分、最后登陆时间等对用户排序并限制结果。
首先须要考虑的是使用索引排序,仍是先检索数据再排序。使用索引排序须要严格限制索引和查询的设计。例如,若是但愿用索引作根据评分的排序,则 WHERE 条件中的 age BETWEEN 18 AND 25 就没法使用索引。若是使用某个索引进行范围查询,也就没法再使用另外一个索引(或该索引的后续字段)进行排序了。
有不少不一样值的列(例如姓名),及频繁在 WHERE 子句中出现的列,能够添加索引。
须要作范围查询的列,尽可能防在索引的后部,以便优化器能使用尽量多的索引列。
例如对于交友类网站,性别和地区是经常使用的筛选条件,且基本上使用 = 来比较,而对于年龄,则常常用范围查询 BETWEEN。city 选择性一般不高(国内有几千个城市)。sex 虽然选择性很低,可是会在不少查询中用到,能够考虑建立不一样组合索引的时候,用(sex, city)列做为前缀。 例如(sex, city, age)。
碰到不须要 sex 的查询,该如何使用这个具备(sex, city)前缀的索引呢?在查询条件中新增 AND sex IN ('m', 'f')
来让 MySQL 选择该索引。加上这个条件不会影响结果,可是能够匹配索引的最左前缀。但对于 city 就不行了,你得 IN 几千个城市。
对于这个 WHERE 子句:
WHERE eye_color IN ('blue', 'brown', 'red')
AND hair_color IN ('red', 'black', 'orange', 'white')
AND sex IN ('m', 'f')
优化器会转化成 3 * 4 * 2 总共 24 种组合,执行计划须要检查 WHERE 子句中的全部 24 中组合。若是组合数达到上千个,则会耗时耗内存,须要避免。
从 EXPLAIN 的输出中很难区分 MySQL 要查询范围值(BETWEEN、>、< 等)仍是列表值(IN,至关于多个等值条件),这两种状况对应的 type 都是 range:
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id IN (1, 4, 99); +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | actor | range | PRIMARY | PRIMARY | 2 | NULL | 3 | Using where; Using index | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.01 sec)
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id > 45; +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | actor | range | PRIMARY | PRIMARY | 2 | NULL | 155 | Using where; Using index | +----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
对于范围条件查询,MySQL 没法再使用范围列后面的其余索引列了。
使用 filesort 文件排序对于小数据集是很快的,可是若是一个查询匹配的结果又上百万行,就须要索引。例如对于下面的查询:
SELECT <cols> FROM profiles WHERE sex='m' ORDER BY rating LIMIT 10;
能够建立索引(sex, rating)。这个查询同时使用了 ORDER BY 和 LIMIT,若是没有索引会很慢。
即便有索引,用户翻页到比较靠后的时候,也会很慢:
SELECT <cols> FROM profiles WHERE sex='m' ORDER BY rating LIMIT 100000,10;
能够经过延迟关联来优化大偏移量的数据查询。先使用覆盖索引查询并返回须要的主键,而后根据这些主键关联原表得到所需数据行。这样能够减小 MySQL 对须要丢弃的行的扫描。
高效使用索引(sex, rating)进行排序和分页:
SELECT <cols> FROM profiles INNER JOIN ( SELECT <primary key cols> FROM profiles WHERE x.sex='m' ORDER BY rating LIMIT 100000, 10 ) AS x USING(<primary key cols>);
若是遇到古怪的问题,能够尝试 CHECK TABLE 检查是否发生了表损坏,一般能找出大多数的表和索引错误。
能够用 REPAIR TABLE 命令来修复损坏的表。若是存储引擎不支持这个命令,能够经过不作任何操做的 ALTER 操做来重建表,例如修改表的存储引擎为当前引擎。
ALTER TABLE tb ENGINE=INNODB;
另外,某些存储引擎提供离线工具,例如 myisamchk。若是损坏的是系统区域或数据区域,而不是索引,则须要从备份中恢复表。
MySQL 的查询优化器会经过两个 API 来了解存储引擎的索引值的分布信息,以决定如何使用索引:
经过 ANALYZE TABLE 能够从新生成统计信息。若是存储引擎提供的扫描行数不许确,或执行计划太复杂以至没法准确得到各个阶段匹配的行数时,优化器会使用索引统计信息来估算扫描行数。
SHOW INDEX FROM 命令能够查看索引的基数(Cardinality),显示了存储引擎估算索引列有多少个不一样的取值,也能够经过 INFORMATION_SCHEMA.STATISTICS:
mysql> SHOW INDEX FROM sakila.actor; +-------+------------+---------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +-------+------------+---------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| actor | 0 | PRIMARY | 1 | actor_id | A | 200 | NULL | NULL | | BTREE | | | | actor | 1 | idx_actor_last_name | 1 | last_name | A | 200 | NULL | NULL | | BTREE | | | +-------+------------+---------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 2 rows in set (0.02 sec) mysql> SELECT CARDINALITY FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME='actor'; +-------------+
| CARDINALITY | +-------------+
| 200 |
| 200 | +-------------+
2 rows in set (0.02 sec)
B-Tree 索引会致使碎片化,下降查询效率。碎片化的索引无序存储在磁盘上。
根据设计,B-Tree 须要随机磁盘访问才能定位到叶子页,没法避免随机访问。可是若是叶子页在物理分布上是顺序且紧密的,查询性能会更好。不然对于范围查询、索引覆盖扫描,速度会慢不少倍。
表的数据存储也可能碎片化,有三种类型:
MyISAM 表会发生上面三种碎片,InnoDB 表不会出现行碎片。
能够经过 OPTIMIZE TABLE 或导出再导入的方式从新整理数据。
MySQL 默认使用 B-Tree 索引。
在选择索引和编写利用索引的查询时,三个原则:
应对措施: