目錄
- 1. 索引基礎
- 1.1 索引的工作原理
- 2. 最左匹配原則
- 2.1 什么是最左匹配原則?
- 2.2 示例說明
- 2.3 最左匹配原則的圖示
- 3. 索引分類
- 3.1 按數據結構分類
- 3.2 按索引列數分類
- 3.3 按唯一性分類
- 3.4 按存儲方式分類
- 4. 聚集索引與非聚集索引的區別
- 4.1 聚集索引
- 4.2 非聚集索引
- 4.3 使用示例
- 5. 索引覆蓋
- 5.1 什么是索引覆蓋?
- 5.2 索引覆蓋的優勢
- 5.3 示例說明
- 5.4 索引覆蓋的圖示
- 6. 索引下推
- 6.1 什么是索引下推?
- 6.2 索引下推的優勢
- 6.3 示例說明
- 7. 為什么選擇B+樹作為索引的結構
- 7.1 B+樹的特點
- 7.2 B+樹vs其他數據結構
- 7.3 B+樹在數據庫中的應用
- 8. 索引優化策略
- 9. 索引失效的情況
- 9.1 常見的索引失效情況
- 9.2 索引失效的圖示
- 9.3 預防索引失效的最佳實踐
- 10. EXPLAIN 命令
- 10.1 EXPLAIN 命令簡介
- 10.2 EXPLAIN 輸出字段詳解
- 10.2.1 字段詳細說明
- 10.3 如何解讀 EXPLAIN 輸出
- 10.4 EXPLAIN 使用示例
- 10.5 優化建議
1. 索引基礎
索引是數據庫中用于提高查詢效率的數據結構。它類似于書籍的目錄,可以幫助數據庫快速定位到所需的數據,而不必掃描整個表。
1.1 索引的工作原理
當我們在數據庫中創建索引時,數據庫會維護一個單獨的數據結構,通常是B+樹,用于存儲索引字段的值及其對應的行指針。這樣,當我們按索引字段進行查詢時,數據庫可以快速找到對應的數據位置,大大減少了I/O操作。
例如,假設我們有一個包含100萬條記錄的用戶表:
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),age INT,email VARCHAR(100)
);
如果我們經常按照用戶名來查詢用戶信息,我們可以在name字段上創建索引:
CREATE INDEX idx_name ON users(name);
這樣,當我們執行以下查詢時:
SELECT * FROM users WHERE name = '吃個早飯';
數據庫會使用索引快速定位到名為’Alice’的用戶,而不是掃描整個表。
2. 最左匹配原則
最左匹配原則是復合索引的一個重要特性,它決定了索引的使用效率。
2.1 什么是最左匹配原則?
最左匹配原則指的是,對于復合索引,數據庫會從左到右依次使用索引中的字段。如果查詢條件中沒有最左邊的字段,索引可能無法被充分利用。比如創建a,b,c的聯合索引,他在創建時會先對a進行排序,在a相同的一組中對b進行排序,b相同的一組中再按照c排序,因此,b只是在a值一定時才是有序的,在整體上b是無序的,c同理。圖示:
2.2 示例說明
假設我們有一個復合索引:
CREATE INDEX idx_name_age_email ON users(name, age, email);
以下查詢可以有效地使用索引:
SELECT * FROM users WHERE name = 'CZF';
SELECT * FROM users WHERE name = 'CZF' AND age = 20;
SELECT * FROM users WHERE name = 'CZF' AND age = 20 AND email = 'CZF@example.com
但是,以下查詢可能無法充分利用索引:
SELECT * FROM users WHERE age = 20;
(沒有使用最左邊的name字段)SELECT * FROM users WHERE email = 'CZF@example.com';
(沒有使用最左邊的name字段)SELECT * FROM users WHERE name = 'CZF' and age < 30 and email = 'CZF@example.com';
(name和age可以使用索引,但是email無法使用,因為age使用了范圍查詢,在范圍內email是無序的)
2.3 最左匹配原則的圖示
讓我們用一個圖表來直觀地展示最左匹配原則:
3. 索引分類
索引可以根據不同的標準進行分類。以下是幾種常見的索引分類方式:
3.1 按數據結構分類
- B-Tree索引:最常見的索引類型,適用于范圍查詢和等值查詢。
- 哈希索引:只適用于等值查詢,查詢速度很快。
- 全文索引:用于全文搜索。
- 空間索引:用于地理空間數據的索引。
3.2 按索引列數分類
- 單列索引:只包含一個列的索引。
- 復合索引:包含多個列的索引。
3.3 按唯一性分類
- 唯一索引:索引列的值必須唯一。
- 非唯一索引:索引列的值可以重復。
3.4 按存儲方式分類
- 聚集索引:決定了表中數據的物理存儲順序。
- 非聚集索引:不影響表中數據的物理存儲順序。
4. 聚集索引與非聚集索引的區別
聚集索引和非聚集索引是兩種重要的索引類型,它們在存儲和查詢數據時有著本質的區別。
4.1 聚集索引
聚集索引決定了表中數據行的物理順序。在聚集索引中,索引的葉子節點直接包含數據行。每個表只能有一個聚集索引。
特點:
- 數據行的物理順序與索引的順序相同。
- 通常是主鍵索引。
- 查找速度快,特別是范圍查詢。
例如,在MySQL的InnoDB存儲引擎中,如果表定義了主鍵,InnoDB會自動使用主鍵作為聚集索引。
4.2 非聚集索引
非聚集索引不決定表中數據行的物理順序。非聚集索引的葉子節點包含索引字段值和一個指向數據行的指針。
特點:
- 索引的邏輯順序與數據行的物理順序無關。
- 一個表可以有多個非聚集索引。
- 需要額外的存儲空間。
4.3 使用示例
假設我們有一個訂單表:
CREATE TABLE orders (order_id INT PRIMARY KEY,customer_id INT,order_date DATE,total_amount DECIMAL(10, 2)
);
在這個表中,order_id
是主鍵,會自動成為聚集索引。如果我們經常按照客戶ID查詢訂單,我們可以創建一個非聚集索引:
CREATE INDEX idx_customer_id ON orders(customer_id);
現在,當我們執行以下查詢時:
SELECT * FROM orders WHERE customer_id = 1001;
數據庫會首先使用非聚集索引 idx_customer_id
找到所有 customer_id = 1001
的記錄,然后通過這些記錄中的指針找到實際的數據頁。
5. 索引覆蓋
索引覆蓋是一種優化查詢的技術,它可以大大提高查詢效率。
5.1 什么是索引覆蓋?
索引覆蓋指的是,當查詢的所有列都包含在索引中時,數據庫可以直接從索引中獲取數據,而不需要回表查詢。
5.2 索引覆蓋的優勢
- 減少I/O操作:不需要訪問表數據。
- 提高查詢速度:直接從索引獲取數據比從表中獲取更快。
5.3 示例說明
繼續使用之前的訂單表例子:
CREATE TABLE orders (order_id INT PRIMARY KEY,customer_id INT,order_date DATE,total_amount DECIMAL(10, 2)
);CREATE INDEX idx_customer_date ON orders(customer_id, order_date);
現在,如果我們執行以下查詢:
SELECT customer_id, order_date FROM orders WHERE customer_id = 1001;
這個查詢可以完全由索引 idx_customer_date
覆蓋,因為查詢的所有列(customer_id
和 order_date
)都包含在索引中。數據庫可以直接從索引中獲取數據,而不需要訪問表。
如果我們執行以下查詢:
SELECT customer_id,total_amount,order_date FROM orders WHERE customer_id = 1001;
那么就索引就沒有覆蓋,因為索引表中并沒有total_amount字段,因此他會拿到對應結果集的id集合,再去聚集索引查詢需要的字段
5.4 索引覆蓋的圖示
讓我們用一個圖表來展示索引覆蓋的工作原理:
6. 索引下推
索引下推(Index Condition Pushdown, ICP)是MySQL 5.6版本引入的一種查詢優化技術。
6.1 什么是索引下推?
索引下推是指在索引遍歷過程中,對索引中包含的字段先做判斷,過濾掉不符合條件的記錄,減少回表次數。
6.2 索引下推的優勢
- 減少回表次數,降低I/O操作。
- 在存儲引擎層面就過濾掉了不滿足條件的記錄,減輕了服務器層的壓力。
6.3 示例說明
假設我們有一個用戶表:
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),age INT,city VARCHAR(50)
);CREATE INDEX idx_name_age ON users(name, age);
考慮以下查詢:
SELECT * FROM users WHERE name LIKE 'A%' AND age > 20;
在沒有索引下推的情況下,MySQL會先通過索引找到所有name以’A’開頭的記錄,然后回表獲取完整的記錄,最后再判斷age > 20
的條件。
而使用了索引下推后,MySQL會在索引遍歷過程中就判斷age > 20的條件,只有滿足條件的記錄才會被回表查詢。
7. 為什么選擇B+樹作為索引的結構
B+樹是最常用的索引數據結構,它有許多優點使其成為數據庫索引的理想選擇。
7.1 B+樹的特點
- 多路平衡查找樹:每個節點可以存儲多個鍵值,減少樹的高度。
- 葉子節點相連:便于范圍查詢。
- 非葉子節點只存儲鍵值:可以存儲更多索引,減少I/O次數。
7.2 B+樹vs其他數據結構
- vs 二叉樹:B+樹的多路特性使其高度更低,減少了查詢時的I/O次數。
- vs 哈希表:B+樹支持范圍查詢和排序,哈希表只支持等值查詢。
- vs B樹:B+樹的所有數據都在葉子節點,便于范圍查詢;非葉子節點不存儲數據,可以存儲更多索引。這就使得B+樹的整體高度較低,從而提高IO效率
7.3 B+樹在數據庫中的應用
在實際的數據庫系統中,B+樹的節點通常對應著磁盤的頁面。每個節點可能包含數百個鍵值,這樣可以顯著減少樹的高度,從而減少磁盤I/O操作。
例如,假設一個B+樹的階數為1000(即每個節點可以存儲1000個鍵值),高度為3,那么它可以索引的記錄數量為:
1000 * 1000 * 1000 = 10億條記錄
而只需要3次磁盤I/O就可以查找到任意一條記錄。
8. 索引優化策略
了解了索引的工作原理后,我們可以采取一些策略來優化索引的使用:
-
選擇合適的列建立索引:
- 經常用于查詢條件的列
- 經常用于連接的列
- 經常用于排序的列
-
考慮列的基數:基數高(即列中不同值的數量多)的列通常更適合建立索引。
-
避免過多索引:索引會占用額外的存儲空間,并且會在插入、更新和刪除數據時帶來額外的維護成本。
-
利用復合索引:合理使用復合索引可以減少索引的數量,并提高查詢效率。
-
定期維護索引:隨著數據的變化,索引可能會變得碎片化,定期重建索引可以提高其效率。
-
使用覆蓋索引:盡可能地在索引中包含所有需要的列,以減少回表操作。
-
注意索引列的數據類型:盡量使用較小的數據類型,這樣索引占用的空間就小,查詢速度就更快。
9. 索引失效的情況
盡管索引能夠顯著提高查詢性能,但在某些情況下,即使表中已經建立了索引,數據庫也可能無法使用它們。了解這些情況并知道如何解決,對于優化查詢性能至關重要。
9.1 常見的索引失效情況
- 在索引列上使用函數或表達式
當在索引列上使用函數或進行計算時,索引通常會失效。
例如:
SELECT * FROM users WHERE YEAR(birth_date) = 1990;
即使 birth_date 列上有索引,這個查詢也無法使用它。
解決方法:重寫查詢,避免在索引列上使用函數。
SELECT * FROM users WHERE birth_date BETWEEN '1990-01-01' AND '1990-12-31';
- 使用 LIKE 進行前綴模糊查詢
當使用 LIKE 進行前綴模糊查詢時,索引會失效。
例如:
SELECT * FROM products WHERE name LIKE '%phone%';
解決方法:如果可能,使用前綴匹配而不是包含匹配。
SELECT * FROM products WHERE name LIKE 'phone%';
對于必須使用包含匹配的情況,考慮使用全文索引。
- 對索引列進行隱式類型轉換
當查詢條件中的數據類型與索引列的數據類型不匹配時,可能發生隱式類型轉換,導致索引失效。
例如,如果 user_id 是整數類型:
SELECT * FROM users WHERE user_id = '100';
解決方法:確保在查詢中使用正確的數據類型。
SELECT * FROM users WHERE user_id = 100;
- 使用 OR 連接多個條件
當使用 OR 連接多個條件時,除非所有條件都有索引,否則可能導致索引失效。
例如:
SELECT * FROM users WHERE name = 'John' OR age = 30;
解決方法:可以使用 UNION 替代 OR,或者為所有條件創建復合索引。
SELECT * FROM users WHERE name = 'John'
UNION
SELECT * FROM users WHERE age = 30;
- 在復合索引中不遵循最左匹配原則
如前所述,在使用復合索引時,如果查詢條件不從最左列開始,可能導致索引失效。
例如,對于索引 (name, age, city):
SELECT * FROM users WHERE age = 30 AND city = 'New York';
解決方法:調整查詢以遵循最左匹配原則,或調整索引順序以適應常見查詢模式。
SELECT * FROM users WHERE name IS NOT NULL AND age = 30 AND city = 'New York';
- 使用 NOT 操作符
使用 NOT 操作符可能導致索引失效。
例如:
SELECT * FROM users WHERE NOT (age > 20);
解決方法:盡可能重寫查詢以避免使用 NOT。
SELECT * FROM users WHERE age <= 20;
9.2 索引失效的圖示
讓我們用一個圖表來直觀地展示索引失效的情況:
9.3 預防索引失效的最佳實踐
- 理解執行計劃:使用 EXPLAIN 命令來分析查詢的執行計劃,了解索引是否被正確使用。
- 定期優化:定期檢查和優化慢查詢,確保索引被正確使用。
- 合理設計索引:根據實際查詢需求設計索引,避免過多或不必要的索引。
- 保持索引列的簡單性:避免在索引列上使用復雜的表達式或函數。
- 正確使用索引:了解并遵循最左匹配原則,特別是在使用復合索引時。
- 注意數據類型:確保查詢條件中使用的數據類型與索引列的數據類型一致。
- 考慮查詢重寫:有時,通過重寫查詢可以更好地利用現有索引。
10. EXPLAIN 命令
10.1 EXPLAIN 命令簡介
EXPLAIN 命令是 MySQL 提供的一個非常有用的工具,用于分析 SELECT 查詢語句的執行計劃。它可以幫助數據庫管理員和開發人員理解 MySQL 如何處理查詢,從而優化查詢性能。
使用 EXPLAIN 的基本語法如下:
EXPLAIN SELECT * FROM users WHERE id = 1;
10.2 EXPLAIN 輸出字段詳解
EXPLAIN 命令的輸出包含多個字段,每個字段都提供了關于查詢執行計劃的重要信息。以下是這些字段的詳細說明:
字段名 | 描述 |
---|---|
id | 查詢中每個 SELECT 的序號,有多個 SELECT 的復雜查詢中很有用 |
select_type | SELECT 的類型 |
table | 輸出結果集的表 |
partitions | 匹配的分區 |
type | 訪問類型 |
possible_keys | 可能會使用的索引 |
key | 實際使用的索引 |
key_len | 使用的索引的長度 |
ref | 與索引比較的列 |
rows | 預計要檢查的行數 |
filtered | 按表條件過濾的行百分比 |
Extra | 額外信息 |
10.2.1 字段詳細說明
-
id
- 標識 SELECT 的序號,在復雜查詢中非常有用
- 如果 id 相同,執行順序從上到下
- 如果 id 不同,id 值越大優先級越高,越先執行
-
select_type
- SIMPLE: 簡單 SELECT 查詢,不包含子查詢或 UNION
- PRIMARY: 最外層的 SELECT
- SUBQUERY: 子查詢中的第一個 SELECT
- DERIVED: 派生表的 SELECT (FROM 子句的子查詢)
- UNION: UNION 中第二個或后面的 SELECT 語句
- UNION RESULT: UNION 的結果
-
table
- 顯示這一行數據是關于哪張表的
-
partitions
- 匹配的分區,對于非分區表該值為 NULL
-
type
- 訪問類型,按照從最優到最差的順序排列:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
- 訪問類型,按照從最優到最差的順序排列:
-
possible_keys
- 顯示可能應用在這張表上的索引,一個或多個
-
key
- 實際使用的索引,如果為 NULL,則沒有使用索引
-
key_len
- 表示索引中使用的字節數,可以通過該列計算查詢中使用的索引的長度
-
ref
- 顯示索引的哪一列被使用了,如果可能的話,是一個常數
-
rows
- MySQL 認為必須檢查的行數
-
filtered
- 表示此查詢條件所過濾的數據的百分比
-
Extra
- 包含不適合在其他列中顯示但十分重要的額外信息
- 常見的值包括:
- Using index: 使用覆蓋索引
- Using where: 使用 WHERE 子句來過濾結果
- Using temporary: MySQL 需要創建一個臨時表來存儲結果
- Using filesort: MySQL 需要額外的一次傳遞來進行排序
10.3 如何解讀 EXPLAIN 輸出
解讀 EXPLAIN 輸出時,需要注意以下幾點:
-
檢查
type
列:希望看到 const, eq_ref, ref 等高效的訪問類型,而不是 range, index 或 ALL。 -
查看
key
列:確保查詢使用了預期的索引。 -
檢查
rows
列:這個值越小越好,表示 MySQL 需要掃描的行數較少。 -
注意
Extra
列:尋找可能的優化點,如 “Using temporary” 或 “Using filesort” 可能表示需要優化。 -
對于復雜查詢,關注
id
和select_type
列,了解查詢的執行順序和類型。
10.4 EXPLAIN 使用示例
讓我們通過一個實際的例子來說明 EXPLAIN 的使用:
假設我們有一個 users
表和一個 orders
表,我們想要查找所有有訂單的用戶的信息:
EXPLAIN SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
EXPLAIN 的輸出可能如下:
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-----------------------------------------------------+
| 1 | SIMPLE | u | NULL | ALL | PRIMARY | NULL | NULL | NULL | 1000 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | o | NULL | ref | user_id | user_id | 4 | mydatabase.u.id | 5 | 100.00 | Using index |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-----------------------------------------------------+
解讀:
- 查詢首先掃描整個
users
表(type = ALL)。 - 然后對每個用戶,通過索引查找對應的訂單(type = ref)。
- 使用了臨時表和文件排序來進行 GROUP BY 操作(Extra 列)。
10.5 優化建議
基于上面的 EXPLAIN 輸出,我們可以考慮以下優化:
- 在
users
表的id
列上創建索引,改善對users
表的訪問類型。 - 考慮是否可以避免使用 GROUP BY,或者在
users
表上添加覆蓋索引來消除 “Using temporary” 和 “Using filesort”。 - 如果
order_count
不需要非常精確,可以考慮將其預先計算并存儲在users
表中,定期更新。