掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流
在業(yè)務(wù)項(xiàng)目發(fā)展過(guò)程中,我們常常會(huì)面對(duì)要處理 MySQL 慢查詢(xún)問(wèn)題,那我們應(yīng)該如何分析解決問(wèn)題呢?

部分同學(xué)在處理 MySQL 慢查詢(xún)時(shí)候主要思路是加索引來(lái)解決,加索引確實(shí)是一個(gè)很好的解決問(wèn)題的手段,但不是全部。既然慢查詢(xún)是問(wèn)題,那就需要明確問(wèn)題發(fā)生原因,和解決問(wèn)題路徑分析。我們一起來(lái) get 下 MySQL 慢查詢(xún)的正確姿勢(shì)。
首先需要明確:一個(gè)查詢(xún) SQL 的執(zhí)行到底經(jīng)歷了什么?
數(shù)據(jù)庫(kù)執(zhí)行 sql 的大致流程如下:
具體執(zhí)行過(guò)程可能會(huì)因 MySQL 服務(wù)器具體配置和執(zhí)行場(chǎng)景有一些差異。
情況如下:
我們可以把查詢(xún) SQL 執(zhí)行看做是一個(gè)任務(wù)的話(huà),那它是由一些列子任務(wù)組成的,每個(gè)子任務(wù)都存在一定的時(shí)間消耗。通常情況下,導(dǎo)致慢查詢(xún)最根本的問(wèn)題就是訪問(wèn)的數(shù)據(jù)太多,導(dǎo)致查詢(xún)不可避免的需要篩選大量的數(shù)據(jù)。
面對(duì)慢查詢(xún),我們需要注意以下兩點(diǎn):
MySQL 并不是只返回需要的數(shù)據(jù),實(shí)際上會(huì)返回全部結(jié)果集再進(jìn)行計(jì)算。
尤其是多表關(guān)聯(lián)查詢(xún) select * 的情況,我們是不是真的需要全部的列呢?如果不是,那我們直接指定對(duì)應(yīng)字段就好了。
例如我們要查詢(xún)用戶(hù)關(guān)聯(lián)訂單下的商品信息,如下所示:
SELECT *
FROM users
LEFT JOIN orders ON orders.user_id = users.user_id
LEFT JOIN goods ON goods.good_id = orders.good_id
WHERE users.name = 'zhangsan';
這將返回三個(gè)表的全部數(shù)據(jù)列,可以調(diào)整為僅取需要的列:
SELECT goods.title, goods.description
FROM users
LEFT JOIN orders ON orders.user_id = users.user_id
LEFT JOIN goods ON goods.good_id = orders.good_id
WHERE users.name = 'zhangsan';
取出全部列,會(huì)讓優(yōu)化器無(wú)法完成索引覆蓋掃描這類(lèi)優(yōu)化,還會(huì)為服務(wù)器帶來(lái)額外的 I/O、內(nèi)存和 CPU 的消耗。
此種情況大部分屬于索引應(yīng)用不當(dāng)造成的(包括:該建的索引沒(méi)有建,或者未應(yīng)用到最佳索引)。
實(shí)例表結(jié)構(gòu)如下:
CREATE TABLE `test_table` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`desc` varchar(32) DEFAULT NULL,
`age` int(16) DEFAULT NULL,
KEY `idx_age` (`age`)
) ENGINE = InnoDB CHARSET = utf8mb4;
示例查詢(xún) SQL 執(zhí)行計(jì)劃:
EXPLAIN SELECT * FROM test_table WHERE age = 10;
應(yīng)用索引 idx_age 后, 預(yù)估訪問(wèn) 1 行數(shù)據(jù),如下圖所示:
如刪除有效索引后則會(huì)變成全表掃描(ALL),預(yù)估需要掃描 121524 條記錄才能完成這個(gè)查詢(xún),如下圖所示:
發(fā)現(xiàn)了慢查詢(xún)之后,關(guān)于如何定位問(wèn)題發(fā)生原因,最常用的方法就是利用 EXPLAIN 關(guān)鍵字模擬查詢(xún)優(yōu)化器執(zhí)行查詢(xún) SQL,從而知道 MySQL 是如何處理你的查詢(xún) SQL,通過(guò)執(zhí)行計(jì)劃來(lái)分析性能瓶頸。
通常我們使用 EXPLAIN,會(huì)得到如下的執(zhí)行計(jì)劃信息:
關(guān)于各字段含義,大家可以通過(guò)檢索自行了解,在此就不再過(guò)多贅述。
關(guān)于定位分析問(wèn)題,關(guān)鍵看如下幾點(diǎn)。
表示查詢(xún)類(lèi)型,用于區(qū)別普通查詢(xún)、聯(lián)合查詢(xún)、子查詢(xún)等復(fù)雜查詢(xún)。
顯示查詢(xún)使用類(lèi)型,從好到差依次為:
system > const > eq_ref > ref > range > index > all
分別指可能應(yīng)用的索引和實(shí)際應(yīng)用的索引。
注意:查詢(xún)中若使用了覆蓋索引(select 后要查詢(xún)的字段剛好和創(chuàng)建的索引字段完全相同),則該索引僅出現(xiàn)在 key 列表中。
大致估算出找到所需記錄所需要讀取的行數(shù)(從效率上來(lái)講,數(shù)值越小越好)。
重要的額外信息。包含 MySQL 解決查詢(xún)的詳細(xì)信息,也是關(guān)鍵參考項(xiàng)之一。
我們通過(guò) EXPLAIN 關(guān)鍵字模擬查詢(xún)優(yōu)化器執(zhí)行查詢(xún) SQL,發(fā)現(xiàn)了慢查詢(xún)問(wèn)題原因,那看看如何才能有效解決呢?
推薦幾種較為實(shí)用的解決方案給大家。
MySQL 支持很多數(shù)據(jù)類(lèi)型,選擇合適的數(shù)據(jù)類(lèi)型存儲(chǔ)數(shù)據(jù)對(duì)性能有很大的影響。
通常來(lái)說(shuō),可以遵循以下一些指導(dǎo)原則。
越小的數(shù)據(jù)類(lèi)型通常在磁盤(pán)、內(nèi)存和 CPU 緩存中都需要更少的空間,處理起來(lái)更快。
整型數(shù)據(jù)比起字符,處理開(kāi)銷(xiāo)更小,因?yàn)樽址谋容^更復(fù)雜。在 MySQL 中,應(yīng)該用內(nèi)置的日期和時(shí)間數(shù)據(jù)類(lèi)型,而不是用字符串來(lái)存儲(chǔ)時(shí)間;以及用整型數(shù)據(jù)類(lèi)型存儲(chǔ) IP 地址。
應(yīng)該指定列為 NOT NULL,在 MySQL 中,含有空值的列很難進(jìn)行查詢(xún)優(yōu)化,因?yàn)樗鼈兪沟盟饕?、索引的統(tǒng)計(jì)信息以及比較運(yùn)算更加復(fù)雜。
你可以用 0、一個(gè)特殊的值或者一個(gè)空串代替 NULL 值。
范式化模型要求滿(mǎn)足下面三大范式:
1. 數(shù)據(jù)庫(kù)表中每個(gè)字段只包含最小的信息屬性,不能再進(jìn)行細(xì)化分解。
2. 模型含有主鍵,非主鍵字段依賴(lài)主鍵(在滿(mǎn)足 1 的基礎(chǔ)上)。
比如用戶(hù)這個(gè)模型,它的主鍵是用戶(hù) ID,那么用戶(hù)模型其它字段都應(yīng)該依賴(lài)于用戶(hù) ID,如商品 ID 和用戶(hù)沒(méi)有直接關(guān)系,則這個(gè)屬性不應(yīng)該放到用戶(hù)模型而應(yīng)該放到”用戶(hù)-商品”關(guān)聯(lián)訂單表。
3. 模型非主鍵字段不能相互依賴(lài)(在滿(mǎn)足 2 的基礎(chǔ)上)。
例如:設(shè)計(jì)訂單表(訂單 ID、用戶(hù) ID、用戶(hù)姓名……)
乍一看該表滿(mǎn)足第二范式,每列都和主鍵列“訂單 ID”相關(guān),但是其中“用戶(hù) ID”和“用戶(hù)姓名”相關(guān),而且“用戶(hù) ID”和“訂單 ID”又也相關(guān),依次推斷:“用戶(hù)姓名”和“訂單 ID”也相關(guān)。不滿(mǎn)足第三范式,應(yīng)去掉訂單表“用戶(hù)姓名” 列,放入到用戶(hù)表中。
反范式化模型即不滿(mǎn)足范式化的模型。主要是為了性能和效率的考慮適當(dāng)?shù)倪`反范式化設(shè)計(jì)要求,允許存在少量的數(shù)據(jù)冗余,即以空間換時(shí)間。
可見(jiàn)一個(gè)良好而實(shí)用的數(shù)據(jù)模型往往是依賴(lài)于具體的需求場(chǎng)景的,在設(shè)計(jì)數(shù)據(jù)模型之前,仔細(xì)分析需求場(chǎng)景,不僅能提高效率,也能有效規(guī)避后期可能遇到的一些意外麻煩。
范式化設(shè)計(jì)和反范式化設(shè)計(jì)的對(duì)比:
索引(MySQL 中也被稱(chēng)為“鍵 Key”),是存儲(chǔ)引擎用于快速找到記錄的一種數(shù)據(jù)結(jié)構(gòu)。索引對(duì)于良好的性能非常關(guān)鍵,尤其當(dāng)表中的數(shù)據(jù)量越來(lái)越大時(shí),索引對(duì)性能的影響愈發(fā)重要(不恰當(dāng)?shù)乃饕龑?duì)會(huì)隨數(shù)據(jù)量增大時(shí),性能急劇下降)。
舉例如下情況:
假設(shè)數(shù)據(jù)庫(kù)中一個(gè)表有 10^6 條記錄,DBMS 的頁(yè)面大小為 4K(約可存儲(chǔ) 100 條記錄)。
如果沒(méi)有索引,查詢(xún)將對(duì)整個(gè)表進(jìn)行掃描,最壞的情況下,如果所有數(shù)據(jù)頁(yè)都不在內(nèi)存,需要讀取 10^4 個(gè)頁(yè)面,如果這 10^4 個(gè)頁(yè)面在磁盤(pán)上隨機(jī)分布,需要進(jìn)行 10^4 次 I/O,假設(shè)磁盤(pán)每次 I/O 時(shí)間為 10ms(忽略數(shù)據(jù)傳輸時(shí)間),則總共需要 100s(但實(shí)際上要好很多很多)。
如果對(duì)之建立 B-Tree 索引,則只需要進(jìn)行 log100( 10^6 )=3 次頁(yè)面讀取,最壞情況下耗時(shí) 30ms。這就是索引帶來(lái)的效果。
了解了索引的優(yōu)點(diǎn)之后,其實(shí)正確的創(chuàng)建和使用索引是實(shí)現(xiàn)高性能查詢(xún)的基礎(chǔ)。
可以利用 B-Tree 索引進(jìn)行全關(guān)鍵字、關(guān)鍵字范圍和關(guān)鍵字前綴查詢(xún),當(dāng)然,如果想使用索引,必須保證按索引的最左邊前綴(leftmost prefix of the index)來(lái)進(jìn)行查詢(xún)。
最左前綴原則主要使用在聯(lián)合索引中。
例如:聯(lián)合索引 idx_col1_col2_col3(col1、col2、col3)。
對(duì)索引中的所有列都指定具體的值:
-- 以下均可以應(yīng)用到此聯(lián)合索引(此情況字段順序無(wú)關(guān))
col1 = 'a' AND col2 = 'b' AND col3 = 'c'
col2 = 'c' AND col3 = 'b' AND col1 = 'a'
col3 = 'c' AND col1 = 'b' AND col2 = 'a'
盡可能應(yīng)用到聯(lián)合索引中的排序靠前的字段:
-- 以下均可以應(yīng)用聯(lián)合索引中的部分索引
-- 以下均可以應(yīng)用聯(lián)合索引中的部分索引
col1 = 'a' AND col2 = 'b'
col1 = 'a'
col1 like 'a%'
僅對(duì)索引進(jìn)行查詢(xún),如果查詢(xún)的列都位于索引中,則不需要讀取列元組的值。
select a, b, c
聚簇索引保證關(guān)鍵字的值相近的元組存儲(chǔ)的物理位置也相同(所以字符串類(lèi)型不宜建立聚簇索引,特別是隨機(jī)字符串,會(huì)使得系統(tǒng)進(jìn)行大量的移動(dòng)操作),且一個(gè)表只能有一個(gè)聚簇索引。因?yàn)橛纱鎯?chǔ)引擎實(shí)現(xiàn)索引,所以,并不是所有的引擎都支持聚簇索引。目前,只有 SolidDB 和 InnoDB 支持。
InnoDB 對(duì)主鍵建立聚簇索引。如果你不指定主鍵,InnoDB 會(huì)用一個(gè)具有唯一且非空值的索引來(lái)代替。如果不存在這樣的索引,InnoDB 會(huì)定義一個(gè)隱藏的主鍵,然后對(duì)其建立聚簇索引。
MySQL 查詢(xún)緩存會(huì)保存查詢(xún)返回的完整結(jié)果。當(dāng)查詢(xún)命中緩存,MySQL 會(huì)立刻返回結(jié)果,而跳過(guò)了后續(xù)解析、優(yōu)化以及執(zhí)行階段,會(huì)有效提升查詢(xún)性能。
但是查詢(xún)緩存不是銀彈,它也會(huì)存在一些問(wèn)題。
-- 不會(huì)使用同一個(gè)緩存
select name from users where id = 1;
SELECT name FROM users WHERE id = 1;
假如緩存過(guò)查詢(xún)結(jié)果,但是由于查詢(xún)緩存設(shè)置內(nèi)存不足,新緩存加入時(shí) MySQL 會(huì)將某些緩存逐出,導(dǎo)致后續(xù)查詢(xún)未命中;
數(shù)據(jù)結(jié)構(gòu)及數(shù)據(jù)修改、內(nèi)存不足、緩存碎片都會(huì)導(dǎo)致緩存失效。
查詢(xún)緩存對(duì)應(yīng)用程序完全透明,應(yīng)用程序無(wú)需關(guān)心 MySQL 是通過(guò)查詢(xún)緩存返回的還是實(shí)際執(zhí)行返回的結(jié)果。但隨著目前服務(wù)器性能越來(lái)越強(qiáng),查詢(xún)緩存被發(fā)現(xiàn)是一個(gè)影響服務(wù)器擴(kuò)展性的因素,它很可能成為整個(gè)服務(wù)器的資源競(jìng)爭(zhēng)點(diǎn),大家采用生產(chǎn)環(huán)境開(kāi)啟應(yīng)用時(shí)候一定要慎重考量。
優(yōu)化慢查詢(xún)時(shí)候,我們可以轉(zhuǎn)換下思路,我們的目標(biāo)是找到一個(gè)更優(yōu)的方法獲取時(shí)間需要的結(jié)果,而不是一定從 MySQL 獲取一模一樣的結(jié)果集。重構(gòu)查詢(xún)的技巧很有必要。
將一個(gè)復(fù)雜查詢(xún)拆分多個(gè)簡(jiǎn)單查詢(xún),考慮是否需要將一個(gè)復(fù)雜查詢(xún)拆分為多個(gè)簡(jiǎn)單查詢(xún)。
實(shí)際開(kāi)發(fā)過(guò)程中,大家往往會(huì)強(qiáng)調(diào)數(shù)據(jù)庫(kù)層完成盡可能多的工作,這樣做的初衷是認(rèn)為網(wǎng)絡(luò)通信、查詢(xún)解析和優(yōu)化是一件代價(jià)很高的事情,其實(shí) MySQL 從設(shè)計(jì)上讓連接和斷開(kāi)都很輕量級(jí),同時(shí)在返回一個(gè)小查詢(xún)結(jié)果方面很高效。況且目前網(wǎng)絡(luò)速度也比之前快很多,無(wú)論是帶寬還是延遲。
對(duì)于大查詢(xún)我們要“分而治之”,將大查詢(xún)切分成多個(gè)小查詢(xún)。不過(guò)在一次查詢(xún)能夠勝任的情況下還拆成多個(gè)獨(dú)立查詢(xún)就不明智了。
例如:做數(shù)據(jù)庫(kù)做 10 次查詢(xún),每次返回一行記錄。
將關(guān)聯(lián)查詢(xún)進(jìn)行分解,對(duì)每一個(gè)表進(jìn)行一次單表查詢(xún),然后將結(jié)果在應(yīng)用程序中進(jìn)行關(guān)聯(lián)。
例如:
SELECT *
FROM users
LEFT JOIN orders ON orders.user_id = users.user_id
LEFT JOIN goods ON goods.good_id = orders.good_id
WHERE users.name = 'zhangsan';
以上查詢(xún)可以分解成下面的查詢(xún)來(lái)代替:
SELECT * FROM users WHERE users.name = 'zhangsan';
SELECT * FROM orders WHERE orders.user_id = 103;
SELECT * FROM goods WHERE goods.good_id IN (123, 456, 789);
為什么要這樣做呢?看起來(lái)好像沒(méi)有什么好處,而且返回?cái)?shù)據(jù)結(jié)果也是一致的。實(shí)際上利用分解查詢(xún)的方式來(lái)重構(gòu)查詢(xún)有很大的優(yōu)勢(shì),主要表現(xiàn)為:
如何處理高性能查詢(xún)難題?
假如把高性能查詢(xún)比作一個(gè)“難題”,它其實(shí)是包括多個(gè)子難題在內(nèi),共同作用的結(jié)果。
今天我們來(lái)歸納總結(jié)下,主要包括以下幾類(lèi)。
良好的庫(kù)表數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)原則是普遍適用的,但是 MySQL 有它自己的實(shí)現(xiàn)細(xì)節(jié)要注意。
總結(jié)應(yīng)用原則如下,注意借鑒:
常見(jiàn)的 B-Tree 索引,按照順序存儲(chǔ)數(shù)據(jù),所以 MySQL 可以用來(lái)做 ORDER BY 和 GROUP BY 操作。因?yàn)閿?shù)據(jù)是有序的,所以便于將相關(guān)的列值都存儲(chǔ)在一起。由于索引中存儲(chǔ)了實(shí)際的列值,所以一些查詢(xún)只通過(guò)索引就能夠完成查詢(xún)(如:聚簇索引)。
根據(jù)索引的特性,總結(jié)索引的優(yōu)點(diǎn)有如下幾點(diǎn):
編寫(xiě)查詢(xún)語(yǔ)句時(shí)候應(yīng)該注意盡可能選擇合適的索引,以避免單行查找,盡可能使用索引覆蓋。
根據(jù)執(zhí)行計(jì)劃依次掃描相關(guān)表中的行,不在數(shù)據(jù)緩沖區(qū)的走 IO 存儲(chǔ)引擎掃描表的性能消耗參考下面的 list,消耗從大到小:
全表掃描 > 全索引掃描 > 部分索引掃描 > 索引查找 > 唯一索引/主鍵查找 > 常量/null
應(yīng)用查詢(xún)優(yōu)化是建立在良好的數(shù)據(jù)結(jié)構(gòu)和合理的索引設(shè)計(jì)之上的。
它主要包括以下幾種情況。
優(yōu)化慢查詢(xún)時(shí),目標(biāo)應(yīng)該是找到一個(gè)更優(yōu)的方案來(lái)達(dá)到我們獲取結(jié)果數(shù)據(jù)的目的。其中可以存在多樣的權(quán)衡方案:
MySQL 查詢(xún)優(yōu)化器并不是對(duì)所有查詢(xún)都適用的,我們可以通過(guò)改寫(xiě)查詢(xún) SQL 來(lái)讓數(shù)據(jù)庫(kù)更高效的完成工作。
常見(jiàn)查詢(xún)優(yōu)化建議如下:
MySQL v5.6 版本以后,消除了很多 MySQL 原本的限制,讓更多的查詢(xún)能夠以盡可能高的效率完成。
良好的表結(jié)構(gòu)設(shè)計(jì)是高性能查詢(xún)的基石,恰當(dāng)?shù)乃饕O(shè)計(jì)是高性能查詢(xún)的助推器,同時(shí)合理的查詢(xún)應(yīng)用也是必不可少的。數(shù)據(jù)結(jié)構(gòu)優(yōu)化、索引設(shè)計(jì)優(yōu)化及應(yīng)用查詢(xún)優(yōu)化猶如三叉戟一般,齊頭并進(jìn),在高性能查詢(xún)應(yīng)用中缺一不可。
全文總結(jié)一下,其實(shí)就是我們要學(xué)會(huì)用數(shù)據(jù)庫(kù)的要求方式來(lái)執(zhí)行 SQL。
即要寫(xiě)好應(yīng)用查詢(xún) SQL,必須要結(jié)合良好的數(shù)據(jù)結(jié)構(gòu)和合理的索引設(shè)計(jì)才可以。
其實(shí) MySQL 查詢(xún)優(yōu)化中的每一項(xiàng)拆開(kāi)講都可以是很大的章節(jié),在此主要是將解決問(wèn)題的思路分享給大家,希望能對(duì)大家今后的工作中能有所幫助。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流