精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

萬字干貨總結(jié):MySQL優(yōu)化原理學(xué)習(xí),這一篇就夠了!

數(shù)據(jù)庫 MySQL
說起MySQL的查詢優(yōu)化,相信大家收藏了一堆奇技淫巧:不能使用SELECT *、不使用 NULL 字段、合理創(chuàng)建索引….. 你是否真的理解這些優(yōu)化技巧?是否理解其背后的工作原理?在實(shí)際場(chǎng)景下性能真有提升嗎?我想未必。因而理解這些優(yōu)化建議背后的原理就尤為重要,希望本文能讓你重新審視這些優(yōu)化建議,并在實(shí)際業(yè)務(wù)場(chǎng)景下合理的運(yùn)用。

萬字干貨總結(jié):MySQL優(yōu)化原理學(xué)習(xí),這一篇就夠了!

本文讓你重新理解并審視那些流行的優(yōu)化建議,并在實(shí)際業(yè)務(wù)場(chǎng)景下合理的運(yùn)用。

說起 MySQL 的查詢優(yōu)化,相信大家收藏了一堆奇技淫巧:不能使用 SELECT *、不使用 NULL 字段、合理創(chuàng)建索引、為字段選擇合適的數(shù)據(jù)類型….. 你是否真的理解這些優(yōu)化技巧?是否理解其背后的工作原理?在實(shí)際場(chǎng)景下性能真有提升嗎?我想未必。因而理解這些優(yōu)化建議背后的原理就尤為重要,希望本文能讓你重新審視這些優(yōu)化建議,并在實(shí)際業(yè)務(wù)場(chǎng)景下合理的運(yùn)用。

MySQL 邏輯架構(gòu)

如果能在頭腦中構(gòu)建一幅 MySQL 各組件之間如何協(xié)同工作的架構(gòu)圖,有助于深入理解 MySQL 服務(wù)器。下圖展示了 MySQL 的邏輯架構(gòu)圖。

MySQL 邏輯架構(gòu)整體分為三層,最上層為客戶端層,并非 MySQL 所獨(dú)有,諸如:連接處理、授權(quán)認(rèn)證、安全等功能均在這一層處理。

MySQL 大多數(shù)核心服務(wù)均在中間這一層,包括查詢解析、分析、優(yōu)化、緩存、內(nèi)置函數(shù) (比如:時(shí)間、數(shù)學(xué)、加密等函數(shù))。所有的跨存儲(chǔ)引擎的功能也在這一層實(shí)現(xiàn):存儲(chǔ)過程、觸發(fā)器、視圖等。

最下層為存儲(chǔ)引擎,其負(fù)責(zé) MySQL 中的數(shù)據(jù)存儲(chǔ)和提取。和 Linux 下的文件系統(tǒng)類似,每種存儲(chǔ)引擎都有其優(yōu)勢(shì)和劣勢(shì)。中間的服務(wù)層通過 API 與存儲(chǔ)引擎通信,這些 API 接口屏蔽了不同存儲(chǔ)引擎間的差異。

MySQL 查詢過程

我們總是希望 MySQL 能夠獲得更高的查詢性能,最好的辦法是弄清楚 MySQL 是如何優(yōu)化和執(zhí)行查詢的。一旦理解了這一點(diǎn),就會(huì)發(fā)現(xiàn):很多的查詢優(yōu)化工作實(shí)際上就是遵循一些原則讓 MySQL 的優(yōu)化器能夠按照預(yù)想的合理方式運(yùn)行而已。當(dāng)向 MySQL 發(fā)送一個(gè)請(qǐng)求的時(shí)候,MySQL 到底做了些什么呢?

MySQL 查詢過程

1. 客戶端 / 服務(wù)端通信協(xié)議

MySQL 客戶端 / 服務(wù)端通信協(xié)議是 “半雙工” 的:在任一時(shí)刻,要么是服務(wù)器向客戶端發(fā)送數(shù)據(jù),要么是客戶端向服務(wù)器發(fā)送數(shù)據(jù),這兩個(gè)動(dòng)作不能同時(shí)發(fā)生。一旦一端開始發(fā)送消息,另一端要接收完整個(gè)消息才能響應(yīng)它,所以我們無法也無須將一個(gè)消息切成小塊獨(dú)立發(fā)送,也沒有辦法進(jìn)行流量控制。

客戶端用一個(gè)單獨(dú)的數(shù)據(jù)包將查詢請(qǐng)求發(fā)送給服務(wù)器,所以當(dāng)查詢語句很長(zhǎng)的時(shí)候,需要設(shè)置 max_allowed_packet 參數(shù)。但是需要注意的是,如果查詢實(shí)在是太大,服務(wù)端會(huì)拒絕接收更多數(shù)據(jù)并拋出異常。

與之相反的是,服務(wù)器響應(yīng)給用戶的數(shù)據(jù)通常會(huì)很多,由多個(gè)數(shù)據(jù)包組成。但是當(dāng)服務(wù)器響應(yīng)客戶端請(qǐng)求時(shí),客戶端必須完整的接收整個(gè)返回結(jié)果,而不能簡(jiǎn)單的只取前面幾條結(jié)果,然后讓服務(wù)器停止發(fā)送。因而在實(shí)際開發(fā)中,盡量保持查詢簡(jiǎn)單且只返回必需的數(shù)據(jù),減小通信間數(shù)據(jù)包的大小和數(shù)量是一個(gè)非常好的習(xí)慣,這也是查詢中盡量避免使用 SELECT * 以及加上 LIMIT 限制的原因之一。

2. 查詢緩存

在解析一個(gè)查詢語句前,如果查詢緩存是打開的,那么 MySQL 會(huì)檢查這個(gè)查詢語句是否命中查詢緩存中的數(shù)據(jù)。如果當(dāng)前查詢恰好命中查詢緩存,在檢查一次用戶權(quán)限后直接返回緩存中的結(jié)果。這種情況下,查詢不會(huì)被解析,也不會(huì)生成執(zhí)行計(jì)劃,更不會(huì)執(zhí)行。

MySQL 將緩存存放在一個(gè)引用表(不要理解成 table,可以認(rèn)為是類似于 HashMap 的數(shù)據(jù)結(jié)構(gòu)),通過一個(gè)哈希值索引,這個(gè)哈希值通過查詢本身、當(dāng)前要查詢的數(shù)據(jù)庫、客戶端協(xié)議版本號(hào)等一些可能影響結(jié)果的信息計(jì)算得來。所以兩個(gè)查詢?cè)谌魏巫址系牟煌ɡ纾嚎崭瘛⒆⑨專紩?huì)導(dǎo)致緩存不會(huì)命中。

如果查詢中包含任何用戶自定義函數(shù)、存儲(chǔ)函數(shù)、用戶變量、臨時(shí)表、MySQL 庫中的系統(tǒng)表,其查詢結(jié)果都不會(huì)被緩存。比如函數(shù) NOW() 或者 CURRENT_DATE() 會(huì)因?yàn)椴煌牟樵儠r(shí)間,返回不同的查詢結(jié)果,再比如包含 CURRENT_USER 或者 CONNECION_ID() 的查詢語句會(huì)因?yàn)椴煌挠脩舳祷夭煌慕Y(jié)果,將這樣的查詢結(jié)果緩存起來沒有任何的意義。

既然是緩存,就會(huì)失效,那查詢緩存何時(shí)失效呢?

MySQL 的查詢緩存系統(tǒng)會(huì)跟蹤查詢中涉及的每個(gè)表,如果這些表(數(shù)據(jù)或結(jié)構(gòu))發(fā)生變化,那么和這張表相關(guān)的所有緩存數(shù)據(jù)都將失效。正因?yàn)槿绱耍谌魏蔚膶懖僮鲿r(shí),MySQL 必須將對(duì)應(yīng)表的所有緩存都設(shè)置為失效。如果查詢緩存非常大或者碎片很多,這個(gè)操作就可能帶來很大的系統(tǒng)消耗,甚至導(dǎo)致系統(tǒng)僵死一會(huì)兒。而且查詢緩存對(duì)系統(tǒng)的額外消耗也不僅僅在寫操作,讀操作也不例外:

  • 任何的查詢語句在開始之前都必須經(jīng)過檢查,即使這條 SQL 語句永遠(yuǎn)不會(huì)命中緩存
  • 如果查詢結(jié)果可以被緩存,那么執(zhí)行完成后,會(huì)將結(jié)果存入緩存,也會(huì)帶來額外的系統(tǒng)消耗

基于此,我們要知道并不是什么情況下查詢緩存都會(huì)提高系統(tǒng)性能,緩存和失效都會(huì)帶來額外消耗,只有當(dāng)緩存帶來的資源節(jié)約大于其本身消耗的資源時(shí),才會(huì)給系統(tǒng)帶來性能提升。但要如何評(píng)估打開緩存是否能夠帶來性能提升是一件非常困難的事情,也不在本文討論的范疇內(nèi)。如果系統(tǒng)確實(shí)存在一些性能問題,可以嘗試打開查詢緩存,并在數(shù)據(jù)庫設(shè)計(jì)上做一些優(yōu)化,比如:

  • 用多個(gè)小表代替一個(gè)大表,注意不要過度設(shè)計(jì)
  • 批量插入代替循環(huán)單條插入
  • 合理控制緩存空間大小,一般來說其大小設(shè)置為幾十兆比較合適
  • 可以通過 SQL_CACHE 和 SQL_NO_CACHE 來控制某個(gè)查詢語句是否需要進(jìn)行緩存

最后的忠告是不要輕易打開查詢緩存,特別是寫密集型應(yīng)用。如果你實(shí)在是忍不住,可以將 query_cache_type 設(shè)置為 DEMAND,這時(shí)只有加入 SQL_CACHE 的查詢才會(huì)走緩存,其他查詢則不會(huì),這樣可以非常自由地控制哪些查詢需要被緩存。

當(dāng)然查詢緩存系統(tǒng)本身是非常復(fù)雜的,這里討論的也只是很小的一部分,其他更深入的話題,比如:緩存是如何使用內(nèi)存的?如何控制內(nèi)存的碎片化?事務(wù)對(duì)查詢緩存有何影響等等,讀者可以自行閱讀相關(guān)資料,這里權(quán)當(dāng)拋磚引玉吧。

3. 語法解析和預(yù)處理

MySQL 通過關(guān)鍵字將 SQL 語句進(jìn)行解析,并生成一棵對(duì)應(yīng)的解析樹。這個(gè)過程解析器主要通過語法規(guī)則來驗(yàn)證和解析。比如 SQL 中是否使用了錯(cuò)誤的關(guān)鍵字或者關(guān)鍵字的順序是否正確等等。預(yù)處理則會(huì)根據(jù) MySQL 規(guī)則進(jìn)一步檢查解析樹是否合法。比如檢查要查詢的數(shù)據(jù)表和數(shù)據(jù)列是否存在等。

4. 查詢優(yōu)化

經(jīng)過前面的步驟生成的語法樹被認(rèn)為是合法的了,并且由優(yōu)化器將其轉(zhuǎn)化成查詢計(jì)劃。多數(shù)情況下,一條查詢可以有很多種執(zhí)行方式,最后都返回相應(yīng)的結(jié)果。優(yōu)化器的作用就是找到這其中最好的執(zhí)行計(jì)劃。

MySQL 使用基于成本的優(yōu)化器,它嘗試預(yù)測(cè)一個(gè)查詢使用某種執(zhí)行計(jì)劃時(shí)的成本,并選擇其中成本最小的一個(gè)。在 MySQL 可以通過查詢當(dāng)前會(huì)話的 last_query_cost 的值來得到其計(jì)算當(dāng)前查詢的成本。

 

  1. mysql> select * from t_message limit 10; 
  2.  
  3. ...省略結(jié)果集 
  4.  
  5.  
  6. mysql> show status like 'last_query_cost'
  7.  
  8. +-----------------+-------------+ 
  9.  
  10. | Variable_name   | Value       | 
  11.  
  12. +-----------------+-------------+ 
  13.  
  14. | Last_query_cost | 6391.799000 | 
  15.  
  16. +-----------------+-------------+ 

示例中的結(jié)果表示優(yōu)化器認(rèn)為大概需要做 6391 個(gè)數(shù)據(jù)頁的隨機(jī)查找才能完成上面的查詢。這個(gè)結(jié)果是根據(jù)一些列的統(tǒng)計(jì)信息計(jì)算得來的,這些統(tǒng)計(jì)信息包括:每張表或者索引的頁面?zhèn)€數(shù)、索引的基數(shù)、索引和數(shù)據(jù)行的長(zhǎng)度、索引的分布情況等等。

有非常多的原因會(huì)導(dǎo)致 MySQL 選擇錯(cuò)誤的執(zhí)行計(jì)劃,比如統(tǒng)計(jì)信息不準(zhǔn)確、不會(huì)考慮不受其控制的操作成本(用戶自定義函數(shù)、存儲(chǔ)過程)、MySQL 認(rèn)為的最優(yōu)跟我們想的不一樣(我們希望執(zhí)行時(shí)間盡可能短,但 MySQL 值選擇它認(rèn)為成本小的,但成本小并不意味著執(zhí)行時(shí)間短)等等。

MySQL 的查詢優(yōu)化器是一個(gè)非常復(fù)雜的部件,它使用了非常多的優(yōu)化策略來生成一個(gè)最優(yōu)的執(zhí)行計(jì)劃:

  • 重新定義表的關(guān)聯(lián)順序(多張表關(guān)聯(lián)查詢時(shí),并不一定按照 SQL 中指定的順序進(jìn)行,但有一些技巧可以指定關(guān)聯(lián)順序)
  • 優(yōu)化 MIN() 和 MAX() 函數(shù)(找某列的最小值,如果該列有索引,只需要查找 B+Tree 索引最左端,反之則可以找到最大值,具體原理見下文)
  • 提前終止查詢(比如:使用 Limit 時(shí),查找到滿足數(shù)量的結(jié)果集后會(huì)立即終止查詢)
  • 優(yōu)化排序(在老版本 MySQL 會(huì)使用兩次傳輸排序,即先讀取行指針和需要排序的字段在內(nèi)存中對(duì)其排序,然后再根據(jù)排序結(jié)果去讀取數(shù)據(jù)行,而新版本采用的是單次傳輸排序,也就是一次讀取所有的數(shù)據(jù)行,然后根據(jù)給定的列排序。對(duì)于 I/O 密集型應(yīng)用,效率會(huì)高很多)

隨著 MySQL 的不斷發(fā)展,優(yōu)化器使用的優(yōu)化策略也在不斷的進(jìn)化,這里僅僅介紹幾個(gè)非常常用且容易理解的優(yōu)化策略,其他的優(yōu)化策略,大家自行查閱吧。

5. 查詢執(zhí)行引擎

在完成解析和優(yōu)化階段以后,MySQL 會(huì)生成對(duì)應(yīng)的執(zhí)行計(jì)劃,查詢執(zhí)行引擎根據(jù)執(zhí)行計(jì)劃給出的指令逐步執(zhí)行得出結(jié)果。整個(gè)執(zhí)行過程的大部分操作均是通過調(diào)用存儲(chǔ)引擎實(shí)現(xiàn)的接口來完成,這些接口被稱為 handler API。查詢過程中的每一張表由一個(gè) handler 實(shí)例表示。實(shí)際上,MySQL 在查詢優(yōu)化階段就為每一張表創(chuàng)建了一個(gè) handler 實(shí)例,優(yōu)化器可以根據(jù)這些實(shí)例的接口來獲取表的相關(guān)信息,包括表的所有列名、索引統(tǒng)計(jì)信息等。存儲(chǔ)引擎接口提供了非常豐富的功能,但其底層僅有幾十個(gè)接口,這些接口像搭積木一樣完成了一次查詢的大部分操作。

6. 返回結(jié)果給客戶端

查詢執(zhí)行的最后一個(gè)階段就是將結(jié)果返回給客戶端。即使查詢不到數(shù)據(jù),MySQL 仍然會(huì)返回這個(gè)查詢的相關(guān)信息,比如該查詢影響到的行數(shù)以及執(zhí)行時(shí)間等。

如果查詢緩存被打開且這個(gè)查詢可以被緩存,MySQL 也會(huì)將結(jié)果存放到緩存中。

結(jié)果集返回客戶端是一個(gè)增量且逐步返回的過程。有可能 MySQL 在生成第一條結(jié)果時(shí),就開始向客戶端逐步返回結(jié)果集了。這樣服務(wù)端就無須存儲(chǔ)太多結(jié)果而消耗過多內(nèi)存,也可以讓客戶端第一時(shí)間獲得返回結(jié)果。需要注意的是,結(jié)果集中的每一行都會(huì)以一個(gè)滿足①中所描述的通信協(xié)議的數(shù)據(jù)包發(fā)送,再通過 TCP 協(xié)議進(jìn)行傳輸,在傳輸過程中,可能對(duì) MySQL 的數(shù)據(jù)包進(jìn)行緩存然后批量發(fā)送。

回頭總結(jié)一下 MySQL 整個(gè)查詢執(zhí)行過程,總的來說分為 6 個(gè)步驟:

  • 客戶端向 MySQL 服務(wù)器發(fā)送一條查詢請(qǐng)求
  • 服務(wù)器首先檢查查詢緩存,如果命中緩存,則立刻返回存儲(chǔ)在緩存中的結(jié)果。否則進(jìn)入下一階段
  • 服務(wù)器進(jìn)行 SQL 解析、預(yù)處理、再由優(yōu)化器生成對(duì)應(yīng)的執(zhí)行計(jì)劃
  • MySQL 根據(jù)執(zhí)行計(jì)劃,調(diào)用存儲(chǔ)引擎的 API 來執(zhí)行查詢
  • 將結(jié)果返回給客戶端,同時(shí)緩存查詢結(jié)果

性能優(yōu)化建議

看了這么多,你可能會(huì)期待給出一些優(yōu)化手段,是的,下面會(huì)從 3 個(gè)不同方面給出一些優(yōu)化建議。但請(qǐng)等等,還有一句忠告要先送給你:不要聽信你看到的關(guān)于優(yōu)化的 “絕對(duì)真理”,包括本文所討論的內(nèi)容,而應(yīng)該是在實(shí)際的業(yè)務(wù)場(chǎng)景下通過測(cè)試來驗(yàn)證你關(guān)于執(zhí)行計(jì)劃以及響應(yīng)時(shí)間的假設(shè)。

1. Scheme 設(shè)計(jì)與數(shù)據(jù)類型優(yōu)化

選擇數(shù)據(jù)類型只要遵循小而簡(jiǎn)單的原則就好,越小的數(shù)據(jù)類型通常會(huì)更快,占用更少的磁盤、內(nèi)存,處理時(shí)需要的 CPU 周期也更少。越簡(jiǎn)單的數(shù)據(jù)類型在計(jì)算時(shí)需要更少的 CPU 周期,比如,整型就比字符操作代價(jià)低,因而會(huì)使用整型來存儲(chǔ) ip 地址,使用 DATETIME 來存儲(chǔ)時(shí)間,而不是使用字符串。

這里總結(jié)幾個(gè)可能容易理解錯(cuò)誤的技巧:

  • 通常來說把可為 NULL 的列改為 NOT NULL 不會(huì)對(duì)性能提升有多少幫助,只是如果計(jì)劃在列上創(chuàng)建索引,就應(yīng)該將該列設(shè)置為 NOT NULL。
  • 對(duì)整數(shù)類型指定寬度,比如 INT(11),沒有任何卵用。INT 使用 32 位(4 個(gè)字節(jié))存儲(chǔ)空間,那么它的表示范圍已經(jīng)確定,所以 INT(1) 和 INT(20) 對(duì)于存儲(chǔ)和計(jì)算是相同的。
  • UNSIGNED 表示不允許負(fù)值,大致可以使正數(shù)的上限提高一倍。比如 TINYINT 存儲(chǔ)范圍是 - 128 ~ 127,而 UNSIGNED TINYINT 存儲(chǔ)的范圍卻是 0 – 255。
  • 通常來講,沒有太大的必要使用 DECIMAL 數(shù)據(jù)類型。即使是在需要存儲(chǔ)財(cái)務(wù)數(shù)據(jù)時(shí),仍然可以使用 BIGINT。比如需要精確到萬分之一,那么可以將數(shù)據(jù)乘以一百萬然后使用 BIGINT 存儲(chǔ)。這樣可以避免浮點(diǎn)數(shù)計(jì)算不準(zhǔn)確和 DECIMAL 精確計(jì)算代價(jià)高的問題。
  • TIMESTAMP 使用 4 個(gè)字節(jié)存儲(chǔ)空間,DATETIME 使用 8 個(gè)字節(jié)存儲(chǔ)空間。因而,TIMESTAMP 只能表示 1970 – 2038 年,比 DATETIME 表示的范圍小得多,而且 TIMESTAMP 的值因時(shí)區(qū)不同而不同。
  • 大多數(shù)情況下沒有使用枚舉類型的必要,其中一個(gè)缺點(diǎn)是枚舉的字符串列表是固定的,添加和刪除字符串(枚舉選項(xiàng))必須使用 ALTER TABLE(如果只是在列表末尾追加元素,不需要重建表)。
  • schema 的列不要太多。原因是存儲(chǔ)引擎的 API 工作時(shí)需要在服務(wù)器層和存儲(chǔ)引擎層之間通過行緩沖格式拷貝數(shù)據(jù),然后在服務(wù)器層將緩沖內(nèi)容解碼成各個(gè)列,這個(gè)轉(zhuǎn)換過程的代價(jià)是非常高的。如果列太多而實(shí)際使用的列又很少的話,有可能會(huì)導(dǎo)致 CPU 占用過高。
  • 大表 ALTER TABLE 非常耗時(shí),MySQL 執(zhí)行大部分修改表結(jié)果操作的方法是用新的結(jié)構(gòu)創(chuàng)建一個(gè)張空表,從舊表中查出所有的數(shù)據(jù)插入新表,然后再刪除舊表。尤其當(dāng)內(nèi)存不足而表又很大,而且還有很大索引的情況下,耗時(shí)更久。當(dāng)然有一些奇技淫巧可以解決這個(gè)問題,有興趣可自行查閱。

2. 創(chuàng)建高性能索引

索引是提高 MySQL 查詢性能的一個(gè)重要途徑,但過多的索引可能會(huì)導(dǎo)致過高的磁盤使用率以及過高的內(nèi)存占用,從而影響應(yīng)用程序的整體性能。應(yīng)當(dāng)盡量避免事后才想起添加索引,因?yàn)槭潞罂赡苄枰O(jiān)控大量的 SQL 才能定位到問題所在,而且添加索引的時(shí)間肯定是遠(yuǎn)大于初始添加索引所需要的時(shí)間,可見索引的添加也是非常有技術(shù)含量的。

接下來將向你展示一系列創(chuàng)建高性能索引的策略,以及每條策略其背后的工作原理。但在此之前,先了解與索引相關(guān)的一些算法和數(shù)據(jù)結(jié)構(gòu),將有助于更好的理解后文的內(nèi)容。

3. 索引相關(guān)的數(shù)據(jù)結(jié)構(gòu)和算法

通常我們所說的索引是指 B-Tree 索引,它是目前關(guān)系型數(shù)據(jù)庫中查找數(shù)據(jù)最為常用和有效的索引,大多數(shù)存儲(chǔ)引擎都支持這種索引。使用 B-Tree 這個(gè)術(shù)語,是因?yàn)?MySQL 在 CREATE TABLE 或其它語句中使用了這個(gè)關(guān)鍵字,但實(shí)際上不同的存儲(chǔ)引擎可能使用不同的數(shù)據(jù)結(jié)構(gòu),比如 InnoDB 就是使用的 B+Tree。

B+Tree 中的 B 是指 balance,意為平衡。需要注意的是,B + 樹索引并不能找到一個(gè)給定鍵值的具體行,它找到的只是被查找數(shù)據(jù)行所在的頁,接著數(shù)據(jù)庫會(huì)把頁讀入到內(nèi)存,再在內(nèi)存中進(jìn)行查找,最后得到要查找的數(shù)據(jù)。

在介紹 B+Tree 前,先了解一下二叉查找樹,它是一種經(jīng)典的數(shù)據(jù)結(jié)構(gòu),其左子樹的值總是小于根的值,右子樹的值總是大于根的值,如下圖①。如果要在這課樹中查找值為 5 的記錄,其大致流程:先找到根,其值為 6,大于 5,所以查找左子樹,找到 3,而 5 大于 3,接著找 3 的右子樹,總共找了 3 次。同樣的方法,如果查找值為 8 的記錄,也需要查找 3 次。所以二叉查找樹的平均查找次數(shù)為 (3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3 次,而順序查找的話,查找值為 2 的記錄,僅需要 1 次,但查找值為 8 的記錄則需要 6 次,所以順序查找的平均查找次數(shù)為:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3 次,因此大多數(shù)情況下二叉查找樹的平均查找速度比順序查找要快。

二叉查找樹和平衡二叉樹

由于二叉查找樹可以任意構(gòu)造,同樣的值,可以構(gòu)造出如圖②的二叉查找樹,顯然這棵二叉樹的查詢效率和順序查找差不多。若想二叉查找數(shù)的查詢性能最高,需要這棵二叉查找樹是平衡的,也即平衡二叉樹(AVL 樹)。

平衡二叉樹首先需要符合二叉查找樹的定義,其次必須滿足任何節(jié)點(diǎn)的兩個(gè)子樹的高度差不能大于 1。顯然圖②不滿足平衡二叉樹的定義,而圖①是一課平衡二叉樹。平衡二叉樹的查找性能是比較高的(性能最好的是最優(yōu)二叉樹),查詢性能越好,維護(hù)的成本就越大。比如圖①的平衡二叉樹,當(dāng)用戶需要插入一個(gè)新的值 9 的節(jié)點(diǎn)時(shí),就需要做出如下變動(dòng)。

平衡二叉樹旋轉(zhuǎn)

通過一次左旋操作就將插入后的樹重新變?yōu)槠胶舛鏄涫亲詈?jiǎn)單的情況了,實(shí)際應(yīng)用場(chǎng)景中可能需要旋轉(zhuǎn)多次。至此我們可以考慮一個(gè)問題,平衡二叉樹的查找效率還不錯(cuò),實(shí)現(xiàn)也非常簡(jiǎn)單,相應(yīng)的維護(hù)成本還能接受,為什么 MySQL 索引不直接使用平衡二叉樹?

隨著數(shù)據(jù)庫中數(shù)據(jù)的增加,索引本身大小隨之增加,不可能全部存儲(chǔ)在內(nèi)存中,因此索引往往以索引文件的形式存儲(chǔ)的磁盤上。這樣的話,索引查找過程中就要產(chǎn)生磁盤 I/O 消耗,相對(duì)于內(nèi)存存取,I/O 存取的消耗要高幾個(gè)數(shù)量級(jí)。可以想象一下一棵幾百萬節(jié)點(diǎn)的二叉樹的深度是多少?如果將這么大深度的一顆二叉樹放磁盤上,每讀取一個(gè)節(jié)點(diǎn),需要一次磁盤的 I/O 讀取,整個(gè)查找的耗時(shí)顯然是不能夠接受的。那么如何減少查找過程中的 I/O 存取次數(shù)?

一種行之有效的解決方法是減少樹的深度,將二叉樹變?yōu)?m 叉樹(多路搜索樹),而 B+Tree 就是一種多路搜索樹。理解 B+Tree 時(shí),只需要理解其最重要的兩個(gè)特征即可:第一,所有的關(guān)鍵字(可以理解為數(shù)據(jù))都存儲(chǔ)在葉子節(jié)點(diǎn)(Leaf Page),非葉子節(jié)點(diǎn)(Index Page)并不存儲(chǔ)真正的數(shù)據(jù),所有記錄節(jié)點(diǎn)都是按鍵值大小順序存放在同一層葉子節(jié)點(diǎn)上。其次,所有的葉子節(jié)點(diǎn)由指針連接。如下圖為高度為 2 的簡(jiǎn)化了的 B+Tree。

簡(jiǎn)化 B+Tree

怎么理解這兩個(gè)特征?MySQL 將每個(gè)節(jié)點(diǎn)的大小設(shè)置為一個(gè)頁的整數(shù)倍(原因下文會(huì)介紹),也就是在節(jié)點(diǎn)空間大小一定的情況下,每個(gè)節(jié)點(diǎn)可以存儲(chǔ)更多的內(nèi)結(jié)點(diǎn),這樣每個(gè)結(jié)點(diǎn)能索引的范圍更大更精確。所有的葉子節(jié)點(diǎn)使用指針鏈接的好處是可以進(jìn)行區(qū)間訪問,比如上圖中,如果查找大于 20 而小于 30 的記錄,只需要找到節(jié)點(diǎn) 20,就可以遍歷指針依次找到 25、30。如果沒有鏈接指針的話,就無法進(jìn)行區(qū)間查找。這也是 MySQL 使用 B+Tree 作為索引存儲(chǔ)結(jié)構(gòu)的重要原因。

MySQL 為何將節(jié)點(diǎn)大小設(shè)置為頁的整數(shù)倍,這就需要理解磁盤的存儲(chǔ)原理。磁盤本身存取就比主存慢很多,在加上機(jī)械運(yùn)動(dòng)損耗(特別是普通的機(jī)械硬盤),磁盤的存取速度往往是主存的幾百萬分之一,為了盡量減少磁盤 I/O,磁盤往往不是嚴(yán)格按需讀取,而是每次都會(huì)預(yù)讀,即使只需要一個(gè)字節(jié),磁盤也會(huì)從這個(gè)位置開始,順序向后讀取一定長(zhǎng)度的數(shù)據(jù)放入內(nèi)存,預(yù)讀的長(zhǎng)度一般為頁的整數(shù)倍。

“頁是計(jì)算機(jī)管理存儲(chǔ)器的邏輯塊,硬件及 OS 往往將主存和磁盤存儲(chǔ)區(qū)分割為連續(xù)的大小相等的塊,每個(gè)存儲(chǔ)塊稱為一頁(許多 OS 中,頁的大小通常為 4K)。主存和磁盤以頁為單位交換數(shù)據(jù)。當(dāng)程序要讀取的數(shù)據(jù)不在主存中時(shí),會(huì)觸發(fā)一個(gè)缺頁異常,此時(shí)系統(tǒng)會(huì)向磁盤發(fā)出讀盤信號(hào),磁盤會(huì)找到數(shù)據(jù)的起始位置并向后連續(xù)讀取一頁或幾頁載入內(nèi)存中,然后一起返回,程序繼續(xù)運(yùn)行。”

MySQL 巧妙利用了磁盤預(yù)讀原理,將一個(gè)節(jié)點(diǎn)的大小設(shè)為等于一個(gè)頁,這樣每個(gè)節(jié)點(diǎn)只需要一次 I/O 就可以完全載入。為了達(dá)到這個(gè)目的,每次新建節(jié)點(diǎn)時(shí),直接申請(qǐng)一個(gè)頁的空間,這樣就保證一個(gè)節(jié)點(diǎn)物理上也存儲(chǔ)在一個(gè)頁里,加之計(jì)算機(jī)存儲(chǔ)分配都是按頁對(duì)齊的,就實(shí)現(xiàn)了讀取一個(gè)節(jié)點(diǎn)只需一次 I/O。假設(shè) B+Tree 的高度為 h,一次檢索最多需要 h-1I/O(根節(jié)點(diǎn)常駐內(nèi)存),復(fù)雜度 $O(h) = O(\log_{M}N)$。實(shí)際應(yīng)用場(chǎng)景中,M 通常較大,常常超過 100,因此樹的高度一般都比較小,通常不超過 3。

最后簡(jiǎn)單了解下 B+Tree 節(jié)點(diǎn)的操作,在整體上對(duì)索引的維護(hù)有一個(gè)大概的了解,雖然索引可以大大提高查詢效率,但維護(hù)索引仍要花費(fèi)很大的代價(jià),因此合理的創(chuàng)建索引也就尤為重要。

仍以上面的樹為例,我們假設(shè)每個(gè)節(jié)點(diǎn)只能存儲(chǔ) 4 個(gè)內(nèi)節(jié)點(diǎn)。首先要插入第一個(gè)節(jié)點(diǎn) 28,如下圖所示。

leaf page 和 index page 都沒有滿

接著插入下一個(gè)節(jié)點(diǎn) 70,在 Index Page 中查詢后得知應(yīng)該插入到 50 – 70 之間的葉子節(jié)點(diǎn),但葉子節(jié)點(diǎn)已滿,這時(shí)候就需要進(jìn)行也分裂的操作,當(dāng)前的葉子節(jié)點(diǎn)起點(diǎn)為 50,所以根據(jù)中間值來拆分葉子節(jié)點(diǎn),如下圖所示。

Leaf Page 拆分

最后插入一個(gè)節(jié)點(diǎn) 95,這時(shí)候 Index Page 和 Leaf Page 都滿了,就需要做兩次拆分,如下圖所示。

Leaf Page 與 Index Page 拆分

拆分后最終形成了這樣一顆樹。

最終樹

B+Tree 為了保持平衡,對(duì)于新插入的值需要做大量的拆分頁操作,而頁的拆分需要 I/O 操作,為了盡可能的減少頁的拆分操作,B+Tree 也提供了類似于平衡二叉樹的旋轉(zhuǎn)功能。當(dāng) Leaf Page 已滿但其左右兄弟節(jié)點(diǎn)沒有滿的情況下,B+Tree 并不急于去做拆分操作,而是將記錄移到當(dāng)前所在頁的兄弟節(jié)點(diǎn)上。通常情況下,左兄弟會(huì)被先檢查用來做旋轉(zhuǎn)操作。就比如上面第二個(gè)示例,當(dāng)插入 70 的時(shí)候,并不會(huì)去做頁拆分,而是左旋操作。

左旋操作

通過旋轉(zhuǎn)操作可以最大限度的減少頁分裂,從而減少索引維護(hù)過程中的磁盤的 I/O 操作,也提高索引維護(hù)效率。需要注意的是,刪除節(jié)點(diǎn)跟插入節(jié)點(diǎn)類似,仍然需要旋轉(zhuǎn)和拆分操作,這里就不再說明。

高性能策略

通過上文,相信你對(duì) B+Tree 的數(shù)據(jù)結(jié)構(gòu)已經(jīng)有了大致的了解,但 MySQL 中索引是如何組織數(shù)據(jù)的存儲(chǔ)呢?以一個(gè)簡(jiǎn)單的示例來說明,假如有如下數(shù)據(jù)表:

  1. CREATE TABLE People( 
  2.  
  3. last_name varchar(50) not null
  4.  
  5. first_name varchar(50) not null
  6.  
  7. dob date not null
  8.  
  9. gender enum(`m`,`f`) not null
  10.  
  11. key(last_name,first_name,dob) 
  12.  
  13. ); 

對(duì)于表中每一行數(shù)據(jù),索引中包含了 last_name、first_name、dob 列的值,下圖展示了索引是如何組織數(shù)據(jù)存儲(chǔ)的。

索引如何組織數(shù)據(jù)存儲(chǔ),來自:高性能 MySQL

可以看到,索引首先根據(jù)第一個(gè)字段來排列順序,當(dāng)名字相同時(shí),則根據(jù)第三個(gè)字段,即出生日期來排序,正是因?yàn)檫@個(gè)原因,才有了索引的 “最左原則”。

1. MySQL 不會(huì)使用索引的情況:非獨(dú)立的列

“獨(dú)立的列” 是指索引列不能是表達(dá)式的一部分,也不能是函數(shù)的參數(shù)。比如:

  1. select * from where id + 1 = 5 

我們很容易看出其等價(jià)于 id = 4,但是 MySQL 無法自動(dòng)解析這個(gè)表達(dá)式,使用函數(shù)是同樣的道理。

2. 前綴索引

如果列很長(zhǎng),通常可以索引開始的部分字符,這樣可以有效節(jié)約索引空間,從而提高索引效率。

3. 多列索引和索引順序

在多數(shù)情況下,在多個(gè)列上建立獨(dú)立的索引并不能提高查詢性能。理由非常簡(jiǎn)單,MySQL 不知道選擇哪個(gè)索引的查詢效率更好,所以在老版本,比如 MySQL5.0 之前就會(huì)隨便選擇一個(gè)列的索引,而新的版本會(huì)采用合并索引的策略。舉個(gè)簡(jiǎn)單的例子,在一張電影演員表中,在 actor_id 和 film_id 兩個(gè)列上都建立了獨(dú)立的索引,然后有如下查詢:

老版本的 MySQL 會(huì)隨機(jī)選擇一個(gè)索引,但新版本做如下的優(yōu)化:

  1. select film_id,actor_id from film_actor where actor_id = 1 
  2.  
  3. union all 
  4.  
  5. select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1 

 

  • 當(dāng)出現(xiàn)多個(gè)索引做相交操作時(shí)(多個(gè) AND 條件),通常來說一個(gè)包含所有相關(guān)列的索引要優(yōu)于多個(gè)獨(dú)立索引。
  • 當(dāng)出現(xiàn)多個(gè)索引做聯(lián)合操作時(shí)(多個(gè) OR 條件),對(duì)結(jié)果集的合并、排序等操作需要耗費(fèi)大量的 CPU 和內(nèi)存資源,特別是當(dāng)其中的某些索引的選擇性不高,需要返回合并大量數(shù)據(jù)時(shí),查詢成本更高。所以這種情況下還不如走全表掃描。

因此 explain 時(shí)如果發(fā)現(xiàn)有索引合并(Extra 字段出現(xiàn) Using union),應(yīng)該好好檢查一下查詢和表結(jié)構(gòu)是不是已經(jīng)是最優(yōu)的,如果查詢和表都沒有問題,那只能說明索引建的非常糟糕,應(yīng)當(dāng)慎重考慮索引是否合適,有可能一個(gè)包含所有相關(guān)列的多列索引更適合。

前面我們提到過索引如何組織數(shù)據(jù)存儲(chǔ)的,從圖中可以看到多列索引時(shí),索引的順序?qū)τ诓樵兪侵陵P(guān)重要的,很明顯應(yīng)該把選擇性更高的字段放到索引的前面,這樣通過第一個(gè)字段就可以過濾掉大多數(shù)不符合條件的數(shù)據(jù)。

索引選擇性是指不重復(fù)的索引值和數(shù)據(jù)表的總記錄數(shù)的比值,選擇性越高查詢效率越高,因?yàn)檫x擇性越高的索引可以讓 MySQL 在查詢時(shí)過濾掉更多的行。唯一索引的選擇性是 1,這時(shí)最好的索引選擇性,性能也是最好的。

理解索引選擇性的概念后,就不難確定哪個(gè)字段的選擇性較高了,查一下就知道了,比如:

  1. SELECT * FROM payment where staff_id = 2 and customer_id = 584 

是應(yīng)該創(chuàng)建 (staff_id,customer_id) 的索引還是應(yīng)該顛倒一下順序?執(zhí)行下面的查詢,哪個(gè)字段的選擇性更接近 1 就把哪個(gè)字段索引前面就好。

  1. select count(distinct staff_id)/count(*) as staff_id_selectivity, 
  2.  
  3. count(distinct customer_id)/count(*) as customer_id_selectivity, 
  4.  
  5. count(*) from payment 

多數(shù)情況下使用這個(gè)原則沒有任何問題,但仍然注意你的數(shù)據(jù)中是否存在一些特殊情況。舉個(gè)簡(jiǎn)單的例子,比如要查詢某個(gè)用戶組下有過交易的用戶信息:

  1. select user_id from trade where user_group_id = 1 and trade_amount > 0 

MySQL 為這個(gè)查詢選擇了索引 (user_group_id,trade_amount),如果不考慮特殊情況,這看起來沒有任何問題,但實(shí)際情況是這張表的大多數(shù)數(shù)據(jù)都是從老系統(tǒng)中遷移過來的,由于新老系統(tǒng)的數(shù)據(jù)不兼容,所以就給老系統(tǒng)遷移過來的數(shù)據(jù)賦予了一個(gè)默認(rèn)的用戶組。這種情況下,通過索引掃描的行數(shù)跟全表掃描基本沒什么區(qū)別,索引也就起不到任何作用。

推廣開來說,經(jīng)驗(yàn)法則和推論在多數(shù)情況下是有用的,可以指導(dǎo)我們開發(fā)和設(shè)計(jì),但實(shí)際情況往往會(huì)更復(fù)雜,實(shí)際業(yè)務(wù)場(chǎng)景下的某些特殊情況可能會(huì)摧毀你的整個(gè)設(shè)計(jì)。

4. 避免多個(gè)范圍條件

實(shí)際開發(fā)中,我們會(huì)經(jīng)常使用多個(gè)范圍條件,比如想查詢某個(gè)時(shí)間段內(nèi)登錄過的用戶:

  1. select user.* from user where login_time > '2017-04-01' and age between 18 and 30; 

這個(gè)查詢有一個(gè)問題:它有兩個(gè)范圍條件,login_time 列和 age 列,MySQL 可以使用 login_time 列的索引或者 age 列的索引,但無法同時(shí)使用它們。

5. 覆蓋索引

如果一個(gè)索引包含或者說覆蓋所有需要查詢的字段的值,那么就沒有必要再回表查詢,這就稱為覆蓋索引。覆蓋索引是非常有用的工具,可以極大的提高性能,因?yàn)椴樵冎恍枰獟呙杷饕龝?huì)帶來許多好處:

  • 索引條目遠(yuǎn)小于數(shù)據(jù)行大小,如果只讀取索引,極大減少數(shù)據(jù)訪問量
  • 索引是有按照列值順序存儲(chǔ)的,對(duì)于 I/O 密集型的范圍查詢要比隨機(jī)從磁盤讀取每一行數(shù)據(jù)的 IO 要少的多

6. 使用索引掃描來排序

MySQL 有兩種方式可以生產(chǎn)有序的結(jié)果集,其一是對(duì)結(jié)果集進(jìn)行排序的操作,其二是按照索引順序掃描得出的結(jié)果自然是有序的。如果 explain 的結(jié)果中 type 列的值為 index 表示使用了索引掃描來做排序。

掃描索引本身很快,因?yàn)橹恍枰獜囊粭l索引記錄移動(dòng)到相鄰的下一條記錄。但如果索引本身不能覆蓋所有需要查詢的列,那么就不得不每掃描一條索引記錄就回表查詢一次對(duì)應(yīng)的行。這個(gè)讀取操作基本上是隨機(jī) I/O,因此按照索引順序讀取數(shù)據(jù)的速度通常要比順序地全表掃描要慢。

在設(shè)計(jì)索引時(shí),如果一個(gè)索引既能夠滿足排序,又滿足查詢,是最好的。

只有當(dāng)索引的列順序和 ORDER BY 子句的順序完全一致,并且所有列的排序方向也一樣時(shí),才能夠使用索引來對(duì)結(jié)果做排序。如果查詢需要關(guān)聯(lián)多張表,則只有 ORDER BY 子句引用的字段全部為第一張表時(shí),才能使用索引做排序。ORDER BY 子句和查詢的限制是一樣的,都要滿足最左前綴的要求(有一種情況例外,就是最左的列被指定為常數(shù),下面是一個(gè)簡(jiǎn)單的示例),其它情況下都需要執(zhí)行排序操作,而無法利用索引排序。

  1. // 最左列為常數(shù),索引:(date,staff_id,customer_id) 
  2.  
  3. select  staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id 

7. 冗余和重復(fù)索引

冗余索引是指在相同的列上按照相同的順序創(chuàng)建的相同類型的索引,應(yīng)當(dāng)盡量避免這種索引,發(fā)現(xiàn)后立即刪除。比如有一個(gè)索引 (A,B),再創(chuàng)建索引 (A) 就是冗余索引。冗余索引經(jīng)常發(fā)生在為表添加新索引時(shí),比如有人新建了索引 (A,B),但這個(gè)索引不是擴(kuò)展已有的索引 (A)。

大多數(shù)情況下都應(yīng)該盡量擴(kuò)展已有的索引而不是創(chuàng)建新索引。但有極少情況下出現(xiàn)性能方面的考慮需要冗余索引,比如擴(kuò)展已有索引而導(dǎo)致其變得過大,從而影響到其他使用該索引的查詢。

8. 刪除長(zhǎng)期未使用的索引

定期刪除一些長(zhǎng)時(shí)間未使用過的索引是一個(gè)非常好的習(xí)慣。

關(guān)于索引這個(gè)話題打算就此打住,最后要說一句,索引并不總是最好的工具,只有當(dāng)索引幫助提高查詢速度帶來的好處大于其帶來的額外工作時(shí),索引才是有效的。對(duì)于非常小的表,簡(jiǎn)單的全表掃描更高效。對(duì)于中到大型的表,索引就非常有效。對(duì)于超大型的表,建立和維護(hù)索引的代價(jià)隨之增長(zhǎng),這時(shí)候其他技術(shù)也許更有效,比如分區(qū)表。最后的最后,explain 后再提測(cè)是一種美德。

特定類型查詢優(yōu)化

1. 優(yōu)化 COUNT() 查詢

COUNT() 可能是被大家誤解最多的函數(shù)了,它有兩種不同的作用,其一是統(tǒng)計(jì)某個(gè)列值的數(shù)量,其二是統(tǒng)計(jì)行數(shù)。統(tǒng)計(jì)列值時(shí),要求列值是非空的,它不會(huì)統(tǒng)計(jì) NULL。如果確認(rèn)括號(hào)中的表達(dá)式不可能為空時(shí),實(shí)際上就是在統(tǒng)計(jì)行數(shù)。最簡(jiǎn)單的就是當(dāng)使用 COUNT(*) 時(shí),并不是我們所想象的那樣擴(kuò)展成所有的列,實(shí)際上,它會(huì)忽略所有的列而直接統(tǒng)計(jì)所有的行數(shù)。

我們最常見的誤解也就在這兒,在括號(hào)內(nèi)指定了一列卻希望統(tǒng)計(jì)結(jié)果是行數(shù),而且還常常誤以為前者的性能會(huì)更好。但實(shí)際并非這樣,如果要統(tǒng)計(jì)行數(shù),直接使用 COUNT(*),意義清晰,且性能更好。

有時(shí)候某些業(yè)務(wù)場(chǎng)景并不需要完全精確的 COUNT 值,可以用近似值來代替,EXPLAIN 出來的行數(shù)就是一個(gè)不錯(cuò)的近似值,而且執(zhí)行 EXPLAIN 并不需要真正地去執(zhí)行查詢,所以成本非常低。通常來說,執(zhí)行 COUNT() 都需要掃描大量的行才能獲取到精確的數(shù)據(jù),因此很難優(yōu)化,MySQL 層面還能做得也就只有覆蓋索引了。如果不還能解決問題,只有從架構(gòu)層面解決了,比如添加匯總表,或者使用 redis 這樣的外部緩存系統(tǒng)。

2. 優(yōu)化關(guān)聯(lián)查詢

在大數(shù)據(jù)場(chǎng)景下,表與表之間通過一個(gè)冗余字段來關(guān)聯(lián),要比直接使用 JOIN 有更好的性能。如果確實(shí)需要使用關(guān)聯(lián)查詢的情況下,需要特別注意的是:

  • 確保 ON 和 USING 字句中的列上有索引。在創(chuàng)建索引的時(shí)候就要考慮到關(guān)聯(lián)的順序。當(dāng)表 A 和表 B 用列 c 關(guān)聯(lián)的時(shí)候,如果優(yōu)化器關(guān)聯(lián)的順序是 A、B,那么就不需要在 A 表的對(duì)應(yīng)列上創(chuàng)建索引。沒有用到的索引會(huì)帶來額外的負(fù)擔(dān),一般來說,除非有其他理由,只需要在關(guān)聯(lián)順序中的第二張表的相應(yīng)列上創(chuàng)建索引(具體原因下文分析)。
  • 確保任何的 GROUP BY 和 ORDER BY 中的表達(dá)式只涉及到一個(gè)表中的列,這樣 MySQL 才有可能使用索引來優(yōu)化。

要理解優(yōu)化關(guān)聯(lián)查詢的第一個(gè)技巧,就需要理解 MySQL 是如何執(zhí)行關(guān)聯(lián)查詢的。當(dāng)前 MySQL 關(guān)聯(lián)執(zhí)行的策略非常簡(jiǎn)單,它對(duì)任何的關(guān)聯(lián)都執(zhí)行嵌套循環(huán)關(guān)聯(lián)操作,即先在一個(gè)表中循環(huán)取出單條數(shù)據(jù),然后在嵌套循環(huán)到下一個(gè)表中尋找匹配的行,依次下去,直到找到所有表中匹配的行為為止。然后根據(jù)各個(gè)表匹配的行,返回查詢中需要的各個(gè)列。

太抽象了?以上面的示例來說明,比如有這樣的一個(gè)查詢:

  1. SELECT A.xx,B.yy 
  2.  
  3. FROM A INNER JOIN B USING(c) 
  4.  
  5. WHERE A.xx IN (5,6) 

假設(shè) MySQL 按照查詢中的關(guān)聯(lián)順序 A、B 來進(jìn)行關(guān)聯(lián)操作,那么可以用下面的偽代碼表示 MySQL 如何完成這個(gè)查詢:

  1. outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6); 
  2.  
  3. outer_row = outer_iterator.next
  4.  
  5. while(outer_row) { 
  6.  
  7. inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c; 
  8.  
  9. inner_row = inner_iterator.next
  10.  
  11. while(inner_row) { 
  12.  
  13. output[inner_row.yy,outer_row.xx]; 
  14.  
  15. inner_row = inner_iterator.next
  16.  
  17.  
  18. outer_row = outer_iterator.next
  19.  

可以看到,最外層的查詢是根據(jù) A.xx 列來查詢的,A.c 上如果有索引的話,整個(gè)關(guān)聯(lián)查詢也不會(huì)使用。再看內(nèi)層的查詢,很明顯 B.c 上如果有索引的話,能夠加速查詢,因此只需要在關(guān)聯(lián)順序中的第二張表的相應(yīng)列上創(chuàng)建索引即可。  

3. 優(yōu) 化 LIMIT 分頁

當(dāng)需要分頁操作時(shí),通常會(huì)使用 LIMIT 加上偏移量的辦法實(shí)現(xiàn),同時(shí)加上合適的 ORDER BY 字句。如果有對(duì)應(yīng)的索引,通常效率會(huì)不錯(cuò),否則,MySQL 需要做大量的文件排序操作。

一個(gè)常見的問題是當(dāng)偏移量非常大的時(shí)候,比如:LIMIT 10000 20 這樣的查詢,MySQL 需要查詢 10020 條記錄然后只返回 20 條記錄,前面的 10000 條都將被拋棄,這樣的代價(jià)非常高。

優(yōu)化這種查詢一個(gè)最簡(jiǎn)單的辦法就是盡可能的使用覆蓋索引掃描,而不是查詢所有的列。然后根據(jù)需要做一次關(guān)聯(lián)查詢?cè)俜祷厮械牧小?duì)于偏移量很大時(shí),這樣做的效率會(huì)提升非常大。考慮下面的查詢:

  1. SELECT film_id,description FROM film ORDER BY title LIMIT 50,5; 

如果這張表非常大,那么這個(gè)查詢最好改成下面的樣子:

  1. SELECT film.film_id,film.description 
  2.  
  3. FROM film INNER JOIN ( 
  4.  
  5. SELECT film_id FROM film ORDER BY title LIMIT 50,5 
  6.  
  7. AS tmp USING(film_id); 

這里的延遲關(guān)聯(lián)將大大提升查詢效率,讓 MySQL 掃描盡可能少的頁面,獲取需要訪問的記錄后在根據(jù)關(guān)聯(lián)列回原表查詢所需要的列。

有時(shí)候如果可以使用書簽記錄上次取數(shù)據(jù)的位置,那么下次就可以直接從該書簽記錄的位置開始掃描,這樣就可以避免使用 OFFSET,比如下面的查詢:

  1. SELECT id FROM t LIMIT 10000, 10; 

改為:

 

  1. SELECT id FROM t WHERE id > 10000 LIMIT 10; 

其它優(yōu)化的辦法還包括使用預(yù)先計(jì)算的匯總表,或者關(guān)聯(lián)到一個(gè)冗余表,冗余表中只包含主鍵列和需要做排序的列。

4. 優(yōu)化 UNION

MySQL 處理 UNION 的策略是先創(chuàng)建臨時(shí)表,然后再把各個(gè)查詢結(jié)果插入到臨時(shí)表中,最后再來做查詢。因此很多優(yōu)化策略在 UNION 查詢中都沒有辦法很好的時(shí)候。經(jīng)常需要手動(dòng)將 WHERE、LIMIT、ORDER BY 等字句 “下推” 到各個(gè)子查詢中,以便優(yōu)化器可以充分利用這些條件先優(yōu)化。

除非確實(shí)需要服務(wù)器去重,否則就一定要使用 UNION ALL,如果沒有 ALL 關(guān)鍵字,MySQL 會(huì)給臨時(shí)表加上 DISTINCT 選項(xiàng),這會(huì)導(dǎo)致整個(gè)臨時(shí)表的數(shù)據(jù)做唯一性檢查,這樣做的代價(jià)非常高。當(dāng)然即使使用 ALL 關(guān)鍵字,MySQL 總是將結(jié)果放入臨時(shí)表,然后再讀出,再返回給客戶端。雖然很多時(shí)候沒有這個(gè)必要,比如有時(shí)候可以直接把每個(gè)子查詢的結(jié)果返回給客戶端。

結(jié)語

理解查詢是如何執(zhí)行以及時(shí)間都消耗在哪些地方,再加上一些優(yōu)化過程的知識(shí),可以幫助大家更好的理解 MySQL,理解常見優(yōu)化技巧背后的原理。希望本文中的原理、示例能夠幫助大家更好的將理論和實(shí)踐聯(lián)系起來,更多的將理論知識(shí)運(yùn)用到實(shí)踐中。其他也沒啥說的了,給大家留兩個(gè)思考題吧,可以在腦袋里想想答案,這也是大家經(jīng)常掛在嘴邊的,但很少有人會(huì)思考為什么?

  • 有非常多的程序員在分享時(shí)都會(huì)拋出這樣一個(gè)觀點(diǎn):盡可能不要使用存儲(chǔ)過程,存儲(chǔ)過程非常不容易維護(hù),也會(huì)增加使用成本,應(yīng)該把業(yè)務(wù)邏輯放到客戶端。既然客戶端都能干這些事,那為什么還要存儲(chǔ)過程?
  • JOIN 本身也挺方便的,直接查詢就好了,為什么還需要視圖呢?

參考資料:

  • 姜承堯 著;MySQL 技術(shù)內(nèi)幕 - InnoDB 存儲(chǔ)引擎;機(jī)械工業(yè)出版社,2013
  • Baron Scbwartz 等著;寧海元 周振興等譯;高性能 MySQL(第三版); 電子工業(yè)出版社, 2013
  • 由 B-/B + 樹看 MySQL 索引結(jié)構(gòu):https://segmentfault.com/a/1190000004690721 
責(zé)任編輯:龐桂玉 來源: ITPUB
相關(guān)推薦

2022-06-20 09:01:23

Git插件項(xiàng)目

2021-04-08 07:37:39

隊(duì)列數(shù)據(jù)結(jié)構(gòu)算法

2017-03-11 22:19:09

深度學(xué)習(xí)

2020-08-03 10:00:11

前端登錄服務(wù)器

2023-04-24 08:00:00

ES集群容器

2018-01-17 09:32:45

人工智能卷積神經(jīng)網(wǎng)絡(luò)CNN

2021-03-03 14:55:10

開發(fā)MySQL代碼

2020-08-17 09:25:51

Docker容器技術(shù)

2018-11-14 11:57:28

2023-02-10 09:04:27

2020-02-18 16:20:03

Redis ANSI C語言日志型

2020-05-14 16:35:21

Kubernetes網(wǎng)絡(luò)策略DNS

2019-08-13 15:36:57

限流算法令牌桶

2023-09-11 08:13:03

分布式跟蹤工具

2022-08-01 11:33:09

用戶分析標(biāo)簽策略

2019-07-02 05:02:56

NVMe接口存儲(chǔ)

2022-04-10 23:21:04

SSH協(xié)議網(wǎng)絡(luò)安全

2020-12-10 15:25:51

Docker容器工具

2021-06-07 06:25:35

畫流程圖開發(fā)技能

2018-11-12 08:07:04

Nginx優(yōu)化并發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

91国语精品自产拍在线观看性色 | a∨色狠狠一区二区三区| 久久久久9999亚洲精品| 91精品在线观| 日本少妇性高潮| 国产成人手机高清在线观看网站| 欧美精品电影在线播放| 我的公把我弄高潮了视频| 国产高清在线| 不卡高清视频专区| 国产精品一区电影| 九九九国产视频| 欧美色图国产精品| 亚洲福利视频二区| 午夜免费福利视频在线观看| 蜜桃麻豆av在线| 亚洲日本青草视频在线怡红院| 久久99影院| 999久久久久| 久久亚洲电影| 午夜精品久久久久久久99热| www.97视频| 国产一区二区三区四区二区| 亚洲成人av在线| 成人黄色一级大片| 免费在线观看一区| 福利视频导航一区| 国产精品久久久久久久久电影网| 992tv免费直播在线观看| 99久久99久久精品国产片果冻| 国产一区二区在线免费| 真实的国产乱xxxx在线91| 亚洲精品欧洲| 欧美激情精品久久久久久久变态| 天堂在线中文视频| 国内精品视频在线观看| 亚洲精品视频网上网址在线观看| 女性生殖扒开酷刑vk| 久久综合给合| 欧美欧美欧美欧美首页| 国产精品久久久久9999小说| 色网在线免费观看| 亚洲18色成人| 日韩精品综合在线| 日本一级理论片在线大全| 亚洲欧洲韩国日本视频| 午夜精品一区二区在线观看的| 五月婷婷丁香网| 不卡av在线网| 韩国一区二区三区美女美女秀| av观看在线免费| 精东粉嫩av免费一区二区三区| 国产精品精品一区二区三区午夜版| 伊人手机在线视频| 一本色道久久综合亚洲精品不| 久久久女女女女999久久| 欧美精品xxxxx| 狠狠入ady亚洲精品经典电影| 久久99久国产精品黄毛片入口| 亚洲xxxx3d动漫| 中文字幕一区二区三区在线视频| 久久久精品影院| 成人免费视频国产免费观看| 91成人观看| 欧美日韩aaaa| 国产精品6666| 亚洲一区二区网站| 国产精品电影网| 一级成人免费视频| 国产美女一区二区| 国产精品一区二区欧美| 神马久久高清| 日本一区二区成人在线| 伊人久久av导航| 欧美黄色一级大片| 久久精品视频91| 玩弄中年熟妇正在播放| 水莓100国产免费av在线播放| www.欧美日韩| 蜜桃麻豆91| 成人动漫在线播放| 亚洲视频资源在线| 亚洲国产精品无码av| av电影一区| 欧美日韩国产区一| 欧洲熟妇的性久久久久久| 亚洲激情播播| 精品国产视频在线| 精品无码一区二区三区电影桃花| 国产精品毛片在线看| 国产欧美在线观看| 丰满肉肉bbwwbbww| 国产亚洲人成网站| 黄色小视频大全| 亚洲一区资源| 欧美一区午夜精品| 青青草视频网站| 青青草综合网| 久久久久中文字幕2018| 中文字字幕在线观看| 成人国产免费视频| 亚洲精品国产精品国自产| 日本不卡影院| 欧美日韩午夜在线视频| 亚洲麻豆一区二区三区| 日本欧美国产| 午夜精品美女自拍福到在线| 中文字幕观看视频| 99久久国产综合精品麻豆| 一本一道久久a久久综合精品| av成人影院在线| 欧美日本韩国一区二区三区视频| 娇妻高潮浓精白浆xxⅹ| 第一会所sis001亚洲| 午夜精品一区二区三区视频免费看| 波多野结衣一区二区三区四区| 国产麻豆视频一区| 日韩中文字幕一区二区| 成年人国产在线观看| 91精品国产一区二区三区蜜臀| 成人免费网站黄| 在线成人国产| 亚洲一区二区久久久久久| 黄色av网址在线免费观看| 亚洲一区二区三区四区在线免费观看| 欧美婷婷精品激情| 国产传媒欧美日韩成人精品大片| 欧美国产视频一区二区| 国产绿帽刺激高潮对白| 日本一区二区视频在线| 日本a级片免费观看| 精品国产导航| 欧美激情成人在线视频| 国产免费黄色录像| 国产精品伦一区二区三级视频| 97成人在线观看视频| 国产欧美啪啪| 欧美夫妻性生活xx| 国产毛片毛片毛片毛片毛片| 国产精品视频观看| 在线观看免费成人av| 亚洲图片久久| 欧美一级视频一区二区| 婷婷丁香一区二区三区| 婷婷激情综合网| 91视频在线免费| 亚洲国产高清一区二区三区| 动漫精品视频| 国产美女一区视频| 精品福利在线导航| 国产乡下妇女做爰毛片| 成人h版在线观看| 亚洲国产精品无码av| 女人抽搐喷水高潮国产精品| 国a精品视频大全| 五月婷婷丁香网| 一本一道久久a久久精品综合蜜臀| 久久一区二区电影| 国产精品入口66mio| 欧美日韩大片一区二区三区| 一区二区三区电影大全| 一区二区在线视频播放| 中文在线资源天堂| 亚洲欧洲日韩综合一区二区| 亚洲精品无码久久久久久久| 亚洲天堂久久| 明星裸体视频一区二区| 国产私拍福利精品视频二区| 视频直播国产精品| 国产日韩免费视频| 亚洲一区二区四区蜜桃| 中国一级特黄录像播放| 性欧美暴力猛交另类hd| 日韩欧美在线电影| 99久久99九九99九九九| 欧美激情按摩在线| 欧美色视频免费| 欧美日韩国产小视频| 2021亚洲天堂| 99久久综合狠狠综合久久| 日本精品www| 91精品国产麻豆国产在线观看 | 久草免费在线观看视频| 波多野结衣在线aⅴ中文字幕不卡| 动漫av网站免费观看| 久久综合88| 国内精品**久久毛片app| 日本成人片在线| 欧美成人激情图片网| 日韩三级电影网| 欧美日本一区二区| 日本少妇激情视频| 国产精品久久久久三级| 精品影片一区二区入口| 蜜桃久久av一区| 给我免费播放片在线观看| 国产一区二区三区日韩精品| 成人蜜桃视频| 2019年精品视频自拍| 久久久久久久国产| www.成人.com| 亚洲成人精品视频| 在线观看国产精品入口男同| 亚洲风情在线资源站| 成年人在线免费看片| 成人永久aaa| 天天干天天玩天天操| 国产日韩亚洲| 欧美这里只有精品| 91精品国产调教在线观看| 久久影院理伦片| 中文字幕一区二区三区日韩精品| 国产精品日韩av| 国产精品一二三产区| 久久在线精品视频| 成人在线二区| 日韩精品视频在线播放| 亚洲精品久久久久久久久久| 欧美揉bbbbb揉bbbbb| 最新中文字幕一区| 亚洲成人av一区| 欧美日韩精品亚洲精品| 国产精品国产精品国产专区不片| 亚洲欧美日本一区| 国产成人精品三级| 伊人网在线综合| 日韩va亚洲va欧美va久久| 人妻熟妇乱又伦精品视频| 欧美日本亚洲韩国国产| 最新精品视频| 久久一区二区中文字幕| 日韩少妇中文字幕| 西野翔中文久久精品字幕| 高清国语自产拍免费一区二区三区| 亚洲精品毛片| 国产精品丝袜白浆摸在线| 丝袜美腿诱惑一区二区三区| 91精品国产91久久久久| 91禁在线看| 午夜精品一区二区三区在线播放| 在线三级中文| 欧美精品免费播放| av毛片在线看| 欧美疯狂性受xxxxx另类| 日韩少妇视频| 欧美激情一级欧美精品| 欧美1—12sexvideos| 欧美成人h版在线观看| 色www永久免费视频首页在线| 久久视频这里只有精品| av免费网站在线| 欧美黑人一区二区三区| sis001亚洲原创区| 国内精品久久久久久中文字幕| 懂色av一区| 2019中文字幕在线免费观看| 新版的欧美在线视频| 欧美中文字幕在线观看| 日韩影片中文字幕| 国产精品视频一区二区三区四| 欧美成人三级| 91手机在线观看| 欧美a一欧美| 日本精品二区| 无需播放器亚洲| 久久99国产精品一区| 欧美三级第一页| 国产亚洲精品网站| 久久精品国产亚洲aⅴ| 天天av天天操| 风间由美一区二区av101| 欧美变态口味重另类| 亚洲av无码一区二区乱子伦| 精品国产免费人成电影在线观看四季 | 中文字幕人妻一区| 久久看人人爽人人| 国精品人伦一区二区三区蜜桃| 亚洲欧美日韩电影| 日本免费观看视| 欧洲av在线精品| 99在线无码精品入口| 亚洲第一av网| 不卡在线视频| 欧美大片免费看| 精品无人乱码一区二区三区| 成人黄色短视频在线观看 | 日韩av高清| 中文字幕日韩欧美精品高清在线| 久久综合九色综合88i| 日韩电影在线看| 中文字幕99页| 欧美韩日一区二区三区| 国产在线观看免费视频今夜| 日本二三区不卡| 午夜精品久久久久久久99热黄桃 | 免费网站在线高清观看| 一区二区三区四区视频精品免费| 黄色一级视频免费看| 日韩欧美国产一区在线观看| 韩日在线视频| 九九热r在线视频精品| 午夜无码国产理论在线| 3d动漫啪啪精品一区二区免费 | 天堂精品久久久久| 日韩中文字幕一区| 亚洲日本免费| 波多野结衣在线免费观看| 久久中文娱乐网| 色老板免费视频| 色综合激情久久| 嫩草影院一区二区| 久久精品久久精品亚洲人| 成人影院网站| 国产无套精品一区二区| 国产精品久久久乱弄| 免费裸体美女网站| 波多野结衣在线一区| 2021亚洲天堂| 欧美日本韩国一区二区三区视频| 男人的天堂在线免费视频| 欧美激情综合色综合啪啪五月| 香蕉久久一区| 日韩精品无码一区二区三区| 亚洲一区二区三区高清不卡| 黄色在线免费播放| 亚洲激情在线激情| 国产精品久久久久久久久久久久久久久久久久 | 国产福利不卡视频| 午夜国产小视频| 欧美日韩国产综合一区二区三区 | 久久这里只有精品视频首页| 在线一区视频观看| 神马影院我不卡| 天堂va蜜桃一区二区三区| 亚洲精品视频大全| 五月天中文字幕一区二区| 亚洲美女性生活| 欧美激情欧美激情| 无码国模国产在线观看| 香蕉视频免费版| 国产精品亚洲综合一区在线观看| 很污很黄的网站| 欧美日韩国产综合久久| 91高清在线视频| 国产欧美va欧美va香蕉在| 日本久久一二三四| 国产九九在线观看| 国产精品乱码人人做人人爱| 一级特黄aaaaaa大片| 日韩在线精品一区| 99精品在线免费观看| 日本xxxxx18| 国产精品自在在线| 久久午夜无码鲁丝片| 精品国产乱子伦一区| 岛国在线视频网站| 久久久久久亚洲精品不卡4k岛国 | 国产精品91在线| 日韩美女一区二区三区在线观看| 一个色综合久久| 一区二区在线观看免费| 少妇一区二区三区四区| 欧美综合第一页| 精品久久美女| 日韩视频在线观看一区二区三区| 亚洲视频狠狠干| 蜜臀av在线观看| 国产精品18久久久久久首页狼| 成人情趣视频网站| 91欧美一区二区三区| 亚洲福利视频一区二区| 男人天堂资源在线| 成人黄色激情网| 精品99视频| 谁有免费的黄色网址| 欧美日韩精品综合在线| 50度灰在线| 免费成人看片网址| 蜜桃久久精品一区二区| 精品少妇久久久久久888优播| 亚洲精品一区久久久久久| 久久福利在线| 国内少妇毛片视频| 国产欧美一区二区三区网站| 99热精品在线播放| 4438全国亚洲精品在线观看视频| av一区二区在线播放| 乳色吐息在线观看| 色综合天天综合色综合av| 欧美日韩在线看片| 国产精品久久久一区二区三区| 午夜亚洲影视| 成年人一级黄色片| 亚洲精品视频在线播放| 免费一级欧美在线大片| 国产免费毛卡片| 亚洲品质自拍视频| 黄色av免费在线看| 成人动漫在线观看视频| 秋霞av亚洲一区二区三|