MySQL 之 索引原理與慢查詢優化
- 一 索引介紹
- 二 索引類型
- 三 索引分類
- 四 聚合索引和輔助索引
- 五 測試索引
- 六 正確使用索引
- 七 組合索引
- 八 註意事項
- 九 查詢計劃
- 十 慢日誌查詢
- 十一 大數據量分頁優化
1. 索引介紹
一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現性能問題,在生產環境中,我們遇到最多的,也是最容易出問題的,還是一些復雜的查詢操作,因此對查詢語句的優化顯然是重中之重。
說起加速查詢,就不得不提到索引了。什麽索引:
簡單的說,相當於圖書的目錄,可以幫助用戶快速的找到需要的內容.
在MySQL中也叫做“鍵”,是存儲引擎用於快速找到記錄的一種數據結構。能夠大大提高查詢效率。特別是當數據量非常大,查詢涉及多個表時,使用索引往往能使查詢速度加快成千上萬倍.
2.索引類型
1. BTREE 類型
就是一種將索引值按一定的算法,存入一個樹形的數據結構中.(如下圖:)
系統從磁盤讀取數據到內存時是以磁盤塊(block)為基本單位的,位於同一磁盤塊中的數據會被一次性讀取出來,而不是按需讀取。InnoDB 存儲引擎使用頁作為數據讀取單位,頁是其磁盤管理的最小單位,默認 page 大小是 16kB。
如上圖,是一顆b+樹,關於b+樹的定義可以參見B+樹,這裏只說一些重點,淺藍色的塊我們稱之為一個磁盤塊,可以看到每個磁盤塊包含幾個數據項(深藍色所示)和指針(黃色所示),如磁盤塊1包含數據項17和35,包含指針P1、P2、P3,P1表示小於17的磁盤塊,P2表示在17和35之間的磁盤塊,P3表示大於35的磁盤塊。真實的數據存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點不存儲真實的數據,只存儲指引搜索方向的數據項,如17、35並不真實存在於數據表中。
b+樹的查找過程
如圖所示,如果要查找數據項29,那麽首先會把磁盤塊1由磁盤加載到內存,此時發生一次IO,在內存中用二分查找確定29在17和35之間,鎖定磁盤塊1的P2指針,內存時間因為非常短(相比磁盤的IO)可以忽略不計,通過磁盤塊1的P2指針的磁盤地址把磁盤塊3由磁盤加載到內存,發生第二次IO,29在26和30之間,鎖定磁盤塊3的P2指針,通過指針加載磁盤塊8到內存,發生第三次IO,同時內存中做二分查找找到29,結束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的數據,如果上百萬的數據查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數據項都要發生一次IO,那麽總共需要百萬次的IO,顯然成本非常非常高。
註意:1.索引字段要盡量的小,磁盤塊可以存儲更多的索引.
2.索引最左匹配特性
2. HASH 類型
hash就是一種(key=>value)形式的鍵值對,允許多個key對應相同的value,但不允許一個key對應多個value,為某一列或幾列建立hash索引,就會利用這一列或幾列的值通過一定的算法計算出一個hash值,對應一行或幾行數據. hash索引可以一次定位,不需要像樹形索引那樣逐層查找,因此具有極高的效率.
假設索引使用hash函數f( ),如下:
f(‘Arjen‘) = 2323 f(‘Baron‘) = 7437 f(‘Peter‘) = 8784 f(‘Vadim‘) = 2458此時,索引的結構大概如下:
![]()
3.HASH與BTREE比較:
hash類型的索引:查詢單條快,範圍查詢慢 btree類型的索引:b+樹,層數越多,數據量越大,範圍查詢和隨機查詢快(innodb默認索引類型) 不同的存儲引擎支持的索引類型也不一樣 InnoDB 支持事務,支持行級別鎖定,支持 Btree、Full-text 等索引,不支持 Hash 索引; MyISAM 不支持事務,支持表級別鎖定,支持 Btree、Full-text 等索引,不支持 Hash 索引; Memory 不支持事務,支持表級別鎖定,支持 Btree、Hash 等索引,不支持 Full-text 索引; NDB 支持事務,支持行級別鎖定,支持 Hash 索引,不支持 Btree、Full-text 等索引; Archive 不支持事務,支持表級別鎖定,不支持 Btree、Hash、Full-text 等索引;
3.索引分類
MySQL中常見索引有:
- 普通索引
- 唯一索引
- 主鍵索引
- 組合索引
- 全文索引
1.普通索引
普通索引僅有一個功能:加速查詢
#創建表同時添加name字段為普通索引 create table tb1( id int not null auto_increment primary key, name varchar(100) not null, index idx_name(name) );創建表+索引![]()
#單獨為表指定普通索引 create index idx_name on tb1(name);
創建索引
drop index idx_name on tb1;刪除索引![]()
show index from tb1;
查看索引2.唯一索引
唯一索引有兩個功能:加速查詢 和 唯一約束(可含一個null 值)
create table tb2( id int not null auto_increment primary key, name varchar(50) not null, age int not null, unique index idx_age (age) )創建表+唯一(unique)索引
create unique index idx_age on tb2(age);創建unique索引
drop unique index idx_age on tb2;刪除unique索引3.主鍵索引
主鍵有兩個功能:加速查詢 和 唯一約束(不可含null)
註意:一個表中最多只能有一個主鍵索引
#方式一: create table tb3( id int not null auto_increment primary key, name varchar(50) not null, age int default 0 ); #方式二: create table tb3( id int not null auto_increment, name varchar(50) not null, age int default 0 , primary key(id) );創建表 + 創建主鍵
alter table tb3 add primary key(id);創建主鍵
#方式一 alter table tb3 drop primary key; #方式二: #如果當前主鍵為自增主鍵,則不能直接刪除.需要先修改自增屬性,再刪除 alter table tb3 modify id int ,drop primary key;刪除主鍵4.組合索引
組合索引是將n個列組合成一個索引
其應用場景為:頻繁的同時使用n列來進行查詢,如:where n1 = ‘alex‘ and n2 = 666。
create table tb4( id int not null primary key, name varchar(50), age int, sex char(1), index idx_age_sex(age,sex) );創建表+組合索引
create index idx_age_sex on tb4(age,sex);創建組合索引
drop index idx_age_sex on tb4;刪除組合索引5.全文索引(了解)
全文索引(也稱全文檢索)是目前搜索引擎使用的一種關鍵技術。它能夠利用 [分詞技術] 等多種算法智能分析出文本文字中關鍵字詞的頻率及重要性,然後按照一定的算法規則智能地篩選出我們想要的搜索結果。
強烈註意:MySQL自帶的全文索引只能用於數據庫引擎為MyISAM的數據表,如果是其他數據引擎,則全文索引不會生效。此外,MySQL(5.6版本之前)自帶的全文索引只能對英文進行全文檢索,目前無法對中文進行全文檢索。如果需要對包含中文在內的文本數據進行全文檢索,可以采用一些外部工具來完成.。比如采用Sphinx來做mysql的全文索引工具是一個很好的選擇。
介紹索引創建的原則: 可以創建索引: 1. 字段經常出現在where子句或者連接條件中 2. 字段識別度高 3. 表經常被訪問、數據量很大,且通常每次訪問的數據量小於記錄總量的2%-4% 不用創建索引: 1. 表很小的情況下,沒有必要使用索引 2. 不經常在Where後使用的字段 3. 字段的識別度低(比如性別) 4. 如果表數據需要頻繁修改,不建議使用索引 5. 如果查詢返回記錄很多(每次訪問的數據量大於記錄總數的2%-4%),不建議使用索引 6. 如果where後含IS NULL /IS NOT NULL/ like ‘%輸入符%’等條件,不建議使用索引。索引創建的原則
4.聚合索引和輔助索引
數據庫中的B+樹索引可以分為聚集索引和輔助索引.
聚集索引:InnoDB表 索引組織表,即表中數據按主鍵B+樹存放,葉子節點直接存放整條數據,每張表只能有一個聚集索引。
1.當你定義一個主鍵時,InnnodDB存儲引擎則把它當做聚集索引
2.如果你沒有定義一個主鍵,則InnoDB定位到第一個唯一索引,且該索引的所有列值均飛空的,則將其當做聚集索引。
3如果表沒有主鍵或合適的唯一索引INNODB會產生一個隱藏的行ID值6字節的行ID聚集索引,
補充:由於實際的數據頁只能按照一顆B+樹進行排序,因此每張表只能有一個聚集索引,聚集索引對於主鍵的排序和範圍查找非常有利.
例子: 比如圖書館新進了一批書。那麽這些書需要放到圖書館內。書如何放呢?一般都有一個規則,雜誌類的放到101房間,文學類的放到102房間,理工類的放到103房間等等。這些存儲的規則決定了每本書應該放到哪裏。而這個例子中聚集索引為書的類別。
輔助索引:(也稱非聚集索引)是指葉節點不包含行的全部數據,葉節點除了包含鍵值之外,還包含一個書簽連接,通過該書簽再去找相應的行數據。下圖顯示了InnoDB存儲引擎輔助索引和聚集索引的關系:
從上圖中可以看出,輔助索引葉節點存放的是主鍵值,獲得主鍵值後,再從聚集索引中查找整行數據。舉個例子,如果在一顆高度為3的輔助索引中查找數據,首先從輔助索引中獲得主鍵值(3次IO),接著從高度為3的聚集索引中查找以獲得整行數據(3次IO),總共需6次IO。一個表上可以存在多個輔助索引。
例子: 同學如果想去圖書館找一本書,而不知道這本書在哪裏?那麽這個同學首先應該找的就是 檢索室吧。對於要查找一本書來說,在檢索室查是一個非常快捷的的途徑了吧。但是,在檢索室中你查到了該書在XX室XX書架的信息。你的查詢結束了嗎?沒有吧。你僅僅找到了目的書的位置信息,你還要去該位置去取書。
對於這種方式來說,你需要兩個步驟:
1、查詢該記錄所在的位置。
2、通過該位置去取要找的記錄。總結二者區別:
相同的是:不管是聚集索引還是輔助索引,其內部都是B+樹的形式,即高度是平衡的,葉子結點存放著所有的數據。
不同的是:聚集索引葉子結點存放的是一整行的信息,而輔助索引葉子結點存放的是單個索引列信息.
為了闡述非聚集索引寫性能問題,我們先來看一個例子: mysql>create table t ( id int auto_increment, name varchar(30), primary key (id)); 我們創建了一個表,表的主鍵是id,id列式自增長的,即當執行插入操作時,id列會自動增長,頁中行記錄按id順序存放,不需要隨機讀取其它頁的數據。因此,在這樣的情況下(即聚集索引),插入操作效率很高。 但是,在大部分應用中,很少出現表中只有一個聚集索引的情況,更多情況下,表上會有多個非聚集的(輔助索引)。比如,對於上一張表t,業務上還需要按非唯一的name字段查找,則表定義改為: mysql>create table t ( id int auto_increment, name varchar(30), primary key (id), key (name)); 這時,除了主鍵聚合索引外,還產生了一個name列的輔助索引,對於該非聚集索引來說,葉子節點的插入不再有序,這時就需要離散訪問非聚集索引頁,插入性能變低。輔助索引寫性能問題
何時使用聚集索引或非聚集索引
下面的表總結了何時使用聚集索引或非聚集索引(很重要):
動作描述
使用聚集索引
使用非聚集索引
列經常被分組排序
應
應
返回某範圍內的數據
應
不應
一個或極少不同值
不應
不應
小數目的不同值
應
不應
大數目的不同值
不應
應
頻繁更新的列
不應
應
外鍵列
應
應
主鍵列
應
應
頻繁修改索引列
不應
應
5.測試索引
1.創建數據
-- 1.創建表 CREATE TABLE userInfo( id int NOT NULL, name VARCHAR(16) DEFAULT NULL, age int, sex char(1) not null, email varchar(64) default null )ENGINE=MYISAM DEFAULT CHARSET=utf8;創建表註意:MYISAM存儲引擎 不產生引擎事務,數據插入速度極快,為方便快速插入測試數據,等我們插完數據,再把存儲類型修改為InnoDB
2.創建存儲過程,插入數據
-- 2.創建存儲過程 delimiter$$ CREATE PROCEDURE insert_user_info(IN num INT) BEGIN DECLARE val INT DEFAULT 0; DECLARE n INT DEFAULT 1; -- 循環進行數據插入 WHILE n <= num DO set val = rand()*50; INSERT INTO userInfo(id,name,age,sex,email)values(n,concat(‘alex‘,val),rand()*50,if(val%2=0,‘女‘,‘男‘),concat(‘alex‘,n,‘@qq.com‘)); set n=n+1; end while; END $$ delimiter;創建存儲過程3.調用存儲過程,插入500萬條數據
call insert_user_info(5000000);
4.此步驟可以忽略。修改引擎為INNODB
ALTER TABLE userinfo ENGINE=INNODB;
5.測試索引
1. 在沒有索引的前提下測試查詢速度
SELECT * FROM userinfo WHERE id = 4567890;
註意:無索引情況,mysql根本就不知道id等於4567890的記錄在哪裏,只能把數據表從頭到尾掃描一遍,此時有多少個磁盤塊就需要進行多少IO操作,所以查詢速度很慢.
2.在表中已經存在大量數據的前提下,為某個字段段建立索引,建立速度會很慢
CREATE INDEX idx_id on userinfo(id);
3.在索引建立完畢後,以該字段為查詢條件時,查詢速度提升明顯
select * from userinfo where id = 4567890;
註意:
1. mysql先去索引表裏根據b+樹的搜索原理很快搜索到id為4567890的數據,IO大大降低,因而速度明顯提升
2. 我們可以去mysql的data目錄下找到該表,可以看到添加索引後該表占用的硬盤空間多了
3.如果使用沒有添加索引的字段進行條件查詢,速度依舊會很慢(如圖:)
6.正確使用索引
數據庫表中添加索引後確實會讓查詢速度起飛,但前提必須是正確的使用索引來查詢,如果以錯誤的方式使用,則即使建立索引也會不奏效。
即使建立索引,索引也不會生效,例如:
1. 範圍查詢(>、>=、<、<=、!= 、between...and) #1. = 等號 select count(*) from userinfo where id = 1000 -- 執行索引,索引效率高 #2. > >= < <= between...and 區間查詢 select count(*) from userinfo where id <100; -- 執行索引,區間範圍越小,索引效率越高 select count(*) from userinfo where id >100; --執行索引,區間範圍越大,索引效率越低 select count(*) from userinfo where id between 10 and 500000 --執行索引,區間範圍越大,索引效率越低 #3. != 不等於 select count(*) from userinfo where id != 1000; -- 索引範圍大,索引效率低 2.like ‘%xx%‘ 為 name 字段添加索引 create index idx_name on userinfo(name); select count(*) from userinfo where name like ‘%xxxx%‘; -- 全模糊查詢,索引效率低 select count(*) from userinfo where name like ‘%xxxx‘; -- 以什麽結尾模糊查詢,索引效率低 例外: 當like使用以什麽開頭會索引使用率高 select * from userinfo where name like ‘xxxx%‘; 3.or select count(*) from userinfo where id = 12334 or email =‘xxxx‘; -- email不是索引字段,索引此查詢全表掃描 例外:當or條件中有未建立索引的列才失效,以下會走索引 select count(*) from userinfo where id = 12334 or name = ‘alex3‘; -- id 和 name 都為索引字段時, or條件也會執行索引 4.使用函數 select count(*) from userinfo where reverse(name) = ‘5xela‘; -- name索引字段,使用函數時,索引失效 例外:索引字段對應的值可以使用函數,我們可以改為一下形式 select count(*) from userinfo where name = reverse(‘5xela‘); 5.類型不一致 如果列是字符串類型,傳入條件是必須用引號引起來,不然... select count(*) from userinfo where name = 454; 6.order by #排序條件為索引,則select字段必須也是索引字段,否則無法命中 select email from userinfo ORDER BY name DESC; -- 無法命中索引 select name from userinfo ORDER BY name DESC; -- 命中索引 特別的:如果對主鍵排序,則還是速度很快: select * from userinfo order by id desc;示例
7.組合索引
組合索引: 是指對表上的多個列組合起來做一個索引.
組合索引好處:簡單的說有兩個主要原因:
- "一個頂三個"。建了一個(a,b,c)的組合索引,那麽實際等於建了(a),(a,b),(a,b,c)三個索引,因為每多一個索引,都會增加寫操作的開銷和磁盤空間的開銷。對於大量數據的表,這可是不小的開銷!
- 索引列越多,通過索引篩選出的數據越少。有1000W條數據的表,有如下sql:select * from table where a = 1 and b =2 and c = 3,假設假設每個條件可以篩選出10%的數據,如果只有單值索引,那麽通過該索引能篩選出1000W*10%=100w 條數據,然後再回表從100w條數據中找到符合b=2 and c= 3的數據,然後再排序,再分頁;如果是組合索引,通過索引篩選出1000w *10% *10% *10%=1w,然後再排序、分頁,哪個更高效,一眼便知
組合索引最左匹配原則: 從左往右依次使用生效,如果中間某個索引沒有使用,那麽斷點前面的索引部分起作用,斷點後面的索引沒有起作用;
select * from mytable where a=3 and b=5 and c=4; abc三個索引都在where條件裏面用到了,而且都發揮了作用 select * from mytable where c=4 and b=6 and a=3; 這條語句列出來只想說明 mysql沒有那麽笨,where裏面的條件順序在查詢之前會被mysql自動優化,效果跟上一句一樣 select * from mytable where a=3 and c=7; a用到索引,b沒有用,所以c是沒有用到索引效果的 select * from mytable where a=3 and b>7 and c=3; a用到了,b也用到了,c沒有用到,這個地方b是範圍值,也算斷點,只不過自身用到了索引 select * from mytable where b=3 and c=4; 因為a索引沒有使用,所以這裏 bc都沒有用上索引效果 select * from mytable where a>4 and b=7 and c=9; a用到了 b沒有使用,c沒有使用 select * from mytable where a=3 order by b; a用到了索引,b在結果排序中也用到了索引的效果 select * from mytable where a=3 order by c; a用到了索引,但是這個地方c沒有發揮排序效果,因為中間斷點了 select * from mytable where b=3 order by a; b沒有用到索引,排序中a也沒有發揮索引效果示例
8.註意事項
1. 避免使用select * 2. count(1)或count(列) 代替 count(*) 3. 創建表時盡量時 char 代替 varchar 4. 表的字段順序固定長度的字段優先 5. 組合索引代替多個單列索引(經常使用多個條件查詢時) 6. 使用連接(JOIN)來代替子查詢(Sub-Queries) 7. 不要有超過5個以上的表連接(JOIN) 8. 優先執行那些能夠大量減少結果的連接。 9. 連表時註意條件類型需一致 10.索引散列值不適合建索引,例:性別不適合
9.查詢計劃
explain + 查詢SQL - 用於顯示SQL執行信息參數,根據參考信息可以進行SQL優化
explain select count(*) from userinfo where id = 1;
執行計劃:讓mysql預估執行操作(一般正確) all < index < range < index_merge < ref_or_null < ref < eq_ref < system/const 慢: explain select * from userinfo where email=‘alex‘; type: ALL(全表掃描) 特別的: select * from userinfo limit 1; 快: explain select * from userinfo where name=‘alex‘; type: ref(走索引)EXPLAIN 參數詳解: http://www.cnblogs.com/wangfengming/articles/8275448.html
10.慢日誌查詢
慢查詢日誌
將mysql服務器中影響數據庫性能的相關SQL語句記錄到日誌文件,通過對這些特殊的SQL語句分析,改進以達到提高數據庫性能的目的。
慢查詢日誌參數:
long_query_time : 設定慢查詢的閥值,超出設定值的SQL即被記錄到慢查詢日誌,缺省值為10s slow_query_log : 指定是否開啟慢查詢日誌 log_slow_queries : 指定是否開啟慢查詢日誌(該參數已經被slow_query_log取代,做兼容性保留) slow_query_log_file : 指定慢日誌文件存放位置,可以為空,系統會給一個缺省的文件host_name-slow.log log_queries_not_using_indexes: 為使用索引的搜索是否記錄查看 MySQL慢日誌信息
#.查詢慢日誌配置信息 : show variables like ‘%query%‘; #.修改配置信息 set global slow_query_log = on;查看不使用索引參數狀態:
# 顯示參數 show variables like ‘%log_queries_not_using_indexes‘; # 開啟狀態 set global log_queries_not_using_indexes = on;查看慢日誌顯示的方式
#查看慢日誌記錄的方式 show variables like ‘%log_output%‘; #設置慢日誌在文件和表中同時記錄 set global log_output=‘FILE,TABLE‘;測試慢查詢日誌
#查詢時間超過10秒就會記錄到慢查詢日誌中 select sleep(3) FROM user ; #查看表中的日誌 select * from mysql.slow_log;
11.大數據量分頁優化
執行此段代碼:
select * from tb1 limit 3000000,10;優化方案:
一. 簡單粗暴,就是不允許查看這麽靠後的數據,比如百度就是這樣的
最多翻到72頁就不讓你翻了,這種方式就是從業務上解決;
二.在查詢下一頁時把上一頁的行id作為參數傳遞給客戶端程序,然後sql就改成了
select * from tb1 where id>3000000 limit 10;這條語句執行也是在毫秒級完成的,id>300w其實就是讓mysql直接跳到這裏了,不用依次在掃描全面所有的行
如果你的table的主鍵id是自增的,並且中間沒有刪除和斷點,那麽還有一種方式,比如100頁的10條數據
select * from tb1 where id>100*10 limit 10;
三.最後第三種方法:延遲關聯
我們在來分析一下這條語句為什麽慢,慢在哪裏。
select * from tb1 limit 3000000,10;玄機就處在這個 * 裏面,這個表除了id主鍵肯定還有其他字段 比如 name age 之類的,因為select * 所以mysql在沿著id主鍵走的時候要回行拿數據,走一下拿一下數據;
如果把語句改成
select id from tb1 limit 3000000,10;你會發現時間縮短了一半;然後我們在拿id分別去取10條數據就行了;
語句就改成這樣了:
select table.* from tb1 inner join ( select id from tb1 limit 3000000,10 ) as tmp on tmp.id=table.id;這三種方法最先考慮第一種 其次第二種,第三種是別無選擇
MySQL 之 索引原理與慢查詢優化