《MySQL 性能優(yōu)化》之覆蓋索引不是索引

作者: 不剪發(fā)的Tony老師
畢業(yè)于北京航空航天大學(xué),十多年數(shù)據(jù)庫管理與開發(fā)經(jīng)驗,目前在一家全球性的金融公司從事數(shù)據(jù)庫架構(gòu)設(shè)計。CSDN學(xué)院簽約講師以及GitChat專欄作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net


 



文章目錄

        覆蓋索引
        優(yōu)化示例
        總結(jié)

大家好,我是只談技術(shù)不剪發(fā)的 Tony 老師。

今天我們來討論一個特殊的性能優(yōu)化技術(shù):覆蓋索引。

如果覺得文章有用,歡迎評論??、點贊??、推薦??
覆蓋索引

在某些情況下,查詢語句通過索引訪問就可以返回所需的結(jié)果,不需要訪問表中的數(shù)據(jù),此時我們把這個索引稱為覆蓋索引(Covering Index)。某些數(shù)據(jù)庫中稱之為 Index Only Scan。

準(zhǔn)確來說,覆蓋索引是優(yōu)化器選擇的一種執(zhí)行計劃;或者也可以說,任何索引在某種情況下都可能稱為覆蓋索引。

顯然,覆蓋索引減少了表的訪問(磁盤 IO 訪問),在某些情況下可以明顯提高查詢的性能。InnoDB 存儲引擎比 MyISAM 存儲引擎可以獲得更多的優(yōu)化提升,因為它的輔助索引存儲的是主鍵字段,覆蓋索引避免了回表操作(通過主鍵訪問表中的數(shù)據(jù))。不過,InnoDB 無法針對正在被其他事務(wù)修改數(shù)據(jù)的表應(yīng)用這一優(yōu)化技術(shù),必須等待其他事務(wù)的結(jié)束。
優(yōu)化示例

我們首先創(chuàng)建一個測試表 test 和一些數(shù)據(jù):

CREATE TABLE test(
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  col1 INT,
  col2 INT,
  col3 INT
) ENGINE = InnoDB;

SET SESSION cte_max_recursion_depth = 999999;

INSERT INTO test(col1, col2, col3)
WITH RECURSIVE d AS (
  SELECT 1 n, 1000*rand() c1, 1000*rand() c2, 1000*rand() c3
  UNION ALL
  SELECT n+1, 1000*rand(), 1000*rand(), 1000*rand()
  FROM d
  WHERE n<10000
)
SELECT c1, c2, c3 FROM d;



除了主鍵之外,test 表上沒有其他索引。

現(xiàn)在我們通過 col1 字段查詢數(shù)據(jù):

EXPLAIN
SELECT col1, col3
FROM test
WHERE col1 = 100;

id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra      |
--|-----------|-----|----------|----|-------------|---|-------|---|----|--------|-----------|
 1|SIMPLE     |test |          |ALL |             |   |       |   |9991|    10.0|Using where|



由于缺少索引,MySQL 只能選擇全表掃描。為了優(yōu)化以上查詢,我們可以在 col1 字段上創(chuàng)建一個索引:

CREATE INDEX idx_test_col1 USING BTREE ON test(col1);



然后再次執(zhí)行以上查詢:

EXPLAIN format=json
SELECT col1, col3
FROM test
WHERE col1 = 100;

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "3.50"
    },
    "table": {
      "table_name": "test",
      "access_type": "ref",
      "possible_keys": [
        "idx_test_col1"
      ],
      "key": "idx_test_col1",
      "used_key_parts": [
        "col1"
      ],
      "key_length": "5",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 10,
      "rows_produced_per_join": 10,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "2.50",
        "eval_cost": "1.00",
        "prefix_cost": "3.50",
        "data_read_per_join": "240"
      },
      "used_columns": [
        "col1",
        "col3"
      ]
    }
  }
}



此時,MySQL 選擇了通過索引 idx_test_col1 查找數(shù)據(jù)。

如果我們想要使用覆蓋索引進(jìn)一步優(yōu)化以上查詢,可以基于 col1 和 col3 創(chuàng)建一個復(fù)合索引:

CREATE INDEX idx_test_col1col3 USING BTREE ON test(col1, col3);



再次查看執(zhí)行計劃:

EXPLAIN format=json
SELECT col1, col3
FROM test
WHERE col1 = 100;

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1.95"
    },
    "table": {
      "table_name": "test",
      "access_type": "ref",
      "possible_keys": [
        "idx_test_col1",
        "idx_test_col1col3"
      ],
      "key": "idx_test_col1col3",
      "used_key_parts": [
        "col1"
      ],
      "key_length": "5",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 10,
      "rows_produced_per_join": 10,
      "filtered": "100.00",
      "using_index": true,
      "cost_info": {
        "read_cost": "0.95",
        "eval_cost": "1.00",
        "prefix_cost": "1.95",
        "data_read_per_join": "240"
      },
      "used_columns": [
        "col1",
        "col3"
      ]
    }
  }
}



索引 idx_test_col1col3 中存儲了查詢所需的全部數(shù)據(jù),因此可以實現(xiàn)覆蓋索引,或者索引覆蓋查詢。執(zhí)行計劃輸出可以看出,query_cost 從之前的 3.50 下降到了 1.95(減少了 44%),“using_index”: true 表示只需要訪問索引就可以返回查詢結(jié)果。

我們再來看一個查詢示例:

EXPLAIN -- format=json
SELECT col2, sum(col3)
FROM test
WHERE col1 = 100
GROUP BY col2
ORDER BY col2;

id|select_type|table|partitions|type|possible_keys                  |key          |key_len|ref  |rows|filtered|Extra                          |
--|-----------|-----|----------|----|-------------------------------|-------------|-------|-----|----|--------|-------------------------------|
 1|SIMPLE     |test |          |ref |idx_test_col1,idx_test_col1col3|idx_test_col1|5      |const|  10|   100.0|Using temporary; Using filesort|



以上查詢使用 col1 字段作為條件,同時按照 col2 字段進(jìn)行分組,統(tǒng)計 col3 字段的和值,最后基于 col2 字段進(jìn)行排序。雖然可以通過索引 idx_test_col1 查找數(shù)據(jù),但是仍然需要回表以及額外的排序。

為了優(yōu)化以上查詢語句,我們可以將 col1、col2 以及 col3 一起創(chuàng)建一個復(fù)合索引:

CREATE INDEX idx_test_col1col2col3 USING BTREE ON test(col1, col2, col3);



注意,創(chuàng)建復(fù)合索引時字段的順序一般是 WHERE 子句中的字段、GROUP BY 以及 ORDER BY 子句中的字段,最后才是 SELECT 列表中的其他字段。

此時,查詢語句的執(zhí)行計劃如下:

id|select_type|table|partitions|type|possible_keys                                        |key                  |key_len|ref  |rows|filtered|Extra      |
--|-----------|-----|----------|----|-----------------------------------------------------|---------------------|-------|-----|----|--------|-----------|
 1|SIMPLE     |test |          |ref |idx_test_col1,idx_test_col1col3,idx_test_col1col2col3|idx_test_col1col2col3|5      |const|  10|   100.0|Using index|



此時查詢同樣可以利用覆蓋索引進(jìn)行優(yōu)化。

通過以上示例可以看出,為了實現(xiàn)覆蓋索引,我們必須將查詢涉及的字段全部添加到索引中。這樣一來可能會導(dǎo)致另一個問題,那就是索引需要占用更多的磁盤空間和維護(hù)成本,當(dāng)表的數(shù)據(jù)量很大時尤其如此。另外,覆蓋索引通常只能用于優(yōu)化特定的查詢語句,大多數(shù)情況下查詢可能不能滿足覆蓋條件。

最后,任何索引都有可能成為覆蓋索引,例如:

EXPLAIN -- format=json
SELECT id
FROM test
WHERE col1 = 100;

id|select_type|table|partitions|type|possible_keys                                        |key                  |key_len|ref  |rows|filtered|Extra      |
--|-----------|-----|----------|----|-----------------------------------------------------|---------------------|-------|-----|----|--------|-----------|
 1|SIMPLE     |test |          |ref |idx_test_col1,idx_test_col1col3,idx_test_col1col2col3|idx_test_col1col2col3|5      |const|  10|   100.0|Using index|

EXPLAIN -- format=json
SELECT count(*)
FROM test
WHERE col1 = 100;

id|select_type|table|partitions|type|possible_keys                                        |key                  |key_len|ref  |rows|filtered|Extra      |
--|-----------|-----|----------|----|-----------------------------------------------------|---------------------|-------|-----|----|--------|-----------|
 1|SIMPLE     |test |          |ref |idx_test_col1,idx_test_col1col3,idx_test_col1col2col3|idx_test_col1col2col3|5      |const|  10|   100.0|Using index|



MySQL 二級索引中包含了主鍵 id 的數(shù)值,因此第一個查詢可以使用覆蓋索引。第二個查詢只需要統(tǒng)計字段 col1 的數(shù)量,也可以使用覆蓋索引。
總結(jié)

利用覆蓋索引技術(shù),MySQL 只需要訪問索引就可以得到查詢所需要的數(shù)據(jù),不需要訪問表中的數(shù)據(jù)。覆蓋索引可以用于優(yōu)化某些情況下的查詢語句。