MySQL 索引與查詢優化

本文介紹一些優化 MySQL 索引設計和查詢的建議。在進行優化工作前,請務必瞭解MySQL EXPLAIN命令: 查看執行計劃

索引

索引在邏輯上是指從索引列(關鍵字)到數據的映射,通過索引可以快速的由關鍵字查找到數據記錄。順序查找複雜度爲O(n), 樹狀索引查找複雜度爲O(logn), 哈希索引爲O(1)。

MySQL中的索引一般是指BTree索引, InnoDB存儲引擎使用B+樹來實現BTree索引。

BTree索引保持數據之間的順序,可以極大的加快精確搜索(=, in)、範圍搜索(<,>), 去重(DISTINCT), 排序(ORDER BY) 和 聚合(GROUP BY)。

總結來說使用索引有三個優點:

  • 極大減少了要掃描的數據量
  • 減少排序和臨時表
  • 將隨機IO變爲順序IO

因爲寫入數據時需要爲新行建立索引,所以索引會減慢寫入速度。請儘量避免創建無用的索引

索引只能用於獨立的列

示例:

SELECT * FROM `user` WHERE `id`=5; -- 可以使用索引
SELECT * FROM `user` WHERE `id` + 1 = 5; -- 索引列作爲表達式一部分時無法使用索引
SELECT * FROM `user` WHERE MD5(first_name)='MD5'; -- 索引列作爲函數參數時無法使用索引

最左匹配原則

BTree索引具有最左匹配性質, 即只能按照索引列的順序自左向右搜索,不能跳過索引列

聯合索引中存在範圍查詢(<, >, like, between) 會導致後面的索引列失效

定義表和索引:

CREATE TABLE `user` (
    `id` INT,
    `first_name` VARCHAR(16),
    `middle_name` VARCHAR(16),
    `last_name` VARCHAR(16),
    PRIMARY KEY (`id`),
    KEY `idx_name` (`first_name`, `middle_name`, `last_name`)
);

示例:

SELECT * FROM `user` WHERE `first_name`='a'; -- 可以使用 idx_name 索引
SELECT * FROM `user` WHERE `first_name`='a' AND `middle_name`='b'; -- 可以使用 idx_name 索引
SELECT * FROM `user` WHERE `first_name`='a' AND `middle_name`='b' AND `last_name`='c'; -- 可以使用 idx_name 索引
SELECT * FROM `user` WHERE `middle_name`='b'; -- 不能使用 idx_name 索引
SELECT * FROM `user` WHERE `middle_name`='b' AND `last_name`='c'; -- 不能使用 idx_name 索引
SELECT * FROM `user` WHERE `first_name`='a' AND `last_name`='c'; -- 不能使用 idx_name 索引

上文中說的"可以使用索引"是指可以用ref,eq_refrange方式進行查詢。

使用 EXPLAIN 命令查看3個不能使用索引示例的執行計劃,可以發現 type 字段爲 index, 這是在索引樹上進行順序查找。雖然性能優於全表掃描, 但比 ref 和 range 查詢來說要慢很多。

索引列爲字符串等類型時, 可以使用索引列的前綴字符串進行模糊查詢

select * from user where first_name = 'abc' AND middle_name like 'de%';

這條語句的類型的爲 range, 即在索引列上進行範圍查詢。

將聯合索引理解爲: 將索引列(關鍵字)按順序拼接, 把拼接後的關鍵字與數據建立映射。最左匹配即是使用關鍵字前綴縮小搜索範圍。

聯合索引

在進行多列搜索時有一條經驗法則: 首先使用選擇性高的列進行搜索

我們可以將選擇性定義爲 count(distinct ) / count(*), 也就是說滿足條件的數據越少,則條件的選擇性越高。

假設用戶名name比性別gender選擇性高, 那麼查詢應該寫作WHERE name='finley' AND gender='M'而不是WHERE gender='M' AND name='finley'

實際上兩條語句是等效的, 當存在多個查詢條件時 MySQL 優化器會根據索引和選擇性決定最優的過濾順序

爲每一個列單獨建立索引,並不能有效支持多列查詢

CREATE TABLE `user` (
    `id` INT,
    `first_name` VARCHAR(16),
    `middle_name` VARCHAR(16),
    `last_name` VARCHAR(16),
    PRIMARY KEY (`id`),
    KEY `idx_first_name` (`first_name`),
    KEY `idx_middle_name` (`middle_name`)
);

查詢語句:

select * from user where first_name = 'a' AND middle_name = 'bc';

查看查詢計劃:

id select_type table type possible_keys key key_len ref rows filtered Extra
1 SIMPLE user ALL idx_first_name,idx_middle_name NULL NULL NULL 4 100.00 Using where

根據最左匹配法則和優先使用高選擇性列的經驗法則,可以得出一條建議:

對於需要進行多列查詢的表,應建立包含所有參與查詢列的聯合索引, 索引的順序應按照列的選擇性從強到弱排列

一些關於索引的建議

通常在使用索引檢索到數據之後,需要訪問磁盤上數據表文件讀取所需要的列,這種操作稱爲"回表"。

若索引中包含查詢的所有列,則不需要回表操作直接從索引文件中讀取數據即可, 這種索引稱爲覆蓋索引。

在查詢時儘量減少"SELECT *"只查詢需要的行, 條件允許時儘量建立覆蓋索引。

《數據庫索引設計與優化》一書中提出了判斷最佳索引的"三星索引"概念:

  1. 1星: 可以在索引上(用 ref 或 eq_ref 方式)完成等值查詢。需要取出等值謂詞涉及的列作爲索引開頭的列以滿足最左匹配原則。
  2. 2星: 可以使用索引進行排序
  3. 3星: 索引中包含要查詢的所有列,不需要回表

MySQL 在索引包含 null 的列時需要額外的開銷, 儘量避免允許索引列上存在 null

除非有非常嚴格的一致性要求,否則應避免使用外鍵

關於主鍵:

  • 避免使用字符串類型作爲主鍵
  • 使用MD5、UUID等隨機的主鍵可能導致更多的磁盤隨機讀寫,但一般不會有太大的性能問題
  • auto_increment 使用鎖機制實現,可能影響寫入性能。

在查詢較多且業務允許的情況下, 推薦使用自增主鍵。

不知道放哪兒好的兩條建議:

  • BLOB 用於存儲較大的二進制串,TEXT 用於存儲較大的字符串; 它們不能被索引;
  • ip地址是32位無符號整數,使用 INT UNSIGNED 存儲ip地址而不是字符串。INET_ATON(), INET_NTOA()可以轉換數字和字符串兩種格式

查詢

一些關於查詢的建議

  • 儘量避免使用 != 或 not in
  • 條件允許時避免使用 join 查詢, 可以先分別查詢然後在應用程序內存中關聯
  • 避免在where語句中進行 is null 判斷, 這可能導致MySQL放棄使用索引而進行全表掃描
  • 條件允許時使用 union all 而非 union, 避免 union 不必要的去重操作
  • 必要時使用 union (all) 代替 or 條件

小表驅動大表

MySQL在執行多表查詢時可以採用Nest Loop Join算法,即選擇數據集較小的一張表(數據集)作爲驅動表, 遍歷驅動表中所有記錄並連接另一張表中符合條件的記錄。

在使用 JOIN 進行查詢時 MySQL 會自動選擇數據集較小的一張表作爲驅動表。

LEFT JOIN 強制左表作爲驅動表, RIGHT JOIN 則強制選擇右表作爲驅動表。

MySQL 的 STRAIGHT_JOIN 結果與 INNER JOIN 相同, 但強制使用左表作爲驅動表, 可用來分析選擇不同驅動表的效果。

在業務允許的情況下, 讓 MySQL 自行決定驅動表

在使用 IN 進行多表查詢時一般會把 IN 內部的嵌套循環作爲驅動表, 應儘量減少IN數據集的大小。實際上, MySQL 也會對 IN 和 EXISTS 查詢進行優化, 選擇最優的驅動表。

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