MySQL具體解釋(9)----------索引具體解釋
寫在前面:索引對查詢的速度有著至關重要的影響,理解索引也是進行數據庫性能調優的起點。
考慮例如以下情況。假設數據庫中一個表有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查詢速度非常慢時,應該想想能否夠建索引。進入正題:
第二章、索引與優化
1、選擇索引的數據類型
MySQL支持非常多數據類型,選擇合適的數據類型存儲數據對性能有非常大的影響。通常來說,能夠遵循下面一些指導原則:
(1)越小的數據類型通常更好:越小的數據類型通常在磁盤、內存和CPU緩存中都須要更少的空間,處理起來更快。
(2)簡單的數據類型更好:整型數據比起字符,處理開銷更小。由於字符串的比較更復雜。
在MySQL中,應該用內置的日期和時間數據類型。而不是用字符串來存儲時間;以及用整型數據類型存儲IP地址。
(3)盡量避免NULL:應該指定列為NOT NULL,除非你想存儲NULL。在MySQL中。含有空值的列非常難進行查詢優化。由於它們使得索引、索引的統計信息以及比較運算更加復雜。你應該用0、一個特殊的值或者一個空串取代空值。
1.1、選擇標識符
選擇合適的標識符是很重要的。選擇時不僅應該考慮存儲類型,並且應該考慮MySQL是如何進行運算和比較的。
一旦選定數據類型,應該保證全部相關的表都使用同樣的數據類型。
(1) 整型:一般是作為標識符的最好選擇。由於能夠更快的處理。並且能夠設置為AUTO_INCREMENT。
(2) 字符串:盡量避免使用字符串作為標識符。它們消耗更好的空間。處理起來也較慢。
並且,通常來說,字符串都是隨機的,所以它們在索引中的位置也是隨機的,這會導致頁面分裂、隨機訪問磁盤,聚簇索引分裂(對於使用聚簇索引的存儲引擎)。
2、索引入門
對於不論什麽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等於某值。
2.1、索引的類型
索引是在存儲引擎中實現的。而不是在server層中實現的。
所以,每種存儲引擎的索引都不一定全然同樣,並非全部的存儲引擎都支持全部的索引類型。
2.1.1、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) ); |
其索引包括表中每一行的last_name、first_name和dob列。其結構大致例如以下:
索引存儲的值按索引列中的順序排列。能夠利用B-Tree索引進行全keyword、keyword範圍和keyword前綴查詢,當然,假設想使用索引。你必須保證按索引的最左邊前綴(leftmost prefix of the index)來進行查詢。
(1)匹配全值(Match the full value):對索引中的全部列都指定詳細的值。
比如,上圖中索引能夠幫助你查找出生於1960-01-01的Cuba Allen。
(2)匹配最左前綴(Match a leftmost prefix):你能夠利用索引查找last name為Allen的人。只使用索引中的第1列。
(3)匹配列前綴(Match a column prefix):比如,你能夠利用索引查找last name以J開始的人,這只使用索引中的第1列。
(4)匹配值的範圍查詢(Match a range of values):能夠利用索引查找last name在Allen和Barrymore之間的人。只使用索引中第1列。
(5)匹配部分精確而其他部分進行範圍匹配(Match one part exactly and match a range on another part):能夠利用索引查找last name為Allen,而first name以字母K開始的人。
(6)僅對索引進行查詢(Index-only queries):假設查詢的列都位於索引中。則不須要讀取元組的值。
因為B-樹中的節點都是順序存儲的,所以能夠利用索引進行查找(找某些值),也能夠對查詢結果進行ORDER BY。當然,使用B-tree索引有下面一些限制:
(1) 查詢必須從索引的最左邊的列開始。
關於這點已經提了非常多遍了。比如你不能利用索引查找在某一天出生的人。
(2) 不能跳過某一索引列。比如,你不能利用索引查找last name為Smith且出生於某一天的人。
(3) 存儲引擎不能使用索引中範圍條件右邊的列。比如,假設你的查詢語句為WHERE last_name="Smith" AND first_name LIKE ‘J%‘ AND dob=‘1976-12-23‘。則該查詢僅僅會使用索引中的前兩列。由於LIKE是範圍查詢。
2.1.2、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;
包括的數據例如以下:
如果索引使用hash函數f( )。例如以下:
f(‘Arjen‘) = 2323 f(‘Baron‘) = 7437 f(‘Peter‘) = 8784 f(‘Vadim‘) = 2458 |
此時,索引的結構大概例如以下:
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並不能加速查詢。
2.1.3、空間(R-Tree)索引
MyISAM支持空間索引,主要用於地理空間數據類型,比如GEOMETRY。
2.1.4、全文(Full-text)索引
全文索引是MyISAM的一個特殊索引類型,主要用於全文檢索。
3、高性能的索引策略
3.1、聚簇索引(Clustered Indexes)
聚簇索引保證keyword的值相近的元組存儲的物理位置也同樣(所以字符串類型不宜建立聚簇索引,特別是隨機字符串,會使得系統進行大量的移動操作),且一個表僅僅能有一個聚簇索引。由於由存儲引擎實現索引,所以,並非全部的引擎都支持聚簇索引。
眼下,僅僅有solidDB和InnoDB支持。
聚簇索引的結構大致例如以下:
註:葉子頁面包括完整的元組,而內節點頁面僅包括索引的列(索引的列為整型)。一些DBMS同意用戶指定聚簇索引,可是MySQL的存儲引擎到眼下為止都不支持。InnoDB對主鍵建立聚簇索引。
假設你不指定主鍵。InnoDB會用一個具有唯一且非空值的索引來取代。假設不存在這種索引。InnoDB會定義一個隱藏的主鍵,然後對其建立聚簇索引。一般來說。DBMS都會以聚簇索引的形式來存儲實際的數據,它是其他二級索引的基礎。
3.1.1、InnoDB和MyISAM的數據布局的比較
為了更加理解聚簇索引和非聚簇索引,或者primary索引和second索引(MyISAM不支持聚簇索引),來比較一下InnoDB和MyISAM的數據布局。對於例如以下表:
CREATE TABLE layout_test ( col1 int NOT NULL, col2 int NOT NULL, PRIMARY KEY(col1), KEY(col2) ); |
如果主鍵的值位於1---10,000之間。且按隨機順序插入。然後用OPTIMIZE TABLE進行優化。col2隨機賦予1---100之間的值,所以會存在很多反復的值。
(1) MyISAM的數據布局
其布局十分簡單,MyISAM依照插入的順序在磁盤上存儲數據。例如以下:
註:左邊為行號(row number),從0開始。由於元組的大小固定。所以MyISAM能夠非常easy的從表的開始位置找到某一字節的位置。
據些建立的primary key的索引結構大致例如以下:
註:MyISAM不支持聚簇索引,索引中每個葉子節點只包括行號(row number),且葉子節點依照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不須要更新索引的行指針。
其結構大致例如以下:
聚簇索引和非聚簇索引表的對照:
3.1.2、按primary key的順序插入行(InnoDB)
假設你用InnoDB,並且不須要特殊的聚簇索引,一個好的做法就是使用代理主鍵(surrogate key)——獨立於你的應用中的數據。最簡單的做法就是使用一個AUTO_INCREMENT的列,這會保證記錄依照順序插入。並且能提高使用primary key進行連接的查詢的性能。
應該盡量避免隨機的聚簇主鍵。比如,字符串主鍵就是一個不好的選擇,它使得插入操作變得隨機。
3.2、覆蓋索引(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) |
在大多數引擎中,僅僅有當查詢語句所訪問的列是索引的一部分時,索引才會覆蓋。
可是,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 |
3.3、利用索引進行排序
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 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‘); |
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)
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)
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) |
當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”。
3.4、索引與加鎖
索引對於InnoDB很重要。由於它能夠讓查詢鎖更少的元組。這點十分重要,由於MySQL 5.0中,InnoDB直到事務提交時才會解鎖。有兩個方面的原因:首先,即使InnoDB行級鎖的開銷很高效,內存開銷也較小,但無論怎麽樣,還是存在開銷。
其次,對不須要的元組的加鎖。會添加鎖的開銷,減少並發性。
InnoDB僅對須要訪問的元組加鎖,而索引可以降低InnoDB訪問的元組數。
可是,僅僅有在存儲引擎層過濾掉那些不須要的數據才幹達到這樣的目的。
一旦索引不同意InnoDB那樣做(即達不到過濾的目的),MySQLserver僅僅能對InnoDB返回的數據進行WHERE操作,此時,已經無法避免對那些元組加鎖了:InnoDB已經鎖住那些元組,server無法解鎖了。
來看個樣例:
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 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‘); |
SET AUTOCOMMIT=0; BEGIN; SELECT actor_id FROM actor WHERE actor_id < 4 AND actor_id <> 1 FOR UPDATE; |
該查詢只返回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)
mysql> |
表明存儲引擎從索引的起始處開始。獲取全部的行。直到actor_id<4為假,server無法告訴InnoDB去掉元組1。
為了證明row 1已經被鎖住,我們另外建一個連接,運行例如以下操作:
SET AUTOCOMMIT=0; BEGIN; SELECT actor_id FROM actor WHERE actor_id = 1 FOR UPDATE; |
該查詢會被掛起,直到第一個連接的事務提交釋放鎖時。才會運行(這樣的行為對於基於語句的復制(statement-based replication)是必要的)。
如上所看到的,當使用索引時,InnoDB會鎖住它不須要的元組。
更糟糕的是,假設查詢不能使用索引,MySQL會進行全表掃描,並鎖住每個元組,無論是否真正須要。
本文借鑒http://www.cnblogs.com/hustcat/archive/2009/10/28/1591648.html
MySQL具體解釋(9)----------索引具體解釋