基於MySQL 5.6.16
一次內存訪問、SSD 硬盤訪問和SATA 硬盤隨機訪問的時間分別約是_______。 html
A 幾微秒,幾毫秒,幾十毫秒mysql
B 幾微秒,幾毫秒,幾十毫秒git
C 幾十納秒,幾十微秒,幾十毫秒程序員
D 幾十納秒,幾十微秒,十幾毫秒
以上騰訊2017實習生題目,答案爲C。github
當前互聯網時代,性能尤其重要,性能差即意味着不可用。既然內存性能最好,是否能夠將數據所有加載在內存中?算法
2018年12月,「美光旗下品牌英睿達(Crucial)宣佈已經開始出貨自家容量最高、速度最快的服務器級內存,128GBDDR4-4266LRDIMM,一條就要3999美圓,約合人民幣2.65萬元。」sql
大數據時代,數據隨隨便便就上T,基本成本、容量等方面考慮,沒法將數據所有加載入內存。因爲沒法所有裝入內存,則必然依賴磁盤存儲。而內存的讀寫速度是磁盤的成千上萬倍(與具體實現有關),所以,存儲的核心問題是「如何減小磁盤讀寫次數」。數據庫
Hash table,也叫散列表, 是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。哈希表最大的優勢,就是把數據的存儲和查找消耗的時間大大下降,幾乎能夠當作是常數時間(不考慮hash衝突的狀況下,時間複雜度爲O(1))。segmentfault
因爲hash索引的上述缺點,因此實際使用hash索引的狀況不多,MySQL的Memory存儲引擎和NDB分佈式存儲引擎使用了hash結構索引。
當一個數據結構,在支持第一點(順序存儲)的狀況下,對第二點(部分匹配)有一個自然的加強:全部前綴同樣的值都是按順序存儲在一塊兒的,當咱們使用左前綴查詢時,從第一個符合前綴條件的值開始掃描,掃到第一個不符合規則的值便可中止,不用掃描所有內容。數組
BST、AVL、RBT很好的將讀寫次數從O(n)優化到O(log2(n));其中,AVL和RBT都比BST多了自平衡的功能,將讀寫次數降到最大O(log2(n))。
假設使用自增主鍵,則主鍵自己是有序的,樹結構的讀寫次數可以優化到樹高(無序的數據會致使插入位置先後的節點移動),樹的高度越低讀寫次數越少;自平衡保證了樹結構的穩定。若是想進一步優化,能夠引入B/B+樹。
示例圖:
若是拋開維護操做,那麼B樹就像一棵「m叉搜索樹」(m是子樹的最大個數),時間複雜度爲O(logm(n))。然而,B樹設計了一種高效簡單的維護操做,使B樹的深度維持在約log(ceil(m/2))(n)~logm(n)之間,大大下降樹高。與單純的算法不一樣,磁盤IO次數纔是更大的影響因素。B樹與AVL的時間複雜度是相同的,但因爲B樹的層數少,磁盤IO次數少,實踐中B樹的性能要優於AVL等二叉樹,例如上圖中查找「10」數據,只須要通過三次磁盤I/O便可。
另外,B樹對局部性原理很是友好:
預讀:對於每一個文件的第一個讀請求,系統讀入所請求的頁面並讀入緊隨其後的少數幾個頁面(X86的Linux中一個標準頁面大小是4KB),這時的預讀稱爲同步預讀。對於第二次讀請求,若是所讀頁面不在 Cache 中,即不在前次預讀的頁中,則代表文件訪問不是順序訪問,系統繼續採用同步預讀;若是所讀頁面在 Cache 中,則代表前次預讀命中,操做系統把預讀頁的大小擴大一倍,此時預讀過程是異步的,應用程序能夠不等預讀完成便可返回,只要後臺慢慢讀頁面便可,這時的預讀稱爲異步預讀。任何接下來的讀請求都會處於兩種狀況之一:
數據庫系統的設計者巧妙利用了操做系統以上特性,將一個節點的大小設爲等於一個頁(MySQL InnoDB的頁爲16KB),新建節點時,InnoDB每次申請磁盤空間時都會是若干地址連續磁盤塊(磁盤塊爲512B)來達到頁的大小16KB,保證一個節點物理上也存儲在一個頁裏。在把磁盤數據讀入到磁盤時會以頁爲基本單位,這樣每一個節點只須要一次I/O就能夠徹底載入。
MySQL 在執行讀操做時,會先從數據庫的緩衝區中讀取,若是不存在與緩衝區中就會嘗試從內存中加載頁面,若是前面的兩個步驟都失敗了,最後就只能執行 I/O 從磁盤中獲取對應的數據頁。
非葉子節點不存儲數據,且是葉子節點的索引(稀疏索引)。
下圖展現了「葉子節點是否存儲數據」狀況下,兩顆樹的差異,爲了顯示的更直觀點,假設每一個節點只夠存儲兩條數據。
能夠看到上面那顆樹最多須要三次磁盤I/O,而下面的那棵樹須要兩次。
例子:
新聞文章的內容一般都很長,因此內容字段類型通常都是大文本類型。平時更多的是顯示文章列表,若是顯示的列表中僅須要展現標題/副標題,而不須要展現內容片斷的話,能夠將內容字段單獨抽出一張表。剝離內容字段後的文章表記錄相對小了不少,這樣在獲取文章列表時也會有比較少的I/O。
爲何like只能左匹配?
由於MySQL InnoDB使用原數據格式進行存儲(函數索引存的是表中的數據應用函數後獲得的數據),加上B+樹的有序特性,以下簡略圖:
若是sql執行「 like '林%' 」,能夠在查找到第一個「林」以後,向後一直取得數據,直到遇到第一個非「林」開頭到數據,「 like '%1%' 」則不行。
注:
以上列出了了種種B/B+樹作索引的優勢,不表明B/B+樹就是最適合作索引,還有其餘適合作索引的數據結構/存儲引擎,好比LSM樹,二者各有優缺點,適用不一樣的場景。
索引能夠分爲主鍵索引(聚簇索引),非主鍵索引(非聚簇索引)。
主鍵索引:每一個表都有一個索引是存儲了全部數據的,這個索引既主鍵索引,通常創建在主鍵上,若是表沒有主鍵,則爲自增鍵,或隱藏的Rowid列。
非主鍵索引:不存儲數據,僅存儲索引數據和主鍵值。非惟一索引以「索引信息+主鍵」來保證鍵的惟一性。
當sql執行「 where name = 'n' 」(name爲非主鍵索引)時:
索引包含查詢中所須要的所有數據列,爲覆蓋索引。
例如:對於「 SELECT username, age FROM users WHERE username='林' 」, (username, age) 就是該查詢的一個覆蓋索引。
覆蓋索引可以避免「回表」查詢。
創建覆蓋索引,查詢速度能夠提高數十倍,甚至上千倍,爲何回表查詢這麼耗時?
HDD
磁盤讀取時間
順序讀寫和隨機讀寫對於機械硬盤來講爲何性能差別巨大?
SSD
固態驅動器(solid state drives SSDs)沒有旋轉磁盤設備,所有都是採用閃存。SSD內部維護了一張映射表(Mapping Table),HOST每寫入一個Host Page,就會產生一個新的映射關係,這個映射關係會加入(第一次寫)或者更改Mapping Table;當讀取某個Host Page時, SSD首先查找MappingTable中該Host Page對應的Physical Page,而後再訪問Flash讀取相應的Host數據。與傳統的機械磁盤相比,省去了尋道時間和旋轉時間。
SSD內部通常使用NAND Flash來做爲存儲介質,其邏輯結構以下:
SSD中通常有多個NAND Flash,每一個NAND Flash包含多個Block,每一個Block包含多個Page。因爲NAND的特性,其存取都必須以page爲單位,即每次讀寫至少是一個page,一般地,每一個page的大小爲4k或者8k。另外,NAND還有一個特性是,其只能是讀或寫單個page,但不能覆蓋寫入某個page,必須先要清空裏面的內容,再寫入。因爲清空內容的電壓較高,必須是以block爲單位。所以,沒有空閒的page時,必需要找到沒有有效內容的block,先擦寫,而後再選擇空閒的page寫入。
Block中的數據變老或者無效,是指沒有任何映射關係指向它們,用戶不會訪問到這些FLASH空間,它們被新的映射關係所取代。好比有一個Host Page A,開始它存儲在FLASH空間的X,映射關係爲A->X。後來,HOST重寫了該Host Page,因爲FLASH不能覆蓋寫,SSD內部必須尋找一個沒有寫過的位置寫入新的數據,假設爲Y,這個時候新的映射關係創建:A->Y,以前的映射關係解除,位置X上的數據變老失效,咱們把這些數據叫垃圾數據。
隨着HOST的持續寫入,FLASH存儲空間慢慢變小,直到耗盡。若是不及時清除這些垃圾數據,HOST就沒法寫入。SSD內部都有垃圾回收機制,它的基本原理是把幾個Block中的有效數據(非垃圾數據,上圖中的綠色小方塊表示的)集中搬到一個新的Block上面去,而後再把這幾個Block擦除掉,這樣就產生新的可用Block了。
上圖中,Block x上面有效數據爲A,B,C,Block y上面有效數據爲D,E,F,G,紅色方塊爲無效數據。垃圾回收機制就是先找一個未寫過的可用Block z,而後把Block x和Block y的有效數據搬移到Block z上面去,這樣Block x和Block y上面就沒有任何有效數據,能夠擦除變成兩個可用的Block。
寫:寫相同數據量的狀況下:
select * from user where name like '林%'
假設按照name索引過濾剩下600條數據。則該索引總共會產生1次隨機訪問(查找第一個匹配的節點),和599次順序訪問(按着第一個匹配的節點順序往下查找匹配)。由於該索引中的列並不能知足須要,因此會作「回表」查詢,每個索引行都會產生一次隨機訪問。以上查詢總共有601次隨機訪問和599次順序訪問。
注:MySQL會對特定的查詢作優化,如MySQL5.6以後引入MMR,以上假設爲未被優化狀況。
爲何分辨度不高的列(如性別)不適合建索引?
每一次回表都是耗時都隨機訪問,索引查找加上回表的性能並不會優於全表掃描。MySQL查詢優化器也會自動優化成全表掃描。
案例
SELECT * FROM t_terminal WHERE token LIKE 'hw%' LIMIT 1006000, 5
設備表t_terminal數量4800W,token字段爲索引字段,符合篩選條件的數據爲370W。MySQL版本5.6.29-log。
以上SQL查詢耗時3.5秒:
若是優化成:
SELECT * FROM t_terminal t1 INNER JOIN ( SELECT id FROM t_terminal WHERE token LIKE 'hw%' LIMIT 1006000, 5 ) t2 USING (id);
查詢耗時0.5秒:子查詢中會先篩選出5條limit結果,也就是隻有5條數據作回表查詢。
有時候排序也會成爲性能殺手,例如查出來的結果集很大,對結果集作排序也會耗去很多時間。
按照索引順序掃描得出的結果天然是有序的,將排序字段加入到索引組中,以免對結果重排序,減小磁盤I/O和內存的使用。
SELECT id, name FROM user WHERE name = '林' ORDER BY age ASC
以上SQL創建組合索引(name, age),利用索引樹已經排好序的特性,查詢結果無需再次排序。
思考:
SELECT id, name FROM user WHERE city = '深圳' AND name LIKE '林%' ORDER BY age ASC
A(city, name, age)和 B(city, age, name)以上哪一種組合索引更合適?
一個 SQL 查詢中同時擁有範圍謂詞和 ORDER BY 時,咱們可以作的就是在這二者之間作出選擇。近幾年排序速度已經提高不少,大多數狀況下A和B同樣快,甚至A比B更快。
可是若是:
那麼B會比A快不少(很大的可能,除非符合的數據都排在後面),由於A須要掃描出全部符合name的條件,再按age排序,以後才limit,而B直接找到前100個符合條件的便可。
group by操做在沒有合適的索引可用的時候,一般先掃描整個表提取數據並建立一個臨時表,而後按照group by指定的列進行排序。在這個臨時表裏面,對於每個group的數據行來講是連續在一塊兒的。完成排序以後,就能夠發現全部的groups,並能夠執行彙集函數(aggregate function)。在沒有使用索引的時候,須要建立臨時表和排序,因此制約group by性能的問題,就是臨時表+排序,儘可能減小磁盤排序,減小磁盤臨時表的建立,是比較有用的處理辦法。在執行計劃中一般能夠看到「Using temporary; Using filesort」。
CREATE TABLE `t1` ( `c1` int(11) DEFAULT NULL, `c2` int(11) DEFAULT NULL, `c3` int(11) DEFAULT NULL, `c4` int(11) DEFAULT NULL, KEY `idx_g` (`c1`,`c2`,`c3`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; mysql> explain extended select c1,c2 from t1 group by c2 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: index possible_keys: idx_g key: idx_g key_len: 15 ref: NULL rows: 15441 filtered: 100.00 Extra: Using index; Using temporary; Using filesort
鬆散索引掃描不須要連續的掃描索引中得每個元組,掃描時僅考慮索引中得一部分。當查詢中沒有where條件的時候,鬆散索引掃描讀取的索引元組的個數和groups的數量相同。若是where條件包含範圍預測,鬆散索引掃描查找每一個group中第一個知足範圍條件,而後再讀取最少可能數的keys。
若是查詢可以使用鬆散索引掃描,那麼執行計劃中Etra中提示「 using index for group-by」。
mysql> explain select c1, min(c2) from t1 group by c1 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: range possible_keys: idx_g key: idx_g key_len: 10 ref: NULL rows: 15442 Extra: Using index for group-by
鬆散索引掃描只須要讀取不多量的數據就能夠完成group by操做,於是執行效率很是高。
鬆散索引條件:
自從5.5開始,鬆散索引掃描能夠做用於在select list中其它形式的彙集函數,除了min()和max()以外,還支持:
緊湊索引掃描實現 GROUP BY 和鬆散索引掃描的區別主要在於:緊湊索引掃描須要在掃描索引的時候,讀取全部知足條件的索引鍵(注意,是索引健,不包含索引樹內沒有的數據),而後再根據讀取出的數據來完成 GROUP BY 操做獲得相應結果。
若是緊湊索引掃描起做用,那麼必須知足:在查詢中存在常量相等where條件字段(索引中的字段),且該字段在group by指定的字段的前面或者中間。
緊湊索引掃描一樣能夠避免額外的排序操做,可是效率低於鬆散索引。使用緊湊索引掃描,執行計劃Extra通常顯示「using index」,至關於使用了覆蓋索引。
mysql> explain extended select c1,c2 from t1 where c1=2 group by c2 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: ref possible_keys: idx_g key: idx_g key_len: 5 ref: const rows: 5 filtered: 100.00 Extra: Using where; Using index
在MySQL中,MySQL Query Optimizer首先會選擇嘗試經過鬆散索引掃描來實現GROUP BY操做,當發現某些狀況沒法知足鬆散索引掃描實現GROUP BY的要求以後,纔會嘗試經過緊湊索引掃描來實現。當MySQL Query Optimizer發現僅僅經過索引掃描並不能直接獲得GROUP BY的結果以後,他就不得不選擇經過使用臨時表而後再排序的方式來實現GROUP BY了。當沒法使用索引完成GROUP BY的時候,因爲要使用到臨時表且須要filesort,因此咱們必需要有足夠的sort_buffer_size來供MySQL排序的時候使用,並且儘可能不要進行大結果集的GROUP BY操做,由於若是超出系統設置的臨時表大小的時候會出現將臨時表數據copy到磁盤上面再進行操做,這時候的排序分組操做性能將是成數量級的降低。此外,GROUP BY若是在沒法利用到索引的狀況下想避免filesort操做,能夠在整個語句最後添加一個以null排序(ORDER BY null)。
DISTINCT實際上和GROUP BY的操做很是類似,只不過是在GROUP BY以後的每組中只取出一條記錄而已。因此,DISTINCT的實現和GROUP BY的實現也基本差很少,沒有太大的區別。一樣能夠經過鬆散索引掃描或者是緊湊索引掃描來實現,固然,在沒法僅僅使用索引即能完成DISTINCT的時候,MySQL只能經過臨時表來完成。可是和GROUP BY有一點差異的是,DISTINCT並不須要進行排序。也就是說,在僅僅只是DISTINCT操做的Query若是沒法僅僅利用索引完成操做的時候,MySQL會利用臨時表來作一次數據的「緩存」,可是不會對臨時表中的數據進行filesort操做。固然,若是咱們在進行DISTINCT的時候還使用了GROUP BY並進行了分組,並使用了相似於MAX之類的聚合函數操做,就沒法避免filesort了。
explain並無真的去執行sql語句從而得出行數,而是進行了某種預估。
增長增長採樣數目,必定程度上緩解了有偏的問題,可是不許確仍是存在的。
參考:
《淺談Mysql的B樹索引與索引優化》
《數據庫中的索引》
《程序員須要知道的SSD基本原理》
《InnoDB一棵B+樹能夠存放多少行數據?》
《MySQL鬆散索引掃描與緊湊索引掃描》
《mysql 原理:explain》
《MySQL order by,group by和distinct原理》