MySQL查詢優化(一):如何分析查詢性能?

查詢優化、索引優化和表設計優化是環環相扣的。如果你有豐富的編寫MySQL查詢語句的經驗,你就會知道如何設計表和索引來支持有效的查詢。同樣的,知曉表設計同樣有助於瞭解表結構如何對查詢語句產生影響。因此,即便表設計和索引都設計得很好,但如果查詢語句寫得很糟糕,那查詢的性能也會很糟糕。

在嘗試編寫快速的查詢語句前,務必記住快速都是基於響應時間進行評估的。查詢語句是一組由多個子任務組成的大任務,每一個子任務都會消耗時間。爲了優化查詢,我們需要儘可能地減少子任務的數量,或者讓子任務執行得更快。
注:有些時候我們也需要考慮查詢對系統其他查詢的影響,在這種情況下,還需要儘可能地減少資源消耗。
_
通常,我們可以認爲查詢的生命週期貫穿於客戶端到服務端的整個交互時序圖中,包括了查詢語句解析、查詢計劃、執行過程和數據返回到客戶端。執行是查詢過程中最爲重要的一環,包括了從存儲引擎獲取數據行而發起的大量調用,以及獲取數據後的處理,例如分組和排序。

當完成所有這些任務後,查詢還會在網絡傳錯、CPU處理、數據統計和策略規劃、等待鎖、從存儲引擎獲取數據行的操作中消耗時間。這些調用會在內存操作、CPU操作和I/O操作中消耗時間。在每一種情況中,如果這些操作被濫用、執行次數過多、或過慢,就會導致額外的時間開銷。查詢優化的目標是避免這些情況——通過消除或減少操作,或者讓操作運行更快。

需要注意的是,我們沒法繪製一個精確的查詢生命週期圖,我們的目的是展示理解查詢生命週期的重要性,並思考這些環節的耗時。有了這個基礎,就能夠着手去優化查詢語句。

慢查詢基礎:優化數據獲取

查詢性能差的最基礎的原因是處理了太多的數據。有些查詢必須從大量數據中進行篩選,這種情況就沒法優化。但這是不太正常的情況。大部分糟糕的查詢可以通過訪問更少的數據進行優化。下面的兩個步驟對分析性能差的查詢十分有用:

  1. 找出應用是不是獲取了你需要之外的數據。通常這意味着應用獲取了太多的數據行或數據列。
  2. 找出MySQL服務器是不是分析了超過需要的行。

檢查是否向數據庫請求了不必要的數據

有些查詢會向數據庫服務器請求所需要的數據,然後將這些數據丟棄。這會增加MySQL服務器的工作、加重網絡負荷、消耗更多內存和應用服務器的CPU資源。下面是一些典型的錯誤:

  1. 獲取不需要的數據行:一個常見的誤區是假設MySQL只提供需要的結果,而不是計算和返回全部的結果集。通常這種錯誤發生在熟悉其他數據庫系統的人身上。這些開發者習慣於使用返回很多行的SELECT語句,然後從中取出前N行,之後不再使用返回的結果集(例如從一個資訊網站獲取最近的100篇文章,然後在前端僅僅展示其中的10條)。他們會認爲MySQL在拿到10行數據後就會停止查詢,而實際MySQL會獲取完整的數據集合。然後,客戶端或獲取全部的數據再將其中的大部分丟棄。最佳的解決方案是在查詢中加上LIMIT條件。
  2. 在一個多表聯合查詢終獲取全部列:如果你需要獲取恐龍時代這部電影的全部演員,不要像下面那樣寫你的SQL語句:
SELECT * FROM sakila.actor
INNER JOIN sakila.file_actor USING(actor_id)
INNER JOIN sakila.file USING (film_id)
WHERE sakila.film.title = 'Academy Dinosaur';

這會返回參與聯合查詢的三張表的全部列。更好的做法是,像下面那樣寫:

SELECT sakila.actor.* FROM sakila.actor
INNER JOIN sakila.file_actor USING(actor_id)
INNER JOIN sakila.file USING (film_id)
WHERE sakila.film.title = 'Academy Dinosaur';
  1. 獲取全部數據列:在你看到SELECT *這樣的查詢時,一定要保持懷疑:真的需要全部的列嗎?很可能不是的。獲取全部的數據列會讓覆蓋索引失效、增加I/O負擔、內存消耗和CPU負荷。有些DBA直接因爲這個禁用SELECT *,並且可以減少人員修改表的列後引發的問題。當然,請求不必要的數據並不總是糟糕。在調查中發現,這種方式可以簡化開發工作,因爲這樣可以提高代碼的複用性。只要你知道這會影響性能,那會是一個正當的理由。同樣的,如果在應用中使用了某些緩存機制,也會提高緩存的命中率。獲取和緩存全部對象可以通過運行多個獲取部分對象的獨立的查詢來處理會更好。
  2. 重複獲取相同數據:如果粗心的話,很容易在應用中編寫獲取相同數據的代碼。例如,如果你要在評論列表中展示用戶個人信息中的頭像,你可能再每一條評論都獲取一次。更有效的方式是第一次獲取後緩存起來直接在評論列表使用。

檢查MySQL是不是處理了過多的數據

一旦確定了查詢語句沒有獲取不必要的數據,就可以查找那些在返回結果前處理過多數據的查詢。在MySQL中,最簡單的查詢消耗標準是:

  1. 響應時間
  2. 處理的數據行數量
  3. 返回的數據行數量

這些標準沒有一個是完美的查詢性能評估手段,但它們大致反映了MySQL執行查詢語句時在內部處理過程中獲取的數據量和查詢運行的速度。這三個標準都在慢查詢日誌中記錄,因此從慢查詢日誌中去發現數據處理過多的查詢是查詢優化的最佳實踐方式。

響應時間
首先,注意查詢響應時間是我們看到的一個表象。實際上,響應時間比我們想象的要更爲複雜。響應時間由兩部分組成:服務時間和隊列時間。服務時間是服務端實際處理查詢的時間。隊列時間是服務端並沒有真正執行查詢的那部分時間——它在等待某些資源,例如I/O操作的完成、行鎖釋放等等。問題在於,你沒法準確將響應時間拆分成這兩部分——除非你能夠單獨測量這兩部分的時間,而這是很難做到的。最常見和最重要的情形是I/O阻塞和等待鎖,但不是百分之百都是這樣。

結果就是,響應時間在不同負荷情況下並不是一成不變的。其他的因素,例如存儲引擎鎖、高併發和硬件都會影響響應時間。因此,當檢查響應時間的時候,首先要決定這個響應時間是不是僅僅是這個查詢引起的。可以通過計算查詢的快速上限估計(QUBE)方法來評估其響應時間:通過檢查查詢計劃和使用的索引,來決定需要的順序和隨機I/O訪問操作,然後乘以機器的硬件執行每次操作的時間來評估。通過將全部的時間求和可以評估查詢響應慢是因爲查詢本身引起的還是其他原因。

處理和返回的數據行數量
在分析查詢語句時,思考處理行的數量十分有用,因爲這樣可以直觀地知道查詢是如何獲取我們所需的數據。然而,這對查找糟糕的查詢並不是完美的測量工具。並不是所有的行訪問都是一致的。更少的行訪問速度更快,而從內存中獲取數據行比在磁盤獲取要快很多。

理想情況下,處理的數據行和返回的數據行是相等的,但是實際上很少會這樣。例如,使用聯合索引構建返回行時,服務端必須從多個行中獲取數據以產生返回的行數據。處理的數據行和返回的數據行的比例通常很小,在1:1到10:1之間,但有時候可能是更大的數量級。

數據行處理和獲取類型

當思考查詢的代價時,可以考慮從數據表獲取單獨一行的代價。MySQL使用多種獲取方法去查找和返回一行數據。有些需要處理多行,而有些則可能不需要檢查直接得到返回結果。

獲取數據的方法在EXPLAIN輸出結果的type列。包括了全表掃描、索引掃描、範圍掃描、唯一索引查找和常量。由於數據讀取量依次減少,因此上述的每一種方法都比它之前的要快。我們不需要記住獲取類型,但需要理解其中的基本概念。

如果沒有好的獲取類型,最佳解決問題的方式是增加一個合適的索引。索引使得MySQL檢查更少的數據,從而更有效地查詢數據行。例如,以下面的簡單查詢爲例:

EXPLAIN SELECT * FROM sakila.film_actor WHERE file_id=1;

這個查詢會返回10行數據,然後EXPLAIN指令顯示了MySQL在idx_fk_film_id索引上使用了ref類型執行查詢語句。

***********************1. row************************
id: 1
select_type: SIMPLE
table:film_actor
type: ref 
possile)keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: const
rows: 10
Extra: 

EXPLAIN指令顯示MySQL估計僅僅需要獲取10行完成查詢。換言之,查詢優化器知道如何選擇獲取類型來讓查詢更有效。如果查詢沒有合適的索引會怎麼樣?MySQL必須使用次優的獲取類型,當刪除掉表索引後再來看結果。

ALTER TABLE sakila.film_actor DROP FOREIGN KEY fk_film_actor_film;
ALTER TABLE sakila.film_actor DROP DROP KEY idx_fk_film_id;
EXPLAIN SELECT * FROM sakila.film_actor WHERE file_id=1;
***********************1. row************************
id: 1
select_type: SIMPLE
table:film_actor
type: ALL 
possile)keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5073
Extra: Using where

如同預期的那樣,獲取類型變成了全表掃描(ALL),MySQL估計需要處理5073行數據才能完成查詢。在Extra列中的Using where顯示MySQL服務器使用了WHERE條件來丟棄存儲引擎讀取的其他不符合條件的數據。通常,MySQL會在下面三種方式中使用WHERE條件,效果依次是從好到差:

  1. 通過索引查找操作去除不匹配的數據行,這發生在存儲引擎層;
  2. 使用覆蓋索引(在Extra列顯示是Using index)去避免數據行訪問,並在獲取到結果後將不符合條件的數據過濾掉。這發生在服務器層,但不需要從數據表讀取數據行。
  3. 從數據表獲取數據行,然後在過濾掉不匹配的數據(在Extra列顯示爲Using where)。這發生在服務器層,並且需要在過濾數據前從數據表讀取數據行。

下面的例子演示了有好的索引的重要性。好的索引有助於使用好的數據獲取類型並且只需要處理所需要的數據行。然而,添加索引並不總是意味着MySQL獲取和返回的數據行是一致的。例如,下面的COUNT()聚合方法。

SELECT actor_id, COUNT(*) FROM sakila.film_actor GROUP BY actor_id;

這個查詢只返回200行,但是在構建返回結果集前需要讀取數千行數據。這種查詢語句,即便有索引也無法減少需要的處理的數據行數。

不幸的是,MySQL並不會告知獲取了多少行來構建返回結果集,它僅僅告知獲取的總行數。很多行通過WHERE條件過濾掉了,而對返回結果集沒有任何作用。在前面的例子中,移除sakila.film_actor索引後,查詢獲取了數據表的全部行,但是隻從中取了10條數據作爲結果集返回。理解服務器獲取的數據行數量和返回的數據行數量有助於理解查詢本身。
如果發現了需要獲取大量數據行而只是在結果使用很少的行,可以通過下面的方式修復這個問題:

  1. 使用覆蓋索引,這使得存儲引擎不需要獲取完整的數據行(直接從索引中獲取)。
  2. 修改查詢表,一個例子是構建彙總表來查詢統計數據。
  3. 重寫複雜的查詢語句,使得MySQL查詢優化器能夠以更優的方式執行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章