MySQL優(yōu)化(索引與查詢優(yōu)化)
1. 如何定位及優(yōu)化SQL語句的性能問題?
對(duì)于低性能的SQL語句的定位,最重要也是最有效的方法就是使用執(zhí)行計(jì)劃,MySQL提供了explain命令來查看語句的執(zhí)行計(jì)劃。 我們知道,不管是哪種數(shù)據(jù)庫,或者是哪種數(shù)據(jù)庫引擎,在對(duì)一條SQL語句進(jìn)行執(zhí)行的過程中都會(huì)做很多相關(guān)的優(yōu)化,對(duì)于查詢語句,最重要的優(yōu)化方式就是使用索引。
而執(zhí)行計(jì)劃,就是顯示數(shù)據(jù)庫引擎對(duì)于SQL語句的執(zhí)行的詳細(xì)情況,其中包含了是否使用索引,使用什么索引,使用的索引的相關(guān)信息等。2. 大表數(shù)據(jù)查詢,怎么優(yōu)化
- 優(yōu)化shema、sql語句+索引;
- 第二加緩存,memcached, redis;
- 主從復(fù)制,讀寫分離;
- 垂直拆分,根據(jù)你模塊的耦合度,將一個(gè)大的系統(tǒng)分為多個(gè)小的系統(tǒng),也就是分布式系統(tǒng);
- 水平切分,針對(duì)數(shù)據(jù)量大的表,這一步最麻煩,最能考驗(yàn)技術(shù)水平,要選擇一個(gè)合理的sharding key, 為了有好的查詢效率,表結(jié)構(gòu)也要改動(dòng),做一定的冗余,應(yīng)用也要改,sql中盡量帶sharding key,將數(shù)據(jù)定位到限定的表上去查,而不是掃描全部的表;
3. 超大分頁怎么處理?
數(shù)據(jù)庫層面,這也是我們主要集中關(guān)注的(雖然收效沒那么大),類似于select * from table where age > 20 limit 1000000,10 這種查詢其實(shí)也是有可以優(yōu)化的余地的. 這條語句需要 load1000000 數(shù)據(jù)然后基本上全部丟棄,只取 10 條當(dāng)然比較慢. 當(dāng)時(shí)我們可以修改為select * from table where id in (select id from table where age > 20 limit 1000000,10).這樣雖然也 load 了一百萬的數(shù)據(jù),但是由于索引覆蓋,要查詢的所有字段都在索引中,所以速度會(huì)很快。
解決超大分頁,其實(shí)主要是靠緩存,可預(yù)測(cè)性的提前查到內(nèi)容,緩存至redis等k-V數(shù)據(jù)庫中,直接返回即可.
在阿里巴巴《Java開發(fā)手冊(cè)》中,對(duì)超大分頁的解決辦法是類似于上面提到的第一種.
【推薦】利用延遲關(guān)聯(lián)或者子查詢優(yōu)化超多分頁場(chǎng)景。
說明:MySQL并不是跳過offset行,而是取offset+N行,然后返回放棄前offset行,返回N行,那當(dāng)offset特別大的時(shí)候,效率就非常的低下,要么控制返回的總頁數(shù),要么對(duì)超過特定閾值的頁數(shù)進(jìn)行SQL改寫。
正例:先快速定位需要獲取的id段,然后再關(guān)聯(lián):
SELECT a.* FROM 表1 a, (select id from 表1 where 條件 LIMIT 100000,20 ) b where a.id=b.id
4. 統(tǒng)計(jì)過慢查詢嗎?對(duì)慢查詢都怎么優(yōu)化過?
在業(yè)務(wù)系統(tǒng)中,除了使用主鍵進(jìn)行的查詢,其他的我都會(huì)在測(cè)試庫上測(cè)試其耗時(shí),慢查詢的統(tǒng)計(jì)主要由運(yùn)維在做,會(huì)定期將業(yè)務(wù)中的慢查詢反饋給我們。
定位執(zhí)行慢的 SQL:慢查詢?nèi)罩?/p>
開啟慢查詢?nèi)罩緟?shù)
開啟slow_query_log set global slow_query_log='ON';
修改long_query_time閾值
查看慢查詢數(shù)目
查詢當(dāng)前系統(tǒng)中有多少條慢查詢記錄
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
案例演示
測(cè)試及分析
慢查詢?nèi)罩痉治龉ぞ撸簃ysqldumpslow
關(guān)閉慢查詢?nèi)罩?/p>
方式1:永久性方式
[圖片上傳失敗...(image-a50598-1655208719518)]
或者,把slow_query_log一項(xiàng)注釋掉 或 刪除 [圖片上傳失敗...(image-43820d-1655208719518)]
重啟MySQL服務(wù),執(zhí)行如下語句查詢慢日志功能。
SHOW VARIABLES LIKE '%slow%'; #查詢慢查詢?nèi)罩舅谀夸?/p>
SHOW VARIABLES LIKE '%long_query_time%'; #查詢超時(shí)時(shí)長
方式2:臨時(shí)性方式:使用SET語句來設(shè)置。
(1)停止MySQL慢查詢?nèi)罩竟δ埽唧wSQL語句如下
(2)重啟MySQL服務(wù),使用SHOW語句查詢慢查詢?nèi)罩竟δ苄畔ⅲ唧wSQL語句如下
SHOW VARIABLES LIKE '%slow%';
-
以及
SHOW VARIABLES LIKE '%long_query_time%';
刪除慢查詢?nèi)罩?/p>
慢查詢的優(yōu)化首先要搞明白慢的原因是什么? 是查詢條件沒有命中索引?是load了不需要的數(shù)據(jù)列?還是數(shù)據(jù)量太大?
所以優(yōu)化也是針對(duì)這三個(gè)方向來的,
- 首先分析語句,看看是否load了額外的數(shù)據(jù),可能是查詢了多余的行并且拋棄掉了,可能是加載了許多結(jié)果中并不需要的列,對(duì)語句進(jìn)行分析以及重寫。
- 分析語句的執(zhí)行計(jì)劃,然后獲得其使用索引的情況,之后修改語句或者修改索引,使得語句可以盡可能的命中索引。
- 如果對(duì)語句的優(yōu)化已經(jīng)無法進(jìn)行,可以考慮表中的數(shù)據(jù)量是否太大,如果是的話可以進(jìn)行橫向或者縱向的分表。
5. 如何優(yōu)化查詢過程中的數(shù)據(jù)訪問
- 訪問數(shù)據(jù)太多導(dǎo)致查詢性能下降
- 確定應(yīng)用程序是否在檢索大量超過需要的數(shù)據(jù),可能是太多行或列
- 確認(rèn)MySQL服務(wù)器是否在分析大量不必要的數(shù)據(jù)行
- 查詢不需要的數(shù)據(jù)。解決辦法:使用limit解決
- 多表關(guān)聯(lián)返回全部列。解決辦法:指定列名
- 總是返回全部列。解決辦法:避免使用SELECT *
- 重復(fù)查詢相同的數(shù)據(jù)。解決辦法:可以緩存數(shù)據(jù),下次直接讀取緩存
- 是否在掃描額外的記錄。解決辦法:使用explain進(jìn)行分析,如果發(fā)現(xiàn)查詢需要掃描大量的數(shù)據(jù),但只返回少數(shù)的行,可以通過如下技巧去優(yōu)化:使用索引覆蓋掃描,把所有的列都放到索引中,這樣存儲(chǔ)引擎不需要回表獲取對(duì)應(yīng)行就可以返回結(jié)果。
- 改變數(shù)據(jù)庫和表的結(jié)構(gòu),修改數(shù)據(jù)表范式
- 重寫SQL語句,讓優(yōu)化器可以以更優(yōu)的方式執(zhí)行查詢。
6. 如何優(yōu)化關(guān)聯(lián)查詢
確定ON或者USING子句中是否有索引。
確保GROUP BY和ORDER BY只有一個(gè)表中的列,這樣MySQL才有可能使用索引。
保證被驅(qū)動(dòng)表的JOIN字段已經(jīng)創(chuàng)建了索引
需要JOIN 的字段,數(shù)據(jù)類型保持絕對(duì)一致。
LEFT JOIN 時(shí),選擇小表作為驅(qū)動(dòng)表, 大表作為被驅(qū)動(dòng)表 。減少外層循環(huán)的次數(shù)。
INNER JOIN 時(shí),MySQL會(huì)自動(dòng)將 小結(jié)果集的表選為驅(qū)動(dòng)表 。選擇相信MySQL優(yōu)化策略。
能夠直接多表關(guān)聯(lián)的盡量直接關(guān)聯(lián),不用子查詢。(減少查詢的趟數(shù))
不建議使用子查詢,建議將子查詢SQL拆開結(jié)合程序多次查詢,或使用 JOIN 來代替子查詢。
衍生表建不了索引
子查詢優(yōu)化
MySQL從4.1版本開始支持子查詢,使用子查詢可以進(jìn)行SELECT語句的嵌套查詢,即一個(gè)SELECT查詢的結(jié)
果作為另一個(gè)SELECT語句的條件。 子查詢可以一次性完成很多邏輯上需要多個(gè)步驟才能完成的SQL操作 。
子查詢是 MySQL 的一項(xiàng)重要的功能,可以幫助我們通過一個(gè) SQL 語句實(shí)現(xiàn)比較復(fù)雜的查詢。但是,子
查詢的執(zhí)行效率不高。原因:
① 執(zhí)行子查詢時(shí),MySQL需要為內(nèi)層查詢語句的查詢結(jié)果 建立一個(gè)臨時(shí)表 ,然后外層查詢語句從臨時(shí)表
中查詢記錄。查詢完畢后,再 撤銷這些臨時(shí)表 。這樣會(huì)消耗過多的CPU和IO資源,產(chǎn)生大量的慢查詢。
② 子查詢的結(jié)果集存儲(chǔ)的臨時(shí)表,不論是內(nèi)存臨時(shí)表還是磁盤臨時(shí)表都 不會(huì)存在索引 ,所以查詢性能會(huì)
受到一定的影響。
③ 對(duì)于返回結(jié)果集比較大的子查詢,其對(duì)查詢性能的影響也就越大。
在MySQL中,可以使用連接(JOIN)查詢來替代子查詢。連接查詢 不需要建立臨時(shí)表 ,其 速度比子查詢
要快 ,如果查詢中使用索引的話,性能就會(huì)更好。
結(jié)論:盡量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
排序優(yōu)化
問題:在 WHERE 條件字段上加索引,但是為什么在 ORDER BY 字段上還要加索引呢?
優(yōu)化建議:
- SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表掃
描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。當(dāng)然,某些情況下全表掃描,或者 FileSort 排
序不一定比索引慢。但總的來說,我們還是要避免,以提高查詢效率。 - 盡量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用單索引列; 如果不同就使用聯(lián)合索引。
- 無法使用 Index 時(shí),需要對(duì) FileSort 方式進(jìn)行調(diào)優(yōu)。
結(jié)論:
- 兩個(gè)索引同時(shí)存在,mysql自動(dòng)選擇最優(yōu)的方案。(對(duì)于這個(gè)例子,mysql選擇 idx_age_stuno_name)。但是, 隨著數(shù)據(jù)量的變化,選擇的索引也會(huì)隨之變化的 。
-
當(dāng)【范圍條件】和【group by 或者 order by】的字段出現(xiàn)二選一時(shí),優(yōu)先觀察條件字段的過
濾數(shù)量,如果過濾的數(shù)據(jù)足夠多,而需要排序的數(shù)據(jù)并不多時(shí),優(yōu)先把索引放在范圍字段
上。反之,亦然。
GROUP BY優(yōu)化
- group by 使用索引的原則幾乎跟order by一致 ,group by 即使沒有過濾條件用到索引,也可以直接
- 使用索引。
- group by 先排序再分組,遵照索引建的最佳左前綴法則
- 當(dāng)無法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 參數(shù)的設(shè)置
- where效率高于having,能寫在where限定的條件就不要寫在having中了
- 減少使用order by,和業(yè)務(wù)溝通能不排序就不排序,或?qū)⑴判蚍诺匠绦蚨巳プ觥rder by、group
- by、distinct這些語句較為耗費(fèi)CPU,數(shù)據(jù)庫的CPU資源是極其寶貴的。
- 包含了order by、group by、distinct這些查詢的語句,where條件過濾出來的結(jié)果集請(qǐng)保持在1000行
- 以內(nèi),否則SQL會(huì)很慢。
優(yōu)化分頁查詢
優(yōu)化思路一
在索引上完成排序分頁操作,最后根據(jù)主鍵關(guān)聯(lián)回原表查詢所需要的其他列內(nèi)容。
EXPLAIN SELECT * FROM student t,
(SELECT id FROM student ORDER BY id LIMIT 2000000,10)a
WHERE t.id = a.id;
優(yōu)化思路二
該方案適用于主鍵自增的表,可以把Limit 查詢轉(zhuǎn)換成某個(gè)位置的查詢 。
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
優(yōu)先考慮覆蓋索引
什么是覆蓋索引?
理解方式一:索引是高效找到行的一個(gè)方法,但是一般數(shù)據(jù)庫也能使用索引找到一個(gè)列的數(shù)據(jù),因此它
不必讀取整個(gè)行。畢竟索引葉子節(jié)點(diǎn)存儲(chǔ)了它們索引的數(shù)據(jù);當(dāng)能通過讀取索引就可以得到想要的數(shù)
據(jù),那就不需要讀取行了。一個(gè)索引包含了滿足查詢結(jié)果的數(shù)據(jù)就叫做覆蓋索引。
理解方式二:非聚簇復(fù)合索引的一種形式,它包括在查詢里的SELECT、JOIN和WHERE子句用到的所有列
(即建索引的字段正好是覆蓋查詢條件中所涉及的字段)。
簡單說就是, 索引列+主鍵 包含 SELECT 到 FROM之間查詢的列 。
覆蓋索引的利弊 好處:
- 避免Innodb表進(jìn)行索引的二次查詢(回表)
-
可以把隨機(jī)IO變成順序IO加快查詢效率
弊端: 索引字段的維護(hù) 總是有代價(jià)的。因此,在建立冗余索引來支持覆蓋索引時(shí)就需要權(quán)衡考慮了。這是業(yè)務(wù)
DBA,或者稱為業(yè)務(wù)數(shù)據(jù)架構(gòu)師的工作。
(前綴索引)如何給字符串添加索引
MySQL是支持前綴索引的。默認(rèn)地,如果你創(chuàng)建索引的語句不指定前綴長度,那么索引就會(huì)包含整個(gè)字
符串。
mysql> alter table teacher add index index1(email);
#或
mysql> alter table teacher add index index2(email(6));
這兩種不同的定義在數(shù)據(jù)結(jié)構(gòu)和存儲(chǔ)上有什么區(qū)別呢?下圖就是這兩個(gè)索引的示意圖。
如果使用的是index1(即email整個(gè)字符串的索引結(jié)構(gòu)),執(zhí)行順序是這樣的:
- 從index1索引樹找到滿足索引值是’ zhangssxyz@xxx.com ’的這條記錄,取得ID2的值;
- 到主鍵上查到主鍵值是ID2的行,判斷email的值是正確的,將這行記錄加入結(jié)果集;
- 取index1索引樹上剛剛查到的位置的下一條記錄,發(fā)現(xiàn)已經(jīng)不滿足email=' zhangssxyz@xxx.com ’的
條件了,循環(huán)結(jié)束。
這個(gè)過程中,只需要回主鍵索引取一次數(shù)據(jù),所以系統(tǒng)認(rèn)為只掃描了一行。
如果使用的是index2(即email(6)索引結(jié)構(gòu)),執(zhí)行順序是這樣的:
- 從index2索引樹找到滿足索引值是’zhangs’的記錄,找到的第一個(gè)是ID1;
- 到主鍵上查到主鍵值是ID1的行,判斷出email的值不是’ zhangssxyz@xxx.com ’,這行記錄丟棄;
- 取index2上剛剛查到的位置的下一條記錄,發(fā)現(xiàn)仍然是’zhangs’,取出ID2,再到ID索引上取整行然 后判斷,這次值對(duì)了,將這行記錄加入結(jié)果集;
- 重復(fù)上一步,直到在idxe2上取到的值不是’zhangs’時(shí),循環(huán)結(jié)束。
也就是說使用前綴索引,定義好長度,就可以做到既節(jié)省空間,又不用額外增加太多的查詢成本。前面
已經(jīng)講過區(qū)分度,區(qū)分度越高越好。因?yàn)閰^(qū)分度越高,意味著重復(fù)的鍵值越少。
前綴索引對(duì)覆蓋索引的影響
結(jié)論:
使用前綴索引就用不上覆蓋索引對(duì)查詢性能的優(yōu)化了,這也是你在選擇是否使用前綴索引時(shí)需要考
慮的一個(gè)因素。
索引下推
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一種在存儲(chǔ)引擎層使用索引過濾數(shù)據(jù)的一種優(yōu)
化方式。ICP可以減少存儲(chǔ)引擎訪問基表的次數(shù)以及MySQL服務(wù)器訪問存儲(chǔ)引擎的次數(shù)。
ICP的使用條件:
① 只能用于二級(jí)索引(secondary index)
②explain顯示的執(zhí)行計(jì)劃中type值(join 類型)為 range 、 ref 、 eq_ref 或者 ref_or_null 。
③ 并非全部where條件都可以用ICP篩選,如果where條件的字段不在索引列中,還是要讀取整表的記錄
到server端做where過濾。
④ ICP可以用于MyISAM和InnnoDB存儲(chǔ)引擎
⑤ MySQL 5.6版本的不支持分區(qū)表的ICP功能,5.7版本的開始支持。
⑥ 當(dāng)SQL使用覆蓋索引時(shí),不支持ICP優(yōu)化方法。
普通索引 vs 唯一索引
從性能的角度考慮,你選擇唯一索引還是普通索引呢?選擇的依據(jù)是什么呢?
假設(shè),我們有一個(gè)主鍵列為ID的表,表中有字段k,并且在k上有索引,假設(shè)字段 k 上的值都不重復(fù)。
- 查詢過程
假設(shè),執(zhí)行查詢的語句是 select id from test where k=5。
- 對(duì)于普通索引來說,查找到滿足條件的第一個(gè)記錄(5,500)后,需要查找下一個(gè)記錄,直到碰到第一個(gè)不滿足k=5條件的記錄。
- 對(duì)于唯一索引來說,由于索引定義了唯一性,查找到第一個(gè)滿足條件的記錄后,就會(huì)停止繼續(xù)檢
- 索。
那么,這個(gè)不同帶來的性能差距會(huì)有多少呢?答案是, 微乎其微 。
- 更新過程
為了說明普通索引和唯一索引對(duì)更新語句性能的影響這個(gè)問題,介紹一下change buffer。
當(dāng)需要更新一個(gè)數(shù)據(jù)頁時(shí),如果數(shù)據(jù)頁在內(nèi)存中就直接更新,而如果這個(gè)數(shù)據(jù)頁還沒有在內(nèi)存中的話,
在不影響數(shù)據(jù)一致性的前提下, InooDB會(huì)將這些更新操作緩存在change buffer中 ,這樣就不需要從磁
盤中讀入這個(gè)數(shù)據(jù)頁了。在下次查詢需要訪問這個(gè)數(shù)據(jù)頁的時(shí)候,將數(shù)據(jù)頁讀入內(nèi)存,然后執(zhí)行change
buffer中與這個(gè)頁有關(guān)的操作。通過這種方式就能保證這個(gè)數(shù)據(jù)邏輯的正確性。
將change buffer中的操作應(yīng)用到原數(shù)據(jù)頁,得到最新結(jié)果的過程稱為 merge 。除了 訪問這個(gè)數(shù)據(jù)頁 會(huì)觸
發(fā)merge外,系統(tǒng)有 后臺(tái)線程會(huì)定期 merge。在 數(shù)據(jù)庫正常關(guān)閉(shutdown) 的過程中,也會(huì)執(zhí)行merge
操作。
如果能夠?qū)⒏虏僮飨扔涗浽赾hange buffer, 減少讀磁盤 ,語句的執(zhí)行速度會(huì)得到明顯的提升。而且,
數(shù)據(jù)讀入內(nèi)存是需要占用 buffer pool 的,所以這種方式還能夠 避免占用內(nèi)存 ,提高內(nèi)存利用率。
唯一索引的更新就不能使用change buffer ,實(shí)際上也只有普通索引可以使用。
change buffer的使用場(chǎng)景
普通索引和唯一索引應(yīng)該怎么選擇?其實(shí),這兩類索引在查詢能力上是沒差別的,主要考慮的是
對(duì) 更新性能 的影響。所以,建議你 盡量選擇普通索引 。在實(shí)際使用中會(huì)發(fā)現(xiàn), 普通索引 和 change buffer 的配合使用,對(duì)于 數(shù)據(jù)量大 的表的更新優(yōu)化
還是很明顯的。如果所有的更新后面,都馬上 伴隨著對(duì)這個(gè)記錄的查詢 ,那么你應(yīng)該 關(guān)閉change buffer 。而在
其他情況下,change buffer都能提升更新性能。由于唯一索引用不上change buffer的優(yōu)化機(jī)制,因此如果 業(yè)務(wù)可以接受 ,從性能角度出發(fā)建議優(yōu)
先考慮非唯一索引。但是如果"業(yè)務(wù)可能無法確保"的情況下,怎么處理呢?
- 首先, 業(yè)務(wù)正確性優(yōu)先 。我們的前提是“業(yè)務(wù)代碼已經(jīng)保證不會(huì)寫入重復(fù)數(shù)據(jù)”的情況下,討論性能
問題。如果業(yè)務(wù)不能保證,或者業(yè)務(wù)就是要求數(shù)據(jù)庫來做約束,那么沒得選,必須創(chuàng)建唯一索引。
這種情況下,本節(jié)的意義在于,如果碰上了大量插入數(shù)據(jù)慢、內(nèi)存命中率低的時(shí)候,給你多提供一
個(gè)排查思路。 - 然后,在一些“ 歸檔庫 ”的場(chǎng)景,你是可以考慮使用唯一索引的。比如,線上數(shù)據(jù)只需要保留半年,
然后歷史數(shù)據(jù)保存在歸檔庫。這時(shí)候,歸檔數(shù)據(jù)已經(jīng)是確保沒有唯一鍵沖突了。要提高歸檔效率,
可以考慮把表里面的唯一索引改成普通索引。
其它查詢優(yōu)化策略
1、EXISTS 和 IN 的區(qū)分
問題:
不太理解哪種情況下應(yīng)該使用 EXISTS,哪種情況應(yīng)該用 IN。選擇的標(biāo)準(zhǔn)是看能否使用表的索引嗎?
2、COUNT()與COUNT(具體字段)效率*
問:在 MySQL 中統(tǒng)計(jì)數(shù)據(jù)表的行數(shù),可以使用三種方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和
SELECT COUNT(具體字段) ,使用這三者之間的查詢效率是怎樣的?
3 關(guān)于SELECT()*
在表查詢中,建議明確字段,不要使用 * 作為查詢的字段列表,推薦使用SELECT <字段列表> 查詢。原
因:
① MySQL 在解析的過程中,會(huì)通過 查詢數(shù)據(jù)字典 將"*"按序轉(zhuǎn)換成所有列名,這會(huì)大大的耗費(fèi)資源和時(shí)
間。
② 無法使用 覆蓋索引
4 LIMIT 1 對(duì)優(yōu)化的影響
針對(duì)的是會(huì)掃描全表的 SQL 語句,如果你可以確定結(jié)果集只有一條,那么加上 LIMIT 1 的時(shí)候,當(dāng)找
到一條結(jié)果的時(shí)候就不會(huì)繼續(xù)掃描了,這樣會(huì)加快查詢速度。
如果數(shù)據(jù)表已經(jīng)對(duì)字段建立了唯一索引,那么可以通過索引進(jìn)行查詢,不會(huì)全表掃描的話,就不需要加
上 LIMIT 1 了。
5 多使用COMMIT
只要有可能,在程序中盡量多使用 COMMIT,這樣程序的性能得到提高,需求也會(huì)因?yàn)?COMMIT 所釋放
的資源而減少。
COMMIT 所釋放的資源:
- 回滾段上用于恢復(fù)數(shù)據(jù)的信息
- 被程序語句獲得的鎖
- redo / undo log buffer 中的空間
- 管理上述 3 種資源中的內(nèi)部花費(fèi)
主鍵如何設(shè)計(jì)的?
自增ID的問題
自增ID做主鍵,簡單易懂,幾乎所有數(shù)據(jù)庫都支持自增類型,只是實(shí)現(xiàn)上各自有所不同而已。自增ID除
了簡單,其他都是缺點(diǎn),總體來看存在以下幾方面的問題:
- 可靠性不高 存在自增ID回溯的問題,這個(gè)問題直到最新版本的MySQL 8.0才修復(fù)。
-
安全性不高 對(duì)外暴露的接口可以非常容易猜測(cè)對(duì)應(yīng)的信息。比如:/User/1/這樣的接口,可以非常容易猜測(cè)用戶ID的
值為多少,總用戶數(shù)量有多少,也可以非常容易地通過接口進(jìn)行數(shù)據(jù)的爬取。 - 性能差 自增ID的性能較差,需要在數(shù)據(jù)庫服務(wù)器端生成。
-
交互多 業(yè)務(wù)還需要額外執(zhí)行一次類似 last_insert_id() 的函數(shù)才能知道剛才插入的自增值,這需要多一次的
網(wǎng)絡(luò)交互。在海量并發(fā)的系統(tǒng)中,多1條SQL,就多一次性能上的開銷。 -
局部唯一性 最重要的一點(diǎn),自增ID是局部唯一,只在當(dāng)前數(shù)據(jù)庫實(shí)例中唯一,而不是全局唯一,在任意服務(wù)器間都
是唯一的。對(duì)于目前分布式系統(tǒng)來說,這簡直就是噩夢(mèng)。
業(yè)務(wù)字段做主鍵
建議盡量不要用跟業(yè)務(wù)有關(guān)的字段做主鍵。畢竟,作為項(xiàng)目設(shè)計(jì)的技術(shù)人員,我們誰也無法預(yù)測(cè)
在項(xiàng)目的整個(gè)生命周期中,哪個(gè)業(yè)務(wù)字段會(huì)因?yàn)轫?xiàng)目的業(yè)務(wù)需求而有重復(fù),或者重用之類的情況出現(xiàn)。
經(jīng)驗(yàn):
剛開始使用 MySQL 時(shí),很多人都很容易犯的錯(cuò)誤是喜歡用業(yè)務(wù)字段做主鍵,想當(dāng)然地認(rèn)為了解業(yè)
務(wù)需求,但實(shí)際情況往往出乎意料,而更改主鍵設(shè)置的成本非常高。
淘寶的主鍵設(shè)計(jì)
從上圖可以發(fā)現(xiàn),訂單號(hào)不是自增ID!我們?cè)敿?xì)看下上述4個(gè)訂單號(hào):
1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113
訂單號(hào)是19位的長度,且訂單的最后5位都是一樣的,都是08113。且訂單號(hào)的前面14位部分是單調(diào)遞增
的。
大膽猜測(cè),淘寶的訂單ID設(shè)計(jì)應(yīng)該是:
訂單ID = 時(shí)間 + 去重字段 + 用戶ID后6位尾號(hào)
這樣的設(shè)計(jì)能做到全局唯一,且對(duì)分布式系統(tǒng)查詢及其友好。
推薦的主鍵設(shè)計(jì)
非核心業(yè)務(wù) :對(duì)應(yīng)表的主鍵自增ID,如告警、日志、監(jiān)控等信息。
核心業(yè)務(wù) :主鍵設(shè)計(jì)至少應(yīng)該是全局唯一且是單調(diào)遞增。全局唯一保證在各系統(tǒng)之間都是唯一的,單調(diào)
遞增是希望插入時(shí)不影響數(shù)據(jù)庫性能。
這里推薦最簡單的一種主鍵設(shè)計(jì):UUID。
UUID的特點(diǎn):
全局唯一,占用36字節(jié),數(shù)據(jù)無序,插入性能差。
認(rèn)識(shí)UUID:
為什么UUID是全局唯一的?
為什么UUID占用36個(gè)字節(jié)?
為什么UUID是無序的?
MySQL數(shù)據(jù)庫的UUID組成如下所示:
UUID = 時(shí)間+UUID版本(16字節(jié))- 時(shí)鐘序列(4字節(jié)) - MAC地址(12字節(jié))
為什么UUID是全局唯一的?
在UUID中時(shí)間部分占用60位,存儲(chǔ)的類似TIMESTAMP的時(shí)間戳,但表示的是從1582-10-15 00:00:00.00
到現(xiàn)在的100ns的計(jì)數(shù)。可以看到UUID存儲(chǔ)的時(shí)間精度比TIMESTAMPE更高,時(shí)間維度發(fā)生重復(fù)的概率降
低到1/100ns。
時(shí)鐘序列是為了避免時(shí)鐘被回?fù)軐?dǎo)致產(chǎn)生時(shí)間重復(fù)的可能性。MAC地址用于全局唯一。
為什么UUID占用36個(gè)字節(jié)?
UUID根據(jù)字符串進(jìn)行存儲(chǔ),設(shè)計(jì)時(shí)還帶有無用"-"字符串,因此總共需要36個(gè)字節(jié)。
為什么UUID是隨機(jī)無序的呢?
因?yàn)閁UID的設(shè)計(jì)中,將時(shí)間低位放在最前面,而這部分的數(shù)據(jù)是一直在變化的,并且是無序。
改造UUID
若將時(shí)間高低位互換,則時(shí)間就是單調(diào)遞增的了,也就變得單調(diào)遞增了。MySQL 8.0可以更換時(shí)間低位和
時(shí)間高位的存儲(chǔ)方式,這樣UUID就是有序的UUID了。
MySQL 8.0還解決了UUID存在的空間占用的問題,除去了UUID字符串中無意義的"-"字符串,并且將字符
串用二進(jìn)制類型保存,這樣存儲(chǔ)空間降低為了16字節(jié)。
可以通過MySQL8.0提供的uuid_to_bin函數(shù)實(shí)現(xiàn)上述功能,同樣的,MySQL也提供了bin_to_uuid函數(shù)進(jìn)行
轉(zhuǎn)化:
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
通過函數(shù)uuid_to_bin(@uuid,true)將UUID轉(zhuǎn)化為有序UUID了。全局唯一 + 單調(diào)遞增,這不就是我們想要
的主鍵!
在當(dāng)今的互聯(lián)網(wǎng)環(huán)境中,非常不推薦自增ID作為主鍵的數(shù)據(jù)庫設(shè)計(jì)。更推薦類似有序UUID的全局
唯一的實(shí)現(xiàn)。
另外在真實(shí)的業(yè)務(wù)系統(tǒng)中,主鍵還可以加入業(yè)務(wù)和系統(tǒng)屬性,如用戶的尾號(hào),機(jī)房的信息等。這樣
的主鍵設(shè)計(jì)就更為考驗(yàn)架構(gòu)師的水平了。
如果不是MySQL8.0 腫么辦?
手動(dòng)賦值字段做主鍵!
比如,設(shè)計(jì)各個(gè)分店的會(huì)員表的主鍵,因?yàn)槿绻颗_(tái)機(jī)器各自產(chǎn)生的數(shù)據(jù)需要合并,就可能會(huì)出現(xiàn)主鍵
重復(fù)的問題。
可以在總部 MySQL 數(shù)據(jù)庫中,有一個(gè)管理信息表,在這個(gè)表中添加一個(gè)字段,專門用來記錄當(dāng)前會(huì)員編
號(hào)的最大值。
門店在添加會(huì)員的時(shí)候,先到總部 MySQL 數(shù)據(jù)庫中獲取這個(gè)最大值,在這個(gè)基礎(chǔ)上加 1,然后用這個(gè)值
作為新會(huì)員的“id”,同時(shí),更新總部 MySQL 數(shù)據(jù)庫管理信息表中的當(dāng) 前會(huì)員編號(hào)的最大值。
這樣一來,各個(gè)門店添加會(huì)員的時(shí)候,都對(duì)同一個(gè)總部 MySQL 數(shù)據(jù)庫中的數(shù)據(jù)表字段進(jìn) 行操作,就解
決了各門店添加會(huì)員時(shí)會(huì)員編號(hào)沖突的問題。
7. 數(shù)據(jù)庫結(jié)構(gòu)優(yōu)化
一個(gè)好的數(shù)據(jù)庫設(shè)計(jì)方案對(duì)于數(shù)據(jù)庫的性能往往會(huì)起到事半功倍的效果。
需要考慮數(shù)據(jù)冗余、查詢和更新的速度、字段的數(shù)據(jù)類型是否合理等多方面的內(nèi)容。
- 將字段很多的表分解成多個(gè)表
對(duì)于字段較多的表,如果有些字段的使用頻率很低,可以將這些字段分離出來形成新表。
因?yàn)楫?dāng)一個(gè)表的數(shù)據(jù)量很大時(shí),會(huì)由于使用頻率低的字段的存在而變慢。
- 增加中間表
對(duì)于需要經(jīng)常聯(lián)合查詢的表,可以建立中間表以提高查詢效率。
通過建立中間表,將需要通過聯(lián)合查詢的數(shù)據(jù)插入到中間表中,然后將原來的聯(lián)合查詢改為對(duì)中間表的查詢。
- 增加冗余字段
設(shè)計(jì)數(shù)據(jù)表時(shí)應(yīng)盡量遵循范式理論的規(guī)約,盡可能的減少冗余字段,讓數(shù)據(jù)庫設(shè)計(jì)看起來精致、優(yōu)雅。但是,合理的加入冗余字段可以提高查詢速度。
表的規(guī)范化程度越高,表和表之間的關(guān)系越多,需要連接查詢的情況也就越多,性能也就越差。
注意:
冗余字段的值在一個(gè)表中修改了,就要想辦法在其他表中更新,否則就會(huì)導(dǎo)致數(shù)據(jù)不一致的問題。
8. MySQL數(shù)據(jù)庫cpu飆升到500%的話他怎么處理?
當(dāng) cpu 飆升到 500%時(shí),先用操作系統(tǒng)命令 top 命令觀察是不是 MySQLd 占用導(dǎo)致的,如果不是,找出占用高的進(jìn)程,并進(jìn)行相關(guān)處理。
如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情況,是不是有消耗資源的 sql 在運(yùn)行。找出消耗高的 sql,看看執(zhí)行計(jì)劃是否準(zhǔn)確, index 是否缺失,或者實(shí)在是數(shù)據(jù)量太大造成。
一般來說,肯定要 kill 掉這些線程(同時(shí)觀察 cpu 使用率是否下降),等進(jìn)行相應(yīng)的調(diào)整(比如說加索引、改 sql、改內(nèi)存參數(shù))之后,再重新跑這些 SQL。
也有可能是每個(gè) sql 消耗資源并不多,但是突然之間,有大量的 session 連進(jìn)來導(dǎo)致 cpu 飆升,這種情況就需要跟應(yīng)用一起來分析為何連接數(shù)會(huì)激增,再做出相應(yīng)的調(diào)整,比如說限制連接數(shù)等。
9. 大表怎么優(yōu)化?
類似的問題:某個(gè)表有近千萬數(shù)據(jù),CRUD比較慢,如何優(yōu)化?分庫分表了是怎么做的?分表分庫了有什么問題?有用到中間件么?他們的原理知道么?
當(dāng)MySQL單表記錄數(shù)過大時(shí),數(shù)據(jù)庫的CRUD性能會(huì)明顯下降,一些常見的優(yōu)化措施如下:
- 限定數(shù)據(jù)的范圍: 務(wù)必禁止不帶任何限制數(shù)據(jù)范圍條件的查詢語句。比如:我們當(dāng)用戶在查詢訂單歷史的時(shí)候,我們可以控制在一個(gè)月的范圍內(nèi);
- 讀/寫分離: 經(jīng)典的數(shù)據(jù)庫拆分方案,主庫負(fù)責(zé)寫,從庫負(fù)責(zé)讀;
- 緩存: 使用MySQL的緩存,另外對(duì)重量級(jí)、更新少的數(shù)據(jù)可以考慮;
- 通過分庫分表的方式進(jìn)行優(yōu)化,主要有垂直分表和水平分表。
[10. 分析查詢語句:EXPLAIN]
MySQL 5.6.3以前只能 EXPLAIN SELECT ;MYSQL 5.6.3以后就可以 EXPLAIN SELECT,UPDATE,
DELETE
EXPLAIN 語句輸出的各個(gè)列的作用如下:
EXPLAIN各列作用
為了讓大家有比較好的體驗(yàn),我們調(diào)整了下 EXPLAIN 輸出列的順序。
1. table
不論我們的查詢語句有多復(fù)雜,里邊兒 包含了多少個(gè)表 ,到最后也是需要對(duì)每個(gè)表進(jìn)行 單表訪問 的,所
以MySQL規(guī)定EXPLAIN語句輸出的每條記錄都對(duì)應(yīng)著某個(gè)單表的訪問方法,該條記錄的table列代表著該
表的表名(有時(shí)不是真實(shí)的表名字,可能是簡稱)。
2. id
我們寫的查詢語句一般都以 SELECT 關(guān)鍵字開頭,比較簡單的查詢語句里只有一個(gè) SELECT 關(guān)鍵字,比
如下邊這個(gè)查詢語句:
SELECT * FROM s1 WHERE key1 = 'a';</pre>
稍微復(fù)雜一點(diǎn)的連接查詢中也只有一個(gè) SELECT 關(guān)鍵字,比如:
SELECT * FROM s1 INNER JOIN s2
ON s1.key1 = s2.key1
WHERE s1.common
field = 'a';
小結(jié):
- id如果相同,可以認(rèn)為是一組,從上往下順序執(zhí)行
- 在所有組中,id值越大,優(yōu)先級(jí)越高,越先執(zhí)行
- 關(guān)注點(diǎn):id號(hào)每個(gè)號(hào)碼,表示一趟獨(dú)立的查詢, 一個(gè)sql的查詢趟數(shù)越少越好
3. select_type
4. partitions (可略)
5. type ☆
完整的訪問方法如下: system , const , eq_ref , ref , fulltext , ref_or_null ,index_merge unique_subquery , index_subquery , range , index , ALL 。
- system
CREATE TABLE t(i int) Engine=MyISAM;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t;
- const
EXPLAIN SELECT * FROM s1 WHERE id = 10005;
- eq_ref
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
從執(zhí)行計(jì)劃的結(jié)果中可以看出,MySQL打算將s2作為驅(qū)動(dòng)表,s1作為被驅(qū)動(dòng)表,重點(diǎn)關(guān)注s1的訪問
方法是 eq_ref ,表明在訪問s1表的時(shí)候可以 通過主鍵的等值匹配 來進(jìn)行訪問。
- ref
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
- fulltext 全文索引
- ref_or_null
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key1 IS NULL;
- index_merge
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
從執(zhí)行計(jì)劃的 type 列的值是 index_merge 就可以看出,MySQL 打算使用索引合并的方式來執(zhí)行
對(duì) s1 表的查詢。
- unique_subquery
EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 =
s2.key1) OR key3 = 'a';
- index_subquery
EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where
s1.key1 = s2.key1) OR key3 = 'a';
- range
EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');
或者:
EXPLAIN SELECT * FROM s1 WHERE key1 > 'a' AND key1 < 'b';
- index
<pre data-language="sql" id="Btx0v" class="ne-codeblock language-sql" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';</pre>
[圖片上傳失敗...(image-b11941-1655208719520)]
- ALL
EXPLAIN SELECT * FROM s1;
小結(jié):
結(jié)果值從最好到最壞依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge >
unique_subquery > index_subquery >range > index > ALL其中比較重要的幾個(gè)提取出來(見上圖中的藍(lán)
色)。SQL 性能優(yōu)化的目標(biāo):至少要達(dá)到 range 級(jí)別,要求是 ref 級(jí)別,最好是 consts級(jí)別。(阿里巴巴
開發(fā)手冊(cè)要求)
【面向校招】全力備戰(zhàn)2023Golang實(shí)習(xí)與校招
https://huchao.blog.csdn.net/article/details/124220802?spm=1001.2014.3001.5502
歡迎共同進(jìn)步:
QQ群:1007576722