在關係型數據庫中設計索引其實並非複雜的事情,不少開發者都以爲設計索引可以提高數據庫的性能,相關的知識必定很是複雜。數據庫
然而這種想法是不正確的,索引其實並非一個多麼高深莫測的東西,只要咱們掌握必定的方法,理解索引的實現就能在不須要 DBA 的狀況下設計出高效的索引。緩存
本文會介紹 數據庫索引設計與優化 中設計索引的一些方法,讓各位讀者可以快速的在現有的工程中設計出合適的索引。性能
一個數據庫必須保證其中存儲的全部數據都是能夠隨時讀寫的,同時由於 MySQL 中全部的數據其實都是以文件的形式存儲在磁盤上的,而從磁盤上隨機訪問對應的數據很是耗時,因此數據庫程序和操做系統提供了緩衝池和內存以提升數據的訪問速度。優化
除此以外,咱們還須要知道數據庫對數據的讀取並非以行爲單位進行的,不管是讀取一行仍是多行,都會將該行或者多行所在的頁所有加載進來,而後再讀取對應的數據記錄;也就是說,讀取所耗費的時間與行數無關,只與頁數有關。編碼
在 MySQL 中,頁的大小通常爲 16KB,不過也多是 8KB、32KB 或者其餘值,這跟 MySQL 的存儲引擎對數據的存儲方式有很大的關係,文中不會展開介紹,不過索引或行記錄是否在緩存池中極大的影響了訪問索引或者數據的成本。spa
數據庫等待一個頁從磁盤讀取到緩存池的所須要的成本巨大的,不管咱們是想要讀取一個頁面上的多條數據仍是一條數據,都須要消耗約 10ms 左右的時間:操作系統
10ms 的時間在計算領域實際上是一個很是巨大的成本,假設咱們使用腳本向裝了 SSD 的磁盤上順序寫入字節,那麼在 10ms 內能夠寫入大概 3MB 左右的內容,可是數據庫程序在 10ms 以內只能將一頁的數據加載到數據庫緩衝池中,從這裏能夠看出隨機讀取的代價是巨大的。設計
這 10ms 的一次隨機讀取是按照每秒 50 次的讀取計算獲得的,其中等待時間爲 3ms、磁盤的實際繁忙時間約爲 6ms,最終數據頁從磁盤傳輸到緩衝池的時間爲 1ms 左右,在對查詢進行估算時並不須要準確的知道隨機讀取的時間,只須要知道估算出的 10ms 就能夠了。code
若是在數據庫的緩存池中沒有找到對應的數據頁,那麼會去內存中尋找對應的頁面:排序
當對應的頁面存在於內存時,數據庫程序就會使用內存中的頁,這可以將數據的讀取時間下降一個數量級,將 10ms 下降到 1ms;MySQL 在執行讀操做時,會先從數據庫的緩衝區中讀取,若是不存在與緩衝區中就會嘗試從內存中加載頁面,若是前面的兩個步驟都失敗了,最後就只能執行隨機 IO 從磁盤中獲取對應的數據頁。
從磁盤讀取數據並非都要付出很大的代價,當數據庫管理程序一次性從磁盤中順序讀取大量的數據時,讀取的速度會異常的快,大概在 40MB/s 左右。
若是一個頁面的大小爲 4KB,那麼 1s 的時間就能夠讀取 10000 個頁,讀取一個頁面所花費的平均時間就是 0.1ms,相比隨機讀取的 10ms 已經下降了兩個數量級,甚至比內存中讀取數據還要快。
數據頁面的順序讀取有兩個很是重要的優點:
數據庫查詢操做的時間大都消耗在從磁盤或者內存中讀取數據的過程,因爲隨機 IO 的代價巨大,如何在一次數據庫查詢中減小隨機 IO 的次數每每可以大幅度的下降查詢所耗費的時間提升磁盤的吞吐量。
在上一節中,文章從數據頁加載的角度介紹了磁盤 IO 對 MySQL 查詢的影響,而在這一節中將介紹 MySQL 查詢的執行過程當中以及數據庫中的數據的特徵對最終查詢性能的影響。
索引片其實就是 SQL 查詢在執行過程當中掃描的一個索引片斷,在這個範圍中的索引將被順序掃描,根據索引片包含的列數不一樣,數據庫索引設計與優化 書中對將索引分爲寬索引和窄索引:
主鍵列
id
在全部的 MySQL 索引中都是必定會存在的。
對於查詢 SELECT id, username, age FROM users WHERE username="draven"
來講,(id, username) 就是一個窄索引,由於該索引沒有包含存在於 SQL 查詢中的 age 列,而 (id, username, age) 就是該查詢的一個寬索引了,它包含這個查詢中所須要的所有數據列。
寬索引可以避免二次的隨機 IO,而窄索引就須要在對索引進行順序讀取以後再根據主鍵 id 從主鍵索引中查找對應的數據:
對於窄索引,每個在索引中匹配到的記錄行最終都須要執行另外的隨機讀取從彙集索引中得到剩餘的數據,若是結果集很是大,那麼就會致使隨機讀取的次數過多進而影響性能。
從上一小節對索引片的介紹,咱們能夠看到影響 SQL 查詢的除了查詢自己還與數據庫表中的數據特徵有關,若是使用的是窄索引那麼對錶的隨機訪問就不可避免,在這時如何讓索引片變『薄』就是咱們須要作的了。
一個 SQL 查詢掃描的索引片大小實際上是由過濾因子決定的,也就是知足查詢條件的記錄行數所佔的比例:
對於 users 表來講,sex=」male」 就不是一個好的過濾因子,它會選擇整張表中一半的數據,因此在通常狀況下咱們最好不要使用 sex 列做爲整個索引的第一列;而 name=」draven」 的使用就能夠獲得一個比較好的過濾因子了,它的使用能過濾整個數據表中 99.9% 的數據;固然咱們也能夠將這三個過濾進行組合,建立一個新的索引 (name, age, sex) 並同時使用這三列做爲過濾條件:
當三個過濾條件都是等值謂詞時,幾個索引列的順序實際上是無所謂的,索引列的順序不會影響同一個 SQL 語句對索引的選擇,也就是索引 (name, age, sex) 和 (age, sex, name) 對於上圖中的條件來講是徹底同樣的,這兩個索引在執行查詢時都有着徹底相同的效果。
組合條件的過濾因子就能夠達到十萬分之 6 了,若是整張表中有 10w 行數據,也只須要在掃描薄索引片後進行 6 次隨機讀取,這種直接使用乘積來計算組合條件的過濾因子其實有一個比較重要的問題:列與列之間不該該有太強的相關性,若是不一樣的列之間有相關性,那麼獲得的結果就會比直接乘積得出的結果大一些,好比:所在的城市和郵政編碼就有很是強的相關性,二者的過濾因子直接相乘其實與實際的過濾因子會有很大的誤差,不過這在多數狀況下都不是太大的問題。
對於一張表中的同一個列,不一樣的值也會有不一樣的過濾因子,這也就形成了同一列的不一樣值最終的查詢性能也會有很大差異:
當咱們評估一個索引是否合適時,須要考慮極端狀況下查詢語句的性能,好比 0% 或者 50% 等;最差的輸入每每意味着最差的性能,在平均狀況下表現良好的 SQL 語句在極端的輸入下可能就徹底沒法正常工做,這也是在設計索引時須要注意的問題。
總而言之,須要掃描的索引片的大小對查詢性能的影響相當重要,而掃描的索引記錄的數量,就是總行數與組合條件的過濾因子的乘積,索引片的大小最終也決定了從表中讀取數據所須要的時間。
假設在 users 表中有 name、age 和 (name, sex, age) 三個輔助索引;當 WHERE 條件中存在相似 age = 21 或者 name = 「draven」 這種等值謂詞時,它們都會成爲匹配列(Matching Column)用於選擇索引樹中的數據行,可是當咱們使用如下查詢時:
1 2 3 |
SELECT * FROM users WHERE name = "draven" AND sex = "male" AND age > 20;
|
雖然咱們有 (name, sex, age) 索引包含了上述查詢條件中的所有列,可是在這裏只有 name 和 sex 兩列纔是匹配列,MySQL 在執行上述查詢時,會選擇 name 和 sex 做爲匹配列,掃描全部知足條件的數據行,而後將 age 當作過濾列(Filtering Column):
過濾列雖然不可以減小索引片的大小,可是可以減小從表中隨機讀取數據的次數,因此在索引中也扮演着很是重要的角色。
做者相信文章前面的內容已經爲索引的設計提供了充足的理論基礎和知識,從整體來看如何減小隨機讀取的次數是設計索引時須要重視的最重要的問題,在這一節中,咱們將介紹 數據庫索引設計與優化 一書中概括出的設計最佳索引的方法。
三星索引是對於一個查詢語句可能的最好索引,若是一個查詢語句的索引是三星索引,那麼它只須要進行一次磁盤的隨機讀及一個窄索引片的順序掃描就能夠獲得所有的結果集;所以其查詢的響應時間比普通的索引會少幾個數量級;根據書中對三星索引的定義,咱們能夠理解爲主鍵索引對於 WHERE id = 1
就是一個特殊的三星索引,咱們只須要對主鍵索引樹進行一次索引訪問而且順序讀取一條數據記錄查詢就結束了。
爲了知足三星索引中的三顆星,咱們分別須要作如下幾件事情:
三星索引的概念和星級的給定來源於 數據庫索引設計與優化 書中第四章三星索引一節。
若是對於一個查詢語句咱們依照上述的三個條件進行設計,那麼就能夠獲得該查詢的三星索引,這三顆星中的最後一顆星每每都是最容易得到的,知足第三顆星的索引也就是上面提到的寬索引,可以避免大量的隨機 IO,若是咱們遵循這個順序爲一個 SQL 查詢設計索引那麼咱們就能夠獲得一個完美的索引了;這三顆星的得到其實也沒有表面上這麼簡單,每一顆星都有本身的意義:
在實際場景中,問題每每沒有這麼簡單,咱們雖然能夠總可以經過寬索引避免大量的隨機訪問,可是在一些複雜的查詢中咱們沒法同時得到第一顆星和第二顆星。
1 2 3 4 5 |
SELECT id, name, age FROM users WHERE age BETWEEN 18 AND 21 AND city = "Beijing" ORDER BY name;
|
在上述查詢中,咱們總能夠經過增長索引中的列以得到第三顆星,可是若是咱們想要得到第一顆星就須要最小化索引片的大小,這時索引的前綴必須爲 (city, age),在這時再想得到第三顆星就不可能了,哪怕在 age 的後面添加索引列 name,也會由於 name 在範圍索引列 age 後面必須進行一次排序操做,最終獲得的索引就是 (city, age, name, id):
若是咱們須要在內存中避免排序的話,就須要交換 age 和 name 的位置了,在這時就能夠獲得索引 (city, name, age, id),當一個 SQL 查詢中同時擁有範圍謂詞和 ORDER BY 時,不管如何咱們都是沒有辦法得到一個三星索引的,咱們可以作的就是在這二者之間作出選擇,是犧牲第一顆星仍是第二顆星。
總而言之,在設計單表的索引時,首先把查詢中全部的等值謂詞所有取出以任意順序放在索引最前面,在這時,若是索引中同時存在範圍索引和 ORDER BY 就須要權衡利弊了,但願最小化掃描的索引片厚度時,應該將過濾因子最小的範圍索引列加入索引,若是但願避免排序就選擇 ORDER BY 中的所有列,在這以後就只須要將查詢中剩餘的所有列加入索引了,經過這種固定的方法和邏輯就能夠最快地得到一個查詢語句的二星或者三星索引了。
在單表上對索引進行設計其實仍是很是容易的,只須要遵循固定的套路就能設計出一個理想的三星索引,在這裏強烈推薦 數據庫索引設計與優化 這本書籍,其中包含了大量與索引設計與優化的相關內容;在以後的文章中讀者也會分析介紹書中提供的幾種估算方法,來幫助咱們經過預估問題設計出更高效的索引。