《高性能MySQL》- 查詢性能優化

二、查詢性能優化

2.1 優化數據訪問

2.1.1 只查詢需要的列

2.1.2 只查詢需要的行

  1. 響應時間

  2. 掃描行數和返回的行數

  3. 掃描行數和訪問類型

    如果掃描行數遠遠大於返回行數,優化方法:

    • 使用覆蓋索引
    • 改變表結構。使用匯總表
    • 重寫複雜SQL

2.2 重構查詢方式

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

  • 連表數據重複很多時,減少冗餘記錄查詢
  • 可以使用緩存
  • 可以使用異步查詢
  • 可以支持應用層分庫分表

2.2.2 切分查詢

使用分治思想,切分大查詢爲小查詢,然後歸併。在DML語句可以減少長事務對連接的持有時間,減少鎖衝突

2.2.3 分解關聯查詢

  • 連表數據重複很多時,減少冗餘記錄查詢
  • 可以使用緩存
  • 可以使用異步查詢
  • 可以支持應用層分庫分表
  • 可能提升性能。比如IN按ID順序查詢,比連表隨機查找更快
  • 相當於使用了哈希索引,而不是嵌套循環查詢

2.3 查詢優化器侷限

循環優化器不是每次都是最優結果。

2.3.1 關聯子查詢優化

  • 一般建議使用左外連接來替代子查詢
  • 當返回結果只有一個表的某些列時,關聯子查詢會更好

不過每個具體的案例會各有不同,有時候子查詢寫法也會快些。例如,當返回結果中只有一個表中的某些列的時候。聽起來,這種情況對於關聯查詢效率也會很好。具體情況具體分析,例如下面的關聯,我們希望返回所有包含同一個演員參演的電影,因爲一個電影會有很多演員參演,所以可能會返回一些重複的記錄:

mysql>SELECT film.film_id FROM sakila.film
->		INNER JOIN sakila.film_actor USING(film_id);

我們需要使用DISTINCT和GROUP BY來移除重複的記錄:

mysql>SELECT DISTINCT film.film_id FROM sakila.film
->		INNER JOIN sakila.film_actor USING(fi1m_id);

但是,回頭看看這個查詢,到底這個查詢返回的結果集意義是什麼?至少這樣的寫法會讓 SQL 的意義很不明顯。如果使用EXISTS則很容易表達“包含同一個參演演員”的邏輯,而且不需要使用DISTINCT和 GROUP BY,也不會產生重複的結果集,我們知道一旦使用了DISTINCT和GROUP BY,那麼在查詢的執行過程中,通常需要產生臨時中間表。下面我們用子查詢的寫法替換上面的關聯:

mysql> SELECT film_id FROM sakila.film

-> 	WHERE EXISTS(SELECT * FROM sakila.film_actor
->	WHERE film.film_id = film_actor.fi1m_id);

EXISTS和關聯性能對比查詢

查詢 每秒查詢數結果(QPS)
INNER J0IN 185 QPS
EXISTS子查詢 325 QPS

2.3.2 UNION的限制

有時, MySQL無法將限制條件從外層“下推”到內存。

比如UNION各個子句只取部分結果

(SELECT 
 	first_name, last_name 
FROM 
	sakila.actor
ORDER BY 
	last_name)
UNION ALL
(SELECT 
	first_name, last_name
FROM 
 	sakila.customer
ORDER BY last_name)
LIMIT 20;

	

可以優化爲

(SELECT 
 	first_name, last_name 
FROM 
	sakila.actor
ORDER BY 
	last_name
LIMIT 20)
UNION ALL
(SELECT 
	first_name, last_name
FROM 
 	sakila.customer
ORDER BY last_name
LIMIT 20)
LIMIT 20;

2.3.3 索引合併優化

當WHERE子句包含多個複雜條件的時候,MySQL能夠訪問單個表的多個索引以合併和交叉過濾的方式來丁誒需要查找的行。

2.3.4 等值傳遞

2.3.5 並行執行

MySQL無法使用多核特性來並行執行查詢。

可以在應用層使用分治歸併來實現

2.3. 哈希關聯

2.3.7 鬆散索引掃描

MySQL不支持鬆散索引掃描,無法按照不連續的方式掃描一個索引

鬆散索引掃描

2.3.8 最大值和最小值優化

對於MIN()和MAX()查詢,MySQL的優化有時候不生效。

mysq1>SELECT MIN(actor_id) FROM sakila.actor NHERE first_name = 'PENELOPE';

應該溢出MIN()使用LIMIT來重寫

mysql>SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY)
->WHERE first_name = 'PENELOPE’ LIMIT 1;

2.3.9 在同一個表上查詢和更新

MySQL不支持對同一張表上同時進行查詢和更新。

mysql>UPDATE tb1 AS outer_tbl
->		SET cnt = (
->			SELECT count(*) FROMtb1 AS inner_tbl 
    		WHERE inner_tbi.type = outer_tbl.type
-);
ERROR 1093'(HYooo): You can't specify target table 'outer_tbl' for update in FROM clause

改寫成


mysql>UPDATE tbl
->	INNER JOIN(
->		SELECT type,count(*) AS cnt
->		FROM tbl
->		GROUP BY type
->	) As der USING(type)
->	SET tbl.cnt = der.cnt;

2.4 查詢優化器的提示(hint)

如果對優化器選擇的執行計劃不滿意,可以使用優化器提供的幾個提示(hint)來控制最終的執行計劃

2.4.1 HIGH_PRIORITY和LOW_PRIORITY

這個提示告訴MySQL,當多個語句同時訪問某一個表的時候,哪些語句的優先級相對高些、哪些語句的優先級相對低些。

2.4.2 DELAYED

這個提示對INSERTREPLACE有效。

MySQL會將使用該提示的語句立即返回給客戶端,並將插入的行數據放入到緩衝區,然後在表空閒時批量將數據寫入。日誌系統使用這樣的提示非常有效,或者是其他需要寫人大量數據但是客戶端卻不需要等待單條語句完成I/O的應用。

這個用法有一些限制:並不是所有的存儲引擎都支持這樣的做法,並且該提示會導致函數LAST_INSERT_ID()無法正常工作。

2.4.3 STRAIGHT_JOIN

這個提示可以放置在SELECT語句的SELECT關鍵字之後,也可以放置在任何兩個關聯表的名字之間。

第一個用法是讓查詢中所有的表按照在語句中出現的順序進行關聯。

第二個用法則是固定其前後兩個表的關聯順序。

2.4.4 SQL_SMALL_RESULT和SQL_BIG_RESULT

這兩個提示只對SELECT語句有效。它們告訴優化器對GROUPBY或者DISTINCT查詢如何使用臨時表及排序。SQL_SMALL_RESULT告訴優化器結果集會很小,可以將結果集放在內存中的索引臨時表,以避免排序操作。如果是SQL_BIG_RESULT,則告訴優化器結果集可能會非常大,建議使用磁盤臨時表做排序操作。

2.4.5 SQL_BUFFER_RESULT

這個提示告訴優化器將查詢結果放入到一個臨時表,然後儘可能快地釋放表鎖。這和前面提到的由客戶端緩存結果不同。當你沒法使用客戶端緩存的時候,使用服務器端的緩存通常很有效。帶來的好處是無須在客戶端上消耗太多的內存,還可以儘可能快地釋放對應的表鎖。代價是,服務器端將需要更多的內存。

2.4.6 SQL_CACHE和SQL_NO_CACHE

這個提示告訴MySQL這個結果集是否應該緩存在查詢緩存中,下一章我們將詳細介紹如何使用。

2.4.7 SQL_CALC_FOUND_ROWS

嚴格來說,這並不是一個優化器提示。它不會告訴優化器任何關於執行計劃的東西。它會讓MySQL返回的結果集包含更多的信息。查詢中加上該提示 MySQL會計算除去LIMIT子句後這個查詢要返回的結果集的總數,而實際上只返回LIMIT要求的結果集。可以通過函數FOUND_ROW()獲得這個值。(參閱後面的“SQL_CALC_FOUND_ROWS優化”部分,瞭解下爲什麼不應該使用該提示。)

2.4.8 USE INDEX、IGNORE INDEX和FORCE INDEX

這幾個提示會告訴優化器使用或者不使用哪些索引來查詢記錄(例如,在決定關聯·順序的時候使用哪個索引)。在MySQL 5.0和更早的版本,這些提示並不會影響到優化器選擇哪個索引進行排序和分組,在MyQL 5.1和之後的版本可以通過新增選項FOR ORDER BYFOR GROUP BY來指定是否對排序和分組有效。 FORCE INDEXUSE INDEX基本相同,除了一點:FORCE INDEX會告訴優化器全表掃描的成本會遠遠高於索引掃描,哪怕實際上該索引用處不大。當發現優化器選擇了錯誤的索引,或者因爲某些原因(比如在不使用ORDER BY的時候希望結果有序)要使用另一個索引時,可以使用該提示。在前面關於如何使用LIMIT高效地獲取最小值的案例中,已經演示過這種用法。

2.5 優化特定類型的查詢

2.5.1 優化COUNT()查詢

COUNT()是一個特殊的函數,有兩種非常不同的作用:它可以統計某個列值的數量,也可以統計行數。在統計列值時要求列值是非空的(不統計NULL)。

一個容易產生的誤解就是:MyISAMCOUNT()函數總是非常快,不過這是有前提條件的,即只有沒有任何wHERE條件的COUNT(*)才非常快,因爲此時無須實際地去計算表的行數。

  • 使用近似值:有時候某些業務場景並不要求完全精確的COUNT值,此時可以用近似值來代替。
  • 更復雜的優化:通常來說,COUNT()都需要掃描大量的行(意味着要訪問大量數據)才能獲得精確的結果,因此是很難優化的。除了前面的方法,在MySQL層面還能做的就只有索引覆蓋掃描了。如果這還不夠,就需要考慮修改應用的架構,可以增加彙總表

2.5.2 優化關聯查詢

  • 確保ON或者USING子句中的列上有索引。
  • 確保任何的GROUP BY和ORDER BY中的表達式只涉及到一個表中的列,這樣MySQL纔有可能使用索引來優化這個過程。

2.5.3 優化子查詢

關於子查詢優化我們給出的最重要的優化建議就是儘可能使用關聯查詢代替

2.5.4 優化GROUP BY和 DISTINCT

在很多場景下,MySQL都使用同樣的辦法優化這兩種查詢,事實上,MySQL優化器會在內部處理的時候相互轉化這兩類查詢。

如果需要對關聯查詢做分組(GROUP BY),並且是按照查找表中的某個列進行分組,那麼通常採用查找表的標識列分組的效率會比其他列更高。

如果沒有通過ORDER BY子句顯式地指定排序列,當查詢使用GROUP BY子句的時候,結果集會自動按照分組的字段進行排序。如果不關心結果集的順序,而這種默認排序又導致了需要文件排序,則可以使用ORDER BY NULL,讓 MySQL不再進行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC關鍵字,使分組的結果集按需要的方向排序。

2.5.5 優化LIMIT分頁

優化此類分頁查詢的一個最簡單的辦法就是儘可能地使用索引覆蓋掃描,而不是查詢所有的列。然後根據需要做一次關聯操作再返回所需的列。對於偏移量很大的時候,這樣做的效率會提升非常大。考慮下面的查詢:

mysq1> SELECT film_id,description FRON sakila.film ORDER BY title LIMIT 50, 5

優化爲

mysql>SELECT film.film_id,film.description
-		FROM sakila.film
->		INNER JOIN (
->			SELECT film_id FROMsakila.film
->			ORDER BY title LIHIT 50,5
->			) AS lim USING(film_id);

mysql> SELECT * FROM sakila.rental
->		ORDER BY rental_id DESC LIMIT 20;

改寫成

mysql> SELECT * FROM sakila.rental
-		WHERE rental_id <16030.
-		ORDER BY rental_id DESC LIMIT 20;

2.5.6 優化SQL_CALC_FOUND_ROWS

分頁的時候,另一個常用的技巧是在LIMIT語句中加上 SQL_CALC_FOUND_RONS提示(hint),這樣就可以獲得去掉LIMIT以後滿足條件的行數,因此可以作爲分頁的總數。看起來,MySQL做了一些非常“高深”的優化,像是通過某種方法預測了總行數。但實際上,MySQL只有在掃描了所有滿足條件的行以後,纔會知道行數,所以加上這個提示以後,不管是否需要,MySQL都會掃描所有滿足條件的行,然後再拋棄掉不需要的行,而不是在滿足LIMIT的行數後就終止掃描。所以該提示的代價可能非常高。

  • 一個更好的設計是將具體的頁數換成“下一頁”按鈕,假設每頁顯示20條記錄,那麼我們每次查詢時都是用LIMIT返回21條記錄並只顯示20條,如果第21條存在,那麼我們就顯示“下一頁”按鈕,否則就說明沒有更多的數據,也就無須顯示“下一頁”按鈕了。

  • 有時候也可以考慮使用EXPLAIN的結果中的rows列的值來作爲結果集總數的近似值.

2.5.7 優化UNION查詢

MySQL總是通過創建並填充臨時表的方式來執行UNION查詢。因此很多優化策略在UNION查詢中都沒法很好地使用。經常需要手工地將wHERE、LIMIT、ORDER BY等子句“下推”到UNION的各個子查詢中,以便優化器可以充分利用這些條件進行優化(例如,直接將這些子句冗餘地寫一份到各個子查詢)。

除非確實需要服務器消除重複的行,否則就一定要使用UNION ALL。如果沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會導致對整個臨時表的數據做唯一性檢查。這樣做的代價非常高。即使有ALL關鍵字,MySQL仍然會使用臨時表存儲結果。事實上,MySQL總是將結果放入臨時表,然後再讀出,再返回給客戶端。

2.5.8 使用用戶自定義變量

用戶自定義變量是一個用來存儲內容的臨時容器,在連接MySQL 的整個過程中都存在。

我們不能使用用戶自定義變量:

  • 使用自定義變量的查詢,無法使用查詢緩存。
  • 不能在使用常量或者標識符的地方使用自定義變量,例如表名.列名和LIMIT子句中。用戶自定義變量的生命週期是在一個連接中有效,所以不能用它們來做連接間的通信。
  • 如果使用連接池或者持久化連接,自定義變量可能讓看起來毫無關係的代碼發生交互(如果是這樣,通常是代碼bug或者連接池bug,這類情況確實可能發生)。在5.0之前的版本,是大小寫敏感的,所以要注意代碼在不同MySQL版本間的兼容性問題。
  • 不能顯式地聲明自定義變量的類型。確定未定義變量的具體類型的時機在不同MySQL版本中也可能不一樣。如果你希望變量是整數類型,那麼最好在初始化的時候就賦值爲0,如果希望是浮點型則賦值爲0.0,如果希望是字符串則賦值爲",用戶自定義變量的類型在賦值的時候會改變。MySQL的用戶自定義變量是一個動態類型。
  • MySQL優化器在某些場景下可能會將這些變量優化掉,這可能導致代碼不按預想的方式運行。
  • 賦值的順序和賦值的時間點並不總是固定的,這依賴於優化器的決定。實際情況可能很讓人困惑,後面我們將看到這一點。
  • 賦值符號:=的優先級非常低,所以需要注意,賦值表達式應該使用明確的括號。
  • 使用未定義變量不會產生任何語法錯誤,如果沒有意識到這一點,非常容易犯錯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章