索引的使用原則:如何通過索引讓SQL查詢效率最大化?

  1. 什麼情況下使用索引?當我們進行數據表查詢的時候,都有哪些特徵需要我們創建索引?
  2. 索引不是萬能的,索引設計的不合理可能會阻礙數據庫和業務處理的性能。那麼什麼情況下不需要創建索引?
  3. 創建了索引不一定代表一定用得上,甚至在有些情況下索引會失效。哪些情況下,索引會失效呢?又該如何避免這一情況?

創建索引有哪些規律?

創建索引有一定的規律。當這些規律出現的時候,我們就可以通過創建索引提升查詢效率,下面我們來看看什麼情況下可以創建索引:

1. 字段的數值有唯一性的限制,比如用戶名

索引本身可以起到約束的作用,比如唯一索引、主鍵索引都是可以起到唯一性約束的,因此在我們的數據表中,如果某個字段是唯一性的,就可以直接創建唯一性索引,或者主鍵索引。

2. 頻繁作爲 WHERE 查詢條件的字段,尤其在數據表大的情況下

在數據量大的情況下,某個字段在 SQL 查詢的 WHERE 條件中經常被使用到,那麼就需要給這個字段創建索引了。創建普通索引就可以大幅提升數據查詢的效率。

我之前列舉了 product_comment 數據表,這張數據表中一共有 100 萬條數據,假設我們想要查詢 user_id=785110 的用戶對商品的評論。

如果我們沒有對 user_id 字段創建索引,進行如下查詢:

 

SELECT comment_id, product_id, comment_text, comment_time, user_id FROM product_comment WHERE user_id = 785110

運行結果:


運行時間爲 0.699s,你能看到查詢效率還是比較低的。當我們對 user_id 字段創建索引之後,運行時間爲 0.047s,不到原來查詢時間的 1/10,效率提升還是明顯的。

3. 需要經常 GROUP BY 和 ORDER BY 的列

索引就是讓數據按照某種順序進行存儲或檢索,因此當我們使用 GROUP BY 對數據進行分組查詢,或者使用 ORDER BY 對數據進行排序的時候,就需要對分組或者排序的字段進行索引。

比如我們按照 user_id 對商品評論數據進行分組,顯示不同的 user_id 和商品評論的數量,顯示 100 個即可。

如果我們不對 user_id 創建索引,執行下面的 SQL 語句:

 

SELECT user_id, count(*) as num FROM product_comment group by user_id limit 100

運行結果(100 條記錄,運行時間 1.666s):


如果我們對 user_id 創建索引,再執行 SQL 語句:

 

SELECT user_id, count(*) as num FROM product_comment group by user_id limit 100

運行結果(100 條記錄,運行時間 0.042s):


你能看到當對 user_id 創建索引後,得到的結果中 user_id 字段的數值也是按照順序展示的,運行時間卻不到原來時間的 1/40,效率提升很明顯。

同樣,如果是 ORDER BY,也需要對字段創建索引。我們再來看下同時有 GROUP BY 和 ORDER BY 的情況。比如我們按照 user_id 進行評論分組,同時按照評論時間降序的方式進行排序,這時我們就需要同時進行 GROUP BY 和 ORDER BY,那麼是不是需要單獨創建 user_id 的索引和 comment_time 的索引呢?

當我們對 user_id 和 comment_time 分別創建索引,執行下面的 SQL 查詢:

 

SELECT user_id, count(*) as num FROM product_comment group by user_id order by comment_time desc limit 100

運行結果(運行時間 >100s):


實際上多個單列索引在多條件查詢時只會生效一個索引(MySQL 會選擇其中一個限制最嚴格的作爲索引),所以在多條件聯合查詢的時候最好創建聯合索引。在這個例子中,我們創建聯合索引 (user_id, comment_time),再來看下查詢的時間,查詢時間爲 0.775s,效率提升了很多。如果我們創建聯合索引的順序爲 (comment_time, user_id) 呢?運行時間爲 1.990s,同樣比兩個單列索引要快,但是會比順序爲 (user_id, comment_time) 的索引要慢一些。這是因爲在進行 SELECT 查詢的時候,先進行 GROUP BY,再對數據進行 ORDER BY 的操作,所以按照這個聯合索引的順序效率是最高的。


4.UPDATE、DELETE 的 WHERE 條件列,一般也需要創建索引

我們剛纔說的是數據檢索的情況。那麼當我們對某條數據進行 UPDATE 或者 DELETE 操作的時候,是否也需要對 WHERE 的條件列創建索引呢?

我們先看一下對數據進行 UPDATE 的情況。

如果我們想要把 comment_text 爲 462eed7ac6e791292a79 對應的 product_id 修改爲 10002,當我們沒有對 comment_text 進行索引的時候,執行 SQL 語句:

 
 

UPDATE product_comment SET product_id = 10002 WHERE comment_text = '462eed7ac6e791292a79'

運行結果爲 Affected rows: 1,運行時間爲 1.173s。

你能看到效率不高,但如果我們對 comment_text 字段創建了索引,然後再把剛纔那條記錄更新回 product_id=10001,執行 SQL 語句:

 

UPDATE product_comment SET product_id = 10001 WHERE comment_text = '462eed7ac6e791292a79'

運行結果爲 Affected rows: 1,運行時間僅爲 0.1110s。你能看到這個運行時間是之前的 1/10,效率有了大幅的提升。

如果我們對某條數據進行 DELETE,效率如何呢?

比如我們想刪除 comment_text 爲 462eed7ac6e791292a79 的數據。當我們沒有對 comment_text 字段進行索引的時候,執行 SQL 語句:

 

DELETE FROM product_comment WHERE comment_text = '462eed7ac6e791292a79'

運行結果爲 Affected rows: 1,運行時間爲 1.027s,效率不高。

如果我們對 comment_text 創建了索引,再來執行這條 SQL 語句,運行時間爲 0.032s,時間是原來的 1/32,效率有了大幅的提升。

你能看到,對數據按照某個條件進行查詢後再進行 UPDATE 或 DELETE 的操作,如果對 WHERE 字段創建了索引,就能大幅提升效率。原理是因爲我們需要先根據 WHERE 條件列檢索出來這條記錄,然後再對它進行更新或刪除。如果進行更新的時候,更新的字段是非索引字段,提升的效率會更明顯,這是因爲非索引字段更新不需要對索引進行維護。

不過在實際工作中,我們也需要注意平衡,如果索引太多了,在更新數據的時候,如果涉及到索引更新,就會造成負擔。

5.DISTINCT 字段需要創建索引

有時候我們需要對某個字段進行去重,使用 DISTINCT,那麼對這個字段創建索引,也會提升查詢效率。

比如我們想要查詢商品評論表中不同的 user_id 都有哪些,如果我們沒有對 user_id 創建索引,執行 SQL 語句,看看情況是怎樣的。

 

SELECT DISTINCT(user_id) FROM `product_comment`

運行結果(600637 條記錄,運行時間 2.283s):


如果我們對 user_id 創建索引,再執行 SQL 語句,看看情況又是怎樣的。

 

SELECT DISTINCT(user_id) FROM `product_comment`

運行結果(600637 條記錄,運行時間 0.627s):


你能看到 SQL 查詢效率有了提升,同時顯示出來的 user_id 還是按照遞增的順序進行展示的。這是因爲索引會對數據按照某種順序進行排序,所以在去重的時候也會快很多。

6. 做多表 JOIN 連接操作時,創建索引需要注意以下的原則

首先,連接表的數量儘量不要超過 3 張,因爲每增加一張表就相當於增加了一次嵌套的循環,數量級增長會非常快,嚴重影響查詢的效率。

其次,對 WHERE 條件創建索引,因爲 WHERE 纔是對數據條件的過濾。如果在數據量非常大的情況下,沒有 WHERE 條件過濾是非常可怕的。

最後,對用於連接的字段創建索引,並且該字段在多張表中的類型必須一致。比如 user_id 在 product_comment 表和 user 表中都爲 int(11) 類型,而不能一個爲 int 另一個爲 varchar 類型。

舉個例子,如果我們只對 user_id 創建索引,執行 SQL 語句:

 

SELECT comment_id, comment_text, product_comment.user_id, user_name FROM product_comment JOIN user ON product_comment.user_id = user.user_id

 

WHERE comment_text = '462eed7ac6e791292a79'

運行結果(1 條數據,運行時間 0.810s):


這裏我們對 comment_text 創建索引,再執行上面的 SQL 語句,運行時間爲 0.046s。

如果我們不使用 WHERE 條件查詢,而是直接採用 JOIN…ON…進行連接的話,即使使用了各種優化手段,總的運行時間也會很長(>100s)。

什麼時候不需要創建索引

我之前講到過索引不是萬能的,有一些情況是不需要創建索引的,這裏再進行一下說明。

WHERE 條件(包括 GROUP BY、ORDER BY)裏用不到的字段不需要創建索引,索引的價值是快速定位,如果起不到定位的字段通常是不需要創建索引的。舉個例子:

 

SELECT comment_id, product_id, comment_time FROM product_comment WHERE user_id = 41251

因爲我們是按照 user_id 來進行檢索的,所以不需要對其他字段創建索引,即使這些字段出現在 SELECT 字段中。

第二種情況是,如果表記錄太少,比如少於 1000 個,那麼是不需要創建索引的。我之前講過一個 SQL 查詢的例子(第 23 篇中的 heros 數據表查詢的例子,一共 69 個英雄不用索引也很快),表記錄太少,是否創建索引對查詢效率的影響並不大。

第三種情況是,字段中如果有大量重複數據,也不用創建索引,比如性別字段。不過我們也需要根據實際情況來做判斷,這一點我在之前的文章裏已經進行了說明,這裏不再贅述。

最後一種情況是,頻繁更新的字段不一定要創建索引。因爲更新數據的時候,也需要更新索引,如果索引太多,在更新索引的時候也會造成負擔,從而影響效率。

什麼情況下索引失效

我們創建了索引,還要避免索引失效,你可以先思考下都有哪些情況會造成索引失效呢?下面是一些常見的索引失效的例子:

1. 如果索引進行了表達式計算,則會失效

我們可以使用 EXPLAIN 關鍵字來查看 MySQL 中一條 SQL 語句的執行計劃,比如

 

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id+1 = 900001

運行結果:

 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where |

 

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

你能看到如果對索引進行了表達式計算,索引就失效了。這是因爲我們需要把索引字段的取值都取出來,然後依次進行表達式的計算來進行條件判斷,因此採用的就是全表掃描的方式,運行時間也會慢很多,最終運行時間爲 2.538 秒。

爲了避免索引失效,我們對 SQL 進行重寫:

 

SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900000

 

運行時間爲 0.039 秒。

2. 如果對索引使用函數,也會造成失效

比如我們想要對 comment_text 的前三位爲 abc 的內容進行條件篩選,這裏我們來查看下執行計劃:

 

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE SUBSTRING(comment_text, 1,3)='abc'

 

運行結果:

 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where |

 

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

複製代碼

你能看到對索引字段進行函數操作,造成了索引失效,這時可以進行查詢重寫:


 
 

SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE 'abc%'

複製代碼

使用 EXPLAIN 對查詢語句進行分析:


 
 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | range | comment_text | comment_text | 767 | NULL | 213 | 100.00 | Using index condition |

 

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

複製代碼

你能看到經過查詢重寫後,可以使用索引進行範圍檢索,從而提升查詢效率。

3. 在 WHERE 子句中,如果在 OR 前的條件列進行了索引,而在 OR 後的條件列沒有進行索引,那麼索引會失效。

比如下面的 SQL 語句,comment_id 是主鍵,而 comment_text 沒有進行索引,因爲 OR 的含義就是兩個只要滿足一個即可,因此只有一個條件列進行了索引是沒有意義的,只要有條件列沒有進行索引,就會進行全表掃描,因此索引的條件列也會失效:


 
 

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900001 OR comment_text = '462eed7ac6e791292a79'

複製代碼

運行結果:


 
 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | ALL | PRIMARY | NULL | NULL | NULL | 996663 | 10.00 | Using where |

 

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

如果我們把 comment_text 創建了索引會是怎樣的呢?

 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | index_merge | PRIMARY,comment_text | PRIMARY,comment_text | 4,767 | NULL | 2 | 100.00 | Using union(PRIMARY,comment_text); Using where |

 

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

你能看到這裏使用到了 index merge,簡單來說 index merge 就是對 comment_id 和 comment_text 分別進行了掃描,然後將這兩個結果集進行了合併。這樣做的好處就是避免了全表掃描。

4. 當我們使用 LIKE 進行模糊查詢的時候,後面不能是 %

 

EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE '%abc'

 

運行結果:


 
 

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

 

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

 

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

 

| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 11.11 | Using where |

 

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

這個很好理解,如果一本字典按照字母順序進行排序,我們會從首位開始進行匹配,而不會對中間位置進行匹配,否則索引就失效了。

5. 索引列與 NULL 或者 NOT NULL 進行判斷的時候也會失效。

這是因爲索引並不存儲空值,所以最好在設計數據表的時候就將字段設置爲 NOT NULL 約束,比如你可以將 INT 類型的字段,默認值設置爲 0。將字符類型的默認值設置爲空字符串 (’’)。

6. 我們在使用聯合索引的時候要注意最左原則

最左原則也就是需要從左到右的使用索引中的字段,一條 SQL 語句可以只使用聯合索引的一部分,但是需要從最左側開始,否則就會失效。我在講聯合索引的時候舉過索引失效的例子。

總結

今天我們對索引的使用原則進行了梳理,使用好索引可以提升 SQL 查詢的效率,但同時 也要注意索引不是萬能的。爲了避免全表掃描,我們還需要注意有哪些情況可能會導致索引失效,這時就需要進行查詢重寫,讓索引發揮作用。

實際工作中,查詢的需求多種多樣,創建的索引也會越來越多。這時還需要注意,我們要儘可能擴展索引,而不是新建索引,因爲索引數量過多需要維護的成本也會變大,導致寫效率變低。同時,我們還需要定期查詢使用率低的索引,對於從未使用過的索引可以進行刪除,這樣才能讓索引在 SQL 查詢中發揮最大價值。


針對 product_comment 數據表,其中 comment_time 已經創建了普通索引。假設我想查詢評論時間在 2018 年 10 月 1 日上午 10 點到 2018 年 10 月 2 日上午 10 點之間的評論,SQL 語句爲:

 

SELECT comment_id, comment_text, comment_time FROM product_comment WHERE DATE(comment_time) >= '2018-10-01 10:00:00' AND comment_time <= '2018-10-02 10:00:00'

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章