概述
例子:模擬100個用戶同時對一個擁有100萬行數據的表進行2000次查詢,對比無索引和有索引的耗時情況。
mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='test' --query="select * from test.t100w where k2='MN89'" engine=innodb --number-of-queries=2000 -uroot -p -verbose
無索引時,運行結果:
Benchmark
Running for engine rbose
Average number of seconds to run all queries: 948.820 seconds
Minimum number of seconds to run all queries: 948.820 seconds
Maximum number of seconds to run all queries: 948.820 seconds
Number of clients running queries: 100
Average number of queries per client: 20
有索引時,運行結果:
爲test.t100w表中的k2列設置索引:
alter table test.t100w add index idx_k2(k2);
由於數據量較大,增加索引耗時會比較久。需要注意的是,設置索引的語句會導致鎖表。
之後再測試一遍:
Benchmark
Running for engine rbose
Average number of seconds to run all queries: 1.589 seconds
Minimum number of seconds to run all queries: 1.589 seconds
Maximum number of seconds to run all queries: 1.589 seconds
Number of clients running queries: 100
Average number of queries per client: 20
對比一下,在設置索引之前,耗時爲948.820s,設置索引之後耗時縮減到了1.589s。由此可見,對相關列使用索引,可大幅提高select操作性能。
索引分類
MySQL的索引是在存儲引擎層實現的,因此,對各種索引算法的支持情況與存儲引擎的類型息息相關。InnoDB引擎默認創建的是BTREE索引。
算法分類
各種常用引擎支持的算法如下表:
|
MyISAM引擎 |
InnoDB引擎 |
Memory引擎 |
B-Tree索引 |
√ |
√ |
√ |
Hash索引 |
× |
× |
√ |
R-Tree索引 |
√ |
× |
× |
Full-Text索引 |
√ |
√ |
× |
需要主要了解B-Tree索引。
B-Tree索引
B-Tree
在瞭解B-Tree索引之前,首先要了解什麼是B-Tree。
B-Tree是一種常用的數據結構,要詳細介紹的話就要從樹講到二叉樹再到平衡二叉樹之類了,這部分不是這篇筆記要記錄的重點,基礎差一些的朋友可以補一下數據結構。
B樹又稱多路平衡查找樹,顯而易見,它是一種樹形結構,主要用於查找算法。下圖是一個3階B樹的示意圖:
如圖,非空m階B樹的特點如下:
1、樹中每個節點,至多有m棵子樹;
2、非空B樹的根節點至少有兩棵子樹;
3、除根節點以外,每個分支節點至少有⌈m/2⌉棵子樹;
4、最後一層方框代表着查找失敗的區間,不計入層數。
B樹的查找過程:
設目標關鍵字值爲n,在上圖的B樹中,首先查找根節點,根節點中關鍵字爲20、37,分割出三個區間(-∞, 20)、(20, 37)、(37, +∞),即三棵子樹的節點的關鍵字取值範圍,如果n沒有命中節點中的關鍵字,則進入對應取值範圍的子樹,以此類推,直到查找命中,或進入查找失敗的區間。
顯然,使用B樹查找算法,可以很大程度減少比較次數,從而提高查找效率。
B+樹
MySQL的BTREE索引算法,實質上是B+樹算法(看了一些視頻也有說是B*樹算法,但是更多文章說是B+樹,這一點暫時存疑,如果我這裏認識有錯誤的話希望有小夥伴能夠指正一下。不過B*樹也是B+樹的變種,差異並不是特別大,根據B+樹理解即可)。B+樹是B樹的變種,3階B+樹示意圖如下:
如圖,非空m階B+樹的特點如下:
1、每個分支節點至多有m棵子樹;
2、非葉根節點至少有兩棵子樹,其他分支節點至少有⌈m/2⌉棵子樹;
3、節點關鍵字數量與子樹數量相等;
4、節點關鍵字值代表着該關鍵字對應子節點中值最大的關鍵字。
可以看到B+樹與B樹的差異主要有以下幾個方面:
1、B樹中的分支節點中的關鍵字數目爲其子樹數目減1,而B+樹中的分支節點中的關鍵字數目等於其子樹數目;
2、B樹中每個節點都包含了關鍵字及指向其對應記錄的指針,而B+樹中,僅有葉結點包含關鍵字及指向其對應記錄的指針,其餘分支節點僅有關鍵字值
3、B+樹的相鄰葉子節點依大小順序鏈接了起來。
根據以上差異,我們可以總結出B樹查找算法與B+樹查找算法的區別:
1、在B樹中,一旦查詢到目標關鍵字即可獲取到指向對應記錄的指針,立即停止查找,而若是沒有目標關鍵字,則需要一直遍歷到查詢樹的最底層。而在B+樹中,僅有葉子節點包含了指向目標關鍵字對應的記錄信息,因此無論查找成功與否,每次查找都是一條從根節點到葉結點的路徑;
2、由於B+樹的相鄰葉子節點被鏈接起來了,進行連續查詢時,當一條查詢命中,若下一查詢關鍵字與該關鍵字相近,則可直接通過鏈接訪問到相鄰的葉子節點,避免再遍歷一次查詢樹。
數據庫中的所有數據,都存放在表空間(tablespace)中,而表空間又被劃分爲不同的段(segment),一個段即對應一個表,而每個表又是由數個連續的頁(page)構成,頁面大小是固定的,默認爲16KB,數據就存放在頁中。對於用戶而言,一次查詢是要讀出一行數據,而對於存儲引擎而言,無論是要讀出多少行數據,都需要從磁盤中讀出整張表,再根據這行數據的頁偏移量去找到目標數據。
輔助索引B+樹的葉子節點中,並沒有包含索引鍵值對應記錄的地址,而是存放着其對應記錄的聚集索引鍵值,而聚集索引B+樹的葉子節點中,包含着索引鍵值對應的整行記錄。所以使用輔助索引進行查詢,需要先搜索輔助索引樹在搜索聚集索引樹,才能得到查詢的記錄。
功能分類
從功能上來講,索引又可分爲聚集索引和輔助索引。
聚集索引
表中數據的物理順序與索引鍵值的順序相同,這樣的索引即聚集索引。由於表中數據的物理順序只有一種情況,顯然,一張表只能有唯一的聚集索引。
聚集索引樹的生成:
1、MySQL會自動選擇主鍵作爲聚集索引列,沒有主鍵會選擇第一個唯一鍵作爲聚集索引列,如果既沒有主鍵也沒有合適的唯一鍵,則生成一個默認的、6字節自增的隱藏主鍵,作爲聚集索引列;
2、MySQL存儲數據時,會按照聚集索引列的值的順序,將數據有序存放在磁盤上;
3、聚集索引直接將原表數據頁作爲葉子節點,然後提取索引列進一步生成分直接點和根節點。
輔助索引
輔助索引也稱二級索引,與聚集索引對應,索引鍵值的順序與數據的物理順序無關。看到一個很形象的例子,如果把一張表比作字典,那麼可以把拼音查找(a ~ z)看作是聚集索引,把部首查找看作是輔助索引。由於索引鍵值與數據的物理順序無關,因此一張表可以有多個輔助索引。根據存儲引擎可以定義每個表的最大索引數和最大索引長度,每種存儲引擎對每個表至少支持16個索引,總索引長度至少爲256字節。
輔助索引樹的生成:
1、提取索引列的所有值,進行排序
2、將排序好的值,均勻地存放在葉子節點,進一步生成分支節點和根節點
3、葉子節點包含鍵值和對應數據行的聚集索引鍵值(頁號 + 頁面偏移量)
進一步細分,輔助索引又可以分爲單列輔助索引、聯合索引、唯一索引和前綴索引。主要說一下聯合索引。
聯合索引
如果經常使用多個約束條件進行查詢的話,可以考慮建立聯合索引。比如經常使用以下約束條件:
where a = ? and b = ? and c = ? …
可以將a、b、c三列建立爲聯合索引idx_a_b_c(a, b, c)。對於a、b、c三列,實際查詢中可能會有以下幾種情況:
1、同時使用了a、b、c列作爲約束,進行等值查詢
約束條件的順序不會影響走索引的情況,即無論是where a = ? and b = ? and c = ?還是where c = ? and b = ? and a = ?這樣的條件排列,都能夠使用到idx_a_b_c這個索引。因爲這幾個約束條件本身就是順序無關的,MySQL優化器會對語句進行一次重新排序,使其能夠滿足索引帶來的效率提升。同理也可以解釋第二點。
2、只使用了a、b、c部分列作爲約束,進行等值查詢
優化器依然會對語句中的約束條件做排序,但是索引覆蓋的鍵值長度會止於缺失的那一列。比如無論是where a = ? and c = ?還是where c = ? and a = ?最終都會優化爲a=? and c=?的順序,由於缺失了b列做約束條件,因此只有a列能走idx_a_b_c索引。所以我們在建立聯合索引時,應該儘量將重複值少的列放在最左邊,力求索引覆蓋的列能夠過濾掉最多的重複數據。
3、<、>、<=、>=、like這類不等值查詢
優化器還是會將約束條件排序,但是索引覆蓋的鍵值長度會止於第一個不等值條件。例如where a = ? and b <= ? and c = ?索引idx_a_b_c覆蓋的僅有a、b兩列,c列的約束條件不會走索引。
對於前綴索引,需要注意的是,對於InnoDB引擎的表,前綴長度最長是3072字節,前綴的限制應以字節爲單位進行測量,而varchar(10)這樣的數據類型中定義的是字符數,還需根據字符集來計算其真實佔用的字節數。
索引的命令操作
查詢索引
desc `tablename`;
show index from `tablename`;
PRI:主鍵索引
MUL:輔助索引
UNI:唯一索引
創建索引
需要注意的是,爲表增加索引的操作將會導致鎖表。
單列輔助索引:
alter table `tablename` add index `index_name`(`col_name`);
多列聯合索引:
alter table `tablename` add index `index_name`(`col_name_1`, `col_name_2`, …);
唯一索引:
alter table `tablename` add unique `index_name`(`col_name`);
前綴索引:
alter table `tablename` add index `index_name`(`col_name(prefix_len)`);
從執行計劃分析索引
在MySQL中,可以通過explain/desc語句,獲取到優化器選擇的語句執行計劃,我們可以以此來分析判斷語句的執行效率。接下來通過MySQL提供的練習庫sakila庫來了解一下執行計劃,加深對索引的認識(sakila庫可以在官網上搜到)。
首先來看看sakila.city表的表結構:
desc `sakila.city`\G;
*************************** 1. row ***************************
Field: city_id
Type: smallint(5) unsigned
Null: NO
Key: PRI
Default: NULL
Extra: auto_increment
*************************** 2. row ***************************
Field: city
Type: varchar(50)
Null: NO
Key:
Default: NULL
Extra:
*************************** 3. row ***************************
Field: country_id
Type: smallint(5) unsigned
Null: NO
Key: MUL
Default: NULL
Extra:
*************************** 4. row ***************************
Field: last_update
Type: timestamp
Null: NO
Key:
Default: CURRENT_TIMESTAMP
Extra: on update CURRENT_TIMESTAMP
4 rows in set (0.00 sec)
主要關注key值,city_id列key值爲PRI,表示該列爲主鍵,country_id列key值爲MUL,表示該列有輔助索引。還可以直接查看一下該表的索引信息:
show index from sakila.city\G;
*************************** 1. row ***************************
Table: city
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: city_id
Collation: A
Cardinality: 600
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: city
Non_unique: 1
Key_name: idx_fk_country_id
Seq_in_index: 1
Column_name: country_id
Collation: A
Cardinality: 109
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.01 sec)
很明顯,該表有聚集索引列city_id和輔助索引列country_id。
接下來,看看查詢語句select * from sakila.city;語句是如何執行的,可以使用desc或explain獲取select語句的執行計劃:
explain select * from sakila.city;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | city | NULL | ALL | NULL | NULL | NULL | NULL | 600 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
select_type
select類型,取值有:SIMPLE-不使用錶鏈接或子查詢、PRIMARY-外層查詢、UNION-union中的第二個或後面的查詢語句、SUNQUERY-子查詢中的第一個select。
table
輸出結果集的表。
type
訪問類型,即MySQL在表中查找到該行的方式,常見取值如下(由上至下,性能由最差到最好):
ALL
無索引,直接遍歷全表。
index
索引全掃描,遍歷整個索引來查詢匹配的行。例如:
獲取查詢city表中所有數據行的city_id列值的語句的執行計劃
explain select city_id from sakila.city\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: city
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_country_id
key_len: 2
ref: NULL
rows: 600
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
city_id列是該表的主鍵索引列,而上述查詢將遍歷所有數據行的city_id列,因此會遍歷整個索引來查詢匹配的行,爲index類型。
range
索引範圍掃描,常見於<、<=、>、>=、between等操作符。需要注意的是,對於輔助索引列,!=、like ‘%xx%’、not in操作符不走索引,而對於主鍵索引列則會走range類型。例如:
sakila.store表,有主鍵索引列store_id和唯一索引列manager_staff_id,分別以這兩個索引爲條件進行查找
desc select * from sakila.store where manager_staff_id != 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: store
partitions: NULL
type: ALL
possible_keys: idx_unique_manager
key: NULL
key_len: NULL
ref: NULL
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
desc select * from sakila.store where store_id != 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: store
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 1
ref: NULL
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
ref
輔助索引等值查詢。例如:
獲取查詢city表中所有country_id爲1的數據行信息的執行計劃
desc select * from sakila.city where country_id = 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: city
partitions: NULL
type: ref
possible_keys: idx_fk_country_id
key: idx_fk_country_id
key_len: 2
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
country_id是city表的非唯一輔助索引列,因此爲ref類型。
eq_ref
多表連接時,主表(from後面的表,又稱驅動表)使用主鍵列或唯一列作爲連接條件。例如:
獲取查詢film表中所有category_id爲1的title的執行計劃
desc
select film.title
from
sakila.film
join
sakila.film_category
on film.film_id=film_category.film_id
where film_category.category_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_category
partitions: NULL
type: ref
possible_keys: PRIMARY,fk_film_category_category
key: fk_film_category_category
key_len: 1
ref: const
rows: 64
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_category.film_id
rows: 1
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
作爲連接條件的film_id列爲film表中的主鍵索引列,因此執行類型爲eq_ref。
const/system
主鍵索引列或唯一索引列的等值查詢。例如:
desc select * from sakila.film where film_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
NULL
不用訪問表或索引,直接就能得到結果。
通常來說,我們在構造查詢語句時應該儘量追求更高效的訪問類型,比如完成同一個查詢——查詢film_category表中所有category_id爲1和2的數據行的film_id——我們有如下兩種語句:
select film_id from sakila.film_category where category_id in(1, 2)\G;
select film_id from sakila.film_category where category_id=1 union all select film_id from sakila.film_category where category_id=2\G;
前者的訪問類型爲range,而後者爲ref。
possible_key & key
possible_key:查詢時可能使用的索引。
key:查詢時實際使用的索引。
有些時候,執行計劃中索引的使用情況可能跟我們預想的並不一致,比如,film表中的language_id列爲輔助索引列,我們往往認爲將language_id作爲約束條件、查詢表中language_id爲1的數據行這樣的語句將會走索引idx_fk_language_id,實際看一下:
desc select * from sakila.film where language_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: ALL
possible_keys: idx_fk_language_id
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
可以發現,possible_key確實是我們期望的idx_fk_language_id,但實際這條語句卻並不會走索引,訪問類型也是遍歷全表的ALL。原因很簡單,sakila庫的初始數據中,film這張表裏所有的數據行,language_id列值都爲1,所以查詢language_id爲1的數據行自然是要遍歷全表的。這也可以看出,索引每一次數據插入,都會導致索引重新組織,而對已有的數據的表新增索引列,MySQL則會根據所有數據的索引列值來構建索引,爲防止數據更新打亂了索引構建的過程,這個過程中肯定是會鎖表的,所以,新增索引要找合適的時機,而創建索引則不要過度,否則也會影響數據寫入的效率。
key_len
使用到的索引長度。需要注意的是,索引在創建時,會爲鍵值預留全部的空間,比如,對於字符集爲utf8mb4的表,int類型佔4字節,那麼鍵值爲int類型的索引列會有4字節的預留空間。特別需要注意的是varchar/char類型,在定義列時這兩個類型後的括號中的數字,是對字符長度的限制,如varchar(10)是指該列爲最多10個字符的可變字符串類型,而utf8mb4中英文數字佔1字節,漢字佔4字節,索引會預留最大字節長度即認爲所有字符都是漢字來預留其鍵值空間。看以下例子:
先創建個測試表:
create table test.test_key_len(
id int primary key,
c1 varchar(6) not null,
c2 varchar(6),
c3 char(6) not null,
c4 char(6)
) charset utf8mb4;
爲c1,c2,c3,c4列分別創建好索引,然後插入數據。接下來就來獲取幾個查詢語句的執行計劃:
desc select * from test.test_key_len where c1='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c1
key: idx_c1
key_len: 26
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
c1列鍵值爲varchar(6),按上述內容推斷,MySQL會爲其預留24字節的空間,而varchar類型還有1字節的起始符和1字節的結束符,因此key_len共有26字節。
desc select * from test.test_key_len where c2='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c2
key: idx_c2
key_len: 27
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
c2列與c1列的區別就在於c2列未指定非空,因此在c1列的基礎上,還需要額外1字節標記該索引鍵值是否爲空。
desc select * from test.test_key_len where c3='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c3
key: idx_c3
key_len: 24
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
char類型不存在起始列的情況,因此對於c3列,索引鍵值預留空間是24字節。可想而知,沒有指定非空的c4列,其key_len值自然就是25字節了。
優化相關
要對SQL進行優化,勢必要定位執行效率低下的SQL。
對於突發性的效率降低甚至是Hang機,可以使用show processlist;語句定位出耗時過長的會話來定位出影響系統性能的SQL;對於某一時間內、持續性的效率低下,則需要通過慢查詢日誌來定位(需要將slow-query-log參數置爲1,MySQL會將超過long_query_time參數所設閾值的SQL寫入slow_query_log_file參數指定的文件中)。慢查詢日誌只會在查詢結束後纔會生成。
得到效率低下的SQL語句後,就可以通過desc/explain命令來獲取其執行計劃並進行具體分析了,大多數情況下具體分析後都可以依靠建索引或是改語句來解決。
索引設計要點
1、要在條件列(常用於where子句中做約束條件的列)上創建索引,而非查詢列(常用於select關鍵字後做選擇列表中的列);
2、儘量使用唯一索引,或是重複值較少的列上創建索引,力求篩選出最少的結果;
3、對字符串列創建索引,應儘量使用短索引。較小的索引涉及的磁盤IO較少,較短的值比較起來更快,更爲重要的是,對於較短的鍵值,索引高速緩存中的塊能容納更多的鍵值,MySQL也能在內存中容納更多的值;
4、通常情況,對於數據量較小的表,除主鍵外,不需要額外創建索引。只有對於數據量龐大的表而言,掃描全表對於系統會造成巨大的負擔,因此才能發揮出索引的效果。對於大表,還需要持續統計操作頻率較高的SQL,並對其進行分析,用於對索引進行改進;
5、不要過度索引。修改表內容時,索引也會進行更新,甚至會需要重構,索引過多反而會在數據寫入時使表鎖住更長的時間;
6、索引維護應避開業務繁忙期。