從理論到實踐,Mysql查詢優化剖析

前言

以前在文章【從I/O到索引的那些事】筆者討論了索引在數據庫查詢中體現的做用,主要表現爲下降查詢的次數來提升執行效率,根本緣由是消減I/O的成本。本文將針對Mysql數據庫作一次相關優化的例證,把查詢和索引作好聯繫,加強實際應用的能力!
mysql

關於Mysql

一旦涉及到查詢優化,就離不開索引的應用,本文選取mysql經常使用的引擎InnoDB做爲研究對象,針對InnoDB引擎利用的索引結構B+樹作個簡單說明。sql

InnoDB的B+樹

假設咱們建立表Student,主鍵爲id:數據庫

CREATE TABLE `Student` (
  `id` int(16) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;複製代碼

插入12條數據:性能優化

insert into Student(id,name) valuse(1,'XiaoHong')
insert into Student(id,name) valuse(2,'XiaoMing')
insert into Student(id,name) valuse(3,'XiaoFang')
....
insert into Student(id,name) valuse(12,'XiaoYou')
複製代碼


此時,Innodb引擎將會根據主鍵id自行建立一個B+樹的索引結構,咱們有以下圖的抽象:bash


如何理解圖中結構的形態?數據結構

表數據首先會根據主鍵id的順序存儲在磁盤空間,圖中葉節點存放表中每行的真實數據,能夠認識到表數據自己屬於主鍵索引的一部分,以下圖,每行數據根據主鍵id按序存放:post


咱們設定id爲Int類型佔據4個字節,name字段爲固定10字節的Char類型,Student表每行將佔據14個字節的磁盤空間。在理想情況下,咱們能夠簡化成這樣的一個認識:假定圖中第一行(1,XiaoHong)在磁盤地址0x01,那麼第二行(2,XiaoMing)則在磁盤地址0x0f(0x01+14=0x0f),以此類推下去。性能

非葉節點存放索引值和對應的指針,咱們看到這12行數據根據主鍵id分紅了五個節點(一個非葉節點四個葉節點),真實環境下Mysql利用磁盤按塊讀取的原理設定每一個磁盤塊(也可理解爲頁,通常爲4kb,innodb中將頁大小設定爲16kb)爲一個樹節點大小,這樣每次一個磁盤I/O產生的內容就能夠獲取對應節點全部數據。測試

對於非葉節點每一個索引值左邊的指針指向小於這個索引值的對應數據的節點地址,索引值右邊的指針指向大於或等於該索引值的對應數據的節點地址:優化


如上圖,索引值爲4的左邊指針的指向結點數據一定都是小於4的,對應右指針指向節點範圍一定是大於或等於4的。並且,在索引值數目必定的狀況下,B+樹爲了控制樹的高度儘量小,會要求每一個非頁節點儘量存放更多數據,通常要求非葉節點索引值的個數至少爲(n-1)/2,n爲一個頁塊大小最多能容納的值個數。按照上圖假設的構造形態,咱們知道每一個頁塊最多隻能容納三個索引值或三行數據(實際會大不少),在這樣的前提下,若是繼續插入行數據,那麼首先是葉節點將沒有空間容納新數據,此時葉節點經過分裂來增長一個新葉節點完成保存:


能夠想象的是,咱們試圖繼續插入2條數據:

insert into Student(id,name) valuse(13,'XiaoRui')
insert into Student(id,name) valuse(14,'XiaoKe')複製代碼

最終將會變成以下形態:


由於每一個非頁節點最多容納3個索引值和對應的4個指針(扇出),整個查詢的複雜度爲O(log4N),N爲表的行數。對於擁有1000個學生數據的Student表來講,根據id查詢的複雜度爲log41000=5,在這裏,查詢複雜度在B+樹中能夠直觀地理解爲樹的高度,經過非葉節點一層層的遞進判斷最終定位到目標數據所在的頁塊地址。

所以,innodb引擎的表數據是經過主鍵索引結構來組織的,葉節點存放着行數據,是一種B+樹文件組織,若是經過主鍵定位行數據將擁有極大的效率,因此在建立表時不管有沒明肯定義主鍵索引,引擎內部都會自動爲表建立一個主鍵索引繼而構造出一個B+樹文件組織。在實際應用中,當經過主鍵去查詢某些數據時,首先是經過B+樹定位到具體的葉節點地址,由於葉節點恰好設定爲磁盤塊連續地址的整數倍大小,因此經過連續地址的快速I/O將整個節點內容加載到內存,而後從內存中對節點內容進行篩選找出目標數據!

但innodb引擎還容許咱們對錶其它字段單獨構建索引,也就是常說的輔助索引,好比咱們這樣建立Student表:

CREATE TABLE `Student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;複製代碼

插入示例數據:

insert into Student(id,name) valuse(1,'A')
insert into Student(id,name) valuse(2,'A')
insert into Student(id,name) valuse(3,'B')
......
......
insert into Student(id,name) valuse(12,'F')複製代碼

如何理解name字段索引結構存在的形式?直接上圖:


可見,輔助索引一樣會構建一個B+樹索引結構,只不過葉節點存放的是主鍵id值,非數字的索引在索引結構中按照預先設定的字符集排序規則進行排序,好比name=A在對應排序規則中是比B要小的。

按照上圖的結構,假定咱們進行以下操做:

select * from Student where name='A';複製代碼

那麼首先會利用輔助索引定位到葉節點1,而後加載到內存,在內存中檢索發現有兩個主鍵id:一、2 符合條件,而後經過主鍵id再從主鍵索引進行檢索,把行數據所有加載出來!

在輔助索引中,innodb還支持組合索引的形式,把多個字段按序組合而成一個索引,好比咱們建立以下Student表:

CREATE TABLE `StudentTmp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name_age` (`name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;複製代碼

name和age組合構成一個索引,對應B+樹索引結構有以下形式:


在該組合索引中,葉節點內容先是按照name字段進行排序,在name字段值相同狀況下再按照age字段進行排序,這樣在對name和age做爲組合條件查詢時將充分利用兩個字段的排序關係實現多級索引的定位。

好的,咱們不在糾結B+樹的更多細節,咱們只需先在腦海中構建索引結構的大致形態,想象着索引的查詢是在一個樹狀結構中層層遞進最終定位到目標數據的過程,而且認識到查詢複雜度和B+樹非葉節點中指針個數存在着聯繫,這對於咱們造成查詢成本的敏感性是很是有幫助的。

經過Explain方法來了解查詢成本

體會了索引構造帶來的查詢效率的提高,在實際應用中咱們又該如何瞭解每一個查詢Sql對索引的利用狀況呢?Explain方法能夠在執行前輔助咱們進行判斷,經過相關參數特別是對於多層嵌套或鏈接的複雜查詢語句具備很是大的幫助。

經過在查詢sql前面加上explain關鍵字就能夠完成計劃分析:

explain select id from Student where id=1;

執行後有以下結果:


咱們看到結果表單有id、table、select_type...等10個參數,每一個參數有對應的結果值,接下來咱們一步步作好認識。

id:用於標識各個子查詢的執行順序,值越大執行優先級越高

上文查詢只是一個簡單查詢,故id只有一個1,咱們如今增長一個子查詢後:

explain select name from Student where id=(select max(id) from Student);

有:


能夠看到有兩個結果行,說明這個sql有兩個查詢計劃,table字段用於指明該查詢計劃對應的表名,而id值的做用在於提示咱們哪一個查詢計劃是優先執行的。

table:指定對應查詢計劃關聯的表名

上文關於id字段的示例說明中,咱們發現id=2的查詢計劃(select max(id) from Student)對應表名是空的,這彷佛不符合常規,難道這個查詢計劃不涉及到表操做?咱們在Extra字段中找到了這樣一個說明:Select tables optimized away這個語句告訴咱們,引擎對該查詢計劃作了優化,基於索引層面的優化像min/max操做或者count(*)操做,不須要等到執行階段對錶進行檢索,該值可能預先保存在某些地方直接讀取。筆者猜測的一種狀況是,由於id字段自己屬於Student表的主鍵索引,引擎自己實時保存着min(id)、max(id)的值供查詢,或者直接讀取主鍵索引樹第一個、最後一個葉節點數據來獲取,因此相似查詢計劃在實際執行中具備極大的執行效率。

select_type:標識查詢計劃的類型

select_type主要有以下幾種不一樣類型:

  • SIMPLE:簡單SELECT,不使用UNION或子查詢等
  • PRIMARY:查詢中若包含任何複雜的子部分,最外層的select被標記爲PRIMARY
  • UNION:UNION中的第二個或後面的SELECT語句
  • SUBQUERY:子查詢中的第一個SELECT
  • DERIVED(派生表的SELECT, FROM子句的子查詢)

對於 explain select id from Student where id=1;

select_type爲SIMPLE,表示該sql是最簡單形式的查詢

對於 explain select name from Student union select name from Course;有:


咱們看到有兩個查詢計劃,對於最外層Student表的查詢爲PRIMARY,表示該語句是複雜語句,包含着其它查詢計劃,而這個包含的查詢計劃就是Course查詢計劃,Course查詢計劃的select_type爲UNION,印證了上面對UNION類型的說明。結合id字段表明的意義,咱們瞭解到引擎先是執行Course表計劃再是執行Student表計劃。

對於 explain select id,(select count(*) from Course) as count from Student; 有:


此次一樣是兩個查詢計劃,但區別在於咱們構建了一個對Course表的子查詢語句,相應的select_type爲SUBQUERY,經過id可知,該sql會優先執行Course表的查詢計劃再執行Student表的查詢計劃。

對於 explain select name from (select name from Student where id=1) tb;有:


這個語句的特別之處在於對Student表的子查詢計劃被外面包裹了一層,所以對應的select_type爲DERIVED。

到這裏,咱們認識到一個sql在執行過程當中會被拆分一個以上的查詢計劃,計劃間有必定的執行優先級,而select_type則很好地定義了不一樣計劃存在的形式,這使得咱們能夠把複雜sql進行結構上的拆解,針對不一樣的查詢計劃一個個分析最後完成總體的優化。

接下來咱們開始重點關注explian分析表單的其它幾個字段:

  • type
  • possible_keys:查詢計劃可能用到的索引
  • key:查詢計劃實際採用的索引
  • rows:查詢複雜度,亦可簡單理解爲查詢計劃須要處理的行數

這些字段和索引緊密聯繫,將真正爲咱們查詢成本的分析提供參考,咱們能夠經過這些字段很好地判斷索引的利用狀況了。

type:對錶進行數據查詢時所利用的檢索方式

type指明瞭該查詢計劃是否利用了索引結構,以及檢索上存在的具體特色,具體類別有:

  • ALL:沒用到索引, MySQL將遍歷全表以找到匹配的行
  • index: 只利用索引結構,在innodb能夠理解爲只在B+樹上進行全局檢索,不直接對錶進行操做
  • range:只檢索給定範圍的行,使用一個索引來選擇行
  • ref: 經過索引檢索,只不過該索引是非惟一索引,可能檢索出多個相同值的記錄
  • eq_ref: 相似ref,區別就在使用的索引是惟一索引,對於每一個索引鍵值,表中只有一條記錄匹配,簡單來講,就是多表鏈接中使用primary key或者 unique key做爲關聯條件
  • const、system: 當MySQL對查詢某部分進行優化,並轉換爲一個常量時,使用這些類型訪問。如將主鍵置於where列表中,MySQL就能將該查詢轉換爲一個常量,system是const類型的特例,當查詢的表只有一行的狀況下,使用system
  • NULL: MySQL在優化過程當中分解語句,執行時甚至不用訪問表或索引,例如從一個索引列裏選取最小值能夠經過單獨索引查找完成

對於 explain select name from Student where name='學生1';有:


type 爲 ALL,即name在Student表中不是索引,爲了查詢name爲'學生1'的數據,數據庫將必要地對錶數據進行全局檢索,其中rows說明了須要檢索的量級,咱們能夠理解爲查詢複雜度,由於數據庫須要對錶數據一行行處理,上面rows=699323咱們能夠判斷出Student表大概是70萬行的量級。

對於 explain select name from Student; 有:


type 爲 index,由於咱們對name已經事先構建了輔助索引,因此查詢表中全部的name信息只需在name對應的B+樹上掃描便可:


如上圖,直接在輔助索引樹的最左葉節點開始掃描,查詢出全部name信息,查詢出來的數據自己是按序排好的,若是你對sql恰好有排序需求:

select name from Student order by name asc;複製代碼

那麼查詢速度相較於從表數據結構獲取將有大幅的提高!

對於 explain select * from Student where id>1 and id<5;有:


type 爲 range,這說明這個查詢是先經過索引結構進行範圍肯定的,以下圖:


對於 explain select name from Student where name='A'; 有:


type 爲 ref,代表 name 索引是非惟一索引,即表中可能存在多個name相同的記錄,在經過name索引結構檢索數據時會把匹配條件的全部記錄都檢索出來。

對於 explain select Student.name,Score.score from Score join Student on Score.s_id=Student.id 有:


咱們注意到在此鏈接查詢中,關於Student的主鍵id做爲鏈接條件時,對應Student表的查詢計劃類型爲eq_ref,指明利用的是惟一索引的特性,每次對Student的一次查詢都將最終定位到一條結果。

對於 explain select id from Student where id=1; 有:


type 爲 const,通常sql對應查詢條件是惟一索引時纔出現此狀況,說明引擎內部對語句作了特殊處理,在計劃執行前將結果先查詢出來並轉化爲一個常量,這樣在實際執行過程當中直接引用常量可免去重複的查詢過程。咱們再給個例子:

explain select Student.name,Score.score from Score join Student on Score.s_id=Student.id where Student.id=1;有:


對於此鏈接查詢,在不考慮執行前const優化的狀況下可利用僞代碼表示成以下執行邏輯:

outerIterator=select A.s_id,A.score from Score as A;
//對Score表進行全局的行掃描
while (outerRow=outerIterator.next){
    innerIterator=select A.id,A.name from Student as A where A.id=1;
    innerRow=innerIterator.next;
    if (innerRow.id=outerRow.s_id){
        //將符合條件的結果記錄輸出
        print(outerRow.score,innerRow.name);
    }
}複製代碼

如上所示,首先是對Score表進行全局查詢,期間每一行都須要和Student表對應id的數據進行比對,但每次比對都是Student表的一次查詢消耗,所以能夠優化成以下邏輯:

//將Student表的查詢計劃優先執行,並將結果賦值到常量
constIterator=select A.id,A.name from Student as A where A.id=1;
constRow=constIterator.next;constId=constRow.id;constName=constRow.name;

//查詢計劃執行過程
outerIterator=select A.s_id,A.score from Score as A;
while (outerRow=outerIterator.next){
    //計劃執行過程當中只需和對應常量比較,大大提升執行效率
    if (innerRow.id=constId){
        print(outerRow.score,constName);
    }
}複製代碼

經過把Student表計劃的結果提取到常量將避免循環檢索中帶來的查詢消耗,由此帶來的性能提高是很是可觀的。

目前爲止咱們把Explain方法作了個基本的介紹,經過對sql查詢計劃的劃分和索引利用程度的斷定已經能提供大部分優化的思路,接下來咱們將結合真實的數據進行一次測試,咱們將重點關注rows字段的變化來斷定咱們優化的效果並但願能在整個過程引伸更多思考。

實戰優化

以前本人在論壇看到一同窗討論關於數據庫的優化過程,這裏咱們參照人家當時面對的表狀況進行演示,咱們假定基於mysql 5.5版本對一個龐大的教務系統的數據庫進行優化,其中涉及3個表:

學生表(id:學生id;name:學生名)

CREATE TABLE `Student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;複製代碼

Student表初始化有10萬行數據:

INSERT INTO `Student` (`id`, `name`)
VALUES
	(1, '學生0'),
	(2, '學生1'),
	(3, '學生2'),
        .....
        .....
        .....
        (700000,'學生699999')複製代碼

課程表(id:課程id;name:課程名稱)

CREATE TABLE `Course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;複製代碼

課程表初始化有100行數據:

INSERT INTO `Course` (`id`, `name`)
VALUES
	(1, '課程0'),
	(2, '課程1'),
	(3, '課程2'),
        .....
        .....
        .....
        (100,'課程99')
複製代碼

成績表(id:記錄id;s_id:學生id;c_id:課程id;score:分數)

CREATE TABLE `Score` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `s_id` int(11) DEFAULT NULL,
  `c_id` int(11) DEFAULT NULL,
  `score` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;複製代碼

成績表記錄着不一樣學生對應課程的分數,咱們在表裏初始化了10萬學生其中20門課程總共200萬行的數據:

INSERT INTO `Score` (`id`, `s_id`, `c_id`, `score`)
VALUES
	(1, 1, 1, 63),
	(2, 2, 1, 67),
	(3, 3, 1, 40),
        .....
        .....
        (20000000,100000,20,95)複製代碼

實際應用中有這麼個需求,須要查詢出某門課程考了100分的全部同窗的名字,寫了以下語句:

select Student.name from Student where id in (select s_id from Score where c_id = 1 and score = 100 );

筆者嘗試運行了一下,通過長達幾分鐘的等待不得不終止這個執行,爲什麼如此耗時?經過Explain分析有以下結果:


該執行計劃包含兩個查詢計劃,咱們注意到對應的rows分別爲十萬級和百萬級,對應的查詢複雜度分別爲O(100000)、O(2000000),恰好和表對應行數同樣,說明都進行了全表掃描,咱們看type字段爲ALL,印證了咱們的設想。接下來對子查詢嘗試創建索引:

alter table Score add index index_cid(c_id)複製代碼

筆者再嘗試了運行一下,通過長達幾分鐘的等待又放棄了,繼續查看Explain分析結果:

看到Score表的查詢複雜度降爲O(200000),只帶來了10倍的性能優化,經過type=ref咱們知道這是一個非惟一索引,說明c_id在表中含有大量相同值其優化效果並不可觀,咱們再嘗試對c_id和score創建一個二級索引:

alter table Score add index_cid_score (c_id,score)複製代碼

此次咱們總共獲取了1566條結果,總共耗時103s:


對應Explain分析結果:


經過組合索引咱們把Score表的查詢複雜度降到了O(1565),較單個索引有了較大幅提高,但整體的執行時間依舊不能滿意。咱們再把目光投向Student表,發現對應的查詢計劃並無利用到索引,根據Explain結果,Score表查詢計劃的id值爲2,Student表的id值爲1,按照優先級規則,應該是先執行Score計劃:

select s_id from Score where c_id = 1 and score = 100複製代碼

總共1566個結果,耗時45ms:


再執行Student計劃:

select Student.name from Student  where id in (55,68,104,243......99688)複製代碼

總共1566個結果,耗時1.06s:


咱們預想兩個查詢計劃總共的執行時間應該是1.06s+0.045s=1.105s,這與實際的103s卻有很大的差距,如何解釋?仔細觀察Score表計劃的select_type爲DEPENDENT SUBQUERY,上文介紹過SUBQUERY的形式,即表示子查詢中的第一個SELECT,這裏的DEPENDENT標識區別於普通的子查詢在於說明該計劃存在依賴關係,即Score表計劃的執行過程依賴於外面計劃(Student表)的執行結果,整個邏輯過程用僞代碼表示有:

//外部先對Student表進行全局的行掃描
outerIterator=select Student.id,Student.name from Student

while (outerRow=outerIterator.next){    
    //內部Score的執行過程依賴於外部Student表的執行結果
    innerIterator=select Score.s_id from Score where c_id = 1 and score = 100
    innerRow=innerIterator.next;
    if (innerRow.s_id=outerRow.id){
        //將符合條件的結果記錄輸出
        print(outerRow.name);
    }
}複製代碼

先是Student表進行全表掃描,而後內部Score的查詢次數取決於Student表的結果行數,由此得出的查詢複雜度爲O(總)=O(Student)*O(Score),此處實例須要Mysql承擔的計算成本爲:O(157337275)=O(100535)*O(1565),這是須要大量查詢時間的緣由!那麼有沒有辦法讓Student表的id索引也發揮做用,至少理論上按照咱們前面的設想,咱們可讓整個查詢控制在1s左右呢?

從執行邏輯上看咱們能夠設想這樣的狀況:

//外部先對Score表按條件查詢
outerIterator=select Score.s_id from Score where c_id = 1 and score = 100

while (outerRow=outerIterator.next){    
    //內部Student的執行過程依賴於外部Score表的執行結果
    innerIterator=select Student.id,Student.name from Student where id=outerRow.s_id
    innerRow=innerIterator.next;
    if (innerRow!=null){
        //將符合條件的結果記錄輸出
        print(innerRow.name);
    }
}複製代碼

先是利用Score表的組合索引檢索出c_id=一、score=100的數據,而後在循環匹配中利用Student表的id索引檢索name信息,在查詢複雜度上:

explain select Score.s_id from Score where c_id = 1 and score = 100有: explain select name from Student where id=? 有:

猜測理論上有:O(1565)=O(1565)*O(1),咱們試圖將sql用鏈接查詢來表示:

select Student.name from Student inner join Score on Student.id=Score.s_id where Score.c_id=1 and Score.score=100;


只花費0.048s!看下Explain分析:


果真知足了咱們的指望,Student表計劃用到了惟一索引、Score表用到了組合索引,最後的查詢複雜度也控制在了O(1565),區別於開始示例的子查詢,鏈接查詢又爲什麼充分利用了索引呢?內部的執行邏輯該如何去理解?

咱們這裏先理一下關於鏈接查詢的問題,在mysql中,鏈接的實現本質是笛卡爾積的過程,笛卡爾積中兩個表的全部行都將一一對應獲得一次鏈接,好比語句:

select * from Student,Course;複製代碼

對應的邏輯過程:

//外部先對Student表進行全局的行掃描
outerIterator=select Student.id,Student.name from Student;

while (outerRow=outerIterator.next){    
    //循環中外部結果每一行都將和Course表每一行進行一次鏈接
    innerIterator=select Course.id,Course.name from Course;
    while (innerRow=innerIterator.next){
        //獲取對應鏈接結果
        print(outerRow.id,outerRow.name,innerRow.id,innerRow.name)
    }  
}複製代碼

在mysql中咱們通常這樣表示:

select Student.id,Student.name,Course.id,Course.name from Student join Course;複製代碼

可知鏈接查詢的複雜度最大可達到O(Student錶行數)*O(Course錶行數),加入n個表進行鏈接查詢,那麼複雜度模型有O=O(表2行數)*O(表2行數)......*O(表n行數),這將是一個接近指數級的爆發增加!而在實際應用中,每每會經過關鍵字on和where來控制數據鏈接規模,具體爲根據實際的數據篩選條件對結果行先進行過濾,而後在內部查詢中結合索引完成優化。

好了,回來上面的問題:

select Student.name from Student inner join Score on Student.id=Score.s_id where Score.c_id=1 and Score.score=100;

筆者以前看過資料,通常數據庫進行join鏈接時會進行笛卡爾積過程,on字段做爲行鏈接時的判斷條件,最後再利用where條件進行結果行的篩選,具體邏輯過程爲:

//外部先對Student表進行全局的行掃描
outerIterator=select Student.id,Student.name from Student;

while (outerRow=outerIterator.next){    
    //內部Score的執行過程依賴於外部Student表的執行結果
    innerIterator=select Score.s_id,Score.c_id,Score.score from Score;
    while(innerRow=innerIterator.next){
         //on字段條件在此處決定是否進行鏈接
         if (outerRow.id=innerRow.s_id){
            //將符合鏈接條件的結果保存
            tmpArr[]=(outerRow.name,innerRow.c_id,innerRow.score);
         }
    }
}

//接下來開始where條件的結果過濾
for i:=0;i<n;i++{
     if (tmpArr[i].c_id=1&&tmpArr[i].score=100){
         resultArr[]=tmpArr[i];
     }
}
//完成最後的結果輸出
print(resultArr)複製代碼

按照上述過程,咱們預測的查詢複雜度應該爲O=O(Student錶行數)*O(Score錶行數);但mysql可沒這麼簡單,經過上文鏈接查詢的Explain分析,咱們看到執行過程都利用了兩個表的索引結構:

Score表利用了組合索引index_cid_score,咱們能夠猜測到引擎是先嚐試對where條件進行了先行判斷,而後再對結果集和Student表進行鏈接操做,此鏈接過程當中咱們發現Student表有利用到主鍵索引,因此一樣猜想on關鍵字的匹配條件被應用到Student表的查詢計劃中,邏輯過程這樣描述:

//外部先對Score表進行where條件篩選,查詢中利用到組合索引
outerIterator=select Score.s_id,Score.c_id,Score.score from Score where c_id=1 and score=100;
while (outerRow=outerIterator.next){    
    //內部Student的執行過程利用到主鍵索引,on字段的判斷條件此時體如今Student查詢計劃的where條件中
    innerIterator=select Student.name from Student where id=outerRow.s_id
    //若是存在對應行則保留
    if(innerRow=innerIterator.next){       
        resultArr[]=(innerRow.name,outerRow.c_id,outerRow.score);
    }
}
//完成最後的結果輸出
print(resultArr)複製代碼

很明顯,上訴邏輯過程的複雜度取決於Score表條件檢索後的行數,也符合咱們實際Explain分析的結果。

然而,筆者思考的一個問題是,對於Mysql來講並非說join鏈接就必定能知足優化需求,一方面不一樣的引擎、不一樣的Mysql版本所採用的優化手段均可能存在差別,這沒有一個固定標準,應對於這種變化在實際的業務處理中還得多結合Explain進行分析。

筆者針對本次示例在Mysql 5.6版本也嘗試執行了一下,發現對於sql:

select Student.name from Student where Student.id in (select s_id from Score where c_id = 1 and score = 100 )

在一樣的索引構建下,Explain 分析結果爲:


Score查詢計劃的id值爲2,擁有更高的執行優先級,但select_type出現了以前在Mysql5.5沒有過的字眼:MATERIALIZED,咱們先看下執行結果:


這和咱們在Mysql5.5版本關於join鏈接的結果是同樣的!回到MATERIALIZED的思考,MATERIALIZED的官方描述是:

The optimizer uses materialization to enable more efficient subquery processing. Materialization speeds up query execution by generating a subquery result as a temporary table, normally in memory. The first time MySQL needs the subquery result, it materializes that result into a temporary table. Any subsequent time the result is needed, MySQL refers again to the temporary table. The optimizer may index the table with a hash index to make lookups fast and inexpensive. The index is unique, which eliminates duplicates and makes the table smaller.

大體意思是,優化控制器爲了讓子查詢更高效,會將子查詢的結果生成一個臨時表,通常放置在內存中,同時對臨時表生成相應的哈希索引來提升內存查詢效率,示例的邏輯過程能夠這樣描述:

//先對子查詢的Score表進行物化操做,即查詢結果放置內存中
materalizedRows=select Score.s_id from Score where c_id = 1 and score = 100

for i=0;i<n;i++{
      //內存中取出對應數據
      materalizedRow=materalizedRows[i]
      //內部Student的執行過程利用到主鍵索引,on字段的判斷條件此時體如今Student查詢計劃的where條件中    
      innerIterator=select Student.name from Student where id=materalizedRow.s_id   
      //若是存在對應行則保留
      if(innerRow=innerIterator.next){      
          resultArr[]=(innerRow.name,outerRow.c_id,outerRow.score);
      }
}
//完成最後的結果輸出
print(resultArr)複製代碼

這個邏輯過程和上文join鏈接是類似的,這裏介紹一種方法用於查看sql進行優化後的表達形式,在控制檯一次輸入兩個語句:

explain select s.name from Student s where s.id in (select s_id from Score sc where sc.c_id = 1 and sc.score = 100 );
show warnings;複製代碼

獲得優化後的sql形式:

select `test`.`Student`.`name` AS `name` from `test`.`Student` semi 
    join (`test`.`Score`) 
    where (
            (`test`.`Student`.`id` = `<subquery2>`.`s_id`) 
            and (`test`.`Score`.`score` = 100) 
            and (`test`.`Score`.`c_id` = 1)
    )複製代碼

果真,Mysql5.6中,會對子查詢轉化爲join鏈接形式,而所謂的MATERIALIZED優化,筆者猜測不過是借用join鏈接所採用的優化形式而已,這說明不一樣mysql版本對sql語句的結構還會進行調整,筆者建議在面對複雜查詢的時候能夠利用此方法先進行了解,而後結合Explain方法進行分析!

到這裏,整個示例的優化過程告一段落了,不管實際環境的查詢需求多麼複雜咱們均可以先嚐試進行查詢計劃的劃分,觀察各個計劃的執行優先級,而後瞭解出引擎內部的執行邏輯,最後算出總體的查詢成本一步步調整優化,大部分狀況下筆者屢試不爽!

總結

全文一開始,咱們先是瞭解innodb引擎的索引構造,目的在於造成查詢成本的敏感性,具有查詢複雜度判斷的理論支撐,而Explain方法則具體到實際應用的過程當中,這是筆者所能想到的最乾脆的優化手段。最後的實例演示體現了優化過程的靈活性,這個靈活體如今Mysql不一樣版本的支持上,這些都須要在實際應用中積累經驗更好應對。筆者須要提醒的是,索引結構同時在影響着數據庫的維護成本,除了提升查詢效率外,在數據刪改和插入上都增長了數據庫的負擔,這個須要結合實際狀況作好權衡!

相關文章
相關標籤/搜索