【總結系列】互聯網服務端技術體系:高性能之數據庫索引

引子

創建最優的數據庫索引是提高數據庫查詢性能的重要手段。在某種意義上,索引就是磁盤記錄在內存中的緩存。索引要作的事情,就是快速找到匹配條件的記錄行,並儘量減小磁盤讀寫次數。本文總結數據庫索引相關的知識及實踐。html

總入口見: 「互聯網應用服務端的經常使用技術思想與機制綱要」

mysql

基本知識

InnoDB 裏表數據是按照主鍵順序存放的。InnoDB 會按照表定義的第一個非空索引(按索引定義順序)做爲主鍵。 索引(在 MySQL 中)是由存儲引擎實現的。索引類型主要有順序索引和哈希索引。順序索引的底層結構是 B+Tree ,哈希索引的底層結構是哈希表。sql

索引是以空間換時間,減小了要掃描的數據量、避免排序、將隨機IO變成順序IO。使用索引的代價是:空間佔用更大、插入和更新成本更大。順序索引可支持:全值匹配、最左順序匹配、列前綴匹配、範圍匹配、精確匹配數列並範圍匹配一列、只訪問索引的查詢、索引掃描排序。哈希索引可支持:全值匹配。

數據庫

順序索引緩存

InnoDB 的順序索引是將主鍵列表構建成一棵 B+ 樹。內節點存放的是均是主鍵值,葉子節點存放的是整張表的行數據。這樣,可讓節點儘量存放更多的主鍵值,從而下降樹的高度。B+ 樹是有序查找平衡樹,高度一般在 2-4 之間,由於要儘量減小磁盤讀寫次數。B+ 樹的插入操做在節點關鍵數滿的狀況下,會分裂成兩個子節點。理解 B+ 樹對於理解順序索引很是關鍵。dom

順序索引能夠分爲聚簇索引和非聚簇索引。ide

  • 聚簇索引:在葉子節點中保存了 B-Tree 索引和數據行。將索引列放在內節點上,而將行數據放在葉子節點上。聚簇索引能夠極大提高 IO 密集型的性能。一個表只能有一個聚簇索引,一般用主鍵列。聚簇索引的最優插入順序是按照主鍵值順序插入。若是是隨機插入,更新聚簇索引的代價較高:更多的查找操做、頻繁的「頁分裂」、移動大量數據、產生碎片。
  • 非聚簇索引:非聚簇索引的內節點存放的是非聚簇索引列的值,葉子節點存儲的是對應數據行的主鍵值。所以,根據非聚簇索引須要兩次索引查找。先從葉子節點找到主鍵值,再根據主鍵值在聚簇索引裏找到數據行。非聚簇索引由於不存儲數據行的信息,所以佔用空間會比聚簇索引更小。

哈希索引函數

使用哈希原理實現,性能很高,只能等值匹配,按索引整列匹配、不支持範圍查找、不能用於排序。哈希函數能夠選擇 crc32 或者 md5 的一部分。哈希索引要避免大量衝突同時不佔用過多空間。哈希索引的選擇性取決於該列哈希列值的衝突度。Memory 引擎支持哈希索引,也支持 B+Tree 索引。能夠爲較長的字符串(好比 URL)建立哈希索引,在條件中必須同時帶上哈希值條件和列值條件。where url = xxx and hashed_url = yyy 。性能

InnoDB 爲某些很是頻繁的索引值在 B+ 上在內存中再建立一個哈希索引,稱爲自適應哈希索引。測試


開發事項

適合作索引的列

選擇性高原則。若是全部行在該列上的「不重複值數量/全部值數量」的比率越高,則選擇性越高,越適合作索引。列的選擇性:count(distinct(col)) / count(col) 。惟一索引的選擇性是 1。使用 show index from tablename ,Cardinality 的值展現了索引列的不重複值的預估值。能夠用來判斷這個索引是否合適。若是 Cardinality 的值接近於表的記錄總數,則是選擇性高的。

注意,在單列索引的時候,這個值對應指定索引列的 Cardinality 值,而在聯合索引中,這個值對應聯合列的 Cardinality 值。以下所示: sid_index 的值爲 41659 , tid_index 的值是 101 , sid_index 的選擇性高於 tid_index ; stc_id_index.t_id 的值是 3443139 ,是指 (s_id, t_id) 聯合索引的值,高於 sid_index 單列索引的選擇性。

如何找到高選擇性的列呢?

  • 定性分析:值比較傾向於惟一的,是高選擇性的;而值域在某個有限集合的,是低選擇性的。好比 ID 值一般是高選擇性的,而 age 值則是低選擇性的。
  • 測量分析:使用 count(distinct(col)) / count(col) 來計算,值越接近於 1 的是高選擇性的。測量分析一般用於驗證或否認。
mysql> show index from student_courses;

+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

| Table           | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |

+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

| student_courses |          0 | PRIMARY      |            1 | id          | A         |     7764823 |     NULL | NULL   |      | BTREE      |         |               |

| student_courses |          1 | stc_id_index |            1 | s_id        | A         |       40417 |     NULL | NULL   |      | BTREE      |         |               |

| student_courses |          1 | stc_id_index |            2 | t_id        | A         |     3443139 |     NULL | NULL   |      | BTREE      |         |               |

| student_courses |          1 | stc_id_index |            3 | c_id        | A         |     7764823 |     NULL | NULL   |      | BTREE      |         |               |

| student_courses |          1 | sid_index    |            1 | s_id        | A         |       41659 |     NULL | NULL   |      | BTREE      |         |               |

| student_courses |          1 | tid_index    |            1 | t_id        | A         |         101 |     NULL | NULL   |      | BTREE      |         |               |

+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

6 rows in set (0.00 sec)

構建索引

先列出全部可能的搜索語句,找到出現的列,將選擇性高的列放在最左邊,有範圍查找的列儘量放最右邊。從左開始逐個將列添加到聯合索引裏,儘量覆蓋全部搜索語句。可能須要創建多個聯合索引來覆蓋。最後,要考慮選擇語句和排序語句的列,儘量使用索引覆蓋獲取列數據,使用索引掃描來排序。

聯合索引

聯合索引也是一棵 B+ 樹,關鍵字是一個元組。相似索引的多級搜索,逐步大幅減小須要掃描和匹配的行。聯合索引搜索遵循最左匹配原則。聯合索引須要創建最優索引列順序。注意,在每一個須要搜索的列上創建單列索引,不是聯合索引(搜索的時候只能單列搜索後,再用到索引合併來合併結果)。

聯合索引匹配遵循最左匹配原則。匹配終止條件:將搜索條件按照聯合索引順序重排列,遇到等值查詢(包括 IN 查詢)繼續,遇到範圍查詢、BETWEEN、LIKE 查詢則終止。沒法使用索引的狀況:在 where 條件中,索引列在表達式中或對索引列使用函數。

實踐中,須要用相同的列但順序不一樣的聯合索引來知足不一樣的查詢需求。

前綴索引

爲長字符串創建索引。使用指定長度的字符串的前綴來創建索引。對於 BLOB, TEXT, 很長的 VARCHAR 列,必須使用前綴索引。前綴索引要選擇一個合適的長度:選擇性與整列的選擇性接近,同時不佔用過多空間。前綴索引沒法使用 GROUP BY 和 ORDER BY,沒法作覆蓋掃描。若是字符串後綴或某個部分的選擇性更高,也能夠作一些預處理轉化爲前綴索引。思想是相同的。

尋找前綴索引最佳長度的步驟:

STEP1 - 先找到該列全部值的 TOPN,可使用 count as c, col from table group by col order by c desc limit N 語句;

STEP2 - 從一個比較合適的值(好比 3)開始,測試選擇性,直到 TOPN 絕大部分列的 c 的數量與 TOPN 的 c 比較接近。


覆蓋索引

覆蓋索引的列包含了全部須要查詢的列,能夠減小大量的磁盤讀,大幅提高性能。若是某個列在 select cols 字句中頻繁出現,也能夠考慮放在聯合索引裏,利用覆蓋索引來優化性能。延遲關聯技術可使用覆蓋索引能力。

索引掃描排序

只有當索引的列順序與 ORDER BY 字句的順序徹底一致,而且全部列的排序方向都同樣時,才能使用索引對結果作排序。有一個例外,就是前導列條件指定爲常數。好比 (date, fans_id) 對於 where date = 'xxx' order by fans_id desc 也可使用索引掃描排序。

索引提示

可使用 FORCE INDEX(a) 強制指定 SQL 語句要使用的索引。

MRR

Multi-Range Read。針對範圍查詢的優化。MRR 會將查詢到的輔助索引鍵放到緩存裏,而後按照主鍵排序(將隨機 IO 轉換爲順序 IO,能夠減小頁替換),再根據排序後主鍵來順序來訪問實際數據。適用於 range, ref, eq_ref 的查詢。

MRR 默認開啓。使用 optimizer_switch 的標記來控制是否使用MRR.設置mrr=on時,表示啓用MRR優化。
SET @@optimizer_switch='mrr=on,mrr_cost_based=on';


「系統帳號」問題

索引列的某個值出現次數很是多。應避免使用系統帳號值出如今查詢語句裏。

索引實驗

準備工做

準備表

假設有個學生選課表。以下所示:

## executed using root account
## mysql -uroot -p < /path/to/project.sql

DROP USER 'test'@'localhost';
drop database if exists test;

CREATE USER 'test'@'localhost' IDENTIFIED BY 'test';
create database test ;
grant all privileges on test.* to 'test'@'localhost' identified by 'test';

use test
drop table if exists student_courses;
create table student_courses (
    id int(10) UNSIGNED not null primary key AUTO_INCREMENT comment 'AUTO_INCREMENT ID',
    s_id varchar(64) not null comment 'student ID',
    t_id varchar(64) not null comment 'teacher ID',
    room varchar(64) not null comment 'room name',
    c_id varchar(32) not null comment 'course ID',
    c_time int(10) not null comment 'course time',
    extra varchar(256) default '' comment 'extra info',
    gmt_create datetime DEFAULT CURRENT_TIMESTAMP,
      gmt_modified datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

準備數據

寫個 groovy 腳本生成 800w 條選課數據。批量插入的效率更高。單個插入,每刷新一次,幾千的插入;批量插入,每刷新一次,20w 的插入。

package cc.lovesq.study.data

class StudentCoursesDataGenerator {

    private static final STUDENT_PREFIX = "STU";
    private static final TEACHER_PREFIX = "TCH";
    private static final ROOM_PREFIX = "ROOM";
    private static final COURSE_PREFIX = "CRE";

    static Random random = new Random(47);
    static int THREE_MONTH = 3 * 60 * 60 * 24 * 30;

    static void main(args) {

        def filePath = "./sql/stu_courses.sql"
        File file = new File(filePath)
        def batchSize = 50

        file.withWriter { writer ->
            for (int i=0; i< 8000000/batchSize; i++) {
                def insertSql = "insert into student_courses(s_id, t_id, room, c_id, c_time) values "
                for (int j=0; j< batchSize; j++) {
                    def sId = STUDENT_PREFIX + "_" + random.nextInt(40000)
                    def tId = TEACHER_PREFIX + random.nextInt(100)
                    def room = ROOM_PREFIX + random.nextInt(50)
                    def cId = COURSE_PREFIX + random.nextInt(60)
                    def cTime = Math.floor((System.currentTimeMillis() - random.nextInt(THREE_MONTH)) / 1000)
                    insertSql += "('$sId', '$tId', '$room', '$cId', $cTime),"
                }
                insertSql = insertSql.substring(0, insertSql.length()-1) + ";\n"
                //print(insertSql)
                writer.write(insertSql)
            }
        }
    }
}

生成的樣例數據以下:

insert into student_courses(s_id, t_id, room, c_id, c_time) values ('STU_29258', 'TCH55', 'ROOM43', 'CRE41', 1.604717694E9),('STU_429', 'TCH68', 'ROOM0', 'CRE42', 1.604714673E9),('STU_38288', 'TCH28', 'ROOM1', 'CRE49', 1.604719218E9),('STU_7278', 'TCH98', 'ROOM11', 'CRE20', 1.604712414E9),('STU_8916', 'TCH40', 'ROOM11', 'CRE42', 1.604715357E9),('STU_17383', 'TCH6', 'ROOM25', 'CRE10', 1.604718551E9),('STU_27674', 'TCH4', 'ROOM0', 'CRE6', 1.604714485E9),('STU_30896', 'TCH33', 'ROOM34', 'CRE4', 1.604716917E9),('STU_28303', 'TCH41', 'ROOM38', 'CRE52', 1.604716827E9),('STU_8689', 'TCH85', 'ROOM42', 'CRE46', 1.604713881E9),('STU_2447', 'TCH68', 'ROOM4', 'CRE35', 1.604713422E9),('STU_10354', 'TCH16', 'ROOM22', 'CRE36', 1.604713187E9),('STU_29257', 'TCH34', 'ROOM2', 'CRE17', 1.604717763E9),('STU_17242', 'TCH80', 'ROOM48', 'CRE1', 1.60471313E9),('STU_17052', 'TCH65', 'ROOM4', 'CRE9', 1.604711894E9),('STU_12209', 'TCH58', 'ROOM8', 'CRE43', 1.604712827E9),('STU_1246', 'TCH94', 'ROOM20', 'CRE4', 1.604715802E9),('STU_33533', 'TCH61', 'ROOM8', 'CRE8', 1.604718404E9),('STU_14367', 'TCH79', 'ROOM5', 'CRE42', 1.604714165E9),('STU_28037', 'TCH99', 'ROOM21', 'CRE13', 1.604718321E9),('STU_31909', 'TCH28', 'ROOM3', 'CRE36', 1.604718883E9),('STU_16994', 'TCH1', 'ROOM19', 'CRE3', 1.604719329E9),('STU_25382', 'TCH34', 'ROOM12', 'CRE26', 1.604714293E9),('STU_21718', 'TCH55', 'ROOM15', 'CRE40', 1.604715585E9),('STU_36228', 'TCH17', 'ROOM1', 'CRE17', 1.604716797E9),('STU_24146', 'TCH62', 'ROOM2', 'CRE12', 1.604714202E9),('STU_36499', 'TCH11', 'ROOM42', 'CRE14', 1.604718307E9),('STU_30843', 'TCH16', 'ROOM35', 'CRE6', 1.604717656E9),('STU_32930', 'TCH15', 'ROOM23', 'CRE33', 1.604718313E9),('STU_12921', 'TCH3', 'ROOM13', 'CRE35', 1.604711955E9),('STU_16669', 'TCH83', 'ROOM20', 'CRE58', 1.604717105E9),('STU_10225', 'TCH1', 'ROOM26', 'CRE5', 1.60471344E9),('STU_9399', 'TCH98', 'ROOM31', 'CRE45', 1.604714572E9),('STU_17332', 'TCH25', 'ROOM10', 'CRE31', 1.604713764E9),('STU_38771', 'TCH10', 'ROOM10', 'CRE11', 1.604716834E9),('STU_9529', 'TCH16', 'ROOM30', 'CRE10', 1.604718969E9),('STU_32513', 'TCH36', 'ROOM40', 'CRE44', 1.604714399E9),('STU_38907', 'TCH34', 'ROOM31', 'CRE33', 1.604716016E9),('STU_31551', 'TCH13', 'ROOM35', 'CRE28', 1.604716906E9),('STU_39883', 'TCH39', 'ROOM46', 'CRE23', 1.604719006E9),('STU_34965', 'TCH47', 'ROOM45', 'CRE10', 1.604713917E9),('STU_12265', 'TCH85', 'ROOM46', 'CRE11', 1.604714663E9),('STU_9348', 'TCH22', 'ROOM4', 'CRE14', 1.604712076E9),('STU_38391', 'TCH35', 'ROOM29', 'CRE37', 1.60471538E9),('STU_25424', 'TCH78', 'ROOM23', 'CRE3', 1.604717869E9),('STU_39334', 'TCH25', 'ROOM14', 'CRE48', 1.604717478E9),('STU_26085', 'TCH17', 'ROOM16', 'CRE23', 1.604718913E9),('STU_35483', 'TCH16', 'ROOM6', 'CRE5', 1.604712875E9),('STU_28009', 'TCH77', 'ROOM47', 'CRE39', 1.604716687E9),('STU_15094', 'TCH71', 'ROOM23', 'CRE18', 1.604712238E9);

能夠查看錶空間大小:

mysql> select CONCAT(ROUND(SUM(DATA_LENGTH) / (1024 * 1024 * 1024),3),' GB') as TABLE_SIZE from information_schema.TABLES where information_schema.TABLES.TABLE_NAME='student_courses'\G

*************************** 1. row ***************************

TABLE_SIZE: 0.538 GB

開始試驗

給裸表添加索引

假設什麼索引都不建,裸表一個,經過 s_id 搜索須要 2.94s; 添加 sid_index 索引後,一樣的搜索不到 0.01s 。

select * from student_courses where s_id = 'STU_17242';

194 rows in set (2.94 sec)




ALTER TABLE `student_courses` ADD INDEX sid_index ( `s_id` );

select * from student_courses where s_id = 'STU_17242';


194 rows in set (0.01 sec)

使用 explain 解釋下:

  • select_type:查詢類型, SIMPLE 表示這是一個簡單的 SELECT 查詢;
  • type: 表的鏈接類型。 const 表示匹配最多一行,一般是根據主鍵查詢;ref 表示使用非主鍵/惟一索引匹配少許行; range 表示範圍查詢,<>, >, <, <=, >=, IN, BETWEEN, LIKE ; index 掃描索引樹,但數量太大,至關於全表掃描;ALL 全表掃描。
  • possible_keys 和 key : 可能使用的索引以及實際使用的索引。
  • ref: 對於 key 給出的列,哪些列或哪些常量被用來比較了。
  • rows: 爲了找到知足條件的行要掃描的預計行數。
  • filtered: 被過濾行數的比例。
  • Extra: 索引使用的額外信息。 Using Where 須要使用 where 字句條件來過濾記錄; Using Index 要獲取的列信息能夠從索引樹上拿到; Using filesort 文件排序; Using MRR 是否使用了 MRR 優化範圍查詢.
mysql> explain select * from student_courses where id = 5;

+----+-------------+-----------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

| id | select_type | table           | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |

+----+-------------+-----------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |

+----+-------------+-----------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+


mysql> explain select * from student_courses where s_id = 'STU_17242';

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------+

| id | select_type | table           | partitions | type | possible_keys | key       | key_len | ref   | rows | filtered | Extra |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | sid_index     | sid_index | 194     | const |  194 |   100.00 | NULL  |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------+


mysql> explain select count(id) from student_courses;

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+-------------+

| id | select_type | table           | partitions | type  | possible_keys | key      | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+-------------+

|  1 | SIMPLE      | student_courses | NULL       | index | NULL          | tc_index | 292     | NULL | 7785655 |   100.00 | Using index |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+-------------+

對索引列使用了函數不會使用索引:

select * from student_courses where REPLACE(s_id,"STU_","") = '17242';



mysql> explain select * from student_courses where REPLACE(s_id,"STU_","") = '17242';

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

|  1 | SIMPLE      | student_courses | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 7764823 |   100.00 | Using where |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

假設有以下語句,能夠看到使用了索引 sid_index ,從 194 條過濾到最終 3 條。由於 sid_index 已通過濾了絕大多數記錄,所以添加 t_id 索引看上去沒有必要。不過,這裏只是某個 s_id 值的查詢結果,不表明其它的 s_id 查詢結果。不能僅僅據此就判定不須要加 (s_id, t_id) 聯合索引。能夠 count group by s_id 看看 s_id 的重複數量,進一步判斷。

select * from student_courses where t_id = 'TCH86' and s_id = 'STU_17242';



mysql> explain select * from student_courses where t_id = 'TCH86' and s_id = 'STU_17242';

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------------+

| id | select_type | table           | partitions | type | possible_keys | key       | key_len | ref   | rows | filtered | Extra       |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | sid_index     | sid_index | 194     | const |  194 |    10.00 | Using where |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+------+----------+-------------+




mysql> select s_id, count(s_id) as c from student_courses group by s_id order by c desc limit 10;

+-----------+-----+

| s_id      | c   |

+-----------+-----+

| STU_36180 | 258 |

| STU_25572 | 255 |

| STU_32924 | 255 |

| STU_20767 | 254 |

| STU_7738  | 253 |

| STU_26647 | 253 |

| STU_22931 | 253 |

| STU_22940 | 252 |

| STU_3963  | 252 |

| STU_25568 | 251 |

+-----------+-----+

10 rows in set (1.75 sec)

如今刪除 sid_index 索引,添加 tid_index 索引。看看狀況如何。因爲 t_id 選擇性較低,添加 tid_index 過濾後仍然有 8w+ 條記錄,兩條搜索語句耗時 0.4s 左右。計算一下 s_id 和 t_id 的不重複行數量, s_id 更大,選擇性更高。這說明:添加選擇性高的索引,性能提高更優。

ALTER TABLE student_courses drop index sid_index;


ALTER TABLE student_courses add index tid_index(t_id);


select * from student_courses where t_id = 'TCH86';

80195 rows in set (0.45 sec)


select * from student_courses where t_id = 'TCH86' and s_id = 'STU_17242';

3 rows in set (0.40 sec)


mysql> explain select * from student_courses where t_id = 'TCH86' and s_id = 'STU_17242';

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+--------+----------+-------------+

| id | select_type | table           | partitions | type | possible_keys | key       | key_len | ref   | rows   | filtered | Extra       |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+--------+----------+-------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | tid_index     | tid_index | 194     | const | 151664 |    10.00 | Using where |

+----+-------------+-----------------+------------+------+---------------+-----------+---------+-------+--------+----------+-------------+


mysql> select count(distinct s_id) / count(*) as s_id_selectivity, count(distinct t_id) / count(*) as t_id_selectivity  from student_courses;

+------------------+------------------+

| s_id_selectivity | t_id_selectivity |

+------------------+------------------+

|           0.0050 |           0.0000 |

+------------------+------------------+

1 row in set (10.11 sec)

聯合索引

考慮以下語句。仍然使用 tid_index ,耗時 0.4s 。若是使用聯合索引 (tid_index, cid_index) , 則耗時 0.03s 。至關於作了兩次索引查找,固然比一次要快。代價是,索引佔用空間更高。

select * from student_courses where t_id = 'TCH86' and c_id = 'CRE33';


1423 rows in set (0.41 sec)


ALTER TABLE student_courses add index tid_cid_index(t_id, c_id);

select * from student_courses where t_id = 'TCH86' and c_id = 'CRE33';
1423 rows in set (0.03 sec)

結合情形一,一般會將多個業務 ID 建成聯合索引 (s_id, t_id, c_id) ,這樣,(s_id), (s_id, t_id), (s_id, t_id, c_id) 的聯合等值查詢均可以應用到這個索引。因爲 s_id 選擇性很是大,能夠單獨建一個索引(節省索引佔用空間);而 (t_id, c_id) 須要建一個聯合索引,由於 (s_id, t_id, c_id) 沒法匹配 t_id 和 c_id 聯合查詢的狀況。根據最左匹配原則,s_id 必須出現。

ALTER TABLE student_courses add index stc_id_index(s_id,t_id,c_id);  或者 ALTER TABLE student_courses add index sid_index(s_id)

ALTER TABLE student_courses add index stc_id_index(t_id, c_id);

聯合索引是應對多條件查詢的性能提高的關鍵。最左匹配原則是應用聯合索引的最重要的原則之一。將查詢條件按照聯合索引定義的順序 (a,b,c,d,e) 從新排列,逐個比較:

  • 若是查詢條件均是等值查詢,則出現順序沒有關係,按照聯合索引定義順序從新排列便可。好比 a=1 and b=2 與 b=2 and a=1 是等同的。順序能夠不一樣,但必須出現。若是 b=2 and c=3 就沒法應用聯合索引 (a,b,c,d,e) 了,由於 a 沒出現。
  • 若是聯合索引裏沒有出現該列,則匹配到此終止。好比 b=2 and a=1 and d = 4 只能應用 (a,b),由於 c 沒出現。
  • 若是聯合索引裏出現了範圍匹配的列,則匹配到該列終止,後面的條件沒法應用索引。好比 b=2 and a=1 and d=4 and c in (2,3) 只能應用 (a,b,c) ,由於 c 出現了範圍匹配。

在 explain 命令中,能夠看 ref , filter 來判斷應用了哪些索引。若是沒有應用到某個列的索引,也能夠刪除相應的查詢條件,用 explain 命令的 ref 和 rows 來對比是否有變化。若是隻應用到了某個索引,則 Extra = Using index condition 。 假設如今只創建了 (s_id, t_id, c_id) 聯合索引。能夠用 show index from student_courses; 查看創建了哪些索引。

mysql> show index from student_courses;
+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table           | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| student_courses |          0 | PRIMARY      |            1 | id          | A         |     7764823 |     NULL | NULL   |      | BTREE      |         |               |
| student_courses |          1 | stc_id_index |            1 | s_id        | A         |       40417 |     NULL | NULL   |      | BTREE      |         |               |
| student_courses |          1 | stc_id_index |            2 | t_id        | A         |     3443139 |     NULL | NULL   |      | BTREE      |         |               |
| student_courses |          1 | stc_id_index |            3 | c_id        | A         |     7764823 |     NULL | NULL   |      | BTREE      |         |               |
+-----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

下面是各語句以及應用聯合索引的狀況:

// 全表掃,沒法應用聯合索引
mysql> explain select * from student_courses where c_id = 'CRE3' and t_id = 'TCH21'; 
+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | student_courses | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 7764823 |     1.00 | Using where |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

// 應用了 (s_id, t_id, c_id) ,由於都是等值查詢且都出現了,在查詢語句的出現順序沒有關係
mysql> explain select * from student_courses where s_id = 'STU_18528' and c_id = 'CRE3' and t_id = 'TCH21';

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref               | rows | filtered | Extra |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 486     | const,const,const |    1 |   100.00 | NULL  |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+


// 應用 (s_id, t_id) ,所以 ref = const, const
mysql> explain select * from student_courses where s_id = 'STU_18528' and t_id = 'TCH21';  

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref         | rows | filtered | Extra |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 388     | const,const |    2 |   100.00 | NULL  |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+



// 僅應用 (s_id) ,由於 t_id 沒出現
mysql> explain select * from student_courses where s_id = 'STU_18528' and c_id = 'CRE3';

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref   | rows | filtered | Extra                 |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 194     | const |  195 |    10.00 | Using index condition |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------+



// 第一個應用 (s_id, t_id, c_id) 估計是將 in ( 'TCH21') 優化爲等值查詢了; 第二個應用了 (s_id, t_id)。
mysql> explain select * from student_courses where s_id = 'STU_18528' and c_id = 'CRE3' and t_id in ( 'TCH21');

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref               | rows | filtered | Extra |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 486     | const,const,const |    1 |   100.00 | NULL  |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

1 row in set, 1 warning (0.00 sec)



mysql> explain select * from student_courses where s_id = 'STU_18528' and c_id = 'CRE3' and t_id > 'TCH21';

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

| id | select_type | table           | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

|  1 | SIMPLE      | student_courses | NULL       | range | stc_id_index  | stc_id_index | 388     | NULL |  171 |    10.00 | Using index condition |

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+

索引覆蓋

索引覆蓋是指 select 中的列均出如今聯合索引列中。以下兩個語句,後面那個語句應用了索引覆蓋,Extra = Using index ,取列數據時能夠直接從索引中獲取,而不須要去讀磁盤。

mysql> explain select * from student_courses where s_id = 'STU_18528' and c_id = 'CRE3' and t_id= 'TCH21';
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+
| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref               | rows | filtered | Extra |
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 486     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------+

mysql> explain select s_id, t_id from student_courses where s_id = 'STU_18528' and c_id = 'CRE3' and t_id= 'TCH21';
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+
| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref               | rows | filtered | Extra       |
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+
|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 486     | const,const,const |    1 |   100.00 | Using index |
+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+

索引掃描排序

索引能夠用來排序,從而減小隨機 IO,提高排序性能。以下三種狀況能夠應用索引排序:

  • 索引列順序與 ORDER BY 子句的順序徹底一致時,而且全部列的排序方向都相同;若是要關聯多張表,則 ORDER BY 引用的排序字段都爲第一張表的字段時;
  • 若是前導列爲等值查詢,後續的 ORDER BY 子句的字段順序與索引列順序一致。

若是使用了索引排序,則 type = index ; 若是未能引用索引排序,那麼 Extra 會提示 Using filesort 。

// 應用索引排序:ORDER BY 字句的全部列與索引列順序一致,且排序方向一致
mysql> explain select * from student_courses order by s_id desc, t_id desc, c_id desc limit 10;

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-------+

| id | select_type | table           | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra |

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-------+

|  1 | SIMPLE      | student_courses | NULL       | index | NULL          | stc_id_index | 486     | NULL |   10 |   100.00 | NULL  |

+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-------+


// 沒有應用索引排序:ORDER BY 字句的全部列與索引列順序一致,但排序方向不一致
mysql> explain select * from student_courses order by s_id asc, t_id desc limit 10;

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+

| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra          |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+

|  1 | SIMPLE      | student_courses | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 7764823 |   100.00 | Using filesort |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+



// 沒有應用索引排序:ORDER BY 字句的全部列順序 (t_id, s_id) 與索引列順序 (s_id, t_id, c_id) 不一致
mysql> explain select * from student_courses order by t_id desc, s_id desc limit 10;

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+

| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra          |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+

|  1 | SIMPLE      | student_courses | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 7764823 |   100.00 | Using filesort |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+----------------+



// 應用索引排序:前導列爲 s_id 與 t_id 聯合,與索引列定義順序一致
mysql> explain select s_id, t_id from student_courses where s_id = 'STU_18528' order by t_id;

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref   | rows | filtered | Extra                    |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 194     | const |  195 |   100.00 | Using where; Using index |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+



// 應用索引排序:前導列爲 s_id, t_id 與 c_id 聯合,與索引列定義順序一致
mysql> explain select s_id, t_id from student_courses where s_id = 'STU_18528' and t_id = 'TCH21' order by c_id desc;

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+--------------------------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref         | rows | filtered | Extra                    |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+--------------------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 388     | const,const |    2 |   100.00 | Using where; Using index |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------------+------+----------+--------------------------+



// 未能應用索引排序:前導列爲 s_id 與 c_id 聯合,與索引列定義順序不一致
mysql> explain select s_id, t_id from student_courses where s_id = 'STU_18528' order by c_id desc;

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+------------------------------------------+

| id | select_type | table           | partitions | type | possible_keys | key          | key_len | ref   | rows | filtered | Extra                                    |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+------------------------------------------+

|  1 | SIMPLE      | student_courses | NULL       | ref  | stc_id_index  | stc_id_index | 194     | const |  195 |   100.00 | Using where; Using index; Using filesort |

+----+-------------+-----------------+------------+------+---------------+--------------+---------+-------+------+----------+------------------------------------------+

MRR

若是使用 MRR 致使的開銷太高,也不會開啓 MRR。此時,可使用強制索引,或者設置不管如何都開啓 MRR。以下所示,t_id < 'T24' 會開啓 MRR,但 t_id < 'T32' 則不會開啓。此時,能夠強制使用索引 tc_index, 這樣,就會使用 MRR。

mysql> explain select * from student_courses where t_id >= 'TCH21' and t_id < 'TCH24';

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+

| id | select_type | table           | partitions | type  | possible_keys | key      | key_len | ref  | rows   | filtered | Extra                            |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+

|  1 | SIMPLE      | student_courses | NULL       | range | tc_index      | tc_index | 194     | NULL | 508500 |   100.00 | Using index condition; Using MRR |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+

1 row in set, 1 warning (0.00 sec)





mysql> explain select * from student_courses where t_id >= 'TCH21' and t_id < 'TCH32';

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

| id | select_type | table           | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

|  1 | SIMPLE      | student_courses | NULL       | ALL  | tc_index      | NULL | NULL    | NULL | 7785655 |    27.09 | Using where |

+----+-------------+-----------------+------------+------+---------------+------+---------+------+---------+----------+-------------+

1 row in set, 1 warning (0.00 sec)





mysql> explain select * from student_courses FORCE INDEX(tc_index) where t_id >= 'TCH21' and t_id < 'TCH32';

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+----------------------------------+

| id | select_type | table           | partitions | type  | possible_keys | key      | key_len | ref  | rows    | filtered | Extra                            |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+----------------------------------+

|  1 | SIMPLE      | student_courses | NULL       | range | tc_index      | tc_index | 194     | NULL | 2109100 |   100.00 | Using index condition; Using MRR |

+----+-------------+-----------------+------------+-------+---------------+----------+---------+------+---------+----------+----------------------------------+

小結

數據庫是開發人員最常打交道的軟件,而索引是高效訪問數據庫的重中之重。深刻理解索引的原理,合理設計適配查詢的索引,是有必要下功夫的。

索引基本功:

  • 根據查詢條件建立高效的索引;
  • 理解最左匹配原則並定義最優的聯合索引;
  • 儘量用好覆蓋索引和索引掃描。

參考資料

相關文章
相關標籤/搜索