第五章 建立高性能的索引

這是《高性能 MySQL(第三版)》第五章的讀書筆記。mysql

索引在 MySQL 中也叫鍵(Key),是存儲引擎用於快速找到記錄的一種數據結構。web

表的數據量增大時,索引對良好的性能很是關鍵。索引是優化查詢性能的最有效的手段。sql

1. 索引基礎

MySQL 中,存儲引擎先在索引中找到對應值,而後根據匹配的索引記錄找到對應的數據行。數據庫

mysql> SELECT * FROM blog.user WHERE user_id = 5;

若是 user_id 列上建有索引,MySQL 將使用該索引找到 user_id 爲 5 的行。MySQL 先在索引上按值進行查找,而後返回全部包含該值的數據行。緩存

索引能夠包含一個或多個列的值。若是包含多個列,MySQL 只能高效使用最左側的前綴列。bash

1.1 索引類型

B-Tree 索引

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 索引適合的查詢類型有:

  • 全值匹配:同時匹配索引中的全部列,例如上面示例中,查找姓名爲 Cuba ALlen,生日爲 1960-01-01 的人。例如:
SELECT * FROM people WHERE last_name = 'Allen' AND first_name = 'Cuba' AND dob = '1960-01-01';
  • 匹配最左值:只匹配索引中的第一列。例如:
SELECT * FROM people WHERE last_name = 'Allen';
  • 匹配列前綴:匹配索引中的第一列的前綴,例如查找姓氏是 A 的人。例如:
SELECT * FROM people WHERE last_name LIKE 'A%';
  • 匹配範圍值:匹配索引中的第一列的範圍,例如查找姓氏在 Allen 和 Eine 之間的人。例如:
SELECT * FROM people WHERE last_name BETWEEN 'Allen' AND 'Eine';
  • 精確匹配某一列並範圍匹配另外一列:索引的第一列徹底匹配,第二列範圍匹配。例如,查找姓氏是 Allen,名字是字母 K 開頭的人。例如:
SELECT * FROM people WHERE last_name = 'Allen' AND first_name LIKE 'K%';
  • 只訪問索引的查詢:查詢只須要訪問索引,無需訪問數據行。例如:
SELECT last_name FROM people WHERE last_name LIKE 'A%';

B-Tree 索引的限制:

  • 要使用 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 子句中只包含哈希值時,衝突時返回多行數據而不是一行數據。

空間數據索引 R-Tree

用於地理數據存儲。空間索引會從全部維度來索引數據,可使用任意維度來組合查詢。MySQL 的 GIS 支持並不完善,可使用 PostgreSQL 的 PostGIS。

全文索引

查找文本中的關鍵詞,而不是直接比較索引中的值。相似於搜索引擎,而不是簡單的 WHERE 條件匹配。

在同一個列上能夠同時建立全文索引和基於值的 B-Tree 索引。全文索引適用於 MATCH AGAINST 操做,而不是普通的 WHERE 條件操做。

2. 索引的優勢

經過索引,服務器能夠快速定位到表的位置。

對於 B-Tree 索引,按照順序存儲數據,MySQL 能夠進行 ORDER BY 和 GROUP BY 操做。由於數據有序存儲,因此 B-Tree 也會將相關的列值存儲在一塊兒。若是要查詢的字段包含在索引中,則只使用索引就能完成查詢。索引優勢:

  1. 減小服務器須要掃描的數據量。
  2. 幫助服務器避免排序和臨時表。
  3. 將隨機 I/O 變爲順序 I/O。

數據量小的表,一般全局掃描更高效。中大型表,使用索引更有效。對於特大型表,創建和使用索引的代價也隨之增加,須要使用其餘技術,例如分區。

3. 高性能的索引策略

3.1 獨立的列

查詢語句中,若是列不是獨立的,MySQL 就不會使用索引。列不是獨立的,指的是列是表達式的一部分,或函數參數。例如:

SELECT ... WHERE id + 1 = 5;
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;

3.2 前綴索引和索引選擇性

在很長的字符列上使用索引時,會使索引大且慢。可使用前面的模擬哈希索引。

也能夠只索引開始的部分字符。對於 BLOB、TEXT 或很長的 VARCHAR 類型的列,必須使用前綴索引。須要選擇足夠長的前綴以保證較高的選擇性(防止重複),同時不能太長以節約空間。

前綴索引的優缺點:

  • 使索引更小、更快。
  • MySQL 沒法使用前綴索引作 ORDER BY 和 GROUP BY,也沒法使用前綴索引作覆蓋掃描。

肯定合適的前綴長度

1. 生成測試數據

在示例數據庫 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

2. 計算最佳前綴長度

能夠經過計算完整列的選擇性來計算合適的前綴長度,使前綴的選擇性接近於完整列的選擇性。計算完整列的選擇性:

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

3.3 多列索引

多列索引同時在多個列上創建一個索引,索引列的順序很重要。對於 AND 條件致使的索引相交時,最好使用一個包含全部相關列的多列索引,而不是多個獨立的單列索引。

3.4 選擇合適的索引列順序

最經常使用的規則:將選擇性最高的列放到索引最前列。

3.5 聚簇索引

聚簇索引是一種數據存儲方式,而不是索引類型。InnoDB 的聚簇索引在同一個結構中保存了 B-Tree 索引和數據行。

表有聚簇索引時,數據行實際上存放在索引的葉子頁中。「聚簇」表示將數據行和相鄰的鍵值存儲在一塊兒。由於數據行只能存儲在一個地方,因此一個表只能有一個聚簇索引。

聚簇索引的優勢:

  • 把相關數據保存在一塊兒。例如電子郵箱,根據用戶 ID 彙集數據,只需從磁盤讀少許數據頁便可獲取某用戶的所有郵件。不使用聚簇索引的話,每封電子郵件均可能致使一次磁盤 I/O。
  • 數據訪問更快。聚簇索引將索引和數據保存在同一個 B-Tree 中,讀數據快。
  • 使用覆蓋索引掃描的查詢可用直接使用頁節點中的主鍵值。

聚簇索引的缺點:

  • 聚簇索引提升了 I/O 密集型應用的性能。對於內存型存儲引擎用不到。
  • 插入速度嚴重依賴插入順序。按主鍵順序插入是加載數據到 InnoDB 表中速度最快的方式。
  • 更新聚簇索引列的代價很高。
  • 基於聚簇索引的表在插入新行,或主鍵被更新致使須要移動行時,可能致使「頁分裂」問題。當行的主鍵值要求必須將這行插入某個已滿的頁中,存儲引擎會將該頁分裂成兩個頁面來容納該行。
  • 聚簇索引使全表掃描變慢。尤爲是行比較稀疏,或頁分裂致使數據不連續的時候。
  • 二級索引(非聚簇索引)訪問須要兩次索引查找,而不是一次。

3.6 覆蓋索引

通常經過查詢的 WHERE 條件來建立合適的索引。可是設計優秀的索引應該考慮整個查詢。對於常用的某幾個列,能夠考慮添加覆蓋索引。

覆蓋索引:索引包含(覆蓋)全部須要查詢的字段的值。MySQL 只能用 B-Tree 索引作覆蓋索引。

覆蓋索引的優勢:

  • 索引條目一般遠小於數據行大小,容易放入內存緩存,且能夠減小數據訪問量
  • 索引按照列值的順序存儲(至少在單個頁內如此)
  • 一些存儲引擎如 MyISAM 在內存中只緩存索引
  • 因爲 InnoDB 的聚簇索引,覆蓋索引對 InnoDB 表特別有用。InnoDB 的二級索引在葉子節點中保存了行的主鍵值,若是二級主鍵可以覆蓋查詢,能夠避免對主鍵索引的二次查詢

3.7 使用索引掃描來作排序

MySQL 有兩種方式生成有序結果:

  • 經過排序操做 ORDER BY 或 GROUP BY。Extra 列顯示「Using filesort」。
  • 按索引順序掃描。若是 EXPLAIN 出來的 type 列的值爲「index」,則說明 MySQL 使用了索引掃描來排序。不要和 Extra 列的「Using index」搞混。

掃描索引自己是很快的,只須要從一條索引記錄移動到緊接着的下一條記錄。但若是索引不能覆蓋查詢所需的所有列(例如 SELECT * 操做),就不得不每掃描一條索引記錄就回表查詢一次對應的行,這基本上都是隨機 I/O。按索引順序讀數據一般比順序地全表掃描速度慢,尤爲是 I/O 密集型工做負載。

MySQL 的索引能夠同時用於排序和查找行。

用索引(而不是排序操做)對結果排序的規則:

  • 只有當索引的列順序和 ORDER BY 子句的順序徹底一致,而且全部列的排序方向(倒序或正序)都同樣時,MySQL 才能使用索引來對結果作排序。
  • 若是查詢須要關聯多張表,則只有當 ORDER BY 子句引用的字段所有爲第一個表時,才能使用索引作排序。
  • ORDER BY 子句和查找型查詢的限制是同樣的:須要知足索引的最左前綴的要求。例外狀況:前導列爲常量,若是 WHERE 子句或 JOIN 子句將這些列指定爲常量,則仍會使用索引排序。

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)

3.8 壓縮(前綴壓縮)索引

MyISAM 使用前綴壓縮來減少索引的大小,將更多索引放入內存中。默認只壓縮字符串,能夠開啓對整數的壓縮。

壓縮後,磁盤空間佔用變爲以前的十分之一,但速度變慢。I/O 密集型應用能夠考慮壓縮,CPU 密集型應用就算了。

CREATE TABLE 時指定 PACK_KEYS 參數來控制索引壓縮方式。

3.9 冗餘和重複索引

能夠在同一個列上建立多個索引。MySQL 須要單獨維護重複的索引,且優化器在優化查詢的時候也須要逐個考慮,影響性能。

重複索引:在同一個列上按相同順序建立相同類型的索引。這是錯誤的用法。

冗餘索引:若是建立了索引(A,B),再建立索引(A)就是冗餘索引,由於這只是前一個索引的前綴索引。索引(A,B)也能夠當作索引(A)來使用(冗餘只是針對 B-Tree 索引而言)。再建立索引(B,A)或(B)都不是冗餘索引。另外,不一樣類型的索引(例如哈希索引或全文索引)也不會是 B-Tree 索引的冗餘索引,無論覆蓋哪一個列。通常不須要冗餘索引。

冗餘索引一般發生在添加新索引時,例如:

  • 增長一個新索引(A,B)而不是擴展已有的索引(A)
  • 將索引(A)擴展爲(A,ID),其中 ID 是主鍵。對於 InnoDB 來講,主鍵列已經包含在二級索引中了,這會冗餘。

3.10 未使用的索引

查找方法:在 MariaDB 中打開 userstates 服務器變量(默認是關閉的),讓服務器運行一段時間後,經過查詢 INFORMATION_SCHEMA.INDEX_STATISTICS 能夠查到每一個索引的使用頻率。

另外,可使用 Perconna Tollkit 中的 pt-index-usage 工具,讀取查詢日誌後對每條查詢進行 EXPLAIN 操做,而後打印關於索引和查詢的報告。

3.11 索引和鎖

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)

4. 索引案例學習

網站用戶信息表具備不少列,例如國家、城市、地區、性別,須要支持這些特徵來搜索用戶。同時,須要根據評分、最後登陸時間等對用戶排序並限制結果。

首先須要考慮的是使用索引排序,仍是先檢索數據再排序。使用索引排序須要嚴格限制索引和查詢的設計。例如,若是但願用索引作根據評分的排序,則 WHERE 條件中的 age BETWEEN 18 AND 25 就沒法使用索引。若是使用某個索引進行範圍查詢,也就沒法再使用另外一個索引(或該索引的後續字段)進行排序了。

4.1 支持多種過濾條件

有不少不一樣值的列(例如姓名),及頻繁在 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 中組合。若是組合數達到上千個,則會耗時耗內存,須要避免。

4.2 避免多個範圍條件

從 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 沒法再使用範圍列後面的其餘索引列了。

4.3 優化排序

使用 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>);

5. 維護索引和表

5.1 找到並修復損壞的表

若是遇到古怪的問題,能夠嘗試 CHECK TABLE 檢查是否發生了表損壞,一般能找出大多數的表和索引錯誤。

能夠用 REPAIR TABLE 命令來修復損壞的表。若是存儲引擎不支持這個命令,能夠經過不作任何操做的 ALTER 操做來重建表,例如修改表的存儲引擎爲當前引擎。

ALTER TABLE tb ENGINE=INNODB;

另外,某些存儲引擎提供離線工具,例如 myisamchk。若是損壞的是系統區域或數據區域,而不是索引,則須要從備份中恢復表。

5.2 更新索引統計信息

MySQL 的查詢優化器會經過兩個 API 來了解存儲引擎的索引值的分佈信息,以決定如何使用索引:

  • records_in_range():經過向存儲引擎傳入兩個邊界值獲取在這個範圍內大概有多少條記錄。MyISAM 能夠返回精確值,可是 InnoDB 返回估計值。
  • info():返回各類類型的數據,包括索引的基數(每一個鍵值有多少條記錄)。

經過 ANALYZE TABLE 能夠從新生成統計信息。若是存儲引擎提供的掃描行數不許確,或執行計劃太複雜以至沒法準確得到各個階段匹配的行數時,優化器會使用索引統計信息來估算掃描行數。

  • Memory 引擎不存儲索引統計信息。
  • MyISAM 將索引統計信息存儲在磁盤上。ANALYZE TABLE 須要進行全索引掃描來計算索引基數。整個過程都須要鎖表。
  • InnoDB 經過隨機索引訪問進行評估並將其存儲在內存中。

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)

5.3 減小索引和數據的碎片

B-Tree 索引會致使碎片化,下降查詢效率。碎片化的索引無序存儲在磁盤上。

根據設計,B-Tree 須要隨機磁盤訪問才能定位到葉子頁,沒法避免隨機訪問。可是若是葉子頁在物理分佈上是順序且緊密的,查詢性能會更好。不然對於範圍查詢、索引覆蓋掃描,速度會慢不少倍。

表的數據存儲也可能碎片化,有三種類型:

  • 行碎片:數據行存儲在多個地方的多個片斷中。只要訪問這一行數據,性能都會降低。
  • 行間碎片:邏輯上順序的頁或行在磁盤上不是順序存儲的。對全表掃描和聚簇索引掃描影響很大。
  • 剩餘空間碎片:數據頁中有大量空餘空間。浪費磁盤。

MyISAM 表會發生上面三種碎片,InnoDB 表不會出現行碎片。

能夠經過 OPTIMIZE TABLE 或導出再導入的方式從新整理數據。

6. 總結

MySQL 默認使用 B-Tree 索引。

在選擇索引和編寫利用索引的查詢時,三個原則:

  • 單行訪問很慢。可使用索引。
  • 按順序訪問範圍數據很快。順序 I/O 不須要屢次磁盤尋道,且順序讀取的數據不須要額外排序。
  • 索引覆蓋查詢很快。索引包含了所需列時,不須要回表查找行。

應對措施:

  • 選擇合適索引以免單行查找
  • 儘量使用數據原生順序,避免額外的排序操做
  • 儘量使用索引覆蓋查詢
相關文章
相關標籤/搜索