當程序中全部的SQL都是用到了一個或者多個索引,許多DBA就會對此感到滿意,認爲一切都看起來正常。可是,使用一個不合適的索引有可能會致使比全表掃面更差的性能。本隨筆將詳細地考慮這些極其重要的問題。首先給出咱們討論問題所需的前提假設。程序員
磁盤隨機讀取一次4k或者8k大小的頁,須要10ms,順序讀取速度40MB/s;CPU掃描一行記錄須要5us,從緩存中獲取一次數據須要100us;算法
對於下面的簡單SQL查詢,僅有的兩個合理的訪問路徑:數據庫
一、索引掃描緩存
二、全表掃描性能
即便對於最廣泛的姓氏(過濾因子1%),這兩種選擇是否可以提供可接受的響應時間呢?優化
對於第一種選擇,數據庫管理系統會根據where條件LNAME=:LNAME掃描索引片。對於索引片中的每個索引行,數據庫管理系統都必須回到表裏檢查CITY的值。因爲表中的行是根據CNO而不是LNAME聚簇的,因此這個檢查操做須要一次磁盤的隨機讀取。對於最廣泛的姓氏,在不考慮CITY的過濾因子的條件下,獲取完整的結果集意味着,需比對10000個索引行和10000個錶行。那麼,這個過程會耗時多少?設計
假設索引(LNAME,FNAME)的大小是10000*100byte=100MB,包括數據和離散的空閒空間,另外再假設順序讀的速度是40MB/s。讀取一個寬度爲1%的索引片,即1MB,需花費10ms+1/40s=35ms,這顯然沒有問題,可是10000次隨機表讀取需花費10000*10ms=100s,這使得這種方式太慢了。3d
對於第二種選擇,只有第一個頁須要隨機讀。若是表的大小爲1000000*600byte=600MB,包括數據即分散的空閒空間,那麼花費的I/O時間爲10ms+600/40s=15s,仍舊很慢。blog
第二種選擇的CPU時間將會比第一種選擇的時間長得多,由於數據庫系統須要比對1000000行而不是20000行,並且還要對這些行進行排序。從另外一個方面看,因爲是順序讀取,cpu時間跟I/O時間交疊。在這個場景下,全表掃描要比在不合適的索引上掃描快,但這還不夠快,須要有一個更好的索引。排序
前一小節咱們討論了CURSOR41的不合適的索引,這一小結咱們討論另外一個極端,三星索引,即對於一個查詢語句可能的最理想索引。相似圖4.2中的查詢語句,若是使用了三星索引,只需一次隨機讀取和一次窄索引片的掃描。所以,其響應時間會比使用普通索引的響應時間少幾個數量級。
即便返回的結果集有1000行,CURSOR41(見SQL4.2)的響應時間也不足1s,這是怎麼作到的呢?圖4.2展現了索引最低一層葉子頁的狀況。
若是結果集只有1000行的話,那麼組合where條件LNAME =: LNAME AND CITY =:CITY的過濾因子就是0.1%。被掃描的索引片就只有1000行,由於索引片的寬度徹底由LNAME和CITY兩個條件所決定。這種狀況下,查詢將花費1*1ms + 1000*0.1ms=0.1s。在這個過程當中,表根本就沒有被訪問過,由於所需的列值都被複制到了索引中了。
若是與一個查詢相關的索引行是相鄰的,或者至少相距足夠靠近的話,那麼這個索引就能夠被標上第一顆星。這最小化了必須掃描的索引片的寬度。
若是索引行的順序跟查詢語句的需求一致,則索引能夠被標記上第二顆星。這排除了排序操做。
若是索引行包含查詢語句中的全部列,那麼索引能夠標記上第三顆星。這能夠避免對錶的操做:由於直接訪問索引就能夠了。
對於這三顆星,第三顆星一般是最重要的,將一個列排除在索引以外可能會致使許多速度較慢的磁盤隨機讀。咱們把至少包含第三顆星的索引稱做對應查詢語句的寬索引。
取出全部等值條件(等值,並非範圍)的列。把這些列做爲索引最開頭的列,以任意順序均可以。對於CURSOR4.1來講,三星索引能夠以LNAME,CITY或CITY,LNAME開頭。在這兩種狀況下,必須掃描的索引片寬度將索至最小。
將order by列加入到索引中。不要改變這些列的順序,可是忽略那些在第一步中已經加入索引的列。例如,若是在CURSOR4.1 order by中有重複的列,好比 order by FNAME,LNAME或者order by CITY,FNAME,只有FNAME列須要被加到索引中。當FNAME是索引的第三列時,結果集中的記錄無需排序就已是以正確的順序排序的了。第一次讀取操做將返回FNAME值最小的那一行。
將查詢語句中剩餘的列加到索引的尾部,列在索引中添加的順序對查詢語句的性能沒有影響,可是將易變的列放在最後能下降更新的成本。如今,索引已包含了知足無須回表的訪問路徑所須要的所有列。
最終三星索引將會是:
(LNAME,CITY,FNAME,CNO)或(CITY,FNAME,CNO,LNAME)
CURSOR4.1在如下三個方面是最爲挑剔的:
下面的SQL4.3須要的信息跟以前相同,只是如今LNAME是在一個範圍內。
讓咱們爲這個CURSOR設計一個三星索引。大部分的推論跟CURSOR4.1相同,可是「between條件」將「=條件」替換後將會有很大的影響。咱們將以相反的順序考慮三顆星。
首先是第三顆星,按照先前所述,確保查詢語句中的全部列都在索引中就能知足第三顆星。這樣不須要訪問表,那麼同步讀也就不會形成問題。
添加order by列能使索引知足第二顆星,可是這個僅在將其放在between範圍條件列LNAME以前的狀況下才成立,如索引(CITY,FNAME,LNAME),因爲CITY的值只有一個,因此使用這個索引可使結果集以FNAME的順序排列,而不須要額外的排序。可是若是order by字段加在between範圍條件列LNAME後面,如索引(CITY,LNAME,FNAME),那麼索引行不是按照FNAME排序的,須要對結果集進行額外的排序。所以爲了知足第二顆星,FNAME必須放在範圍條件列LNAME以前,如索引(FNAME,...)或者(CITY,FNAME,...)。
再考慮第一顆星,若是CITY放在索引的第一列,那咱們將會有一個相對較窄的索引片須要掃描,這取決於CITY的過濾因子。可是若是使用(CITY,LNAME)的話,索引片將會更窄,這樣在有兩個列的狀況下咱們只須要訪問真正須要的索引列。可是,爲了作到這樣,並從一個很窄的索引片中獲益,其餘列(如FNAME)就不能放在這兩列之間。
因此,咱們的理想索引會有幾顆星呢?首先,確定能有第三顆星,可是,正如咱們剛纔所說,咱們只能有第一顆星或者第二顆星,而不能同時擁有二者!換句話說,咱們只能二選一:
在這個例子中,因爲between或者其餘任何訪問條件的出現,意味着咱們不能同時擁有第一和第二顆星。也就是說咱們不能擁有一個三星索引。
根據以上的討論,理想的索引是一個三星索引。然而,正如咱們所見,當存在訪問條件時,這是不可能實現的。咱們(也許)不得不犧牲第二顆星來知足一個更窄的索引片,這樣最佳索引就只擁有兩顆星。這也就是爲何咱們要仔細區分理想和最佳。在這個例子中,理想索引是不可能實現的。將這層因素考慮在內,咱們能夠對全部狀況下建立最佳索引的過程公式化。建立出的索引將擁有三顆星或者兩顆星。
首先設計一個索引片儘量窄(第一顆星)的寬索引(第三顆星)。若是查詢使用時不須要排序,那這個索引就是三星索引。不然,這個索引就只能是二星索引,犧牲第二顆星。或者採起另外一種選擇,避免排序,犧牲第一顆星保留第二顆星。
下面咱們闡述爲查詢語句建立最佳索引的算法。
若是候選A引發了所給查詢語句的一次排序操做,那麼還能夠設計候選B。根據定義,對於候選B來講,第二顆星比第一顆星更重要。
近幾年來,排序速度已經提高了不少。如今大多數的排序過程都在內存中進行,用當下最快的處理器排序一行花費的CPU時間大約在5us左右。所以,排序50000行的數據所耗費的時間只有0.5s。這對於一次事務操做來講是能夠接受的,但對於CPU時間來講是一個比較大的開銷。
因爲在如今的硬件條件下排序速度很快,因此若是一個程序取出結果集的全部行,那麼候選A可能和候選B同樣快,甚至比候選B更快。對於程序員來講,這是最方便的解決方案。許多環境都提供了靈活的命令來瀏覽結果集。
然而,若是一個程序只需獲取可以填充滿一個屏幕的數據量,那麼候選B可能會比候選A快得多。若是結果集很大的話,爲了產生第一屏的數據,二星索引候選A(須要排序)可能會花費很是長的時間。咱們須要時刻記着,客戶端的一次錯誤輸入可能會使得結果集變得很是大。
若是訪問路徑中沒有排序的話,使用CURSOR44程序將會很是快(假設LNAME和CITY是索引的前兩列,無論順序如何),即便結果集包含數以百萬級的數據行。每一個事務永遠都不會使數據庫管理系統物化大於20行的數據。