我的Mysql查詢SQL優化總結

點擊上方程序員小明”,選擇“星標”

今晚可以不加班!

當我們遇到一個慢查詢語句時,首先要做的是檢查所編寫的 SQL 語句是否合理,優化 SQL 語句從而提升查詢效率。所以對 SQL 有一個整體的認識是有必要的。

MySQL 服務器接收到一條 SQL 語句時,其處理過程爲

640?wx_fmt=pngmysql執行過程

當然,作爲一個開發,更應該關心的是 SQL 解析後的執行情況,這時還需要用到 EXPLAIN 命令,瞭解數據庫執行 SQL 時是怎麼做的。

1、SQL 執行順序

理解 SQL 執行順序有助於找出查詢慢的原因。

以下爲 MySQL SELECT 官方給出的語句格式。

SELECT
    [ALL | DISTINCT | DISTINCTROW ]
    select_expr [, select_expr ...]
    [FROM table_references
      [PARTITION partition_list]
    [[LEFT]JOIN joined_table_references]
    [ON search_condition | USING (join_column_list)]
    [WHERE where_condition]
    [GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]]
    [HAVING having_condition]
    [WINDOW window_name AS (window_spec)
        [, window_name AS (window_spec)] ...]
    [ORDER BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]]

SQL 語句被解釋後,按照關鍵字的信息開始逐步執行,每個步驟都會存在一些中間結果,稱之爲虛表 VT (虛表指的是一個邏輯上存在的數據集合,在實際上不一定存在)。

  1. FROM | JOIN : 獲取 FROM 指定的表(或分區表)。如果爲聯表查詢,將對相關表進行聯表計算。產生虛表 VT₁ 。

  2. WHERE : 對虛表 VT₁ 根據 where_condition 進行過濾,過濾後的結果爲虛表 VT₂ 。

  3. GROUP BY & WINDOW : 根據 GROUP BY 和 WINDOW 的子句,對 VT₂ 進行聚合統計計算,得到的結果爲虛表 VT₃ 。

  4. WITH : 對 VT₃ 進行相關的 CUBE 或是 ROLLUP 操作。

  5. HAVING : 對 VT₃ 根據 having_condition 進行過濾,過濾後的結果爲虛表 VT₄ 。

  6. SELECT : 執行 SELECT 操作,根據 select_expr 對 VT₄ 選擇列,根據結果產生虛表 VT₅ 。

  7. ALL | DISTINCT | DISTINCTROW :對 VT₅ 進行過濾操作,ALL 可以理解爲一個空操作,即什麼也不做。DISTINCT 和 DISTINCTROW 將會進行去重操作。產生表虛表 VT₆ 。

  8. ORDER BY : 根據 ORDER BY 子句給出的信息對 VT₆ 進行排序,產生虛表 VT₇ 。

  9. LIMIT : 根據 LIMIT 子句給出的信息對 VT₇ 取出指定行的記錄,產生虛表 VT₈ ,該結果將會返回給客戶端。

清楚 SQL 的執行順序後,接下來可以看一下在日常查詢使用中,常見的拖慢查詢的 SQL 使用,這些原因可以通過改寫 SQL 來進行優化。

2、聯表查詢

過於複雜的聯表查詢通常是導致查詢效率低下的原因。MySQL 的聯表查詢實現主要還是嵌套循環算法,效率實在不高,所以有很多關於數據庫的優化知識都會限制 JOIN 表的數量,如《阿里巴巴 Java 開發手冊》就禁止超過三個表的 JOIN 。如果確實需要多表的關聯查詢,可以考慮分解關聯查詢,在應用端進行數據的關聯處理。不過分解關聯查詢是否提高了效率還是需要進行比較檢驗。

3、子查詢

在 MySQL 5.6 版本後對子查詢進行了優化,但是優化器的優化始終是有限的,在某些場景下子查詢仍然是會稱爲導致查詢效率低下的一個點。根據 MySQL 官方手冊中的子查詢優化章節,子查詢的優化主要有以下三種方式:

  • Semi-join : 半聯接,即有左表和右表進行聯接,聯接結果只顯示左表的結果而不顯示右表

  • Materialization : 物化,即使用臨時表去存儲子查詢的查詢結果

  • EXISTS strategy : 使用 EXISTS 去代替子查詢

  • Merging : 合併查詢,即合併子查詢與外(父)查詢,針對於派生表的一種優化方式。

根據子查詢的使用方式,能夠使用的優化方案也是不同的。如果使用子查詢作爲查詢條件(即跟在 WHERE 後邊),如 WHERE IN (subquery_expr) 或者是 WHERE NOT IN (subquery_expr) ,如果是 IN (或 = ANY) 的話,Mysql能夠根據實際查詢來選擇除 Merging 之外的三種優化方案,而 NOT IN (或 <> ANY) 只能選擇 MaterializationEXISTS strategy 兩種優化方案。實際上 MySQL 對於子查詢的優化最好方案爲將其轉化爲聯表查詢,所以如果能夠使用 JOIN 則儘量使用 JOIN 。如果爲使用 Materialization 或是 EXISTS strategy 優化方案,子查詢的查詢類型可能爲 SUBQUERY 或者是 DEPENDENT SUBQUERY ,這是一種性能不好的查詢方式。這時候就要去優化掉子查詢的使用。

如果子查詢跟在 FROM 後面,即子查詢爲派生表,能夠使用 Materialization 或是 Merging 優化方案優化。使用 Materialization 優化方案很好理解,將子查詢的結果存儲到臨時表中,將該臨時表作爲被查詢表。而 Merging 即是將子查詢提上一級,成爲外(父)查詢。

當然也不需要將子查詢視爲洪水猛獸,子查詢比起聯表查詢具有更好的可讀性,在修改維護 SQL 時更加友好,而且在特定場景下可以作爲一個優化的手段使用。

一般的子查詢使用,常爲 WHERE IN 或是派生表的使用。一般情況優化器下會幫我們轉爲聯表查詢以提高效率,兼備了可讀性與效率。而在一定場景下,派生表還可以提升查詢的效率。可以通過子查詢派生表實現“延遲關聯”,在查詢時,先通過子查詢和覆蓋索引快速查詢構建出一個數據量較小的派生表,然後派生表再去與實際要查詢的表做關聯操作,可以使整體的查詢執行速度會有所提升(當然並不總是這樣,還需要通過實際場景和構建派生表的子查詢做實際的分析、實踐,因爲派生表也是有成本的)。

覆蓋索引(covering index)指一個查詢語句的執行只用從索引中就能夠取得,不必從數據表中讀取也可以稱之爲實現了索引覆蓋。如果一個索引包含了(或覆蓋了)滿足查詢語句中字段與條件的數據就叫做覆蓋索引。

如果慢查詢中存在子查詢,不要想當然的去優化掉子查詢,使用 EXPLAIN 確認清楚執行情況,如果子查詢爲 DEPENDENT SUBQUERY ,那麼問題確實就出在子查詢上(如果 SQL 語句爲非 SELECT 的子查詢,總是 DEPENDENT SUBQUERY)。而如果問題沒有出在子查詢上,那就是查詢本身的複雜導致的,這時同樣應該考慮分解關聯查詢。

4、分頁

在範圍檢索數據時,沒有分頁是可怕的,如果數據量龐大不僅會使數據庫查詢很慢,還會大量消耗應用端的內存,影響到應用端的運行效率,嚴重的還會使應用掛掉(真事兒,當時阿里雲Mysql應用飆的老高了)。所以在對數據庫進行範圍查詢時,進行分頁是很有必要的。

分頁的實現基於 LIMIT 關鍵字的使用。LIMIT 的使用方式爲 LIMIT offset num ,每次從 offset + 1 條記錄開始獲取 num 條記錄。而當 offset 非常大時,就有可能影響到查詢性能。因爲 LIMIT 每次都需要查找獲取到 offset + num 條記錄,然後再進行記錄的截取。

好在一般情況下,也沒有需要翻到一千頁,一萬頁以後,但若是硬要說有這大分頁的需求,同樣可以利用覆蓋索引優化,即利用索引查詢並且返回符合條件的列,這樣可以提升大分頁的查詢效率。查詢結果默認將以主鍵排序,這時使用的是主鍵索引:先只 SELECT 主鍵列並且分頁,獲取到的主鍵值是通過覆蓋索引獲取的,再利用查詢獲取到的主鍵進行回表查詢。如下:

SELECT * FROM t1 JOIN (SELECT id FROM t1 ORDER BY id LIMIT 100000,10) AS temp USING(id);

除此之外,還有一種分頁的方式,可以稱爲遊標翻頁。在一些網站或者 APP 中,我們有時候會發現沒有頁數可以選擇,只有下一頁(不斷下拉)的選項,這大概率就是利用遊標實現的翻頁。假如有表 t1 ,有自增主鍵 id (或是其他非空可排序列),就可以利用 id 作爲遊標進行翻頁。如下:

SELECT * FROM t1 WHERE id > ${cursor} ORDER BY id LIMIT 1000;

之後獲取最大的 id 值,更新遊標 cursor ,再次進行查詢即可,這樣就可以一頁頁的將整個數據表遍歷,如果有遍歷整個數據表的需求,這是一種很好的實現方法。

5、贅述:in、or與union all的性能對比

在開發過程中,總會遇到這種情況,比如項目中存在兩個數據源,一個sql查詢語句中的篩選條件是從另外一個數據庫中查詢獲得,這時就不得不採用以上這3種方式(in/or/union all)中的一種方式。比如最近線上有一張90w+數據的庫表,小明編寫的sql查詢語句在開發環境執行地好好的,一但部署到線上,阿里雲數據庫CPU飆到老高,前端請求後端一次響應時長也不忍直視。下面對這3種sql查詢效率進行對比:

前提:爲了方便查詢,查詢字段索引存在的情況

in、or與union all的性能對比

對於某大表的查詢

使用or:

SELECT * FROM article
WHERE article_category=2
OR article_category=3
// 執行時間:11.0777

使用 in:

SELECT * FROM article
WHERE article_category IN (2,3)
// 執行時間:11.2850

使用union all:

SELECT * FROM article
WHERE article_category=2
UNION ALL
SELECT * FROM article
WHERE article_category=3
// 執行時間:0.0261

顯而易見,性能上union all的方式完勝, 以上主要針對的是單表,而多表聯合查詢來說,考慮的地方就比較多了,比如連接方式,查詢表數據量分佈、索引等,再結合單表的策略選擇合適的關鍵字。 但不管哪種情況都不要迷信union all 就比 or及in 快,要結合實際情況分析到底使用哪種情況。

今日段子

互聯網常用口頭禪

產品:沒排上期

技術:實現不了

設計:別戳屏幕

公關:建議別做

品牌:預算多少

財務:還沒到你

採購:按流程走

行政:你問問那誰

數據:你們要這個幹啥

測試:醒醒,有bug!

策劃:客戶那邊不接受

客服:剛碰到一XX

法務:有風險

文案/運營:

好的收到

辛苦辛苦

求求你了

謝謝爸爸

感恩(跪下)

死鬼~

640?wx_fmt=png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章