1. 程式人生 > 其它 >優化SQL 查詢效能

優化SQL 查詢效能

為什麼查詢會很慢

如果把查詢看作是一個任務,那麼它由一系列子任務組成,每個子任務都會消耗一定的時間。要優化查詢,實際上是要優化其子任務,要麼消除其中一些子任務,要麼減少子任務的執行次數,要麼讓子任務執行得更快。
通常來說,查詢的生命週期大致可以按照順序來看:從客戶端,到伺服器,然後在伺服器上進行解析,生成執行計劃,執行,並返回結果給客戶端。“執行”可以認為是整個生命週期中最重要的階段,這其中包括了大量為了檢索資料到儲存引擎的呼叫以及呼叫後的資料處理,包括排序、分組等
在完成這些任務的時候,查詢需要在不同的地方花費時間,包括網路,CPU計算,生成統計資訊和執行計劃、鎖等待(互斥等待)等操作,尤其是向底層儲存引擎檢索資料的呼叫操作,這些呼叫需要在記憶體操作、CPU操作和記憶體不足時導致的I/O操作上消耗時間。根據儲存引擎不同,可能還會產生大量的上下文切換以及系統呼叫。

優化資料訪問

大部分效能低下的查詢都可以通過減少訪問資料量的方式進行優化。對於低效的查詢,我們發現通過下面兩個步驟來分析總是很有效:

  • 確認應用程式是否在檢索大量超過需要的資料。這通常意味著訪問了太多的行,但有時候也可能是訪問了太多的列。
  • 確認MySQL伺服器層是否在分析大量超過需要的資料行。

是否向資料庫請求了不需要的資料

有些查詢會請求超過實際需要的資料,然後這些多餘的資料會被應用程式丟棄。這會給MySQL伺服器帶來額外的負擔,並增加網路開銷,另外也會消耗應用伺服器的CPU和記憶體資源。

  • 查詢不需要的記錄,如獲取前一百條記錄:select * from sales_order where region_no = 86; 這種情況MySQL會返回全部結果集,造成資源浪費,可在查詢之後加上Limit。
  • 單/多表關聯查詢時,只獲取需要的列,避免使用select *
  • 對於查詢頻率高的資料,可以將其快取起來

MySQL是否在掃描額外的記錄

在確定查詢只返回需要的資料以後,接下來應該看看查詢為了返回結果是否掃描了過多的資料,最簡單的兩個指標為:掃描行數、返回行數。但對於找出那些“糟糕”的查詢,這兩個指標可能還不夠完美,因為並不是所有的行的訪問代價都是相同的。較短的行的訪問速度更快,記憶體中的行也比磁碟中的行的訪問速度要快得多。
掃描的行數和訪問型別
在EXPLAIN 語句中的type 列反應了訪問型別。訪問型別有很多種,從全表掃描到索引掃描、範圍掃描、唯一索引查詢、常數引用等。這裡列的這些,速度是從慢到快,掃描的行數也是從小到大。如果查詢沒有辦法找到合適的訪問型別,那麼解決的最好辦法通常就是增加一個合適的索引

一般MySQL能夠使用如下三種方式應用WHERE 條件,從好到壞依次為:

  • 在索引中使用WHERE 條件來過濾不匹配的記錄。這是在儲存引擎層完成的。
  • 使用索引覆蓋掃描(在Extra 列中出現了Using index)來返回記錄,直接從索引中過濾不需要的記錄並返回命中的結果。這是在MySQL伺服器層完成的,但無須再回表查詢記錄。
  • 從資料表中返回資料,然後過濾不滿足條件的記錄(在Extra 列中出現Using Where)。這在MySQL伺服器層完成,MySQL需要先從資料表讀出記錄然後過濾。

在使用聚合函式時,MySQL 需要掃描的行與實際返回的行數往往差異很大,對於這種掃描大量資料但只返回少數的行,通常可以嘗試使用下面的技巧來優化:

  • 使用索引覆蓋掃描,無需回表
  • 改變庫表結構,如使用單獨的彙總表
  • 重寫複雜查詢,讓Mysql能夠以更優化的方式執行查詢

重構查詢方式

有時候,可以將查詢轉換一種寫法讓其返回一樣的結果,但是效能更好;也可以通過修改應用程式碼,用另一種方式完成查詢。

一個複雜查詢還是多個簡單查詢

MySQL內部每秒能夠掃描記憶體中上百萬行資料,相比之下,MySQL響應資料給客戶端就慢得多了。在其他條件都相同的時候,使用盡可能少的查詢是更好的。但隨著網路頻寬的提升,延遲的下降,Mysql在應對多個小查詢也不是什麼問題了,在必要時,將一個大查詢拆分為多個小查詢也是能接受的。

切分查詢

有時候對於一個大查詢我們需要“分而治之”,將大查詢切分成小查詢。刪除舊的資料就是一個很好的例子。定期地清除大量資料時,如果用一個大的語句一次性完成的話,則可能需要一次鎖住很多資料、佔滿整個事務日誌、耗盡系統資源、阻塞很多小的但重要的查詢。將一個大的DELETE語句切分成多個較小的查詢可以儘可能小地影響MySQL效能,同時還可以減少MySQL複製的延遲。如每個月執行一次的查詢:
delete from warn_log where entry_datetime < DATE_SUB(NOW(),INTERVAL 3 MONTH)
可使用以下方式來完成相同的工作:對於事務型儲存引擎,很多時候小事務更加高效,將原本一次性刪除的資料分擔到多次來刪除,也減輕了伺服器的壓力。

rows_affected = 0
 do {
 rows_affected = do_query(
 "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)
 LIMIT 10000")
 } while rows_affected > 0 

分解關聯查詢

很多高效能的應用都會對關聯查詢進行分解。簡單地,可以對每一個表進行一次單表查詢,然後將結果在應用程式中進行關聯。例如下面這個查詢:

乍一看,這樣做並沒有什麼好處,原本一條查詢,這裡卻變成多條查詢,返回的結果又是一模一樣的。事實上,用分解關聯查詢的方式重構查詢有如下的優勢:

  • 讓快取的效率更高。許多應用程式可以方便地快取單表查詢對應的結果物件。
  • 將查詢分解後,執行單個查詢可以減少鎖的競爭。
  • 在應用層做關聯,可以更容易對資料庫進行拆分,更容易做到高效能和高擴充套件
  • 可以減少冗餘記錄的查詢。在應用層做關聯查詢,意味著對於某條記錄應用只需要查詢一次,而在資料庫中做關聯查詢,則可能需要重複地訪問一部分資料。

在很多場景下,通過重構查詢將關聯放到應用程式中將會更加高效,比如:當應用能夠方便地快取單個查詢的結果的時候、當可以將資料分佈到不同的MySQL伺服器上的時候、當能夠使用IN() 的方式代替關聯查詢的時候、當查詢中使用同一個資料表的時候。