1. 程式人生 > 其它 >MySQL查詢的本質——單表與多表的查詢方法

MySQL查詢的本質——單表與多表的查詢方法

單表查詢 單表查詢的語句實際上是一種宣告式的語法,只是告訴MySQL要獲取的資料符合哪些規則,至於具體的執行方式是MySQL自己來定。針對不同的場景,也有不同的執行計劃: const 通過主鍵或者唯一二級索引與常數的等值比較來定位一條記錄,是最快的執行方式。主鍵定位直接返回,而唯一二級索引只需查詢一次+回表一次即可返回 ref 較為常見的,在建立二級索引後直接使用其為等值查詢條件。因為是等值的,索引會形成單點掃描區間。這種方法稱為ref,規則是:搜尋條件為二級索引列常數進行等值比較,形成的掃描區間為單點掃描區間,採用二級索引來執行查詢。這種方式的缺點就是每查到一次就回表一次,消耗大。
  • 且當二級索引列允許儲存null時,都是用此ref方式訪問。
  • ref允許最左匹配機制,即最左連續列匹配聯合索引,就可以使用ref
  • 當查詢條件不為等值時不能用ref
(另外:在多表查詢中,對被驅動表的主鍵或者不允許儲存NULL值的唯一二級索引進行等值查詢,使用的方法稱為eq_ref) ref_or_null 比ref多掃描了一些值為null的二級索引記錄 range 使用索引執行查詢時,對應的掃描區間為若干個單點掃描區間或者範圍掃描區間。即包含多個單點掃描區間的查詢條件,包含一個或全表掃描都不能稱為range index 直接掃描全部二級索引記錄,且查詢結果包含在索引列中,無需回表操作。即索引覆蓋,因為二級索引比聚簇索引是要小很多的,就算全部掃描也不會消耗很多資源 all
全表掃描,適用於所有查詢,但缺點就是慢 注意: 一般來說,具體使用哪種方法來執行,是MySQL中優化器的工作,它會訪問表中少量資料獲等方式,分析出多種執行計劃中成本最小的來查詢。 一般來說,使用二級索引查詢時,當拿到匹配的二級記錄後,就算還有其他匹配條件,也會先根據結果(主鍵)執行回表操作,再檢測該記錄是否滿足其他條件,滿足則傳送給客戶端,不滿足則忽略。 index_merge 索引合併。使用多個索引來完成一次查詢,有以下三種方式:
  • Intersection索引合併
舉個栗子:SELECT * FROM sing_table WHERE key1 = 'a' AND key3 = 'b'; 一共有四種方式可以執行該查詢:
    1. 首先,是全表就不談了,慢。
    2. 其次,是使用key1的索引查詢,則掃描區間為['a','a'],查詢到之後立刻回表,再匹配key3。
    3. 然後,是按照key3的索引查詢,同樣的則掃描區間為['b','b'],查詢到之後立刻回表,再匹配key1
    4. 最後,同時使用key1和key3的索引查詢。在key1索引中掃描出匹配key1=a的記錄,在key3索引中掃描出匹配key1=b的記錄。由於在普通二級索引中,重複的索引值會按照主鍵來排序。所以在這個執行計劃中,使用各自的索引掃描出來的記錄也是主鍵排序的。又因為這裡使用了 AND,需要查詢兩者條件都匹配的結果,所以找出兩者查詢的結果中主鍵相同的記錄,再使用這些相同的主鍵一併執行回表,這樣可能會省下很多回錶帶來的開銷。這就是Intersection索引合併,它要求二級索引是排序的,也就是說我們的查詢條件必須是等值的,只有等值才會排序。如果不使用等值條件,則不會使用Intersection索引合併(相當於各自使用ref,最後取重複集)
注意:如果條件中含有主鍵索引,則並不會掃描主鍵索引,而是和非主鍵索引一起形成等效的掃描區間。
  • Union索引合併
再舉個栗子:SELECT * FROM sing_table WHERE key1 = 'a' OR key3 = 'b'; 和之前的例子差不多,只是AND換成了OR,但完全不同了,你肯定不能單獨使用其中一個索引來查,否則會先查詢二級索引所有記錄,再全部回表,災難性的回表代表巨大消耗!所以要麼你全表查詢,要麼你就是用Union索引合併: 在key1索引中掃描出匹配key1=a的記錄,在key3索引中掃描出匹配key1=b的記錄。因為這裡用的是 OR ,只要匹配一個條件即可返回,所以只要根據兩個結果集進行去重,把重複的ID去掉,接著就可以拿著剩下的ID區一併回表了,其實和Intersection索引是類似的,只不過它要的是重複的記錄,這裡要的是去掉重複的記錄。同樣,只有等值比較才可以使用這種Union索引合併。(相當於各自ref,然後去重)
  • Sort-Union索引合併
看這個名字其實就是比Union新增了一個Sort,那麼它在什麼地方加了排序呢?我們知道使用Union的條件是二級索引記錄必須是按主鍵排序的,也就是說在SQL語句中,必須是等值的,類似上面的:key1 = 'a' OR key3 = 'b'; 但如果是這種:key1 < 'a' OR key3 > 'z'; 呢?使用Sort-Union排序可以先從key1索引中獲得條件為“key1 < 'a'”的二級索引記錄,並對其根據主鍵進行排序;再從key3索引中獲得條件為“key3 > 'z'”的二級索引記錄,也對其根據主鍵進行排序。如此,又變成Union索引合併了。又可以減少很多回表操作。這種方法比普通的Union多了一層各自索引的主鍵值的排序,也稱Sort-Union索引合併。 注意:只有Sort-Union而沒有Sort-Intersecion的原因作者也有解釋,但具體沒怎麼看明白:如果加入Sort-Intersecion,就需要為大量的二級索引記錄按照主鍵值進行排序,這個成本可能比使用單個二級索引執行查詢的成本都要高,所以mysql沒有引入。 多表查詢 MySQL是關係型資料庫,一個非常重要的概念就是Join 。表關聯的實質其實就是把各個表的記錄都取出來依此匹配,並把匹配後的組合發給客戶端。如果不加條件限制,給到客戶端的記錄將是幾個表的乘積,也稱笛卡爾積,因為需要每個表的每一條記錄都與另一個表的每條記錄相互匹配。這個過程有點像大規模的回表操作,或者程式碼中的巢狀for迴圈。 因此,我們一般在連線查詢時並不會全部匹配,而是會加上限制條件。這裡的限制條件一般只有兩種情況:
  • 單表限制條件。如:key=3或key>1或key<4。只針對key所在的表的限制條件
  • 多表限制條件。如:t1.m1=t2.m1、t1.n1<t2.n2。就針對了t1和t2兩個表的限制條件。
在這裡有必要提一下基本連線查詢過程:假設有表t1和t2,t1作為驅動表,t2作為被驅動表。首先在t1表中,按照對應的限制條件,使用成本最小的單表查詢方法去查詢。每次在t1查詢到一條記錄,都需要根據這條記錄去t2表查詢匹配的記錄。即驅動表只需要查詢一次,而被驅動表可能需要訪問多次。 這種方式有個缺點,如果驅動表的某條記錄,沒有在被驅動表中找到相應記錄,那麼就連驅動表的那條記錄也不會查詢出來,這樣就導致查詢出來的資料有缺失。為了解決該問題,引出了連線另一個概念:外連線。在外連線中,即時驅動表中的記錄在被驅動表中沒有匹配的記錄,也仍然需要加入到結果集。 連線查詢也需要有過濾條件,單表查詢中使用WHERE,連線查詢中的WHERE可能是不夠用的。因此在連線查詢中,存在著兩種過濾條件:
  • WHERE:不分內外連線,只要不匹配WHERE子句的條件,就不加入結果集。
  • ON:
    • 對於外連線的驅動表來說,如果在被驅動表中找不到匹配ON子句的過濾條件,驅動表的記錄還是會加入結果集,被驅動表找不到記錄沒有關係,填NULL即可。
    • 對於內連線的驅動表來說,ON等效於WHERE。
內連線的語法: select * from t1,t2;等效於select * from t1 join t2;等效於 select * from t1 inner join t2;等效於 select * from t1 cross join t2; 。 推薦INNER JOIN的寫法,語義明確 外連線的語法: select * from t1 left join t2 on連線條件 [where普通過濾條件]; left join中左側的表稱為外表或者驅動表,右側的表稱為內表或被驅動表。外連線必須使用on來指出連線條件。 right join也是一樣的,只是左右驅動表位置換了,不再贅述 連線查詢的原理 巢狀迴圈連線 最基本的簡單的連線查詢算演算法。驅動表只訪問一次,但被驅動表卻可能訪問多次,且訪問的次數取決於對驅動表執行單表查詢後的結果集中有多少條記錄(驅動表匹配到記錄後立即去被驅動表查) 多次如果每條驅動表的記錄在被驅動表中都有很多,那麼單表查詢被驅動表的次數就會非常多,效率自然會不高。如果被驅動表的限制條件帶有索引,那麼有可能會使用ref、ref_or_null或者range等方式來查詢,效率相對會快一些。所以在查詢時,儘量避免select *,而是把真正使用到的列作為查詢條件,這是在人為可控的情況下,優化查詢。一般常用的列都會有索引,索引覆蓋下查詢效率會更高。 基於塊的巢狀迴圈連線 在表資料過於龐大的情況下,記錄有可能是千萬、億級的規模,這種情況下的連線查詢有可能會非常慢。基於塊的巢狀迴圈連線是從減少被驅動表的訪問次數入手,達到減少IO、提高效率的效果的。具體的做法就是在執行連線查詢前申請一塊固定大小的記憶體(也稱Join Buffer),先將若干驅動表結果集裡的記錄裝在這個記憶體中,然後開始掃描被驅動表,每條被驅動表的記錄一次性地與Join Buffer中的多條驅動表記錄進行匹配,由於匹配在記憶體中完成,無需IO,所以可以大大減少IO量。 JoinBuffer的大小通過啟動項或系統變數進行配置,預設為256KB。一般在不能使用索引且自己機器的記憶體較大的情況下,可以嘗試調大JoinBuffer來進行連線查詢的優化。需要注意的是JoinBuffer並不會放所有驅動表的列,只存放查詢列表中的列和過濾條件的列。因此我們查詢時少用select * ,這樣Join Buffer可以存放更多的記錄。