【MySQL調優】索引優化
寫在前面:索引對查詢的速度有著至關重要的影響,理解索引也是進行數據庫性能調優的起點。考慮如下情況,假設數據庫中一個表有10^ 6條記錄,DBMS的頁面大小為4K,并存儲100條記錄。如果沒有索引,查詢將對整個表進行掃描,最壞的情況下,如果所有數據頁都不在內存,需要讀取10^ 4個頁面,如果這10^ 4個頁面在磁盤上隨機分布,需要進行10^ 4次I/O,假設磁盤每次I/O時間為10ms(忽略數據傳輸時間),則總共需要100s(但實際上要好很多很多)。如果對之建立B-Tree索引,則只需要進行log100(10^6)=3次頁面讀取,最壞情況下耗時30ms。這就是索引帶來的效果,很多時候,當你的應用程序進行SQL查詢速度很慢時,應該想想是否可以建索引。

選擇索引的數據類型
Mysql支持很多數據類型,選擇合適的數據類型存儲數據對性能有很大的影響。通常來說,可以遵循以下一些指導原則:
(1)越小的數據類型通常更好:越小的數據類型通常在磁盤、內存和CPU緩存中都需要更少的空間,處理起來更快。
(2)簡單的數據類型更好:整型數據比起字符,處理開銷更小,因為字符串的比較更復雜。在Mysql中,應該用內置的日期和時間數據類型,而不是用字符串來存儲時間;以及用整型數據類型存儲IP地址。
(3)盡量避免NULL:應該指定列為NOT NULL,除非你想存儲NULL。在MySQL中,含有空值的列很難進行查詢優化,因為它們使得索引、索引的統計信息以及比較運算更加復雜。你應該用0、一個特殊的值或者一個空串代替空值。
選擇標識符
選擇合適的標識符是非常重要的。選擇時不僅應該考慮存儲類型,而且應該考慮MySQL是怎樣進行運算和比較的。一旦選定數據類型,應該保證所有相關的表都使用相同的數據類型。
整型:通常是作為標識符的最好選擇,因為可以更快的處理,而且可以設置為AUTO_INCREMENT。
字符串:盡量避免使用字符串作為標識符,它們消耗更好的空間,處理起來也較慢。而且,通常來說,字符串都是隨機的,所以它們在索引中的位置也是隨機的,這會導致頁面分裂、隨機訪問磁盤,聚簇索引分裂(對于使用聚簇索引的存儲引擎)。
索引入門
對于任何DBMS,索引都是進行優化的最主要的因素。對于少量的數據,沒有合適的索引影響不是很大,但是,當隨著數據量的增加,性能會急劇下降。如果對多列進行索引(組合索引),列的順序非常重要,MySQL僅能對索引最左邊的前綴進行有效的查找。
例如:假設存在組合索引it1c1c2(c1,c2),查詢語句select * from t1 where c1=1 and c2=2能夠使用該索引。查詢語句select * from t1 where c1=1也能夠使用該索引。但是,查詢語句select * from t1 where c2=2不能夠使用該索引,因為沒有組合索引的引導列,即,要想使用c2列進行查找,必需出現c1等于某值。
索引的類型
索引是在存儲引擎中實現的,而不是在服務器層中實現的。所以,每種存儲引擎的索引都不一定完全相同,并不是所有的存儲引擎都支持所有的索引類型。
B-Tree索引
假設有如下一個表:
CREATE TABLE People ( last_name varchar(50) not null, first_name varchar(50) not null, dob date not null, gender enum('m', 'f') not null, key(last_name, first_name, dob) );
1
2
3
4
5
6
7
其索引包含表中每一行的last_name、first_name和dob列。其結構大致如下:
索引存儲的值按索引列中的順序排列。可以利用B-Tree索引進行全關鍵字、關鍵字范圍和關鍵字前綴查詢,當然,如果想使用索引,你必須保證按索引的最左邊前綴(leftmost prefix of the index)來進行查詢。
匹配全值(Match the full value):對索引中的所有列都指定具體的值。例如,上圖中索引可以幫助你查找出生于1960-01-01的Cuba Allen。
匹配最左前綴(Match a leftmost prefix):你可以利用索引查找last name為Allen的人,僅僅使用索引中的第1列。
匹配列前綴(Match a column prefix):例如,你可以利用索引查找last name以J開始的人,這僅僅使用索引中的第1列。
匹配值的范圍查詢(Match a range of values):可以利用索引查找last name在Allen和Barrymore之間的人,僅僅使用索引中第1列。
匹配部分精確而其它部分進行范圍匹配(Match one part exactly and match a range on another part):可以利用索引查找last name為Allen,而first name以字母K開始的人。
僅對索引進行查詢(Index-only queries):如果查詢的列都位于索引中,則不需要讀取元組的值。由于B-樹中的節點都是順序存儲的,所以可以利用索引進行查找(找某些值),也可以對查詢結果進行ORDER BY。
當然,使用B-tree索引有以下一些限制:
查詢必須從索引的最左邊的列開始。關于這點已經提了很多遍了。例如你不能利用索引查找在某一天出生的人。
不能跳過某一索引列。例如,你不能利用索引查找last name為Smith且出生于某一天的人。
存儲引擎不能使用索引中范圍條件右邊的列。例如,如果你的查詢語句為WHERE last_name=“Smith” AND first_name LIKE ‘J%’ AND dob=‘1976-12-23’,則該查詢只會使用索引中的前兩列,因為LIKE是范圍查詢。
Hash索引
MySQL中,只有Memory存儲引擎顯示支持hash索引,是Memory表的默認索引類型,盡管Memory表也可以使用B-Tree索引。Memory存儲引擎支持非唯一hash索引,這在數據庫領域是罕見的,如果多個值有相同的hash code,索引把它們的行指針用鏈表保存到同一個hash表項中。假設創建如下一個表:
CREATE TABLE testhash ( fname VARCHAR(50) NOT NULL, lname VARCHAR(50) NOT NULL, KEY USING HASH(fname) ) ENGINE=MEMORY;
1
2
3
4
5
包含的數據如下:
假設索引使用hash函數f( ),如下:
f('Arjen') = 2323 f('Baron') = 7437 f('Peter') = 8784 f('Vadim') = 2458
1
2
3
4
此時,索引的結構大概如下:
Slots是有序的,但是記錄不是有序的。當你執行
mysql> SELECT lname FROM testhash WHERE fname='Peter';
MySQL會計算’Peter’的hash值,然后通過它來查詢索引的行指針。因為f('Peter') = 8784,MySQL會在索引中查找8784,得到指向記錄3的指針。因為索引自己僅僅存儲很短的值,所以,索引非常緊湊。Hash值不取決于列的數據類型,一個TINYINT列的索引與一個長字符串列的索引一樣大。
Hash索引有以下一些限制:
(1)由于索引僅包含hash code和記錄指針,所以,MySQL不能通過使用索引避免讀取記錄。但是訪問內存中的記錄是非常迅速的,不會對性造成太大的影響。
(2)不能使用hash索引排序。
(3)Hash索引不支持鍵的部分匹配,因為是通過整個索引值來計算hash值的。
(4)Hash索引只支持等值比較,例如使用=,IN( )和<=>。對于WHERE price>100并不能加速查詢。
空間(R-Tree)索引
MyISAM支持空間索引,主要用于地理空間數據類型,例如GEOMETRY。
全文(Full-text)索引
全文索引是MyISAM的一個特殊索引類型,主要用于全文檢索。
高性能的索引策略
聚簇索引(Clustered Indexes)
聚簇索引保證關鍵字的值相近的元組存儲的物理位置也相同(所以字符串類型不宜建立聚簇索引,特別是隨機字符串,會使得系統進行大量的移動操作),且一個表只能有一個聚簇索引。因為由存儲引擎實現索引,所以,并不是所有的引擎都支持聚簇索引。目前,只有solidDB和InnoDB支持。聚簇索引的結構大致如下:
注:葉子頁面包含完整的元組,而內節點頁面僅包含索引的列(索引的列為整型)。一些DBMS允許用戶指定聚簇索引,但是MySQL的存儲引擎到目前為止都不支持。InnoDB對主鍵建立聚簇索引。如果你不指定主鍵,InnoDB會用一個具有唯一且非空值的索引來代替。如果不存在這樣的索引,InnoDB會定義一個隱藏的主鍵,然后對其建立聚簇索引。一般來說,DBMS都會以聚簇索引的形式來存儲實際的數據,它是其它二級索引的基礎。
為了更加理解聚簇索引和非聚簇索引,或者primary索引和second索引(MyISAM不支持聚簇索引),來比較一下InnoDB和MyISAM的數據布局,對于如下表:
CREATE TABLE layout_test ( col1 int NOT NULL, col2 int NOT NULL, PRIMARY KEY(col1), KEY(col2) );
1
2
3
4
5
6
假設主鍵的值位于1—10,000之間,且按隨機順序插入,然后用OPTIMIZE TABLE進行優化。col2隨機賦予1—100之間的值,所以會存在許多重復的值。
MyISAM的數據布局其布局十分簡單,MyISAM按照插入的順序在磁盤上存儲數據,如下:
82.jpg)
注:左邊為行號(row number),從0開始。因為元組的大小固定,所以MyISAM可以很容易的從表的開始位置找到某一字節的位置。
據些建立的primary key的索引結構大致如下:
,且葉子節點按照col1的順序存儲。來看看col2的索引結構:
實際上,在MyISAM中,primary key和其它索引沒有什么區別。Primary key僅僅只是一個叫做PRIMARY的唯一,非空的索引而已。
2. InnoDB的數據布局InnoDB按聚簇索引的形式存儲數據,所以它的數據布局有著很大的不同。它存儲表的結構大致如下:
注:聚簇索引中的每個葉子節點包含primary key的值,事務ID和回滾指針(rollback pointer)——用于事務和MVCC,和余下的列(如col2)。相對于MyISAM,二級索引與聚簇索引有很大的不同。InnoDB的二級索引的葉子包含primary key的值,而不是行指針(row pointers),這減小了移動數據或者數據頁面分裂時維護二級索引的開銷,因為InnoDB不需要更新索引的行指針。其結構大致如下:
聚簇索引和非聚簇索引表的對比:
按primary key的順序插入行(InnoDB)
如果你用InnoDB,而且不需要特殊的聚簇索引,一個好的做法就是使用代理主鍵(surrogate key)——獨立于你的應用中的數據。最簡單的做法就是使用一個AUTO_INCREMENT的列,這會保證記錄按照順序插入,而且能提高使用primary key進行連接的查詢的性能。應該盡量避免隨機的聚簇主鍵,例如,字符串主鍵就是一個不好的選擇,它使得插入操作變得隨機。
覆蓋索引(Covering Indexes)
如果索引包含滿足查詢的所有數據,就稱為覆蓋索引。覆蓋索引是一種非常強大的工具,能大大提高查詢性能。只需要讀取索引而不用讀取數據有以下一些優點:
(1)索引項通常比記錄要小,所以MySQL訪問更少的數據;
(2)索引都按值的大小順序存儲,相對于隨機訪問記錄,需要更少的I/O;
(3)大多數據引擎能更好的緩存索引。比如MyISAM只緩存索引。
(4)覆蓋索引對于InnoDB表尤其有用,因為InnoDB使用聚集索引組織數據,如果二級索引中包含查詢所需的數據,就不再需要在聚集索引中查找了。
覆蓋索引不能是任何索引,只有B-TREE索引存儲相應的值。而且不同的存儲引擎實現覆蓋索引的方式都不同,并不是所有存儲引擎都支持覆蓋索引(Memory和Falcon就不支持)。
對于索引覆蓋查詢(index-covered query),使用EXPLAIN時,可以在Extra一列中看到Using index。例如,在sakila的inventory表中,有一個組合索引(store_id,film_id),對于只需要訪問這兩列的查詢,MySQL就可以使用索引,如下:
mysql> EXPLAIN SELECT store_id, film_id FROM sakila.inventory\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: inventory type: index possible_keys: NULL key: idx_store_id_film_id key_len: 3 ref: NULL rows: 5007 Extra: Using index 1 row in set (0.17 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
在大多數引擎中,只有當查詢語句所訪問的列是索引的一部分時,索引才會覆蓋。但是,InnoDB不限于此,InnoDB的二級索引在葉子節點中存儲了primary key的值。因此,sakila.actor表使用InnoDB,而且對于是last_name上有索引,所以,索引能覆蓋那些訪問actor_id的查詢,如:
mysql> EXPLAIN SELECT actor_id, last_name -> FROM sakila.actor WHERE last_name = 'HOPPER'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: ref possible_keys: idx_actor_last_name key: idx_actor_last_name key_len: 137 ref: const rows: 2 Extra: Using where; Using index
1
2
3
4
5
6
7
8
9
10
11
12
13
利用索引進行排序
MySQL中,有兩種方式生成有序結果集:一是使用filesort,二是按索引順序掃描。利用索引進行排序操作是非常快的,而且可以利用同一索引同時進行查找和排序操作。當索引的順序與ORDER BY中的列順序相同且所有的列是同一方向(全部升序或者全部降序)時,可以使用索引來排序。如果查詢是連接多個表,僅當ORDER BY中的所有列都是第一個表的列時才會使用索引。其它情況都會使用filesort。
create table actor( actor_id int unsigned NOT NULL AUTO_INCREMENT, name varchar(16) NOT NULL DEFAULT '', password varchar(16) NOT NULL DEFAULT '', PRIMARY KEY(actor_id), KEY (name) ) ENGINE=InnoDB
1
2
3
4
5
6
7
insert into actor(name,password) values('cat01','1234567'); insert into actor(name,password) values('cat02','1234567'); insert into actor(name,password) values('ddddd','1234567'); insert into actor(name,password) values('aaaaa','1234567');
1
2
3
4
mysql> explain select actor_id from actor order by actor_id \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: index possible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 4 Extra: Using index 1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain select actor_id from actor order by password \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 4 Extra: Using filesort 1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain select actor_id from actor order by name \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: index possible_keys: NULL key: name key_len: 18 ref: NULL rows: 4 Extra: Using index 1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
當MySQL不能使用索引進行排序時,就會利用自己的排序算法(快速排序算法)在內存(sort buffer)中對數據進行排序,如果內存裝載不下,它會將磁盤上的數據進行分塊,再對各個數據塊進行排序,然后將各個塊合并成有序的結果集(實際上就是外排序)。對于filesort,MySQL有兩種排序算法。
(1) 兩遍掃描算法(Two passes)實現方式是先將須要排序的字段和可以直接定位到相關行數據的指針信息取出,然后在設定的內存(通過參數sort_buffer_size設定)中進行排序,完成排序之后再次通過行指針信息取出所需的Columns。注:該算法是4.1之前采用的算法,它需要兩次訪問數據,尤其是第二次讀取操作會導致大量的隨機I/O操作。另一方面,內存開銷較小。
(3) 一次掃描算法(single pass)該算法一次性將所需的Columns全部取出,在內存中排序后直接將結果輸出。
注:從 MySQL 4.1 版本開始使用該算法。它減少了I/O的次數,效率較高,但是內存開銷也較大。如果我們將并不需要的Columns也取出來,就會極大地浪費排序過程所需要的內存。在 MySQL 4.1 之后的版本中,可以通過設置 max_length_for_sort_data 參數來控制 MySQL 選擇第一種排序算法還是第二種。當取出的所有大字段總大小大于 max_length_for_sort_data 的設置時,MySQL 就會選擇使用第一種排序算法,反之,則會選擇第二種。為了盡可能地提高排序性能,我們自然更希望使用第二種排序算法,所以在 Query 中僅僅取出需要的 Columns 是非常有必要的。當對連接操作進行排序時,如果ORDER BY僅僅引用第一個表的列,MySQL對該表進行filesort操作,然后進行連接處理,此時,EXPLAIN輸出“Using filesort”;否則,MySQL必須將查詢的結果集生成一個臨時表,在連接完成之后進行filesort操作,此時,EXPLAIN輸出“Using temporary;Using filesort”。
索引與加鎖
索引對于InnoDB非常重要,因為它可以讓查詢鎖更少的元組。這點十分重要,因為MySQL 5.0中,InnoDB直到事務提交時才會解鎖。有兩個方面的原因:首先,即使InnoDB行級鎖的開銷非常高效,內存開銷也較小,但不管怎么樣,還是存在開銷。其次,對不需要的元組的加鎖,會增加鎖的開銷,降低并發性。InnoDB僅對需要訪問的元組加鎖,而索引能夠減少InnoDB訪問的元組數。但是,只有在存儲引擎層過濾掉那些不需要的數據才能達到這種目的。一旦索引不允許InnoDB那樣做(即達不到過濾的目的),MySQL服務器只能對InnoDB返回的數據進行WHERE操作,此時,已經無法避免對那些元組加鎖了:InnoDB已經鎖住那些元組,服務器無法解鎖了。來看個例子:
create table actor( actor_id int unsigned NOT NULL AUTO_INCREMENT, name varchar(16) NOT NULL DEFAULT '', password varchar(16) NOT NULL DEFAULT '', PRIMARY KEY(actor_id), KEY (name) ) ENGINE=InnoDB
1
2
3
4
5
6
7
insert into actor(name,password) values('cat01','1234567'); insert into actor(name,password) values('cat02','1234567'); insert into actor(name,password) values('ddddd','1234567'); insert into actor(name,password) values('aaaaa','1234567');
1
2
3
4
SET AUTOCOMMIT=0; BEGIN; SELECT actor_id FROM actor WHERE actor_id < 4 AND actor_id <> 1 FOR UPDATE;
1
2
3
4
該查詢僅僅返回2—3的數據,實際已經對1—3的數據加上排它鎖了。InnoDB鎖住元組1是因為MySQL的查詢計劃僅使用索引進行范圍查詢(而沒有進行過濾操作,WHERE中第二個條件已經無法使用索引了):
mysql> EXPLAIN SELECT actor_id FROM test.actor -> WHERE actor_id < 4 AND actor_id <> 1 FOR UPDATE \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: index possible_keys: PRIMARY key: PRIMARY key_len: 4 ref: NULL rows: 4 Extra: Using where; Using index 1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
表明存儲引擎從索引的起始處開始,獲取所有的行,直到actor_id<4為假,服務器無法告訴InnoDB去掉元組1。為了證明row 1已經被鎖住,我們另外建一個連接,執行如下操作:
SET AUTOCOMMIT=0; BEGIN; SELECT actor_id FROM actor WHERE actor_id = 1 FOR UPDATE;
1
2
3
該查詢會被掛起,直到第一個連接的事務提交釋放鎖時,才會執行(這種行為對于基于語句的復制(statement-based replication)是必要的)。如上所示,當使用索引時,InnoDB會鎖住它不需要的元組。更糟糕的是,如果查詢不能使用索引,MySQL會進行全表掃描,并鎖住每一個元組,不管是否真正需要。
使索引支持多種過濾條件
根據實際業務需求,判斷哪些列是經常被作為過濾條件的,考慮在創建聯合索引的時候,將這些列放在聯合索引中較靠前面的位置,比如性別,國籍等。當用戶在篩選的時候不去對比較靠前的列進行篩選的時候,可以通過 where sex IN (f, m)的方式來強制讓MySQL使用到這個索引。這里就和有的人提到把離散度較大的列放在索引前面的做法略有不同,所以說任何一種索引創建方式都不是銀彈,需要根據實際的業務場景來進行處理。
同時需要注意的一點是,如果確實存在離散度較大的列,使用IN后面括號內有上千條數據的時候,MySQL可能會報錯,同時會占用大量的內存。
在使用過程中,需要避免一個查詢的過濾條件中有多個范圍的場景,因為MySQL在優化過程中,遇到索引中第一個范圍條件之后,后面的列即使被索引覆蓋,也沒辦法利用索引 進行查詢了。可以根據實際的業務場景,把部分范圍查詢轉換為定值查詢。
索引在優化排序的過程中也具有很大的作用,當一個查詢同時使用ORDER BY和LIMIT的時候,索引的作用就體現出來了,但是這里需要注意一個問題,當偏移量很大的時候,MySQL需要大量的時間進行掃描和數據拋棄。反范式化、預先計算和緩存可能是解決這個問題的好辦法。
另一種方法就是延遲關聯,所謂延遲關聯就是:先通過針對主鍵的字查詢分頁查詢到需要的主鍵,然后根據主鍵去查找需要的數據,這樣可以避免多次回表取數據丟棄數據。
比如:
Mysql> SELECT
1
2
3
4
維護索引和表
維護表
針對MyISAM引擎,使用Check table可以檢查一張表是否發生了損壞,如果發現了損壞,可以通過Repaire table來對其進行修復。
一般情況下InnoDB引擎不會發生表損壞,如果出現異常,首先應當檢查數據查詢語句,確認查詢語句無誤后,考慮是否是有人在MySQL之外修改了數據表文件或者通過rsync備份InnoDB。
更新索引統計:
MySQL的優化器需要通過索引統計信息來完成優化判斷,如果統計信息不準確,將會影響到優化器的正常工作,同時會造成MySQL語句執行緩慢。可以通過運行ANALYZE TABLE的方式來重新生成統計信息。
SHOW INDEX FROM TABLE;
的結果中基數(Cardinality)也可以稱之為區分度表示的就是該索引被估算出可能有多少個不同的取值。
當數據庫中表過多,或者文件過多,索引檢索估算成本過高時,在進行INFORMATION_SCHEMA表打開,SHOW TABLE STATUS或者SHOW INDEX的時候都會出發索引統計信息的重新更新。這時可能會造成大量的鎖并給服務器帶來額外的壓力。通過關閉innodb_stats_on_metatata來避此類問題。
減少索引和數據的碎片
可以通過OPTIMIZE TABLE或者導出再倒入數據來進行數據整理。MyISAM可以通過排序算法重建索引來消除碎片,而InnoDB可以通過在線添加和刪除索引來實現索引碎片化的消除。對于不支持OPTIMIZE TABLE的引擎,可以通過將表的存儲引擎修改為當前引擎的方式來進行表的重建。
MySQL 數據庫
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。