国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

《高性能MySQL》——查詢性能優(yōu)化(筆記)

這篇具有很好參考價(jià)值的文章主要介紹了《高性能MySQL》——查詢性能優(yōu)化(筆記)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

六、查詢性能優(yōu)化

6.1 查詢?yōu)槭裁磿?huì)慢

將查詢看作一個(gè)任務(wù),那么它由一系列子任務(wù)組成,實(shí)際我們所做的就是:

  • 消除一些子任務(wù)
  • 減少子任務(wù)的執(zhí)行次數(shù)
  • 讓子任務(wù)運(yùn)行更快

查詢的生命周期大概可分為 = { 客戶端 服務(wù)器 : 進(jìn)行解析 , 生成執(zhí)行計(jì)劃 執(zhí)行:包括到存儲(chǔ)引擎的調(diào)用,以及用后的數(shù)據(jù)處理 { 排序 分組 結(jié)果返回客戶端 查詢的生命周期大概可分為=\left\{ \begin{matrix} 客戶端 \\ 服務(wù)器:進(jìn)行解析,生成執(zhí)行計(jì)劃 \\ 執(zhí)行:包括到存儲(chǔ)引擎的調(diào)用,以及用后的數(shù)據(jù)處理 \left\{ \begin{matrix}排序\\分組\end{matrix} \right.\\ 結(jié)果返回客戶端 \end{matrix} \right. 查詢的生命周期大概可分為=? ? ??客戶端服務(wù)器:進(jìn)行解析,生成執(zhí)行計(jì)劃執(zhí)行:包括到存儲(chǔ)引擎的調(diào)用,以及用后的數(shù)據(jù)處理{排序分組?結(jié)果返回客戶端?

6.2 慢查詢基礎(chǔ):優(yōu)化數(shù)據(jù)訪問(wèn)

查詢性能低下最基本的原因是 訪問(wèn)的數(shù)據(jù)太多 。

某些查詢可能不可避免地需要篩選大量數(shù)據(jù),但這并不常見(jiàn)。大部分性能低下的查詢都可以通過(guò)減少訪問(wèn)的數(shù)據(jù)量的方式進(jìn)行優(yōu)化。

  1. 確認(rèn)應(yīng)用程序是否在檢索大量超過(guò)需要的數(shù)據(jù)。這通常意味著訪問(wèn)了太多的行,但有時(shí)候也可能是訪問(wèn)了太多的列。
  2. 確認(rèn)MySQL服務(wù)器層是否在分析大量超過(guò)需要的數(shù)據(jù)行。

6.2.1 是否向數(shù)據(jù)庫(kù)請(qǐng)求了不需要的數(shù)據(jù)

查詢不需要的記錄

一個(gè)常見(jiàn)的錯(cuò)誤是常常會(huì)誤以為MySQL會(huì)只返回需要的數(shù)據(jù),實(shí)際上MySQL卻是先返回全部結(jié)果集再進(jìn)行計(jì)算。

一些開(kāi)發(fā)者習(xí)慣使用這樣的技術(shù),先使用SELECT語(yǔ)句查詢大量的結(jié)果,然后獲取前面的N行后關(guān)閉結(jié)果集(例如在新聞網(wǎng)站中取出100條記錄,但是只是在頁(yè)面上顯示前面10條)。他們認(rèn)為MySQL會(huì)執(zhí)行查詢,并只返回他們需要的10條數(shù)據(jù),然后停止查詢。

實(shí)際情況是MySQL會(huì)查詢出全部的結(jié)果集,客戶端的應(yīng)用程序會(huì)接收全部的結(jié)果集數(shù)據(jù),然后拋棄其中大部分?jǐn)?shù)據(jù)。最簡(jiǎn)單有效的解決方法就是在這樣的查詢后面加上LIMIT。

多表關(guān)聯(lián)時(shí)返回全部列

如果你想查詢所有在電影Academy Dinosaur中出現(xiàn)的演員,千萬(wàn)不要按下面的寫(xiě)法編寫(xiě)查詢:

mysql> SELECT * FROM sakila.actor
-> INNER JOIN sakila.film_actor USING(actor_id)
-> INNER JOIN sakila.film USING(film_id)
-> WHERE sakila.film.title = 'Academy Dinosaur';

這將返回這三個(gè)表的全部數(shù)據(jù)列。

正確的方式應(yīng)該是像下面這樣只取需要的列:

mysql> SELECT sakila.actor.* FROM sakila.actor...;
總是取出全部列

每次看到SELECT*的時(shí)候都需要用懷疑的眼光審視,是不是真的需要返回全部的列?很可能不是必需的。

取出全部列,會(huì)讓優(yōu)化器無(wú)法完成索引覆蓋掃描這類優(yōu)化,還會(huì)為服務(wù)器帶來(lái)額外的I/O、內(nèi)存和CPU的消耗。因此,一些DBA是嚴(yán)格禁止SELECT *的寫(xiě)法的,這樣做有時(shí)候還能避免某些列被修改帶來(lái)的問(wèn)題。

重復(fù)查詢相同的數(shù)據(jù)

如果你不太小心,很容易出現(xiàn)這樣的錯(cuò)誤一不斷地重復(fù)執(zhí)行相同的查詢,然后每次都返回完全相同的數(shù)據(jù)。

6.2.2 MySQL 是否在掃描額外的記錄

在確定查詢只返回需要的數(shù)據(jù)以后,接下來(lái)應(yīng)該看看查詢?yōu)榱朔祷亟Y(jié)果是否掃描了過(guò)多的數(shù)據(jù)。對(duì)于MySQL,最簡(jiǎn)單的衡量查詢開(kāi)銷的三個(gè)指標(biāo)如下:

  • 響應(yīng)時(shí)間
  • 掃描的行數(shù)
  • 返回的行數(shù)

沒(méi)有哪個(gè)指標(biāo)能夠完美地衡量查詢的開(kāi)銷,但它們大致反映了MySQL在內(nèi)部執(zhí)行查詢時(shí)需要訪問(wèn)多少數(shù)據(jù),并可以大概推算出查詢運(yùn)行的時(shí)間。這三個(gè)指標(biāo)都會(huì)記錄到MySQL的慢日志中,所以檢查慢日志記錄是找出掃描行數(shù)過(guò)多的查詢的好辦法。

響應(yīng)時(shí)間

響應(yīng)時(shí)間 { 服務(wù)時(shí)間:實(shí)際處理任務(wù)花費(fèi)時(shí)間 排隊(duì)時(shí)間:等待任務(wù)開(kāi)始的時(shí)間 響應(yīng)時(shí)間\left\{ \begin{matrix}服務(wù)時(shí)間:實(shí)際處理任務(wù)花費(fèi)時(shí)間\\排隊(duì)時(shí)間:等待任務(wù)開(kāi)始的時(shí)間\end{matrix} \right. 響應(yīng)時(shí)間{服務(wù)時(shí)間:實(shí)際處理任務(wù)花費(fèi)時(shí)間排隊(duì)時(shí)間:等待任務(wù)開(kāi)始的時(shí)間?

掃描的行數(shù)與返回的行數(shù)

理想情況:掃描行數(shù)=返回行數(shù),實(shí)際一般 掃描:返回 的值一般在 1:1~10:1。

掃描的行數(shù)與返回的類型

MySQL有好幾種訪問(wèn)方式可以查找并返回一行結(jié)果。有些訪問(wèn)方式可能需要掃描很多行才能返回一行結(jié)果,也有些訪問(wèn)方式可能無(wú)須掃描就能返回結(jié)果。

在EXPLAIN語(yǔ)句中的type列反應(yīng)了訪問(wèn)類型。訪問(wèn)類型有很多種,從全表掃描到:索引掃描、范圍掃描、唯一索引查詢、常數(shù)引用等。這里列的這些,速度是從慢到快,掃描的行數(shù)也是從小到大。

需要明白掃描表、掃描索引、范圍訪問(wèn)和單值訪問(wèn)的概念。

如果查詢沒(méi)有辦法找到合適的訪問(wèn)類型,那么解決的最好辦法通常就是增加一個(gè)合適的索引。

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

  • 在索引中使用WHERE條件來(lái)過(guò)濾不匹配的記錄。這是在存儲(chǔ)引擎層完成的。
  • 使用索引覆蓋掃描(在Extra列中出現(xiàn)了Using index)來(lái)返回記錄,直接從索引中過(guò)濾不需要的記錄并返回命中的結(jié)果。這是在MySQL服務(wù)器層完成的,但無(wú)須再回表查詢記錄。
  • 從數(shù)據(jù)表中返回?cái)?shù)據(jù),然后過(guò)濾不滿足條件的記錄(在Extra列中出現(xiàn)Using Where)。這在MySQL服務(wù)器層完成,MySQL需要先從數(shù)據(jù)表讀出記錄然后過(guò)濾。

6.3 重構(gòu)查詢的方式

在優(yōu)化有問(wèn)題的查詢時(shí),目標(biāo)應(yīng)該是找到一個(gè)更優(yōu)的方法獲得實(shí)際需要的結(jié)果一而不一定總是需要從MySQL獲取一模一樣的結(jié)果集。

6.3.1 一個(gè)復(fù)雜查詢還是多個(gè)簡(jiǎn)單查詢

MySQL從設(shè)計(jì)上讓連接和斷開(kāi)連接都很輕量級(jí),在返回一個(gè)小的查詢結(jié)果方面很高效。現(xiàn)代的網(wǎng)絡(luò)速度比以前要快很多,運(yùn)行多個(gè)小查詢現(xiàn)在已經(jīng)不是大問(wèn)題了。

MySQL內(nèi)部每秒能夠掃描內(nèi)存中上百萬(wàn)行數(shù)據(jù),相比之下,MySQL響應(yīng)數(shù)據(jù)給客戶端就慢得多了。在其他條件都相同的時(shí)候,使用盡可能少的查詢當(dāng)然是更好的。但是有時(shí)候,將一個(gè)大查詢分解為多個(gè)小查詢是很有必要的。

不過(guò),在應(yīng)用設(shè)計(jì)的時(shí)候,如果一個(gè)查詢能夠勝任時(shí)還寫(xiě)成多個(gè)獨(dú)立查詢是不明智的。

6.3.2 切分查詢

有時(shí)候?qū)τ谝粋€(gè)大查詢我們需要“分而治之”,將大查詢切分成小查詢,每個(gè)查詢功能完全一樣,只完成一小部分,每次只返回一小部分查詢結(jié)果。

刪除舊的數(shù)據(jù)就是一個(gè)很好的例子。定期地清除大量數(shù)據(jù)時(shí),如果用一個(gè)大的語(yǔ)句一次性完成的話,則可能需要一次鎖住很多數(shù)據(jù)、占滿整個(gè)事務(wù)日志、耗盡系統(tǒng)資源、阻塞很多小的但重要的查詢。

將一個(gè)大的DELETE語(yǔ)句切分成多個(gè)較小的查詢可以盡可能小地影響MySQL性能,同時(shí)還可以減少M(fèi)ySQL復(fù)制的延遲。

6.3.3 分解關(guān)聯(lián)查詢

就是把一個(gè)復(fù)雜的關(guān)聯(lián)查詢,拆解到業(yè)務(wù)中去分開(kāi)查詢

用分解關(guān)聯(lián)查詢的方式重構(gòu)查詢有如下的優(yōu)勢(shì):

  • 讓緩存的效率更高。許多應(yīng)用程序可以方便地緩存單表查詢對(duì)應(yīng)的結(jié)果對(duì)象。另外,對(duì)MySQL的查詢緩存來(lái)說(shuō),如果關(guān)聯(lián)中的某個(gè)表發(fā)生了變化,那么就無(wú)法使用查詢緩存了,而拆分后,如果某個(gè)表很少改變,那么基于該表的查詢就可以重復(fù)利用查詢緩存結(jié)果了。

  • 將查詢分解后,執(zhí)行單個(gè)查詢可以減少鎖的競(jìng)爭(zhēng)。

  • 在應(yīng)用層做關(guān)聯(lián),可以更容易對(duì)數(shù)據(jù)庫(kù)進(jìn)行拆分,更容易做到高性能和可擴(kuò)展。查詢本身效率也可能會(huì)有所提升。這個(gè)例子中,使用IN()代替關(guān)聯(lián)查詢,可以讓MySQL按照ID順序進(jìn)行查詢,這可能比隨機(jī)的關(guān)聯(lián)要更高效。

  • 可以減少冗余記錄的查詢。在應(yīng)用層做關(guān)聯(lián)查詢,意味著對(duì)于某條記錄應(yīng)用只需要查詢一次,而在數(shù)據(jù)庫(kù)中做關(guān)聯(lián)查詢,則可能需要重復(fù)地訪問(wèn)一部分?jǐn)?shù)據(jù)。從這點(diǎn)看,這樣的重構(gòu)還可能會(huì)減少網(wǎng)絡(luò)和內(nèi)存的消耗。

  • 更進(jìn)一步,這樣做相當(dāng)于在應(yīng)用中實(shí)現(xiàn)了哈希關(guān)聯(lián),而不是使用MySQL的嵌套循環(huán)關(guān)聯(lián)。某些場(chǎng)景哈希關(guān)聯(lián)的效率要高很多。

6.4 查詢執(zhí)行的基礎(chǔ)

當(dāng)希望MySQL能夠以更高的性能運(yùn)行查詢時(shí),最好的辦法就是弄清楚MySQL是如何優(yōu)化和執(zhí)行查詢的。

根據(jù)圖6-1,我們可以看到當(dāng)向MySQL發(fā)送一個(gè)請(qǐng)求的時(shí)候,MySQL到底做了些什么:
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

  1. 客戶端發(fā)送一條查詢給服務(wù)器。
  2. 服務(wù)器先檢查查詢緩存,如果命中了緩存,則立刻返回存儲(chǔ)在緩存中的結(jié)果。否則進(jìn)入下一階段。
  3. 服務(wù)器端進(jìn)行SQL解析、預(yù)處理,再由優(yōu)化器生成對(duì)應(yīng)的執(zhí)行計(jì)劃。
  4. MySQL 根據(jù)優(yōu)化器生成的執(zhí)行計(jì)劃,調(diào)用存儲(chǔ)引擎的API來(lái)執(zhí)行查詢。
  5. 將結(jié)果返回給客戶端。

6.4.1 MySQL 客戶端/服務(wù)器通信協(xié)議

MySQL客戶端和服務(wù)器之間的通信協(xié)議是“半雙工”的,在任何一個(gè)時(shí)刻,要么是由服務(wù)器向客戶端發(fā)送數(shù)據(jù),要么是由客戶端向服務(wù)器發(fā)送數(shù)據(jù),這兩個(gè)動(dòng)作不能同時(shí)發(fā)生。所以,我們無(wú)法也無(wú)須將一個(gè)消息切成小塊獨(dú)立來(lái)
發(fā)送。

這種協(xié)議讓MySQL通信簡(jiǎn)單快速,但是也從很多地方限制了MySQL。一個(gè)明顯的限制是,這意味著沒(méi)法進(jìn)行流量控制。一旦一端開(kāi)始發(fā)生消息,另一端要接收完整個(gè)消息才能響應(yīng)它。

  • 客戶端用一個(gè)單獨(dú)的數(shù)據(jù)包將查詢傳給服務(wù)器。這也是為什么當(dāng)查詢的語(yǔ)句很長(zhǎng)的時(shí)候,參數(shù)max_allowed_ packet 就特別重要了。一旦客戶端發(fā)送了請(qǐng)求,它能做的事情就只是等待結(jié)果了。

  • 相反的,一般服務(wù)器響應(yīng)給用戶的數(shù)據(jù)通常很多,由多個(gè)數(shù)據(jù)包組成。當(dāng)服務(wù)器開(kāi)始響應(yīng)客戶端請(qǐng)求時(shí),客戶端必須完整地接收整個(gè)返回結(jié)果,而不能簡(jiǎn)單地只取前面幾條結(jié)果,然后讓服務(wù)器停止發(fā)送數(shù)據(jù)。這種情況下,客戶端若接收完整的結(jié)果,然后取前面幾條需要的結(jié)果,或者接收完幾條結(jié)果后就“粗暴”地?cái)嚅_(kāi)連接,都不是好主意。這也是在必要的時(shí)候一定要在查詢中加上LIMIT限制的原因。


多數(shù)連接MySQL的庫(kù)函數(shù)都可以獲得全部結(jié)果集并緩存到內(nèi)存里,還可以逐行獲取需要的數(shù)據(jù)。默認(rèn)一般是獲得全部結(jié)果集并緩存到內(nèi)存中。

MySQL通常需要等所有的數(shù)據(jù)都已經(jīng)發(fā)送給客戶端才能釋放這條查詢所占用的資源,所以接收全部結(jié)果并緩存通常可以減少服務(wù)器的壓力,讓查詢能夠早點(diǎn)結(jié)束、早點(diǎn)釋放相應(yīng)的資源。

  • 當(dāng)使用多數(shù)連接MySQL的庫(kù)函數(shù)從MySQL獲取數(shù)據(jù)時(shí),其結(jié)果看起來(lái)都像是從MySQL服務(wù)器獲取數(shù)據(jù),而實(shí)際上都是從這個(gè)庫(kù)函數(shù)的緩存獲取數(shù)據(jù)。
  • 多數(shù)情況下這沒(méi)什么問(wèn)題,但是如果需要返回一個(gè)很大的結(jié)果集的時(shí)候,這樣做并不好,因?yàn)閹?kù)函數(shù)會(huì)花很多時(shí)間和內(nèi)存來(lái)存儲(chǔ)所有的結(jié)果集。如果能夠盡早開(kāi)始處理這些結(jié)果集,就能大大減少內(nèi)存的消耗,這種情況下可以不使用緩存來(lái)記錄結(jié)果而是直接處理。

這樣做的缺點(diǎn)是,對(duì)于服務(wù)器來(lái)說(shuō),需要查詢完成后才能釋放資源,所以在和客戶端交互的整個(gè)過(guò)程中,服務(wù)器的資源都是被這個(gè)查詢所占用的。

我們看看當(dāng)使用PHP的時(shí)候是什么情況。首先,下面是我們連接MySQL的通常寫(xiě)法:

< ?php
$link = mysql_connect('localhost', 'user' , 'p4ssword');
$result = mysql_query('SELECT * FROM HUGE_TABLE', $link);
while($row = mysql_fetch_array($result)) {
	// Do something with result
}
?>

這段代碼看起來(lái)像是只有當(dāng)你需要的時(shí)候,才通過(guò)循環(huán)從服務(wù)器端取出數(shù)據(jù)。而實(shí)際上,在上面的代碼中,在調(diào)用mysql_query() 的時(shí)候,PHP就已經(jīng)將整個(gè)結(jié)果集緩存到內(nèi)存中。

下面的while循環(huán)只是從這個(gè)緩存中逐行取出數(shù)據(jù),相反如果使用下面的查詢,用mysql_unbuffered_ query() 代替mysql_query(), PHP則不會(huì)緩存結(jié)果:

<?php
$link = mysql_connect('localhost', 'user' , 'password');
$result = mysql_unbuffered_query('SELECT * FROM HUGE_TABLE', $link);
while($row = mysql_fetch_array($result)) {
	// Do something with result
}
?>

mybatis的緩存使用-MyBatis的緩存機(jī)制

查詢狀態(tài)

對(duì)于一個(gè)MySQL連接,或者說(shuō)一個(gè)線程,任何時(shí)刻都有一個(gè)狀態(tài),該狀態(tài)表示了MySQL當(dāng)前正在做什么。

有很多種方式能查看當(dāng)前的狀態(tài),最簡(jiǎn)單的是使用

SHOW FULL PROCESSLIST

命令( 該命令返回結(jié)果中的Command列就表示當(dāng)前的狀態(tài))。在一個(gè)查詢的生命周期中,狀態(tài)會(huì)變化很多次。

  • Sleep

線程正在等待客戶端發(fā)送新的請(qǐng)求。

  • Query

線程正在執(zhí)行查詢或者正在將結(jié)果發(fā)送給客戶端。

  • Locked

在MySQL服務(wù)器層,該線程正在等待表鎖。

在存儲(chǔ)引擎級(jí)別實(shí)現(xiàn)的鎖,例如InnoDB的行鎖,并不會(huì)體現(xiàn)在線程狀態(tài)中。

對(duì)于MyISAM來(lái)說(shuō)這是一個(gè)比較典型的狀態(tài),但在其他沒(méi)有行鎖的引擎中也經(jīng)常會(huì)出現(xiàn)。

  • Analyzing and statistics

線程正在收集存儲(chǔ)引擎的統(tǒng)計(jì)信息,并生成查詢的執(zhí)行計(jì)劃。

  • Copying to tmp table [on disk]

線程正在執(zhí)行查詢,并且將其結(jié)果集都復(fù)制到一個(gè)臨時(shí)表中,這種狀態(tài)一般要么是在做GROUP BY 操作,要么是文件排序操作,或者是UNION操作。

如果這個(gè)狀態(tài)后面還有“on disk”標(biāo)記,那表示MySQL正在將-一個(gè)內(nèi)存臨時(shí)表放到磁盤上。

  • Sorting result

線程正在對(duì)結(jié)果集進(jìn)行排序。

  • Sending data

這表示多種情況:線程可能在多個(gè)狀態(tài)之間傳送數(shù)據(jù),或者在生成結(jié)果集,或者在向客戶端返回?cái)?shù)據(jù)。

6.4.2 查詢緩存

在解析一個(gè)查詢語(yǔ)句之前,如果查詢緩存是打開(kāi)的,那么MySQL會(huì)優(yōu)先檢查這個(gè)查詢是否命中查詢緩存中的數(shù)據(jù)。

這個(gè)檢查是通過(guò)一個(gè)對(duì)大小寫(xiě)敏感的哈希查找實(shí)現(xiàn)的。查詢和緩存中的查詢即使只有一個(gè)字節(jié)不同,那也不會(huì)匹配緩存結(jié)果,這種情況下查詢就會(huì)進(jìn)入下一階段的處理。

如果當(dāng)前的查詢恰好命中了查詢緩存,那么在返回查詢結(jié)果之前MySQL會(huì)檢查一次用戶權(quán)限。

這仍然是無(wú)須解析查詢SQL語(yǔ)句的,因?yàn)樵诓樵兙彺嬷幸呀?jīng)存放了當(dāng)前查詢需要訪問(wèn)的表信息。

如果權(quán)限沒(méi)有問(wèn)題,MySQL會(huì)跳過(guò)所有其他階段,直接從緩存中拿到結(jié)果并返回給客戶端。這種情況下,查詢不會(huì)被解析,不用生成執(zhí)行計(jì)劃,不會(huì)被執(zhí)行。

6.4.3 查詢優(yōu)化處理

查詢的生命周期的下一步是將一個(gè)SQL轉(zhuǎn)換成一個(gè)執(zhí)行計(jì)劃,MySQL再依照這個(gè)執(zhí)行計(jì)劃和存儲(chǔ)引擎進(jìn)行交互。

這包括多個(gè)子階段:解析SQL、預(yù)處理、優(yōu)化SQL執(zhí)行計(jì)劃。

這個(gè)過(guò)程中任何錯(cuò)誤(例如語(yǔ)法錯(cuò)誤)都可能終止查詢。

語(yǔ)法解析器和預(yù)處理

首先,MySQL通過(guò)關(guān)鍵字將SQL語(yǔ)句進(jìn)行解析,并生成一棵對(duì)應(yīng)的“解析樹(shù)”。

MySQL解析器將使用MySQL語(yǔ)法規(guī)則驗(yàn)證和解析查詢。

預(yù)處理器則根據(jù)一些MySQL規(guī)則進(jìn)一步檢查解析樹(shù)是否合法。

下一步預(yù)處理器會(huì)驗(yàn)證權(quán)限。這通常很快,除非服務(wù)器上有非常多的權(quán)限配置。

查詢優(yōu)化器

一條查詢可以有很多種執(zhí)行方式,最后都返回相同的結(jié)果。優(yōu)化器的作用就是找到這其中最好的執(zhí)行計(jì)劃。

MySQL使用基于成本的優(yōu)化器,它將嘗試預(yù)測(cè)一個(gè)查詢使用某種執(zhí)行計(jì)劃時(shí)的成本,并選擇其中成本最小的一個(gè)。

最初,成本的最小單位是隨機(jī)讀取一個(gè)4K數(shù)據(jù)頁(yè)的成本,后來(lái)(成本計(jì)算公式)變得更加復(fù)雜,并且引入了一些“因子”來(lái)估算某些操作的代價(jià),如當(dāng)執(zhí)行一次 WHERE條件比較的成本。

可以通過(guò)查詢當(dāng)前會(huì)話的Last_query_cost的值來(lái)得知MySQL計(jì)算的當(dāng)前查詢的成本。

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
有很多種原因會(huì)導(dǎo)致MySQL優(yōu)化器選擇錯(cuò)誤的執(zhí)行計(jì)劃,如下所示:

  • 統(tǒng)計(jì)信息不準(zhǔn)確。MySQL依賴存儲(chǔ)引擎提供的統(tǒng)計(jì)信息來(lái)評(píng)估成本,但是有的存儲(chǔ)引擎提供的信息是準(zhǔn)確的,有的偏差可能非常大。

例如,InnoDB因?yàn)槠銶VCC的架構(gòu),并不能維護(hù)一個(gè)數(shù)據(jù)表的行數(shù)的精確統(tǒng)計(jì)信息。

  • 執(zhí)行計(jì)劃中的成本估算不等同于實(shí)際執(zhí)行的成本。所以即使統(tǒng)計(jì)信息精準(zhǔn),優(yōu)化器給出的執(zhí)行計(jì)劃也可能不是最優(yōu)的。

例如有時(shí)候某個(gè)執(zhí)行計(jì)劃雖然需要讀取更多的頁(yè)面,但是它的成本卻更小。因?yàn)槿绻@些頁(yè)面都是順序讀或者這些頁(yè)面都已經(jīng)在內(nèi)存中的話,那么它的訪問(wèn)成本將很小。MySQL層面并不知道哪些頁(yè)面在內(nèi)存中、哪些在磁盤上,所以查詢實(shí)際執(zhí)行過(guò)程中到底需要多少次物理I/O是無(wú)法得知的。

  • MySQL的最優(yōu)可能和你想的最優(yōu)不-一樣。你可能希望執(zhí)行時(shí)間盡可能的短,但是MySQL只是基于其成本模型選擇最優(yōu)的執(zhí)行計(jì)劃,而有些時(shí)候這并不是最快的執(zhí)行方式。所以,這里我們看到根據(jù)執(zhí)行成本來(lái)選擇執(zhí)行計(jì)劃并不是完美的模型。
  • MySQL從不考慮其他并發(fā)執(zhí)行的查詢,這可能會(huì)影響到當(dāng)前查詢的速度。
  • MySQL也并不是任何時(shí)候都是基于成本的優(yōu)化。有時(shí)也會(huì)基于一些固定的規(guī)則

例如,如果存在全文搜索的MATCH()子句,則在存在全文索引的時(shí)候就使用全文索引。即使有時(shí)候使用別的索引和WHERE條件可以遠(yuǎn)比這種方式要快,MySQL 也仍然會(huì)使用對(duì)應(yīng)的全文索引。

  • MySQL不會(huì)考慮不受其控制的操作的成本,例如執(zhí)行存儲(chǔ)過(guò)程或者用戶自定義函數(shù)的成本。
  • 優(yōu)化器有時(shí)候無(wú)法去估算所有可能的執(zhí)行計(jì)劃,所以它可能錯(cuò)過(guò)實(shí)際上最優(yōu)的執(zhí)行計(jì)劃。

MySQL的查詢優(yōu)化器是一個(gè)非常復(fù)雜的部件,它使用了很多優(yōu)化策略來(lái)生成一個(gè)最優(yōu)的執(zhí)行計(jì)劃。

優(yōu)化策略可以簡(jiǎn)單地分為兩種:

  • 一種是靜態(tài)優(yōu)化:靜態(tài)優(yōu)化可以直接對(duì)解析樹(shù)進(jìn)行分析,并完成優(yōu)化。
    • 例如,優(yōu)化器可以通過(guò)一些簡(jiǎn)單的代數(shù)變換將WHERE條件轉(zhuǎn)換成另一種等價(jià)形式。
    • 靜態(tài)優(yōu)化不依賴于特別的數(shù)值,如WHERE條件中帶入的一些常數(shù)等。靜態(tài)優(yōu)化在第一次完成后就一直有效,即使使用不同的參數(shù)重復(fù)執(zhí)行查詢也不會(huì)發(fā)生變化。
    • 可以認(rèn)為這是一種“編譯時(shí)優(yōu)化”。
  • 一種是動(dòng)態(tài)優(yōu)化:動(dòng)態(tài)優(yōu)化則和查詢的上下文有關(guān),也可能和很多其他因素有關(guān)。
    • 例如WHERE條件中的取值、索引中條目對(duì)應(yīng)的數(shù)據(jù)行數(shù)等。
    • 這需要在每次查詢的時(shí)候都重新評(píng)估,可以認(rèn)為這是“運(yùn)行時(shí)優(yōu)化”。

在執(zhí)行語(yǔ)句和存儲(chǔ)過(guò)程的時(shí)候,動(dòng)態(tài)優(yōu)化和靜態(tài)優(yōu)化的區(qū)別非常重要。

MySQL對(duì)查詢的靜態(tài)優(yōu)化只需要做一次,但對(duì)查詢的動(dòng)態(tài)優(yōu)化則在每次執(zhí)行時(shí)都需要重新評(píng)估。有時(shí)候甚至在查詢的執(zhí)行過(guò)程中也會(huì)重新優(yōu)化。

下面是一些MySQL能夠處理的優(yōu)化類型:

  • 重新定義關(guān)聯(lián)表的順序

數(shù)據(jù)表的關(guān)聯(lián)并不總是按照在查詢中指定的順序進(jìn)行。決定關(guān)聯(lián)的順序是優(yōu)化器很重要的一部分功能。

  • 將外連接轉(zhuǎn)化成內(nèi)連接

并不是所有的 OUTER JOIN 語(yǔ)句都必須以外連接的方式執(zhí)行。

諸多因素,例如WHERE條件、庫(kù)表結(jié)構(gòu)都可能會(huì)讓外連接等價(jià)于一個(gè)內(nèi)連接。MySQL能夠識(shí)別這點(diǎn)并重寫(xiě)查詢,讓其可以調(diào)整關(guān)聯(lián)順序。

  • 使用等價(jià)變換規(guī)則

MySQL可以使用一些等價(jià)變換來(lái)簡(jiǎn)化并規(guī)范表達(dá)式。它可以合并和減少一些比較,還可以移除一些恒成立和一些恒不成立的判斷。例如,(5=5 AND a>5) 將被改寫(xiě)為a>5。類似的,如果有(a<b AND b=c) AND a=5 則會(huì)改寫(xiě)為b>5 AND b=c AND a=5。

這些規(guī)則對(duì)于我們編寫(xiě)條件語(yǔ)句很有用,我們將在本章后續(xù)繼續(xù)討論。

  • 優(yōu)化COUNT()、 MIN() 和MAX()

索引和列是否可為空通??梢詭椭鶰ySQL優(yōu)化這類表達(dá)式。

例如,要找到某一列的最小值,只需要查詢對(duì)應(yīng)B-Tree索引最左端的記錄,MySQL可以直接獲取索引的第一行記錄。在優(yōu)化器生成執(zhí)行計(jì)劃的時(shí)候就可以利用這一點(diǎn),在B-Tree索引中,優(yōu)化器會(huì)將這個(gè)表達(dá)式作為一個(gè)常數(shù)對(duì)待。

類似的,如果要查找一個(gè)最大值,也只需讀取B-Tree索引的最后一條記錄。

如果MySQL使用了這種類型的優(yōu)化,那么在EXPLAIN中就可以看到“ Select tables optimized away”。從字面意思可以看出,它表示優(yōu)化器已經(jīng)從執(zhí)行計(jì)劃中移除了該表,并以一個(gè)常數(shù)取而代之。

類似的,沒(méi)有任何WHERE條件的COUNT(*)查詢通常也可以使用存儲(chǔ)引擎提供的一些優(yōu)化(例如,MyISAM維護(hù)了一個(gè)變量來(lái)存放數(shù)據(jù)表的行數(shù))。

  • 預(yù)估并轉(zhuǎn)化為常數(shù)表達(dá)式

當(dāng)MySQL檢測(cè)到一個(gè)表達(dá)式可以轉(zhuǎn)化為常數(shù)的時(shí)候,就會(huì)一直把該表達(dá)式作為常數(shù)進(jìn)行優(yōu)化處理。

例如,一個(gè)用戶自定義變量在查詢中沒(méi)有發(fā)生變化時(shí)就可以轉(zhuǎn)換為一個(gè)常數(shù)。

數(shù)學(xué)表達(dá)式則是另一種典型的例子。在優(yōu)化階段,有時(shí)候甚至一個(gè)查詢也能夠轉(zhuǎn)化為一個(gè)常數(shù)。

一個(gè)例子是在索引列上執(zhí)行MIN()函數(shù)。甚至是主鍵或者唯一鍵查找語(yǔ)句也可以轉(zhuǎn)換為常數(shù)表達(dá)式。

如果WHERE子句中使用了該類索引的常數(shù)條件,MySQL可以在查詢開(kāi)始階段就先查找到這些值,這樣優(yōu)化器就能夠知道并轉(zhuǎn)換為常數(shù)表達(dá)式。

下面是一個(gè)例子:

mysql> EXPLAIN SELECT film.film_id,film_actor.actor_id
-> FROM sakila.film
-> INNER JOIN sakila.film_actor USING(film_id)
-> WHERE film.film_id = 1;

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

MySQL分兩步來(lái)執(zhí)行這個(gè)查詢,也就是上面執(zhí)行計(jì)劃的兩行輸出。

  • 第一步先從film表找到需要的行。因?yàn)樵趂ilm_id 字段上有主鍵索引,所以MySQL優(yōu)化器知道這只會(huì)返回一行數(shù)據(jù),優(yōu)化器在生成執(zhí)行計(jì)劃的時(shí)候,就已經(jīng)通過(guò)索引信息知道將返回多少行數(shù)據(jù)。因?yàn)閮?yōu)化器已經(jīng)明確知道有多少個(gè)值(WHERE條件中的值)需要做索引查詢,所以這里的表訪問(wèn)類型是const。

  • 在執(zhí)行計(jì)劃的第二步,MySQL將第一步中返回的film_id列當(dāng)作一個(gè)已知取值的列來(lái)處理。因?yàn)閮?yōu)化器清楚在第一步執(zhí)行完成后,該值就會(huì)是明確的了。注意到正如第一步中一樣,使用film_actor字段對(duì)表的訪問(wèn)類型也是const。

另一種會(huì)看到常數(shù)條件的情況是通過(guò)等式將常數(shù)值從一個(gè)表傳到另一個(gè)表,這可以通過(guò)WHERE、USING 或者ON語(yǔ)句來(lái)限制某列取值為常數(shù)。

在上面的例子中,因?yàn)槭褂昧薝SING子句,優(yōu)化器知道這也限制了film_id在整個(gè)查詢過(guò)程中都始終是一個(gè)常量——因?yàn)樗仨毜?于WHERE子句中的那個(gè)取值。


  • 覆蓋索引掃描

當(dāng)索引中的列包含所有查詢中需要使用的列的時(shí)候,MySQL就可以使用索引返回需要的數(shù)據(jù),而無(wú)須查詢對(duì)應(yīng)的數(shù)據(jù)行。

  • 子查詢優(yōu)化

MySQL在某些情況下可以將子查詢轉(zhuǎn)換-種效率更高的形式,從而減少多個(gè)查詢多次對(duì)數(shù)據(jù)進(jìn)行訪問(wèn)。

  • 提前終止查詢

在發(fā)現(xiàn)已經(jīng)滿足查詢需求的時(shí)候,MySQL總是能夠立刻終止查詢。-一個(gè)典型的例子就是當(dāng)使用了LIMIT子句的時(shí)候。

除此之外,MySQL還有幾類情況也會(huì)提前終止查詢,例如發(fā)現(xiàn)了一個(gè)不成立的條件,這時(shí)MySQL可以立刻返回一個(gè)空結(jié)果。

從下面的例子可以看到這一點(diǎn):

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

從這個(gè)例子看到查詢?cè)趦?yōu)化階段就已經(jīng)終止。

除此之外,MySQL在執(zhí)行過(guò)程中,如果發(fā)現(xiàn)某些特殊的條件,則會(huì)提前終止查詢。

當(dāng)存儲(chǔ)引擎需要檢索“不同取值”或者判斷存在性的時(shí)候,MySQL都可以使用這類優(yōu)化。例如,我們現(xiàn)在需要找到?jīng)]有演員的所有電影。

  • 等值傳播

如果兩個(gè)列的值通過(guò)等式關(guān)聯(lián),那么MySQL能夠把其中一個(gè)列的WHERE條件傳遞到另一列上。

例如,我們看下面的查詢:

mysql> SELECT film.film_id
-> FROM sakila.film
-> INNER JOIN sakila.film_actor USING(fi1m_ id)
-> WHERE film.film_ id > 500;

因?yàn)檫@里使用了film_ id字段進(jìn)行等值關(guān)聯(lián),MySQL知道這里的WHERE子句不僅適用于film表,而且對(duì)于film_actor表同樣適用。

如果使用的是其他的數(shù)據(jù)庫(kù)管理系統(tǒng),可能還需要手動(dòng)通過(guò)一些條件來(lái)告知優(yōu)化器這個(gè)WHERE條件適用于兩個(gè)表,那么寫(xiě)法就會(huì)如下:

... WHERE film.film_id > 500 AND film_actor.film_id > 500

在MySQL中不需要。

  • 列表IN()的比較

在很多數(shù)據(jù)庫(kù)系統(tǒng)中,IN() 完全等同于多個(gè)OR條件的子句,因?yàn)檫@兩者是完全等價(jià)的。

在MySQL中這點(diǎn)是不成立的,MySQL將IN()列表中的數(shù)據(jù)先進(jìn)行排序,然后通過(guò)二分查找的方式來(lái)確定列表中的值是否滿足條件,這是一個(gè)O(log n)復(fù)雜度的操作,等價(jià)地轉(zhuǎn)換成OR查詢的復(fù)雜度為O(n),對(duì)于IN()列表中有大量取值的時(shí)候,MySQL的處理速度將會(huì)更快。

數(shù)據(jù)和索引的統(tǒng)計(jì)信息

MySQL架構(gòu)由多個(gè)層次組成。在服務(wù)器層有查詢優(yōu)化器,卻沒(méi)有保存數(shù)據(jù)和索引的統(tǒng)計(jì)信息。

統(tǒng)計(jì)信息由存儲(chǔ)引擎實(shí)現(xiàn),不同的存儲(chǔ)引擎可能會(huì)存儲(chǔ)不同的統(tǒng)計(jì)信息(也可以按照不同的格式存儲(chǔ)統(tǒng)計(jì)信息)。

某些引擎,例如Archive引擎,則根本就沒(méi)有存儲(chǔ)任何統(tǒng)計(jì)信息.

因?yàn)榉?wù)器層沒(méi)有任何統(tǒng)計(jì)信息,所以MySQL查詢優(yōu)化器在生成查詢的執(zhí)行計(jì)劃時(shí),需要向存儲(chǔ)引擎獲取相應(yīng)的統(tǒng)計(jì)信息。

存儲(chǔ)引擎則提供給優(yōu)化器對(duì)應(yīng)的統(tǒng)計(jì)信息,包括:

  • 每個(gè)表或者索引有多少個(gè)頁(yè)面
  • 每個(gè)表的每個(gè)索引的基數(shù)是多少
  • 數(shù)據(jù)行和索引長(zhǎng)度
  • 索引的分布信息
  • … …

優(yōu)化器根據(jù)這些信息來(lái)選擇一個(gè)最優(yōu)的執(zhí)行計(jì)劃。

MySQL如何執(zhí)行關(guān)聯(lián)查詢

MySQL中“關(guān)聯(lián)”一詞所包含的意義比一般意義上理解的要更廣泛??偟膩?lái)說(shuō),MySQL認(rèn)為任何一個(gè)查詢都是一次“關(guān)聯(lián)”一并不僅僅是一個(gè) 查詢需要到兩個(gè)表匹配才叫關(guān)聯(lián),所以在MySQL中,每一個(gè)查詢,每一個(gè)片段(包括子查詢,甚至基于單表的SELECT)都可能是關(guān)聯(lián)。

對(duì)于UNION查詢,MySQL先將一系列的 單個(gè)查詢結(jié)果放到一個(gè)臨時(shí)表中,然后再重新讀出臨時(shí)表數(shù)據(jù)來(lái)完成UNION查詢。在MySQL的概念中,每個(gè)查詢都是一次關(guān)聯(lián),所以讀取結(jié)果臨時(shí)表也是一次關(guān)聯(lián)。


當(dāng)前MySQL關(guān)聯(lián)執(zhí)行的策略很簡(jiǎn)單:

MySQL對(duì)任何關(guān)聯(lián)都執(zhí)行嵌套循環(huán)關(guān)聯(lián)操作,即MySQL先在一個(gè)表中循環(huán)取出單條數(shù)據(jù),然后再嵌套循環(huán)到下一個(gè)表中尋找匹配的行,依次下去,直到找到所有表中匹配的行為止。

然后根據(jù)各個(gè)表匹配的行,返回查詢中需要的各個(gè)列。

MySQL會(huì)嘗試在最后一個(gè)關(guān)聯(lián)表中找到所有匹配的行,如果最后一個(gè)關(guān)聯(lián)表無(wú)法找到更多的行以后,MySQL返回到上一層次關(guān)聯(lián)表,看是否能夠找到更多的匹配記錄,依此類推迭代執(zhí)行。

按照這樣的方式查找第一個(gè)表記錄,再嵌套查詢下一個(gè)關(guān)聯(lián)表,然后回溯到上一個(gè)表,在MySQL中是通過(guò)嵌套循環(huán)的方式實(shí)現(xiàn)一正如其名“嵌套循環(huán)關(guān)聯(lián)”。


如下例子中的簡(jiǎn)單查詢:

mysql> SELECT tbl1.co1, tbl2.col2
-> FROM tbl1 INNER JOIN tbl2 USING(col3)
-> WHERE tbl1.col1 IN(5,6);

假設(shè)MySQL按照查詢中的表順序進(jìn)行關(guān)聯(lián)操作,我們則可以用下面的偽代碼表示MySQL將如何完成這個(gè)查詢:

outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_row = outer_iter.next
while outer_row
	inner_iter = iterator over tbl2 where col3 = outer_row.col3
	inner_row = inner_ iter.next
	while inner_ row
		output[outer row.col1,inner_row.col2]
		inner_row = inner_iter.next
	end
	outer_row = outer_iter.next
end

上面的執(zhí)行計(jì)劃對(duì)于單表查詢和多表關(guān)聯(lián)查詢都適用,如果是一個(gè)單表查詢,那么只需完成上面外層的基本操作。

對(duì)于外連接上面的執(zhí)行過(guò)程仍然適用。

例如,我們將上面查詢修改如下:

mysql> SELECT tbl1.col1, tbl2.col2
-> FROM tbl1 LEFT OUTER JOIN tbl2 USING(col3)
-> WHERE tbl1.col1 IN(5,6);

對(duì)應(yīng)的偽代碼如下:

outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_ row = outer_ iter.next
while outer_ row
	inner_ iter = iterator over tbl2 where col3 = outer_ row.col3
	inner_ row = inner_ iter.next
	if inner_ row
		while inner_row
			output [outer_ row.col1, inner_ row.col2]
			inner_row = inner_ iter.next
		end
	else
		output[outer_row.col1, NULL ]
	end
	outer_row = outer_iter.next
end

或者如下6-2“泳道圖”
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
從本質(zhì)上說(shuō),MySQL對(duì)所有的類型的查詢都以同樣的方式運(yùn)行。例如,MySQL在FROM子句中遇到子查詢時(shí),先執(zhí)行子查詢并將其結(jié)果放到一個(gè)臨時(shí)表中,然后將這個(gè)臨時(shí)表當(dāng)作一個(gè)普通表對(duì)待(正如其名‘派生表”)。 MySQL在執(zhí)行UNION查詢時(shí)也使用類似的臨時(shí)表,在遇到右外連接的時(shí)候,MySQL將其改寫(xiě)成等價(jià)的左外連接。

簡(jiǎn)而言之,當(dāng)前版本的MySQL會(huì)將所有的查詢類型都轉(zhuǎn)換成類似的執(zhí)行計(jì)劃。

不過(guò),不是所有的查詢都可以轉(zhuǎn)換成上面的形式。

例如,全外連接就無(wú)法通過(guò)嵌套循環(huán)和回溯的方式完成,這時(shí)當(dāng)發(fā)現(xiàn)關(guān)聯(lián)表中沒(méi)有找到任何匹配行的時(shí)候,則可能是因?yàn)殛P(guān)聯(lián)是恰好從一個(gè)沒(méi)有任何匹配的表開(kāi)始。這大概也是MySQL并不支持全外連接的原因。

還有些場(chǎng)景,雖然可以轉(zhuǎn)換成嵌套循環(huán)的方式,但是效率卻非常差。

執(zhí)行計(jì)劃

和很多其他關(guān)系數(shù)據(jù)庫(kù)不同,MySQL并不會(huì)生成查詢字節(jié)碼來(lái)執(zhí)行查詢。MySQL生成查詢的一棵指令樹(shù),然后通過(guò)存儲(chǔ)引擎執(zhí)行完成這棵指令樹(shù)并返回結(jié)果。最終的執(zhí)行計(jì)劃包含了重構(gòu)查詢的全部信息。

如果對(duì)某個(gè)查詢執(zhí)行EXPLAIN EXTENDED 后,再執(zhí)行SHOWWARNINGS,就可以看到重構(gòu)出的查詢。

任何多表查詢都可以使用一棵樹(shù)表示,例如,可以按照?qǐng)D6-3執(zhí)行一個(gè)四表的關(guān)聯(lián)操作。

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
在計(jì)算機(jī)科學(xué)中,這被稱為一顆平衡樹(shù)。但是,這并不是MySQL執(zhí)行查詢的方式。

MySQL總是從一個(gè)表開(kāi)始一直嵌套循環(huán)、回溯完成所有表關(guān)聯(lián)。所以,MySQL的執(zhí)行計(jì)劃總是如圖6-4所示,是一棵左測(cè)深度優(yōu)先的樹(shù)。

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

關(guān)聯(lián)查詢優(yōu)化器

MySQL優(yōu)化器最重要的一部分就是關(guān)聯(lián)查詢優(yōu)化,它決定了多個(gè)表關(guān)聯(lián)時(shí)的順序。

通常多表關(guān)聯(lián)的時(shí)候,可以有多種不同的關(guān)聯(lián)順序來(lái)獲得相同的執(zhí)行結(jié)果。聯(lián)查詢優(yōu)化器則通過(guò)評(píng)估不同順序時(shí)的成本來(lái)選擇一個(gè)代價(jià)最小的關(guān)聯(lián)順序。

下面的查詢可以通過(guò)不同順序的關(guān)聯(lián)最后都獲得相同的結(jié)果:

mysql> SELECT film.film_id,film.title,film.release_year,actor.actor.id,
actor.first_ name,actor.last_ name
-> FROM sakila.film
-> INNER JOIN sakila.film_actor USING(film_ id)
-> INNER JOIN sakila.actor USING(actor_ id);

容易看出,可以通過(guò)一些不同的執(zhí)行計(jì)劃來(lái)完成上面的查詢。

例如,MySQL可以從film表開(kāi)始,使用film actor表的索引film id來(lái)查找對(duì)應(yīng)的actor. id值,然后再根據(jù)actor表的主鍵找到對(duì)應(yīng)的記錄。Oracle 用戶會(huì)用下面的術(shù)語(yǔ)描述: “film表作為驅(qū)動(dòng)表先查找fle_ actor 表,然后以此結(jié)果為驅(qū)動(dòng)表再查找actor表”。

這樣做效率應(yīng)該會(huì)不錯(cuò),我們?cè)偈褂肊XPLAIN看看MySQL將如何執(zhí)行這個(gè)查詢:
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

與我們假設(shè)的不同。

我們先使用STRAIGHT_J0IN關(guān)鍵字,按照之前的順序執(zhí)行

MySQL基礎(chǔ)之STRAIGHT JOIN用法簡(jiǎn)介

這里是對(duì)應(yīng)的EXPLAIN輸出結(jié)果:
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

我們來(lái)分析一下為什么MySQL會(huì)將關(guān)聯(lián)順序倒轉(zhuǎn)過(guò)來(lái):可以看到,關(guān)聯(lián)順序倒轉(zhuǎn)后的第一個(gè)關(guān)聯(lián)表只需要掃描很少的行數(shù)。在

兩種關(guān)聯(lián)順序下,第二個(gè)和第三個(gè)關(guān)聯(lián)表都是根據(jù)索引查詢,速度都很快,不同的是需要掃描的索引項(xiàng)的數(shù)量是不同的:

  • 將film表作為第一個(gè)關(guān)聯(lián)表時(shí),會(huì)找到951條記錄,然后對(duì)film_ actor和actor表進(jìn)行嵌套循環(huán)查詢。
  • 如果MySQL選擇首先掃描actor表,只會(huì)返回200條記錄進(jìn)行后面的嵌套循環(huán)查詢。

換句話說(shuō),倒轉(zhuǎn)的關(guān)聯(lián)順序會(huì)讓查詢進(jìn)行更少的嵌套循環(huán)和回溯操作。為了驗(yàn)證優(yōu)化器的選擇是否正確,我們單獨(dú)執(zhí)行這兩個(gè)查詢,并且看看對(duì)應(yīng)的Last_ query_cost狀態(tài)值。我們看到倒轉(zhuǎn)的關(guān)聯(lián)順序的預(yù)估成本為241,而原來(lái)的查詢的預(yù)估成本為1154。


關(guān)聯(lián)優(yōu)化器會(huì)嘗試在所有的關(guān)聯(lián)順序中選擇一個(gè)成本最小的來(lái)生成執(zhí)行計(jì)劃樹(shù)。如果可能,優(yōu)化器會(huì)遍歷每一個(gè)表然后逐個(gè)做嵌套循環(huán)計(jì)算每–棵可能的執(zhí)行計(jì)劃樹(shù)的成本,最后返回一個(gè)最優(yōu)的執(zhí)行計(jì)劃。

不過(guò),如果有超過(guò)n個(gè)表的關(guān)聯(lián),那么需要檢查n的階乘種關(guān)聯(lián)順序。我們稱之為所有可能的執(zhí)行計(jì)劃的“搜索空間”,搜索空間的增長(zhǎng)速度非???。當(dāng)搜索空間非常大的時(shí)候,優(yōu)化器選擇使用“貪婪”搜索的方式查找“ 最優(yōu)”的關(guān)聯(lián)順序。

實(shí)際上,當(dāng)需要關(guān)聯(lián)的表超過(guò)optimizer_ search_depth的限制的時(shí)候,就會(huì)選擇“ 貪婪”搜索模式了(optimizer_ search_ depth 參數(shù)可以根據(jù)需要指定大小)。

排序優(yōu)化

無(wú)論如何排序都是一個(gè)成本很高的操作,所以從性能角度考慮,應(yīng)盡可能避免排序或者盡可能避免對(duì)大量數(shù)據(jù)進(jìn)行排序。

無(wú)法使用索引排序的時(shí)候,MySQL 需要自己進(jìn)行排序,如果數(shù)據(jù)量小則在內(nèi)存中進(jìn)行,如果數(shù)據(jù)量大則需要使用磁盤,不過(guò)MySQL將這個(gè)過(guò)程統(tǒng)一稱為文件排序(filesort), 即使完全是內(nèi)存排序不需要任何磁盤文件時(shí)也是如此。

如果需要排序的數(shù)據(jù)量小于“排序緩沖區(qū)”,MySQL使用內(nèi)存進(jìn)行“快速排序”操作。如果內(nèi)存不夠排序,那么MySQL會(huì)先將數(shù)據(jù)分塊,對(duì)每個(gè)獨(dú)立的塊使用“ 快速排序”進(jìn)行排序,并將各個(gè)塊的排序結(jié)果存放在磁盤上,然后將各個(gè)排好序的塊進(jìn)行 合并(merge) ,最后返回排序結(jié)果。

MySQL有如下兩種排序算法

  • 兩次傳輸排序(舊版本使用)

讀取行指針和需要排序的字段,對(duì)其進(jìn)行排序,然后再根據(jù)排序結(jié)果讀取所需要的數(shù)據(jù)行。

這需要進(jìn)行兩次數(shù)據(jù)傳輸,即需要從數(shù)據(jù)表中讀取兩次數(shù)據(jù),第二次讀取數(shù)據(jù)的時(shí)候,因?yàn)槭亲x取排序列進(jìn)行排序后的所有記錄,這會(huì)產(chǎn)生大量的隨機(jī)I/O,所以兩次數(shù)據(jù)傳輸?shù)某杀痉浅8?。?dāng)使用的是MyISAM表的時(shí)候,成本可能會(huì)更高,因?yàn)镸yISAM使用系統(tǒng)調(diào)用進(jìn)行數(shù)據(jù)的讀取(MyISAM非常依賴操作系統(tǒng)對(duì)數(shù)據(jù)的緩存)。

不過(guò)這樣做的優(yōu)點(diǎn)是,在排序的時(shí)候存儲(chǔ)盡可能少的數(shù)據(jù),這就讓“排序緩沖區(qū)”中可能容納盡可能多的行數(shù)進(jìn)行排序。

  • 單次傳輸排序(新版本使用)

先讀取查詢所需要的所有列,然后再根據(jù)給定列進(jìn)行排序,最后直接返回排序結(jié)果。

這個(gè)算法只在MySQL 4.1和后續(xù)更新的版本才引入。因?yàn)椴辉傩枰獜臄?shù)據(jù)表中讀取兩次數(shù)據(jù),對(duì)于I/O密集型的應(yīng)用,這樣做的效率高了很多。另外,相比兩次傳輸排序,這個(gè)算法只需要一次順序I/O讀取所有的數(shù)據(jù),而無(wú)須任何的隨機(jī)I/O。

缺點(diǎn)是,如果需要返回的列非常多、非常大,會(huì)額外占用大量的空間,而這些列對(duì)排序操作本身來(lái)說(shuō)是沒(méi)有任何作用的。因?yàn)閱螚l排序記錄很大,所以可能會(huì)有更多的排序塊需要合并。


兩種算法都有各自最好和最糟的場(chǎng)景。當(dāng)查詢需要所有列的總長(zhǎng)度不超過(guò)參數(shù)max_ length_ for_ sort_data 時(shí),MySQL使用“單次傳輸排序”,可以通過(guò)調(diào)整這個(gè)參數(shù)來(lái)影響MySQL排序算法的選擇。

MySQL文件排序使用的臨時(shí)空間可能比較多。MySQL在進(jìn)行文件排序的時(shí)候需要使用的臨時(shí)存儲(chǔ)空間可能會(huì)比想象的要大得多。原因在于MySQL在排序時(shí),對(duì)每一個(gè)排序記錄都會(huì)分配一個(gè)足夠長(zhǎng)的定長(zhǎng)空間來(lái)存放。

這個(gè)定長(zhǎng)空間必須足夠長(zhǎng)以容納其中 最長(zhǎng)的字符串 ,例如,如果是VARCHAR列則需要分配其完整長(zhǎng)度;如果使用UTF-8字符集,那么MySQL將會(huì)為每個(gè)字符預(yù)留三個(gè)字節(jié)。


在關(guān)聯(lián)查詢的時(shí)候如果需要排序,MySQL會(huì)分兩種情況來(lái)處理這樣的文件排序。

  1. 如果ORDER BY 子句中的所有列都來(lái)自關(guān)聯(lián)的第一個(gè)表,那么MySQL在關(guān)聯(lián)處理第一個(gè)表的時(shí)候就進(jìn)行文件排序。如果是這樣,那么在MySQL的EXPLAIN結(jié)果中可以看到Extra字段會(huì)有“Using filesort" 。
  2. 除此之外的所有情況,MySQL 都會(huì)先將關(guān)聯(lián)的結(jié)果存放到一個(gè)臨時(shí)表中,然后在所有的關(guān)聯(lián)都結(jié)束后,再進(jìn)行文件排序。這種情況下,在MySQL的EXPLAIN結(jié)果的Extra字段可以看到“Using temporary; Using filesort”。

  • 如果查詢中有LIMIT的話,LIMIT也會(huì)在排序之后應(yīng)用,所以即使需要返回較少的數(shù)據(jù),臨時(shí)表和需要排序的數(shù)據(jù)量仍然會(huì)非常大。
  • MySQL 5.6在這里做了很多重要的改進(jìn)。當(dāng)只需要返回部分排序結(jié)果的時(shí)候,例如使用了LIMIT子句,MySQL不再對(duì)所有的結(jié)果進(jìn)行排序,而是根據(jù)實(shí)際情況,選擇拋棄不滿足條件的結(jié)果,然后再進(jìn)行排序。

6.4.4 查詢執(zhí)行引擎

在解析和優(yōu)化階段,MySQL將生成查詢對(duì)應(yīng)的執(zhí)行計(jì)劃,MySQL的查詢執(zhí)行引擎則根據(jù)這個(gè)執(zhí)行計(jì)劃來(lái)完成整個(gè)查詢。這里執(zhí)行計(jì)劃是-一個(gè)數(shù)據(jù)結(jié)構(gòu),而不是和很多其他的關(guān)系型數(shù)據(jù)庫(kù)那樣會(huì)生成對(duì)應(yīng)的字節(jié)碼。

相對(duì)于查詢優(yōu)化階段,查詢執(zhí)行階段不是那么復(fù)雜:MySQL只是簡(jiǎn)單地根據(jù)執(zhí)行計(jì)劃給出的指令逐步執(zhí)行。在根據(jù)執(zhí)行計(jì)劃逐步執(zhí)行的過(guò)程中,有大量的操作需要通過(guò)調(diào)用存儲(chǔ)弓|擎實(shí)現(xiàn)的接口來(lái)完成,這些接口也就是我們稱為“handler API”的接口。

查詢中的每一個(gè)表由一個(gè)handler的實(shí)例表示。MySQL在優(yōu)化階段就為每個(gè)表創(chuàng)建了一個(gè)handler實(shí)例,優(yōu)化器根據(jù)這些實(shí)例的接口可以獲取表的相關(guān)信息,包括表的所有列名、索引統(tǒng)計(jì)信息,等等。


存儲(chǔ)引擎接口有著非常豐富的功能,但是底層接口卻只有幾十個(gè),這些接口像“搭積木”一樣能夠完成查詢的大部分操作。簡(jiǎn)單的接口模式,讓MySQL的存儲(chǔ)引擎插件式架構(gòu)成為可能,但也給優(yōu)化器帶來(lái)了一定的限制。

并不是所有的操作都由handler完成。

例如,當(dāng)MySQL需要進(jìn)行表鎖的時(shí)候。handler可能會(huì)實(shí)現(xiàn)自己的級(jí)別的、更細(xì)粒度的鎖,如InnoDB就實(shí)現(xiàn)了自己的行基本鎖,但這并不能代替服務(wù)器層的表鎖。

6.4.5 返回結(jié)果給客戶端

查詢執(zhí)行的最后一個(gè)階段是將結(jié)果返回給客戶端。即使查詢不需要返回結(jié)果集給客戶端,MySQL仍然會(huì)返回這個(gè)查詢一些信息,如該查詢影響到的行數(shù)。

如果查詢可以被緩存,那么MySQL在這個(gè)階段也會(huì)將結(jié)果存放到查詢緩存中。

MySQL將結(jié)果集返回客戶端是一個(gè)增量、逐步返回的過(guò)程。例如,我們回頭看看前面的關(guān)聯(lián)操作,一旦服務(wù)器處理完最后一個(gè)關(guān)聯(lián)表,開(kāi)始生成第一條結(jié)果時(shí),MySQL就可以開(kāi)始向客戶端逐步返回結(jié)果集了。

這樣處理有兩個(gè)好處:

  • 服務(wù)器端無(wú)須存儲(chǔ)太多的結(jié)果,也就不會(huì)因?yàn)橐祷靥嘟Y(jié)果而消耗太多內(nèi)存。
  • 這樣的處理也讓MySQL客戶端第一時(shí)間獲得返回的結(jié)果。

結(jié)果集中的每一行都會(huì)以一個(gè)滿足MySQL客戶端/服務(wù)器通信協(xié)議的封包發(fā)送,再通過(guò)TCP協(xié)議進(jìn)行傳輸,在TCP傳輸?shù)倪^(guò)程中,可能對(duì)MySQL的封包進(jìn)行緩存然后批量傳輸。

6.5 MySQL查詢優(yōu)化器的局限性

對(duì)于開(kāi)發(fā)中使用的版本是否存在這類缺陷, 請(qǐng)自行在使用中判斷。

6.5.1 關(guān)聯(lián)子查詢

MySQL的子查詢實(shí)現(xiàn)得非常糟糕。

最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語(yǔ)句。

例如,我們希望找到Sakila數(shù)據(jù)庫(kù)中,演員Penelope Guiness (他的actor_ id為1)參演過(guò)的所有影片信息。很自然的,我們會(huì)按照下面的方式用子查詢實(shí)現(xiàn):

mysql> SELECT * FROM sakila.film
-> WHERE film_id IN(
->	SELECT film_id FROM sakila.film_actor WHERE actor_id = 1
-> );

因?yàn)镸ySQL對(duì)IN()列表中的選項(xiàng)有專門的優(yōu)化策略,一般會(huì)認(rèn)為MySQL會(huì)先執(zhí)行子查詢返回所有包含actor_id為1的film_id。一般來(lái)說(shuō),IN()列表查詢速度很快,所以我們會(huì)認(rèn)為上面的查詢會(huì)這樣執(zhí)行:

-- SELECT GROUP_ CONCAT(film_id) FROM sakila.film _actor WHERE actor_id = 1;
-- Result: 1,23,25, 106, 140,166,277,361,438,499, 506, 509, 605, 635, 749,832, 939,970, 980
SELECT * FROM sakila.film
WHERE film_id
IN(1,23,25, 106, 140, 166,277,361,438,499, 506, 509, 605,635, 749,832,939,970,980); 

很不幸,MySQL不是這樣做的。MySQL會(huì)將相關(guān)的外層表壓到子查詢中,它認(rèn)為這樣
可以更高效率地查找到數(shù)據(jù)行。也就是說(shuō),MySQL會(huì)將查詢改寫(xiě)成下面的樣子:

SELECT * FROM sakila.film
WHERE EXISTS (
	SELECT * FROM sakila.film actor WHERE actor_id = 1
	AND film_actor.film_id = film.film_id
);

這時(shí),子查詢需要根據(jù)film_id來(lái)關(guān)聯(lián)外部表film,因?yàn)樾枰猣ilm_ id字段,所以MySQL認(rèn)為無(wú)法先執(zhí)行這個(gè)子查詢。通過(guò)EXPLAIN我們可以看到子查詢是一個(gè)相關(guān)子查詢(DEPENDENT SUBQUERY) ( 可以使用EXPLAIN EXTENDED 來(lái)查看這個(gè)查詢被改寫(xiě)成了什么樣子) :

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

可以看到,MySQL將film表全部掃描出來(lái),然后判斷exists里面的條件

可以用下面的辦法來(lái)重寫(xiě)這個(gè)查詢:

mysql> SELECT film.* FROM sakila.film
-> INNER JOIN sakila.film_actor USING(film_id)
-> WHERE actor_id = 1;
是否使用關(guān)聯(lián)子查詢

建議自己寫(xiě)一遍后判斷。

6.5.2 UNION的限制

有時(shí),MySQL無(wú)法將限制條件從外層“下推”到內(nèi)層,這使得原本能夠限制部分返回
結(jié)果的條件無(wú)法應(yīng)用到內(nèi)層查詢的優(yōu)化上。

如果希望UNION的各個(gè)子句能夠根據(jù)LIMIT只取部分結(jié)果集,或者希望能夠先排好序再合并結(jié)果集的話,就需要在UNION的各個(gè)子句中分別使用這些子句。

例如,想將兩個(gè)子查詢結(jié)果聯(lián)合起來(lái),然后再取前20條記錄,那么MySQL會(huì)將兩個(gè)表都存放到同一個(gè)臨時(shí)表中,然后再取出前20行記錄:

(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;

這條查詢將會(huì)把a(bǔ)ctor中的200條記錄和customer表中的599條記錄存放在一個(gè)臨時(shí)表中,然后再?gòu)呐R時(shí)表中取出前20條??梢酝ㄟ^(guò)在UNION的兩個(gè)子查詢中分別加上一個(gè)LIMIT 20來(lái)減少臨時(shí)表中的數(shù)據(jù):

(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;

現(xiàn)在中間的臨時(shí)表只會(huì)包含40條記錄了,除了性能考慮之外,在這里還需要注意一點(diǎn):

  • 從臨時(shí)表中取出數(shù)據(jù)的順序并不是一定的,所以如果想獲得正確的順序,還需要加上一個(gè)全局的ORDERBY和LIMIT操作。

6.5.3 索引合并優(yōu)化

前文

6.5.4 等值傳遞

某些時(shí)候,等值傳遞會(huì)帶來(lái)一些意想不到的額外消耗。

例如,有-個(gè)非常大的IN()列表,而MySQL優(yōu)化器發(fā)現(xiàn)存在WHERE、ON或者USING的子句,將這個(gè)列表的值和另-一個(gè)表的某個(gè)列相關(guān)聯(lián)。

那么優(yōu)化器會(huì)將IN()列表都復(fù)制應(yīng)用到關(guān)聯(lián)的各個(gè)表中。通常,因?yàn)楦鱾€(gè)表新增了過(guò)濾條件,優(yōu)化器可以更高效地從存儲(chǔ)引擎過(guò)濾記錄。但是如果這個(gè)列表非常大,則會(huì)導(dǎo)致優(yōu)化和執(zhí)行都會(huì)變慢。至?xí)霭鏁r(shí)間,除了修改MySQL源代碼,目前還沒(méi)有什么.辦法能夠繞過(guò)該問(wèn)題(不過(guò)這個(gè)問(wèn)題很少會(huì)碰到)。

6.5.5 并行執(zhí)行

MySQL無(wú)法利用多核特性來(lái)并行執(zhí)行查詢。

很多其他的關(guān)系型數(shù)據(jù)庫(kù)能夠提供這個(gè)特性,但是MySQL做不到。

6.5.6 哈希關(guān)聯(lián)

至?xí)霭鏁r(shí)間,MySQL并不支持哈希關(guān)聯(lián)——MySQL 的所有關(guān)聯(lián)都是嵌套循環(huán)關(guān)聯(lián)。

不過(guò),可以通過(guò)建立一個(gè)哈希索引來(lái)曲線地實(shí)現(xiàn)哈希關(guān)聯(lián)。

如果使用的是Memory存儲(chǔ)引擎,則索引都是哈希索引,所以關(guān)聯(lián)的時(shí)候也類似于哈希關(guān)聯(lián)。

MariaDB 已經(jīng)實(shí)現(xiàn)了真正的哈希關(guān)聯(lián)。

6.5.7松散索引掃描

由于歷史原因,MySQL并不支持松散索引掃描,也就無(wú)法按照不連續(xù)的方式掃描一個(gè)索引。

通常,MySQL的索引掃描需要先定義一個(gè)起點(diǎn)和終點(diǎn),即使需要的數(shù)據(jù)只是這段索引中很少數(shù)的幾個(gè),MySQL仍需要掃描這段索引中每一個(gè)條目。

假設(shè)我們有如下索引(a, b),有下面的查詢:

mysql> SELECT ... FROM tbl WHERE b BETWEEN 2 AND 3;

索引的前導(dǎo)字段是列a,在查詢中只指定了字段b, 于是MySQL無(wú)法使用這個(gè)索引,從而只能通過(guò)全表掃描找到匹配的行,如圖6-5所示。

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

松散的查詢則是可以通過(guò)查詢所有a=1下的b,然后再查詢所有a=2下的b,但是MySQL不支持。
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

MySQL 5.0之后的版本,在某些特殊的場(chǎng)景下是可以使用松散索引掃描的,例如,在一個(gè)分組查詢中需要找到分組的最大值和最小值。

6.5.8 最大值和最小值優(yōu)化

對(duì)于MIN()和MAX()查詢,MySQL的優(yōu)化做得并不好。

mysql> SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';

first_name 字段上并沒(méi)有索引,此時(shí)MySQL將會(huì)進(jìn)行一次全表掃描。

如果MySQL能夠進(jìn)行主鍵掃描,那么理論上,當(dāng)MySQL讀到第一個(gè)滿足條件的記錄的時(shí)候,就是我們需要找的最小值了,因?yàn)橹麈I是嚴(yán)格按照actor_ id 字段的大小順序排列的。

但是MySQL這時(shí)只會(huì)做全表掃描,我們可以通過(guò)查看SHOW STATUS 的全表掃描計(jì)數(shù)器來(lái)驗(yàn)證這一點(diǎn)。一個(gè)曲線的優(yōu)化辦法是移除MIN(),然后使用LIMIT來(lái)將查詢重寫(xiě)如下:

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

這個(gè)策略可以讓MySQL掃描盡可能少的記錄數(shù)。如果你是一個(gè)完美主義者,可能會(huì)說(shuō)這個(gè)SQL已經(jīng)無(wú)法表達(dá)她的本意了。一般我們通過(guò)SQL告訴服務(wù)器我們需要什么數(shù)據(jù),由服務(wù)器來(lái)決定如何最優(yōu)地獲取數(shù)據(jù),不過(guò)在這個(gè)案例中,我們其實(shí)是告訴MySQL如何去獲取我們需要的數(shù)據(jù),通過(guò)SQL并不能一眼就看出我們其實(shí)是想要一個(gè)最小值。確實(shí)如此,有時(shí)候?yàn)榱双@得更高的性能,我們不得不放棄一些原則。

6.5.9 在同一個(gè)表.上查詢和更新

MySQL不允許對(duì)同一張表同時(shí)進(jìn)行查詢和更新。

這其實(shí)并不是優(yōu)化器的限制,如果清楚MySQL是如何執(zhí)行查詢的,就可以避免這種情況。

下面是一個(gè)無(wú)法運(yùn)行的SQL,雖然這是一個(gè)符合標(biāo)準(zhǔn)的SQL語(yǔ)句。這個(gè)SQL語(yǔ)句嘗試將兩個(gè)表中相似行的數(shù)量記錄到字段cnt中:

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

可以通過(guò)使用生成表的形式來(lái)繞過(guò)上面的限制,因?yàn)镸ySQL只會(huì)把這個(gè)表當(dāng)作一個(gè)臨
時(shí)表來(lái)處理。

實(shí)際上,這執(zhí)行了兩個(gè)查詢:

  • 一個(gè)是子查詢中的SELECT語(yǔ)句
  • 一個(gè)是多表關(guān)聯(lián)UPDATE,只是關(guān)聯(lián)的表是一個(gè)臨時(shí)表

子查詢會(huì)在UPDATE語(yǔ)句打開(kāi)表之前就完成,所以下面的查詢將會(huì)正常執(zhí)行:

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

6.6 查詢優(yōu)化器的提示

提示、引導(dǎo)優(yōu)化器如何生成最終的SQL語(yǔ)句,已達(dá)到預(yù)想的效果。

這里可以直接查看,書(shū)中文字略。

  • MySQL中文文檔-優(yōu)化器提示
  • MySQL英文文檔-Optimizer Hints

6.7 優(yōu)化特定的查詢

6.7.1 優(yōu)化COUNT()查詢

COUNT()的作用

COUNT()是一個(gè)特殊的函數(shù),有兩種非常不同的作用

  1. 可以統(tǒng)計(jì)某個(gè)列值的數(shù)量

在統(tǒng)計(jì)列值時(shí)要求列值是 非空的 (不統(tǒng)計(jì)NULL)。

如果在COUNT()的括號(hào)中指定了列或者列的表達(dá)式,則統(tǒng)計(jì)的就是這個(gè)表達(dá)式有值的結(jié)果數(shù)(NOT NULL)。

  1. 可以統(tǒng)計(jì)行數(shù)

最簡(jiǎn)單的就是當(dāng)我們使用COUNT(*)的時(shí)候,這種情況下通配符*并不會(huì)像我們猜想的那樣擴(kuò)展成所有的列,實(shí)際上,它會(huì)忽略所有的列而直接統(tǒng)計(jì)所有的行數(shù)。

我們發(fā)現(xiàn)一個(gè)最常見(jiàn)的錯(cuò)誤就是,在括號(hào)內(nèi)指定了一個(gè)列卻希望統(tǒng)計(jì)結(jié)果集的行數(shù)。如果希望知道的是結(jié)果集的行數(shù),最好使用COUNT(*),這樣寫(xiě)意義清晰,性能也會(huì)很好。

關(guān)于MyISAM的神話

一個(gè)容易產(chǎn)生的誤解就是:

  • MyISAM的COUNT()函數(shù)總是非???/li>

不過(guò)這是有前提條件的,即只有沒(méi)有任何WHERE條件的COUNT(*)才非???因?yàn)榇藭r(shí)無(wú)須實(shí)際地去計(jì)算表的行數(shù)。

MySQL可以利用存儲(chǔ)引擎的特性直接獲得這個(gè)值。如果MySQL知道某列coL不可能為NULL值,那么MySQL內(nèi)部會(huì)將COUNT(col)表達(dá)式優(yōu)化為COUNT(*)

當(dāng)統(tǒng)計(jì)帶WHERE子句的結(jié)果集行數(shù),可以是統(tǒng)計(jì)某個(gè)列值的數(shù)量時(shí),MyISAM的COUNT()和其他存儲(chǔ)引擎沒(méi)有任何不同。

所以在MyISAM引擎表上執(zhí)行COUNT( )有時(shí)候比別的引擎快,有時(shí)候比別的引擎慢。

簡(jiǎn)單的優(yōu)化

有時(shí)候可以使用MyISAM在COUNT(*)全表非??斓倪@個(gè)特性,來(lái)加速一些特定條件的COUNT()的查詢。

在下面的例子中,我們使用標(biāo)準(zhǔn)數(shù)據(jù)庫(kù)world來(lái)看看如何快速查找到所有ID大于5的城市。

可以像下面這樣來(lái)寫(xiě)這個(gè)查詢:

mysql> SELECT COUNT(*) FROM world.City WHERE ID > 5;

通過(guò)SHOW STATUS 的結(jié)果可以看到該查詢需要掃描4097行數(shù)據(jù)。

如果將條件反轉(zhuǎn)一下,卻可以將掃描的行數(shù)減少到5行以內(nèi):

mysql> SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
-> FROM world.City WHERE ID <= 5;

在郵件組和IRC聊天頻道中,通常會(huì)看到這樣的問(wèn)題:如何在同一個(gè)查詢中統(tǒng)計(jì)同一個(gè)列的不同值的數(shù)量,以減少查詢的語(yǔ)句量。

例如,假設(shè)可能需要通過(guò)一個(gè)查詢返回各種不同顏色的商品數(shù)量

  • 不能使用OR語(yǔ)句(比如SELECT COUNT(color= ‘blue’ OR color=‘red’) FROM items;), 因?yàn)檫@樣做就無(wú)法區(qū)分不同顏色的商品數(shù)量
  • 不能在WHERE條件中指定顏色( 比如SELECT COUNT(*) FROM ‘items WHERE color=’ blue’ AND color=’ RED’ ; ),因?yàn)轭伾臈l件是互斥的。

下面的查詢可以在一定程度上解決這個(gè)問(wèn)題。

mysql> SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0))
-> AS red FROM items;

也可以使用COUNT()而不是SUM()實(shí)現(xiàn)同樣的目的,只需要將滿足條件設(shè)置為真,不滿
足條件設(shè)置為NULL即可:

mysql> SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULL)
-> AS red FROM items;
使用近似值

有時(shí)候某些業(yè)務(wù)場(chǎng)景并不要求完全精確的COUNT值,此時(shí)可以用近似值來(lái)代替。

EXPLAIN出來(lái)的優(yōu)化器估算的行數(shù)就是一個(gè)不錯(cuò)的近似值,執(zhí)行EXPLAIN并不需要真正地去執(zhí)行查詢,所以成本很低。

更復(fù)雜的優(yōu)化

通常來(lái)說(shuō),COUNT()都需要掃描大量的行(意味著要訪問(wèn)大量數(shù)據(jù))才能獲得精確的結(jié)果,因此是很難優(yōu)化的。

除了前面的方法,在MySQL層面還能做的就只有索引覆蓋掃描了。

如果這還不夠,就需要考慮修改應(yīng)用的架構(gòu),可以增加匯總表,或者增加類似Memcached這樣的外部緩存系統(tǒng)。

“快速,精確和實(shí)現(xiàn)簡(jiǎn)單”,三者永遠(yuǎn)只能滿足其二,必須舍掉其中一個(gè)。

6.7.2 優(yōu)化關(guān)聯(lián)查詢

  • 確保ON或者USING子句中的列上有索引
  • 確保任何的GROUP BY和ORDER BY中的表達(dá)式只涉及到一個(gè)表中的列
  • 當(dāng)升級(jí)MySQL的時(shí)候需要注意:關(guān)聯(lián)語(yǔ)法、運(yùn)算符優(yōu)先級(jí)等其他可能會(huì)發(fā)生變化的地方。因?yàn)橐郧笆瞧胀P(guān)聯(lián)的地方可能會(huì)變成笛卡兒積,不同類型的關(guān)聯(lián)可能會(huì)生成不同的結(jié)果等

6.7.3 優(yōu)化子查詢

關(guān)于子查詢優(yōu)化書(shū)中(因?yàn)榈堑冒姹荆┙o出的最重要的優(yōu)化建議就是盡可能使用關(guān)聯(lián)查詢代替。

實(shí)際如何,需要自己跑一遍

6.7.4 優(yōu)化GROUP BY和DISTINCT

在很多場(chǎng)景下,MySQL都使用同樣的辦法優(yōu)化這兩種查詢,事實(shí)上,MySQL優(yōu)化器會(huì)在內(nèi)部處理的時(shí)候相互轉(zhuǎn)化這兩類查詢。它們都可以使用索引來(lái)優(yōu)化,這也是最有效的優(yōu)化辦法。

在MySQL中,當(dāng)無(wú)法使用索引的時(shí)候,GROUP BY 使用兩種策略來(lái)完成:

  • 使用臨時(shí)表
  • 文件排序來(lái)做分組

對(duì)于任何查詢語(yǔ)句,這兩種策略的性能都有可以提升的地方。可以通過(guò)使用提示SQL_BIG_RESULT 和SQL_SMALL_RESULT 來(lái)讓優(yōu)化器按照你希望的方式運(yùn)行。


如果需要對(duì)關(guān)聯(lián)查詢做分組(GROUP BY), 并且是按照查找表中的某個(gè)列進(jìn)行分組,那么通常采用查找表的標(biāo)識(shí)列分組的效率會(huì)比其他列更高。

但顯然不是所有的關(guān)聯(lián)語(yǔ)句的分組查詢都可以改寫(xiě)成在SELECT中直接使用非分組列的形式
的。

甚至可能會(huì)在服務(wù)器上設(shè)置SQL_MODE來(lái)禁止這樣的寫(xiě)法。

如果是這樣,也可以通過(guò)MIN()或者M(jìn)AX()函數(shù)來(lái)繞過(guò)這種限制,但一定要清楚,SELECT 后面出現(xiàn)的非分組列一定是直接依賴分組列,并且在每個(gè)組內(nèi)的值是唯一的,或者是業(yè)務(wù)上根本不在乎這個(gè)值
具體是什么:

mysql> SELECT MIN(actor.first_ name), MAX(actor.last_name), ...;

較真的人可能會(huì)說(shuō)這樣寫(xiě)的分組查詢是有問(wèn)題的,確實(shí)如此。從MIN()或者M(jìn)AX()函數(shù)的用法就可以看出這個(gè)查詢是有問(wèn)題的。但若更在乎的是MySQL運(yùn)行查詢的效率時(shí)這樣做也無(wú)可厚非。如果實(shí)在較真的話也可以改寫(xiě)成下面的形式:

mysql> SELECT actor.first_name, actor.last_name, c.cnt
-> FROM sakila.actor
->INNER JOIN (
-> SELECT actor_id, COUNT(*) AS cnt 
-> FROM sakila.film_actor
-> GROUP BY actor_id
->) AS C USING(actor_id);

這樣寫(xiě)更滿足關(guān)系理論,但成本有點(diǎn)高,因?yàn)樽硬樵冃枰獎(jiǎng)?chuàng)建和填充臨時(shí)表,而子查詢中創(chuàng)建的臨時(shí)表是沒(méi)有任何索引的。

在分組查詢的SELECT中直接使用非分組列通常都不是什么好主意,因?yàn)檫@樣的結(jié)果通常是不定的,當(dāng)索引改變,或者優(yōu)化器選擇不同的優(yōu)化策略時(shí)都可能導(dǎo)致結(jié)果不一樣。

大多數(shù)這種查詢最后都導(dǎo)致了故障(因?yàn)镸ySQL不會(huì)對(duì)這類查詢返回錯(cuò)誤),而且這種寫(xiě)法大部分是由于偷懶而不是為優(yōu)化而故意這么設(shè)計(jì)的。

事實(shí)上,我們建議將MySQL的SQL_MODE 設(shè)置為包含ONLY_FULL GROUP_BY,這時(shí)MySQL會(huì)對(duì)這類查詢直接返回一個(gè)錯(cuò)誤,提醒你需要重寫(xiě)這個(gè)查詢。


如果沒(méi)有通過(guò)ORDER BY 子句顯式地指定排序列,當(dāng)查詢使用GROUP BY子句的時(shí)候,結(jié)果集會(huì)自動(dòng)按照分組的字段進(jìn)行排序。

如果不關(guān)心結(jié)果集的順序,而這種默認(rèn)排序又導(dǎo)致了需要文件排序,則可以使用ORDER BY NULL, 讓MySQL不再進(jìn)行文件排序。也可以在GROUPBY子句中直接使用DESC或者ASC關(guān)鍵字,使分組的結(jié)果集按需要的方向排序。

優(yōu)化GROUP BY WITH ROLLUP

分組查詢的一個(gè)變種就是要求MySQL對(duì)返回的分組結(jié)果再做一次超級(jí)聚合??梢允褂肳ITH ROLLUP 子句來(lái)實(shí)現(xiàn)這種邏輯,但可能會(huì)不夠優(yōu)化??梢酝ㄟ^(guò)EXPLAIN來(lái)觀察其執(zhí)行計(jì)劃,特別要注意分組是否是通過(guò)文件排序或者臨時(shí)表實(shí)現(xiàn)的。然后再去掉WITH ROLLUP子句看執(zhí)行計(jì)劃是否相同。也可以通過(guò)本節(jié)前面介紹的優(yōu)化器提示來(lái)固定執(zhí)行計(jì)劃。

很多時(shí)候,如果可以,在應(yīng)用程序中做超級(jí)聚合是更好的,雖然這需要返回給客戶端更多的結(jié)果。也可以在FROM子句中嵌套使用子查詢,或者是通過(guò)一個(gè)臨時(shí)表存放中間數(shù)據(jù),然后和臨時(shí)表執(zhí)行UNION來(lái)得到最終結(jié)果。

最好的辦法是盡可能的將WITH ROLLUP 功能轉(zhuǎn)移到應(yīng)用程序中處理。

6.7.5 優(yōu)化LIMIT分頁(yè)

在系統(tǒng)中需要進(jìn)行分頁(yè)操作的時(shí)候,我們通常會(huì)使用LIMIT加上偏移量的辦法實(shí)現(xiàn),同時(shí)加上合適的ORDER BY 子句。

如果有對(duì)應(yīng)的索引,通常效率會(huì)不錯(cuò),否則,MySQL需要做大量的文件排序操作。

一個(gè)非常常見(jiàn)又令人頭疼的問(wèn)題就是,在偏移量非常大的時(shí)候,例如可能是LIMIT1000 , 20這樣的查詢,這時(shí)MySQL需要查詢10 020條記錄然后只返回最后20條,前面10000條記錄都將被拋棄,這樣的代價(jià)非常高。

要優(yōu)化這種查詢,要么是在頁(yè)面中限制分頁(yè)的數(shù)量,要么是優(yōu)化大偏移量的性能。

優(yōu)化此類分頁(yè)查詢的一個(gè)最簡(jiǎn)單的辦法就是盡可能地使用索引覆蓋掃描,而不是查詢所有的列。然后根據(jù)需要做一次關(guān)聯(lián)操作再返回所需的列。對(duì)于偏移量很大的時(shí)候,這樣做的效率會(huì)提升非常大。

考慮下面的查詢:

mysql> SELECT fi1m_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;

如果這個(gè)表非常大,那么這個(gè)查詢最好改寫(xiě)成下面的樣子:

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

這里的“延遲關(guān)聯(lián)”將大大提升查詢效率,它讓MySQL掃描盡可能少的頁(yè)面,獲取需要訪問(wèn)的記錄后再根據(jù)關(guān)聯(lián)列回原表查詢需要的所有列。

這個(gè)技術(shù)也可以用于優(yōu)化關(guān)聯(lián)查詢中的LIMIT子句。

有時(shí)候也可以將LIMIT查詢轉(zhuǎn)換為已知位置的查詢,讓MySQL通過(guò)范圍掃描獲得到對(duì)應(yīng)的結(jié)果。例如,如果在一個(gè)位置列上有索引,并且預(yù)先計(jì)算出了邊界值,上面的查詢就可以改寫(xiě)為:

mysql> SELECT film_id, description FROM sakila.film
-> WHERE position BETWEEN 50 AND 54 ORDER BY position;

6.7.6 優(yōu)化SQL_CALC_FOUND_ROWS

分頁(yè)的時(shí)候,另一個(gè)常用的技巧是在LIMIT語(yǔ)句中加上SQL_CALC_FOUND_ROWS 提示(hint),這樣就可以獲得去掉LIMIT以后滿足條件的行數(shù),因此可以作為分頁(yè)的總數(shù)。

看起來(lái),MySQL做了一些非?!案呱睢钡膬?yōu)化,像是通過(guò)某種方法預(yù)測(cè)了總行數(shù)。但實(shí)際上,MySQL只有在掃描了所有滿足條件行以后,才會(huì)知道行數(shù),所以加上這個(gè)提示以后,不管是否需要,MySQL都會(huì)掃描所有滿足條件的行,然后再拋棄掉不需要的行,而不是在滿足LIMIT的行數(shù)后就終止掃描。所以該提示的代價(jià)可能非常高。

  • 一個(gè)更好的設(shè)計(jì)是將具體的頁(yè)數(shù)換成“下一頁(yè)”按鈕,假設(shè)每頁(yè)顯示20條記錄,那么我們每次查詢時(shí)都是用LIMIT返回21條記錄并只顯示20條,如果第21條存在,那么我們就顯示“下一頁(yè)”按鈕,否則就說(shuō)明沒(méi)有更多的數(shù)據(jù),也就無(wú)須顯示“下一頁(yè)"按鈕了。
  • 另一種做法是先獲取并緩存較多的數(shù)據(jù)一例如, 緩存1000 條一然后 每次分頁(yè)都從這個(gè)緩存中獲取。這樣做可以讓?xiě)?yīng)用程序根據(jù)結(jié)果集的大小采取不同的策略,如果結(jié)果集少于1 000,就可以在頁(yè)面上顯示所有的分頁(yè)鏈接,因?yàn)閿?shù)據(jù)都在緩存中,所以這樣做性能不會(huì)有問(wèn)題。如果結(jié)果集大于1 000,則可以在頁(yè)面上設(shè)計(jì)一個(gè)額外的“找到的結(jié)果多于1000條”之類的按鈕。

這兩種策略都比每次生成全部結(jié)果集再拋棄掉不需要的數(shù)據(jù)的效率要高很多。

有時(shí)候也可以考慮使用EXPLAIN的結(jié)果中的rows列的值來(lái)作為結(jié)果集總數(shù)的近似值。

當(dāng)需要精確結(jié)果的時(shí)候,再單獨(dú)使用COUNT(*)來(lái)滿足需求,這時(shí)如果能夠使用索引覆蓋掃描則通常也會(huì)比SQL_CALC_ FOUND_ROWS快得多。

6.7.7 優(yōu)化UNION查詢

MySQL總是通過(guò)創(chuàng)建并填充臨時(shí)表的方式來(lái)執(zhí)行UNION查詢。因此很多優(yōu)化策略在UNION查詢中都沒(méi)法很好地使用。經(jīng)常需要手工地將WHERE、LIMIT. ORDER BY等子句“下推”到UNION的各個(gè)子查詢中,以便優(yōu)化器可以充分利用這些條件進(jìn)行優(yōu)化。

除非確實(shí)需要服務(wù)器消除重復(fù)的行,否則就一定要使用UNION ALL, 這一點(diǎn)很重要。如果沒(méi)有ALL關(guān)鍵字,MySQL會(huì)給臨時(shí)表加上DISTINCT選項(xiàng),這會(huì)導(dǎo)致對(duì)整個(gè)臨時(shí)表的數(shù)據(jù)做唯一性檢查。這樣做的代價(jià)非常高。即使有ALL關(guān)鍵字,MySQL仍然會(huì)使用臨時(shí)表存儲(chǔ)結(jié)果。事實(shí)上, MySQL總是將結(jié)果放入臨時(shí)表,然后再讀出,再返回給客戶端。

6.7.8靜態(tài)查詢分析

Percona Toolkit 中的pt-query advisor能夠解析查詢?nèi)罩?、分析查詢模式,然后給出所有可能存在潛在問(wèn)題的查詢,并給出足夠詳細(xì)的建議。這像是給MySQL所有的查詢做一次全面的健康檢查。它能檢測(cè)出許多常見(jiàn)的問(wèn)題,諸如我們前面介紹的內(nèi)容。

6.7.9 使用用戶自定義變量

用戶自定義變量是一個(gè)容易被遺忘的MySQL特性,但是如果能夠用好,發(fā)揮其潛力,在某些場(chǎng)景可以寫(xiě)出非常高效的查詢語(yǔ)句。

在查詢中混合使用過(guò)程化和關(guān)系化邏輯的時(shí)候,自定義變量可能會(huì)非常有用。

單純的關(guān)系查詢將所有的東西都當(dāng)成無(wú)序的數(shù)據(jù)集合,并且一次性操作它們。MySQL則采用了更加程序化的處理方式。MySQL的這種方式有
它的弱點(diǎn),但如果能熟練地掌握,則會(huì)發(fā)現(xiàn)其強(qiáng)大之處,而用戶自定義變量也可以給這種方式帶來(lái)很大的幫助。

用戶自定義變量是-個(gè)用來(lái)存儲(chǔ)內(nèi)容的臨時(shí)容器,在連接MySQL的整個(gè)過(guò)程中都存在。

可以使用下面的SET和SELECT語(yǔ)句來(lái)定義它們 :

mysql> SET @one:= 1;
mysql> SET @min_actor:= (SELECT MIN(actor_id) FROM sakila.actor);
mysql> SET @last_week:= CURRENT_DATE-INTERVAL 1 WEEK;

select、declare、set均可申明變量預(yù)賦值,具體可以參考-SQL server數(shù)據(jù)庫(kù)declare和set、用法技巧,賦值建議使用:=而不是=

然后可以在任何可以使用表達(dá)式的地方使用這些自定義變量:

mysql> SELECT ... WHERE col <= @last_week;

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

  • 使用自定義變量的查詢,無(wú)法使用查詢緩存。
  • 不能在使用常量或者標(biāo)識(shí)符的地方使用自定義變量,例如表名、列名和LIMIT子句中。
  • 用戶自定義變量的生命周期是在一個(gè)連接中有效,所以不能用它們來(lái)做連接間的通信。
  • 如果使用連接池或者持久化連接,自定義變量可能讓看起來(lái)毫無(wú)關(guān)系的代碼發(fā)生交互。
  • 在5.0之前的版本,是大小寫(xiě)敏感的,所以要注意代碼在不同MySQL版本間的兼容性問(wèn)題。
  • 不能顯式地聲明自定義變量的類型。確定未定義變量的具體類型的時(shí)機(jī)在不同MySQL版本中也可能不一樣。如果你希望變量是整數(shù)類型,那么最好在初始化的時(shí)候就賦值為0,如果希望是浮點(diǎn)型則賦值為0.0,如果希望是字符串則賦值為",用戶自定義變量的類型在賦值的時(shí)候會(huì)改變。MySQL的用戶自定義變量是一個(gè)動(dòng)態(tài)類型。
  • MySQL優(yōu)化器在某些場(chǎng)景下可能會(huì)將這些變量?jī)?yōu)化掉,這可能導(dǎo)致代碼不按預(yù)想的方式運(yùn)行。賦值的順序和賦值的時(shí)間點(diǎn)并不總是固定的,這依賴于優(yōu)化器的決定。實(shí)際情況可能很讓人困惑,后面我們將看到這一點(diǎn)。
  • 賦值符號(hào):= 的優(yōu)先級(jí)非常低,所以需要注意,賦值表達(dá)式應(yīng)該使用明確的括號(hào)。
  • 使用未定義變量不會(huì)產(chǎn)生任何語(yǔ)法錯(cuò)誤,如果沒(méi)有意識(shí)到這一點(diǎn),非常容易犯錯(cuò)。
優(yōu)化排名語(yǔ)句

下面的例子展示了如何使用變量來(lái)實(shí)現(xiàn)一個(gè)類似“行號(hào)(row number)”的功能:

mysql> SET @rownum := 0;
mysql> SELECT actor_ id, @rownum := @rownum + 1 AS rownum
-> FROM sakila.actor LIMIT 3;

在我們來(lái)看一個(gè)更復(fù)雜的用法。我們先編寫(xiě)一個(gè)查詢獲取演過(guò)最多電影的前10位演員,然后根據(jù)他們的出演電影次數(shù)做一個(gè)排名,如果出演的電影數(shù)量一樣,則排名相同。我們先編寫(xiě)一個(gè)查詢,返回每個(gè)演員參演電影的數(shù)量:

mysql> SELECT actor_id, COUNT(*) as cnt
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10; 

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
現(xiàn)在我們?cè)侔雅琶由先?,這里看到有四名演員都參演了35部電影,所以他們的排名應(yīng)該是相同的。

我們使用三個(gè)變量來(lái)實(shí)現(xiàn):

  • 一個(gè)用來(lái)記錄當(dāng)前的排名
  • 一個(gè)用來(lái)記錄前一個(gè)演員的排名
  • 還有一個(gè)用來(lái)記錄當(dāng)前演員參演的電影數(shù)量

只有當(dāng)前演員參演的電影的數(shù)量和前一個(gè)演員不同時(shí),排名才變化。

mysql> SET @curr_cnt:= 0, @prev_cnt:= 0, @rank:= 0;
mysql> SELECT actor_id,
-> @curr_cnt:= COUNT(*) AS cnt,
-> @rank:= IF(@prev_cnt<> @curr_cnt,@rank + 1, @rank) AS rank,
-> @prev_cnt:= @curr_cnt AS dummy
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10;

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

  1. 可以使用子查詢生成子表
mysql> SET @curr_cnt:= 0, @prev_cnt:= 0, @rank:= 0;
mysql> SELECT actor_id,
-> @curr_cnt:= cnt AS cnt,
-> @rank:= IF(@prev_cnt<> @curr_cnt,@rank + 1, @rank) AS rank,
-> @prev_cnt:= @curr_cnt AS dummy
-> FROM (SELECT actor_id,COUNT(*) as cnt
-> 	FROM sakila.film_actor
-> 	GROUP BY actor_id
-> 	ORDER BY cnt DESC
-> 	LIMIT 10
-> )AS der;

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

避免重復(fù)查詢剛剛更新的數(shù)據(jù)

ySQL并不支持像PostgreSQL那樣的UPDATE RETURNING 語(yǔ)法,這個(gè)語(yǔ)法可以幫你在更新行的時(shí)候同時(shí)返回該行的信息。

在MySQL中你可以使用變量來(lái)解決這個(gè)問(wèn)題。

例如,我們的一個(gè)客戶希望能夠更高效地更新一條記錄的時(shí)間戳,同時(shí)希望查詢當(dāng)前記錄中存放的時(shí)間戳是什么。

簡(jiǎn)單地,可以用下面的代碼來(lái)實(shí)現(xiàn):

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 WHERE id = 1;

使用變量,我們可以按如下方式重寫(xiě)查詢:

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now:= NOW();
SELECT @now;

上面看起來(lái)仍然需要兩個(gè)查詢,需要兩次網(wǎng)絡(luò)來(lái)回,但是這里的第二個(gè)查詢無(wú)須訪問(wèn)任何數(shù)據(jù)表,所以會(huì)快非常多。

統(tǒng)計(jì)更新和插入的數(shù)量

當(dāng)使用了INSERT ON DUPLICATE KEY UPDATE的時(shí)候,如果想知道到底插入了多少行數(shù)據(jù),到底有多少數(shù)據(jù)是因?yàn)闆_突而改寫(xiě)成更新操作的?

現(xiàn)辦法的本質(zhì)如下:

INSERT INTO t1(c1, c2) 
VALUES(4, 4), (2, 1), (3, 1)
ON DUPLICATE KEY UPDATE C1 = VALUES(c1) + (0*(@x :=@x+1 ) );

當(dāng)每次由于沖突導(dǎo)致更新時(shí)對(duì)變量@x自增一次。然后通過(guò)對(duì)這個(gè)表達(dá)式乘以0來(lái)讓其不影響要更新的內(nèi)容。

另外,MySQL的協(xié)議會(huì)返回被更改的總行數(shù),所以不需要單獨(dú)統(tǒng)計(jì)這個(gè)值。

確定取值的順序

使用用戶自定義變量的一個(gè)最常見(jiàn)的問(wèn)題就是沒(méi)有注意到在賦值和讀取變量的時(shí)候可能是在查詢的不同階段。

例如,在SELECT子句中進(jìn)行賦值然后在WHERE子句中讀取變量,則可能變量取值并不如你所想。下面的查詢看起來(lái)只返回一個(gè)結(jié)果,但事實(shí)并非如此:

mysql> SET @rownum:= 0;
mysql> SELECT actor_id, @rownum:=@rownum + 1 AS cnt
->FROM sakila.actor
-> WHERE @rownum <= 1;

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記
因?yàn)閃HERE和SELECT是在查詢執(zhí)行的不同階段被執(zhí)行的。如果在查詢中再加入ORDERBY的話,結(jié)果可能會(huì)更不同:

mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
-> FROM sakila.actor
-> WHERE @rownum <= 1
-> ORDER BY first name;

這是因?yàn)镺RDERBY引入了文件排序,而WHERE條件是在文件排序操作之前取值的,所以這條查詢會(huì)返回表中的全部記錄。解決這個(gè)問(wèn)題的辦法是讓變量的賦值和取值發(fā)生在執(zhí)行查詢的同一階段:

mysq1> SET @rownum := 0;
mysql> SELECT actor.id, @rownum AS rownum
-> FROM sakila.actor
-> WHERE (@rownum := @rownum + 1) <= 1;

《高性能MySQL》——查詢性能優(yōu)化(筆記),MySQL,mysql,性能優(yōu)化,筆記

編寫(xiě)偷懶的UNION

假設(shè)需要編寫(xiě)一個(gè)UNION查詢,其第一個(gè)子查詢作為分支條件先執(zhí)行,如果找到了匹配的行,則跳過(guò)第二個(gè)分支。

在某些業(yè)務(wù)場(chǎng)景中確實(shí)會(huì)有這樣的需求,比如先在一個(gè)頻繁訪問(wèn)的表中查找“ 熱”數(shù)據(jù),找不到再去另外一個(gè)較少訪問(wèn)的表中查找“冷”數(shù)據(jù)。(區(qū)分熱數(shù)據(jù)和冷數(shù)據(jù)是一個(gè)很好的提高緩存命中率的辦法)。

下面的查詢會(huì)在兩個(gè)地方查找一個(gè)用戶一個(gè)主用戶表、 一個(gè)長(zhǎng)時(shí)間不活躍的用戶表,不活躍用戶表的目的是為了實(shí)現(xiàn)更高效的歸檔。

SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;

上述的查詢可以用用戶變量來(lái)優(yōu)化

SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'
FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1,'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;

其余案例略

附錄

《高性能MySQL》
Baron Schwartz, Peter Zaitsev, Vadim Tkachenko 著
寧海元 周振興 彭立勛 翟衛(wèi)祥 劉輝 譯文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-637826.html

到了這里,關(guān)于《高性能MySQL》——查詢性能優(yōu)化(筆記)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 讀高性能MySQL(第4版)筆記06_優(yōu)化數(shù)據(jù)類型(上)

    讀高性能MySQL(第4版)筆記06_優(yōu)化數(shù)據(jù)類型(上)

    3.2.2.1.?字符集和排序規(guī)則(collation)使字符型數(shù)據(jù)的比較更復(fù)雜 3.2.2.2.?應(yīng)該將日期和時(shí)間存儲(chǔ)為MySQL的內(nèi)置類型而不是字符串類型 3.2.2.3.?應(yīng)該用整型數(shù)據(jù)存儲(chǔ)IP地址 4.1.1.1.?使用8、16、24、32和64位存儲(chǔ)空間 5.2.4.1.?VARCHAR(1000)的列則需要1002個(gè)字節(jié),因?yàn)樾枰?字節(jié)存儲(chǔ)長(zhǎng)度

    2024年02月09日
    瀏覽(24)
  • 讀高性能MySQL(第4版)筆記07_優(yōu)化數(shù)據(jù)類型(下)

    讀高性能MySQL(第4版)筆記07_優(yōu)化數(shù)據(jù)類型(下)

    2.3.1.1.?與UNIX時(shí)間戳相同 2.3.2.1.?會(huì)遇到2038年的問(wèn)題 2.3.2.1.1.?使用帶符號(hào)的32位INT,可以表達(dá)直到2038年的時(shí)間 2.3.2.1.2.?使用無(wú)符號(hào)的32位INT,可以表達(dá)直到2106年的時(shí)間 2.3.2.1.3.?使用64位,還可以超出這些范圍 2.3.3.1.?MySQL服務(wù)器、操作系統(tǒng)和客戶端連接都有時(shí)區(qū)設(shè)置 2.3.3.2.?存

    2024年02月09日
    瀏覽(21)
  • 讀高性能MySQL(第4版)筆記05_優(yōu)化服務(wù)器設(shè)置

    讀高性能MySQL(第4版)筆記05_優(yōu)化服務(wù)器設(shè)置

    2.1.3.1.?MySQL只需要少量的內(nèi)存就能保持一個(gè)連接(通常是一個(gè)相關(guān)的專用線程)打開(kāi) 2.2.1.1.?InnoDB緩沖池大小 2.2.1.2.?需要的內(nèi)存比其他任何組件都多 2.2.1.3.?不僅緩存索引,還緩存行數(shù)據(jù)、自適應(yīng)哈希索引、更改緩沖區(qū)、鎖和其他內(nèi)部結(jié)構(gòu)等 2.2.1.4.?InnoDB嚴(yán)重依賴緩沖池,應(yīng)

    2024年02月09日
    瀏覽(32)
  • 讀高性能MySQL(第4版)筆記04_操作系統(tǒng)和硬件優(yōu)化

    讀高性能MySQL(第4版)筆記04_操作系統(tǒng)和硬件優(yōu)化

    4.9.2.1.?允許在內(nèi)存中更改頁(yè)面,而不用將更改刷新到磁盤,這通常涉及隨機(jī)I/O,速度非常慢 4.9.2.2.?將更改的記錄寫(xiě)入順序日志文件,這樣要快得多 4.9.2.3.?后臺(tái)線程可以稍后將修改過(guò)的頁(yè)面刷新到磁盤,這樣做可以優(yōu)化寫(xiě)操作的性能 5.11.2.1.?只有進(jìn)行特殊的擦除操作之后,

    2024年02月09日
    瀏覽(23)
  • 高性能MySQL實(shí)戰(zhàn)(三):性能優(yōu)化

    大家好,我是 方圓 。這篇主要介紹對(duì)慢 SQL 優(yōu)化的一些手段,而在講解具體的優(yōu)化措施之前,我想先對(duì) EXPLAIN 進(jìn)行介紹,它是我們?cè)诜治霾樵儠r(shí)必要的操作,理解了它輸出結(jié)果的內(nèi)容更有利于我們優(yōu)化 SQL。為了方便大家的閱讀,在下文中規(guī)定類似 key1 的表示二級(jí)索引,key_

    2024年02月11日
    瀏覽(93)
  • MySQL高性能優(yōu)化規(guī)范建議

    數(shù)據(jù)庫(kù)命令規(guī)范 數(shù)據(jù)庫(kù)基本設(shè)計(jì)規(guī)范 1. 所有表必須使用 Innodb 存儲(chǔ)引擎 2. 數(shù)據(jù)庫(kù)和表的字符集統(tǒng)一使用 UTF8 3. 所有表和字段都需要添加注釋 4. 盡量控制單表數(shù)據(jù)量的大小,建議控制在 500 萬(wàn)以內(nèi)。 5. 謹(jǐn)慎使用 MySQL 分區(qū)表 6.盡量做到冷熱數(shù)據(jù)分離,減小表的寬度 7. 禁止在表中建

    2024年02月12日
    瀏覽(20)
  • 《高性能MySQL》——?jiǎng)?chuàng)建高性能的索引(筆記)

    《高性能MySQL》——?jiǎng)?chuàng)建高性能的索引(筆記)

    索引(在MySQL中也叫做“鍵(key)”) 是存儲(chǔ)引擎用于快速找到記錄的一種數(shù)據(jù)結(jié)構(gòu)。 索引對(duì)于良好的性能非常關(guān)鍵。尤其是當(dāng)表中的數(shù)據(jù)量越來(lái)越大時(shí),索引對(duì)性能的影響愈發(fā)重要。 在數(shù)據(jù)量較小且負(fù)載較低時(shí),不恰當(dāng)?shù)乃饕龑?duì)性能的影響可能還不明顯,但當(dāng)數(shù)據(jù)量逐漸增大時(shí)

    2024年02月07日
    瀏覽(95)
  • 數(shù)據(jù)庫(kù)——MySQL高性能優(yōu)化規(guī)范

    所有數(shù)據(jù)庫(kù)對(duì)象名稱必須使用小寫(xiě)字母并用下劃線分割 所有數(shù)據(jù)庫(kù)對(duì)象名稱禁止使用 MySQL 保留(如果表名中包含查詢時(shí),需要將其用單引號(hào)括起來(lái)) 數(shù)據(jù)庫(kù)對(duì)象的命名要能做到見(jiàn)名識(shí)意,并且最后不要超過(guò) 32 個(gè)字符 臨時(shí)庫(kù)表必須以 tmp_為前綴并以日期為后綴,

    2024年02月11日
    瀏覽(49)
  • 讀高性能MySQL(第4版)筆記09_創(chuàng)建高性能索引(下)

    讀高性能MySQL(第4版)筆記09_創(chuàng)建高性能索引(下)

    1.4.4.1.?InnoDB的二級(jí)索引在葉子節(jié)點(diǎn)中保存了記錄的主鍵值,所以如果二級(jí)索引能夠覆蓋查詢,則可以避免對(duì)主鍵索引的二次查詢 7.1.5.1.?常見(jiàn)的類似錯(cuò)誤通常是由于嘗試使用rsync備份InnoDB導(dǎo)致的 7.3.3.1.?否則,對(duì)于范圍查詢、索引覆蓋掃描等操作來(lái)說(shuō),速度可能會(huì)降低很多 7

    2024年02月08日
    瀏覽(97)
  • 讀高性能MySQL(第4版)筆記08_創(chuàng)建高性能索引(上)

    讀高性能MySQL(第4版)筆記08_創(chuàng)建高性能索引(上)

    2.4.2.1.?按照索引列中的數(shù)據(jù)大小順序存儲(chǔ)的 2.4.3.1.?鍵前綴查找只適用于根據(jù)最左前綴的查找 2.4.4.1.?在查詢某些條件的數(shù)據(jù)時(shí),存儲(chǔ)引擎不再需要進(jìn)行全表掃描 2.4.4.2.?通過(guò)比較節(jié)點(diǎn)頁(yè)的值和要查找的值可以找到合適的指針進(jìn)入下層子節(jié)點(diǎn),這些指針實(shí)際上定義了子節(jié)點(diǎn)頁(yè)中

    2024年02月08日
    瀏覽(94)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包