MySQL 之 索引原理與慢查詢優化


1. 索引介紹

需求:

  一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現性能問題,在生產環境中,我們遇到最多的,也是最容易出問題的,還是一些復雜的查詢操作,因此對查詢語句的優化顯然是重中之重。
說起加速查詢,就不得不提到索引了。

索引:

   簡單的說,相當於圖書的目錄,可以幫助用戶快速的找到需要的內容.

   在MySQL中也叫做“鍵”,是存儲引擎用於快速找到記錄的一種數據結構。能夠大大提高查詢效率。特別是當數據量非常大,查詢涉及多個表時,使用索引往往能使查詢速度加快成千上萬倍.

本質:

  索引本質:通過不斷地縮小想要獲取數據的范圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查找方式來鎖定數據。

2.索引方法

 1. B+TREE 索引

  B+樹是一種經典的數據結構,由平衡樹二叉查找樹結合產生,它是為磁盤或其它直接存取輔助設備而設計的一種平衡查找樹,在B+樹中,所有的記錄節點都是按鍵值大小順序存放在同一層的葉節點中,葉節點間用指針相連,構成雙向循環鏈表,非葉節點(根節點、枝節點)只存放鍵值,不存放實際數據。下面看一個2層B+樹的例子:

注意:通常其高度都在2~3層,查詢時可以有效減少IO次數。

  系統從磁盤讀取數據到內存時是以磁盤塊(block)為基本單位的,位於同一磁盤塊中的數據會被一次性讀取出來,而不是按需讀取。InnoDB 存儲引擎使用頁作為數據讀取單位,頁是其磁盤管理的最小單位,默認 page 大小是 16kB。

b+樹的查找過程

  如圖所示,如果要查找數據項30,那么首先會把磁盤塊1由磁盤加載到內存,此時發生一次IO,在內存中用二分查找確定30在28和65之間,鎖定磁盤塊1的P2指針,內存時間因為非常短(相比磁盤的IO)可以忽略不計,通過磁盤塊1的P2指針的磁盤地址把磁盤塊由磁盤加載到內存,發生第二次IO,30在28和35之間,鎖定當前磁盤塊的P1指針,通過指針加載磁盤塊到內存,發生第三次IO,同時內存中做二分查找找到30,結束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的數據,如果上百萬的數據查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數據項都要發生一次IO,那么總共需要百萬次的IO,顯然成本非常非常高。

 強烈注意: 索引字段要盡量的小,磁盤塊可以存儲更多的索引.

 2. HASH 索引

    hash就是一種(key=>value)形式的鍵值對,允許多個key對應相同的value,但不允許一個key對應多個value,為某一列或幾列建立hash索引,就會利用這一列或幾列的值通過一定的算法計算出一個hash值,對應一行或幾行數據.   hash索引可以一次定位,不需要像樹形索引那樣逐層查找,因此具有極高的效率.

 

假設索引使用hash函數f( ),如下:

1
2
3
4
f( 'Arjen' ) = 2323
f( 'Baron' ) = 7437
f( 'Peter' ) = 8784
f( 'Vadim' ) = 2458

此時,索引的結構大概如下:

  

   3.HASH與BTREE比較:

復制代碼
hash類型的索引:查詢單條快,范圍查詢慢
btree類型的索引:b+樹,層數越多,數據量越大,范圍查詢和隨機查詢快(innodb默認索引類型)

不同的存儲引擎支持的索引類型也不一樣
InnoDB 支持事務,支持行級別鎖定,支持 Btree、Hash 等索引,不支持Full-text 索引;
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;
查看索引
1、Table 表的名稱。

2、 Non_unique 如果索引為唯一索引,則為0,如果可以則為1。

3、 Key_name 索引的名稱

4、 Seq_in_index 索引中的列序列號,從1開始。

5、 Column_name 列名稱。

6、 Collation 列以什么方式存儲在索引中。在MySQL中,有值‘A’(升序)或NULL(無分類)。

7、Cardinality 索引中唯一值的數目的估計值。

8、Sub_part 如果列只是被部分地編入索引,則為被編入索引的字符的數目。如果整列被編入索引,則為NULL。

9、 Packed 指示關鍵字如何被壓縮。如果沒有被壓縮,則為NULL。

10、 Null 如果列含有NULL,則含有YES。如果沒有,則該列含有NO。

11、 Index_type 用過的索引方法(BTREE, FULLTEXT, HASH, RTREE)。

12、 Comment 多種評注
查看索引 列介紹

 

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索引

 

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 ,
  name varchar(50) not null,
  age int not null,
  index idx_name_age (name,age)   
)
創建表+組合索引
create index idx_name_age on tb4(name,age);
創建組合索引
舉個例子來說,比如你在為某商場做一個會員卡的系統。

這個系統有一個會員表
有下列字段:
會員編號 INT
會員姓名 VARCHAR(10)
會員身份證號碼 VARCHAR(18)
會員電話 VARCHAR(10)
會員住址 VARCHAR(50)
會員備注信息 TEXT

那么這個 會員編號,作為主鍵,使用 PRIMARY
會員姓名 如果要建索引的話,那么就是普通的 INDEX
會員身份證號碼 如果要建索引的話,那么可以選擇 UNIQUE (唯一的,不允許重復)
索引應用場景

 

4.聚合索引和輔助索引 

數據庫中的B+樹索引可以分為聚集索引和輔助索引.

聚集索引:InnoDB表 索引組織表,即表中數據按主鍵B+樹存放,葉子節點直接存放整條數據,每張表只能有一個聚集索引。

如圖:

1.當你定義一個主鍵時,InnnodDB存儲引擎則把它當做聚集索引

2.如果你沒有定義一個主鍵,則InnoDB定位到第一個唯一索引,且該索引的所有列值均飛空的,則將其當做聚集索引。

3如果表沒有主鍵或合適的唯一索引INNODB會產生一個隱藏的行ID值6字節的行ID聚集索引,

補充:由於實際的數據頁只能按照一顆B+樹進行排序,因此每張表只能有一個聚集索引,聚集索引對於主鍵的排序和范圍查找非常有利.

 

輔助索引:(也稱非聚集索引)是指葉節點不包含行的全部數據,葉節點除了包含鍵值之外,還包含一個書簽連接,通過該書簽再去找相應的行數據。下圖顯示了

InnoDB存儲引擎輔助索引獲得數據的查找方式:

從上圖中可以看出,輔助索引葉節點存放的是主鍵值,獲得主鍵值后,再從聚集索引中查找整行數據。舉個例子,如果在一顆高度為3的輔助索引中查找數據,首先從輔助索引中獲得主鍵值(3次IO),接着從高度為3的聚集索引中查找以獲得整行數據(3次IO),總共需6次IO。一個表上可以存在多個輔助索引。

總結二者區別:

  相同的是:不管是聚集索引還是輔助索引,其內部都是B+樹的形式,即高度是平衡的,葉子結點存放着所有的數據。

  不同的是:聚集索引葉子結點存放的是一整行的信息,而輔助索引葉子結點存放的是單個索引列信息.

何時使用聚集索引或非聚集索引

下面的表總結了何時使用聚集索引或非聚集索引(很重要):

動作描述

使用聚集索引

使用非聚集索引

列經常被分組排序

返回某范圍內的數據

不應

一個或極少不同值

不應

不應

頻繁更新的列

不應

外鍵列

主鍵列

頻繁修改索引列

不應

 

 

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萬條數據

1
call insert_user_info(5000000);

 4.此步驟可以忽略。修改引擎為INNODB

1
ALTER  TABLE  userinfo ENGINE=INNODB;

5.測試索引

1. 在沒有索引的前提下測試查詢速度

1
SELECT  FROM  userinfo  WHERE  id = 4567890;
注意:無索引情況,mysql根本就不知道id等於4567890的記錄在哪里,只能把數據表從頭到尾掃描一遍,此時有多少個磁盤塊就需要進行多少IO操作,所以查詢速度很慢.

2.在表中已經存在大量數據的前提下,為某個字段段建立索引,建立速度會很慢

1
CREATE  INDEX  idx_id  on  userinfo(id);

   

 3.在索引建立完畢后,以該字段為查詢條件時,查詢速度提升明顯

1
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;
        
    #類型一致
    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 id 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
2
3
4
5
6
7
8
9
10
1. 避免使用 select  *
2. 其他數據庫中使用 count (1)或 count (列) 代替  count (*),而mysql數據庫中 count (*)經過優化后,效率與前兩種基本一樣.
3. 創建表時盡量時  char  代替  varchar
4. 表的字段順序固定長度的字段優先
5. 組合索引代替多個單列索引(經常使用多個條件查詢時)
6. 使用連接( JOIN )來代替子查詢(Sub-Queries)
7. 不要有超過4個以上的表連接( JOIN
8. 優先執行那些能夠大量減少結果的連接。
9. 連表時注意條件類型需一致
10.索引散列值不適合建索引,例:性別不適合

  

9.查詢計划

 explain + 查詢SQL - 用於顯示SQL執行信息參數,根據參考信息可以進行SQL優化

1
explain   select  count (*)  from  userinfo  where   id = 1;

復制代碼
執行計划:讓mysql預估執行操作(一般正確)
  type : 查詢計划的連接類型, 有多個參數,先從最佳類型到最差類型介紹
  性能: null > system/const > eq_ref > ref > ref_or_null > index_merge > range > index > all 慢: 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語句分析,改進以達到提高數據庫性能的目的。

慢查詢日志參數:

1
2
3
4
5
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: 如果值設置為 ON ,則會記錄所有沒有利用索引的查詢.

查看 MySQL慢日志信息

1
2
3
4
#.查詢慢日志配置信息 :
show variables  like  '%query%' ;
#.修改配置信息
set  global  slow_query_log  =  on ;

查看不使用索引參數狀態:

1
2
3
4
# 顯示參數  
show variables  like  '%log_queries_not_using_indexes' ;
# 開啟狀態
set  global  log_queries_not_using_indexes  =  on ;

查看慢日志顯示的方式

1
2
3
4
5
#查看慢日志記錄的方式
show variables  like  '%log_output%' ;
 
#設置慢日志在文件和表中同時記錄
set  global  log_output= 'FILE,TABLE' ;

測試慢查詢日志

1
2
3
4
5
#查詢時間超過10秒就會記錄到慢查詢日志中
select  sleep(3)  FROM  user  ;
 
#查看表中的日志
select  from  mysql.slow_log;

 

11.大數據量分頁優化 

 執行此段代碼:

1
select  from  userinfo limit 3000000,10;

優化方案:

一. 簡單粗暴,就是不允許查看這么靠后的數據,比如百度就是這樣的

最多翻到72頁就不讓你翻了,這種方式就是從業務上解決;

 

二.在查詢下一頁時把上一頁的行id作為參數傳遞給客戶端程序,然后sql就改成了

1
select  from  userinfo  where  id>3000000 limit 10;

這條語句執行也是在毫秒級完成的,id>300w其實就是讓mysql直接跳到這里了,不用依次在掃描全面所有的行

如果你的table的主鍵id是自增的,並且中間沒有刪除和斷點,那么還有一種方式,比如100頁的10條數據

1
select  from  userinfo  where  id>100*10 limit 10;

  

三.最后第三種方法:延遲關聯

我們在來分析一下這條語句為什么慢,慢在哪里。

1
select  from  userinfo limit 3000000,10;

玄機就處在這個 * 里面,這個表除了id主鍵肯定還有其他字段  比如 name  age  之類的,因為select  *  所以mysql在沿着id主鍵走的時候要回行拿數據,走一下拿一下數據;

如果把語句改成 

1
select  id  from  userinfo limit 3000000,10;

你會發現時間縮短了一半;然后我們在拿id分別去取10條數據就行了;

語句就改成這樣了:

1
select  table .*  from  userinfo  inner  join  select  id  from  userinfo limit 3000000,10 )  as  tmp  on  tmp.id=userinfo.id;

這三種方法最先考慮第一種 其次第二種,第三種是別無選擇


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM