1. 無索引、索引失效導致慢查詢
1.1 無索引、索引失效導致慢查詢
如果在一張幾千萬數據的表中以一個沒有索引的列作爲查詢條件,大部分情況下查詢會非常耗時,這種查詢毫無疑問是一個慢 SQL 查詢。所以對於大數據量的查詢,我們需要建立適合的索引來優化查詢。雖然我們很多時候建立了索引,但在一些特定的場景下,索引還有可能會失效,所以索引失效也是導致慢查詢的主要原因之一。
1.2 鎖等待
我們常用的存儲引擎有 InnoDB 和 MyISAM,前者支持行鎖和表鎖,後者只支持表鎖。
如果數據庫操作是基於表鎖實現的,試想下,如果一張訂單表在更新時,需要鎖住整張表,那麼其它大量數據庫操作(包括查詢)都將處於等待狀態,這將嚴重影響到系統的併發性能。
這時,InnoDB 存儲引擎支持的行鎖更適合高併發場景。但在使用 InnoDB 存儲引擎時,我們要特別注意行鎖升級爲表鎖的可能。在批量更新操作時,行鎖就很可能會升級爲表鎖。
MySQL 認爲如果對一張表使用大量行鎖,會導致事務執行效率下降,從而可能造成其它事務長時間鎖等待和更多的鎖衝突問題發生,致使性能嚴重下降,所以 MySQL 會將行鎖升級爲表鎖。還有,行鎖是基於索引加的鎖,如果我們在更新操作時,條件索引失效,那麼行鎖也會升級爲表鎖。
因此,基於表鎖的數據庫操作,會導致 SQL 阻塞等待,從而影響執行速度。在一些更新操作(insert\update\delete)大於或等於讀操作的情況下,MySQL 不建議使用 MyISAM 存儲引擎。
除了鎖升級之外,行鎖相對錶鎖來說,雖然粒度更細,併發能力提升了,但也帶來了新的問題,那就是死鎖。因此,在使用行鎖時,我們要注意避免死鎖。
1.3 不恰當的sql語句
習慣使用<select *> ,<select count(*)>語句,在大數據表中使用<limit m,n>分頁查詢,以及對非索引字段進行排序等等。
2. 優化sql語句的步驟
通常,我們在執行一條 SQL 語句時,要想知道這個 SQL 先後查詢了哪些表,是否使用了索引,這些數據從哪裏獲取到,獲取到數據遍歷了多少行數據等等,我們可以通過 EXPLAIN 命令來查看這些執行信息。這些執行信息被統稱爲執行計劃。
2.1 通過explain分析sql執行計劃
假設現在我們使用 EXPLAIN 命令查看當前 SQL 是否使用了索引,先通過 SQL EXPLAIN 導出相應的執行計劃如下:
下面對圖示中的每一個字段進行一個說明,從中你也能收穫到很多零散的知識點。
2.2 通過 Show Profile 分析 SQL 執行性能
上述通過 EXPLAIN 分析執行計劃,僅僅是停留在分析 SQL 的外部的執行情況,如果我們想要深入到 MySQL 內核中,從執行線程的狀態和時間來分析的話,這個時候我們就可以選擇 Profile。
Profile 除了可以分析執行線程的狀態和時間,還支持進一步選擇 ALL、CPU、MEMORY、BLOCK IO、CONTEXT SWITCHES 等類型來查詢 SQL 語句在不同系統資源上所消耗的時間。
3. 常用的 SQL 優化
3.1 優化分頁查詢
通常我們是使用<limit m,n> + 合適的 order by 來實現分頁查詢,這種實現方式在沒有任何索引條件支持的情況下,需要做大量的文件排序操作(file sort),性能將會非常得糟糕。如果有對應的索引,通常剛開始的分頁查詢效率會比較理想,但越往後,分頁查詢的性能就越差。
這是因爲我們在使用 LIMIT 的時候,偏移量 M 在分頁越靠後的時候,值就越大,數據庫檢索的數據也就越多。例如 LIMIT 10000,10 這樣的查詢,數據庫需要查詢 10010 條記錄,最後返回 10 條記錄。也就是說將會有 10000 條記錄被查詢出來沒有被使用到。
我們模擬一張 10 萬數量級的 order 表,進行以下分頁查詢:
通過 EXPLAIN 分析可知:該查詢使用到了索引,掃描行數爲 10020 行,但所用查詢時間爲 0.018s,相對來說時間偏長了。
利用子查詢優化分頁查詢
以上分頁查詢的問題在於,我們查詢獲取的 10020 行數據結果都返回給我們了,我們能否先查詢出所需要的 20 行數據中的最小 ID 值,然後通過偏移量返回所需要的 20 行數據給我們呢?我們可以通過索引覆蓋掃描,使用子查詢的方式來實現分頁查詢:
select *
from `demo`.`order`
where id> (
select id from
`demo`.`order`
order by order_no limit 10000, 1
) limit 20;
通過 EXPLAIN 分析可知:子查詢遍歷索引的範圍跟上一個查詢差不多,而主查詢掃描了更多的行數,但執行時間卻減少了,只有 0.004s。這就是因爲返回行數只有 20 行了,執行效率得到了明顯的提升。
3.2 優化 SELECT COUNT(*)
COUNT() 是一個聚合函數,主要用來統計行數,有時候也用來統計某一列的行數量(不統計 NULL 值的行)。我們平時最常用的就是 COUNT(*) 和 COUNT(1) 這兩種方式了,其實兩者沒有明顯的區別,在擁有主鍵的情況下,它們都是利用主鍵列實現了行數的統計。
但 COUNT() 函數在 MyISAM 和 InnoDB 存儲引擎所執行的原理是不一樣的,通常在沒有任何查詢條件下的 COUNT(*),MyISAM 的查詢速度要明顯快於 InnoDB。
這是因爲 MyISAM 存儲引擎記錄的是整個表的行數,在 COUNT(*) 查詢操作時無需遍歷表計算,直接獲取該值即可。而在 InnoDB 存儲引擎中就需要掃描表來統計具體的行數。而當帶上 where 條件語句之後,MyISAM 跟 InnoDB 就沒有區別了,它們都需要掃描表來進行行數的統計。
如果對一張大表經常做 SELECT COUNT(*) 操作,這肯定是不明智的。那麼我們該如何對大表的 COUNT() 進行優化呢?
使用近似值
有時候某些業務場景並不需要返回一個精確的 COUNT 值,此時我們可以使用近似值來代替。我們可以使用 EXPLAIN 對錶進行估算,要知道,執行 EXPLAIN 並不會真正去執行查詢,而是返回一個估算的近似值。
增加彙總統計
如果需要一個精確的 COUNT 值,我們可以額外新增一個彙總統計表或者緩存字段來統計需要的 COUNT 值,這種方式在新增和刪除時有一定的成本,但卻可以大大提升 COUNT() 的性能。
3.3 優化 SELECT *
MySQL 常用的存儲引擎有 MyISAM 和 InnoDB,其中 InnoDB 在默認創建主鍵時會創建主鍵索引,而主鍵索引屬於聚簇索引,即在存儲數據時,索引是基於 B + 樹構成的,具體的行數據則存儲在葉子節點。
而 MyISAM 默認創建的主鍵索引、二級索引以及 InnoDB 的二級索引都屬於非聚簇索引,即在存儲數據時,索引是基於 B + 樹構成的,而葉子節點存儲的是主鍵值。
假設我們的訂單表是基於 InnoDB 存儲引擎創建的,且存在 order_no、status 兩列組成的組合索引。此時,我們需要根據訂單號查詢一張訂單表的 status,如果我們使用 select * from order where order_no='xxx’來查詢,則先會查詢組合索引,通過組合索引獲取到主鍵 ID,再通過主鍵 ID 去主鍵索引中獲取對應行所有列的值。
如果我們使用 select order_no, status from order where order_no='xxx’來查詢,則只會查詢組合索引,通過組合索引獲取到對應的 order_no 和 status 的值。