日活3kw下,如何應(yīng)對實(shí)際業(yè)務(wù)場景中SQL過慢的優(yōu)化挑戰(zhàn)?
在面試中,SQL調(diào)優(yōu)是一個(gè)常見的問題,通過這個(gè)問題可以考察應(yīng)聘者對于提升SQL性能的理解和掌握程度。通常來說,SQL調(diào)優(yōu)需要按照以下步驟展開。
首先,需要發(fā)現(xiàn)問題。最好結(jié)合具體業(yè)務(wù)情況,比如某次線下報(bào)警顯示出現(xiàn)了慢SQL,或者接口響應(yīng)時(shí)間較長,經(jīng)過性能分析發(fā)現(xiàn)問題出現(xiàn)在SQL查詢上。無論何種情況,都要有一個(gè)背景故事。
一旦問題被確定,就需要進(jìn)行問題分析了。
接著首先要定位具體的SQL語句,這可以通過各種監(jiān)控工具或平臺(tái)來實(shí)現(xiàn)。一旦定位到SQL語句,就能知道是哪張表、哪個(gè)SQL語句在拖慢性能。
接下來就是進(jìn)行分析了。一般來說,一個(gè)SQL查詢變慢可能有以下幾個(gè)原因:
- 索引失效
- 多表連接
- 查詢字段過多
- 數(shù)據(jù)量過大
- 索引字段基數(shù)太小
- 數(shù)據(jù)庫連接不足
- 數(shù)據(jù)庫表結(jié)構(gòu)不合理
- 數(shù)據(jù)庫IO或CPU負(fù)載高
- 數(shù)據(jù)庫參數(shù)設(shè)置不合理
- 長時(shí)間事務(wù)
- 鎖競爭導(dǎo)致的等待
因此,進(jìn)行完整的SQL調(diào)優(yōu)通常需要考慮以上因素中的一個(gè)或多個(gè)。在優(yōu)化過程中,會(huì)逐個(gè)解決這些問題。
情況一:索引失效
首先,當(dāng)遇到索引失效的問題時(shí),我們通常會(huì)通過執(zhí)行計(jì)劃來分析數(shù)據(jù)庫查詢是否有效地利用了索引。執(zhí)行計(jì)劃可以告訴我們查詢是如何執(zhí)行的,是否使用了索引以及索引的效率如何。如果發(fā)現(xiàn)查詢沒有使用索引或者索引效率低下,可能是因?yàn)樗饕O(shè)計(jì)不合理或者數(shù)據(jù)分布不均勻?qū)е滤饕?。在這種情況下,我們可以考慮優(yōu)化索引設(shè)計(jì),重新構(gòu)建索引,或者調(diào)整SQL查詢語句以更好地利用索引。有時(shí)候,我們也可以通過強(qiáng)制指定特定的索引來引導(dǎo)查詢優(yōu)化器選擇正確的索引。除了修改索引和SQL語句,還可以考慮優(yōu)化查詢條件,避免使用通配符開頭的LIKE語句,盡量避免在WHERE子句中對字段進(jìn)行函數(shù)操作,以及盡量減少JOIN操作的復(fù)雜度。這些方法都可以幫助提高查詢性能和優(yōu)化索引使用。如果遇到索引失效問題,還可以考慮使用數(shù)據(jù)庫提供的工具和分析功能來進(jìn)一步診斷和解決問題。
特殊情況-Explain 執(zhí)行計(jì)劃中,key有值,還是很慢怎么辦?
這是在實(shí)際中遇到的一種情況。我相信大家或多或少也是遇到過這種情況的。
在執(zhí)行計(jì)劃中,當(dāng)看到key字段有值且type為index時(shí),很多人錯(cuò)誤地認(rèn)為這表示查詢已經(jīng)利用了索引。當(dāng)我們查看一個(gè)SQL查詢的執(zhí)行計(jì)劃時(shí),經(jīng)常會(huì)遇到類似以下的情況:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
1 | SIMPLE | users | index | NULL | email_index | 767 | const | 1 | Using where; Using index |
這個(gè)執(zhí)行計(jì)劃中,type=index,key=email_index 很多人會(huì)認(rèn)為這表示這條SQL走了索引,但是其實(shí)這么理解是不對的。
如果是走了索引Extra中的內(nèi)容應(yīng)該是Using index 而不是Using where; Using index
以上的這個(gè)執(zhí)行計(jì)劃表明,這個(gè)SQL確實(shí)用到了email_index的這個(gè)索引樹,但是他并沒有直接通過索引進(jìn)行匹配或者范圍查詢,而是掃描了整顆索引樹。
所以,type=index 意味著進(jìn)行了全索引掃描, 會(huì)遍歷索引樹來查找匹配的行,這個(gè)效率比掃表掃描快一些,但是很有限,和我們通常意義上理解的走了索引 是兩回事兒。
遇到這種情況,大概率是因?yàn)闆]有遵守最左前綴匹配導(dǎo)致的索引失效了。所以需要調(diào)整查詢語句,或者修改索引來解決。
情況二:多表JOIN
在SQL查詢中,多表連接是導(dǎo)致執(zhí)行速度變慢的常見原因之一。當(dāng)我們需要從多個(gè)表中檢索數(shù)據(jù)并將它們組合在一起時(shí),就會(huì)使用多表連接。然而,如果不加以優(yōu)化,這種連接可能會(huì)導(dǎo)致查詢性能下降。
多表連接的執(zhí)行速度變慢主要是因?yàn)閿?shù)據(jù)庫系統(tǒng)需要同時(shí)處理多個(gè)表,進(jìn)行數(shù)據(jù)匹配和組合。這可能涉及大量的數(shù)據(jù)掃描、比較和排序,導(dǎo)致查詢變得緩慢。
為了解決多表連接導(dǎo)致的性能問題,我們可以采取一些優(yōu)化措施:
- 優(yōu)化查詢條件:確保在連接表時(shí)使用有效的查詢條件,限制返回的數(shù)據(jù)量。這可以減少不必要的數(shù)據(jù)匹配,提高查詢效率。
- 合理使用索引:為連接字段創(chuàng)建索引,這樣可以加快數(shù)據(jù)匹配的速度。索引可以幫助數(shù)據(jù)庫系統(tǒng)快速定位需要匹配的數(shù)據(jù)。
- 限制返回字段:只選擇需要的字段,避免返回過多的數(shù)據(jù)。減少返回字段的數(shù)量可以降低數(shù)據(jù)傳輸和處理的負(fù)擔(dān)。
- 考慮表的大小和結(jié)構(gòu):在設(shè)計(jì)數(shù)據(jù)庫表結(jié)構(gòu)時(shí),考慮到表的大小和關(guān)系,可以更好地優(yōu)化多表連接的性能。
- 使用適當(dāng)?shù)倪B接類型:根據(jù)查詢需求選擇合適的連接類型,如INNER JOIN、LEFT JOIN、RIGHT JOIN等。不同的連接類型會(huì)影響查詢的結(jié)果和性能。
舉個(gè)例子,假設(shè)我們有兩個(gè)表:users和orders,我們想要查詢用戶及其對應(yīng)的訂單信息。如果我們使用以下SQL查詢:
SELECT users.name, orders.order_id
FROM users
JOIN orders ON users.user_id = orders.user_id;
在這個(gè)查詢中,我們通過user_id字段將users表和orders表連接起來,但如果這兩個(gè)表的數(shù)據(jù)量很大,且沒有合適的索引,查詢可能會(huì)變得很慢。通過優(yōu)化查詢條件、添加索引、限制返回字段等方式,可以改善這個(gè)查詢的性能,使其執(zhí)行更加高效。
為什么互聯(lián)網(wǎng)公司都不建議使用多表join?
在SQL查詢中,使用JOIN操作可能會(huì)導(dǎo)致效率較低的主要原因在于其實(shí)現(xiàn)方式
MySQL通常使用嵌套循環(huán)(Nested-Loop Join)來執(zhí)行關(guān)聯(lián)查詢。簡單來說,這意味著要通過兩層循環(huán)來比較兩個(gè)表的記錄,外循環(huán)遍歷第一個(gè)表,內(nèi)循環(huán)遍歷第二個(gè)表,然后逐條比較記錄,符合條件的結(jié)果被輸出。
具體到算法實(shí)現(xiàn)上,MySQL主要采用了三種方式:簡單嵌套循環(huán)(Simple Nested Loop)、塊嵌套循環(huán)(Block Nested Loop)和索引嵌套循環(huán)(Index Nested Loop)。然而,這三種方式的效率都不是特別高。
在實(shí)際應(yīng)用中,如果有兩個(gè)表進(jìn)行JOIN操作,復(fù)雜度最高可以達(dá)到O(n^2),而對于三個(gè)表則是O(n^3),隨著表的數(shù)量和數(shù)據(jù)量的增加,JOIN操作的效率會(huì)呈指數(shù)級下降。
值得一提的是,在MySQL 8.0中引入了哈希連接(Hash Join)算法,這種算法可以提高JOIN操作的效率。哈希連接通過構(gòu)建哈希表來快速查找匹配的記錄,相比于嵌套循環(huán),可以更有效地處理JOIN操作,提升查詢性能。
因此,盡管JOIN操作在處理多表關(guān)聯(lián)查詢時(shí)很常見,但需要注意其效率問題。為了優(yōu)化查詢性能,可以考慮使用適當(dāng)?shù)乃饕?、?yōu)化查詢條件、限制返回字段數(shù)量,以及利用新的算法如哈希連接來改善JOIN操作的效率。
關(guān)于什么是Hash Join ,感興趣的話后期可以單獨(dú)出一期文章單獨(dú)說明一下。
情況三:索引基數(shù)太小不合理
- 什么時(shí)字段基數(shù)?
舉個(gè)例子,有一個(gè)字段它一共在10萬行數(shù)據(jù)里有10萬個(gè)值對吧?結(jié)果呢?這個(gè)10萬值,要不然就是0,要不然就是1,那么他的基數(shù)就是2,為什么?因?yàn)檫@個(gè)字段的值就倆選擇,0和1。假設(shè)你要是針對上面說的這種字段建立索引的話,那就還不如全表掃描了,因?yàn)槟愕乃饕龢淅锞蛢H僅包含0和1兩種值,根本沒法進(jìn)行快速的二分查找,也根本就沒有太大的意義了,所以這種時(shí)候,選用這種基數(shù)很低的字段放索引里意義就不大了。
情況四:查詢字段太多
在數(shù)據(jù)庫查詢中,查詢字段過多通常是因?yàn)槲覀冨e(cuò)誤地使用了SELECT *,導(dǎo)致返回了所有字段的數(shù)據(jù)。一般來說,如果查詢字段少于100個(gè),通常不會(huì)造成太大問題,除非字段數(shù)量非常龐大。在這種情況下,我們可以采取兩種方法來解決。
首先,避免查詢那些不必要的字段,只選擇需要的少部分字段進(jìn)行查詢。這樣可以減少數(shù)據(jù)傳輸和處理的負(fù)擔(dān),提高查詢效率。
其次,可以考慮進(jìn)行分表,即垂直分表,將數(shù)據(jù)拆分到多個(gè)表中。通過這種方式,可以將數(shù)據(jù)分散存儲(chǔ)在不同的表中,降低單張表的數(shù)據(jù)量,提升查詢性能。然而,需要注意的是,分表可能會(huì)引入多表JOIN的問題,因此在進(jìn)行拆分時(shí)需要考慮數(shù)據(jù)冗余的情況。
所以,對于查詢字段過多的情況,除了避免不必要字段的查詢外,還可以考慮通過垂直分表的方式來優(yōu)化數(shù)據(jù)存儲(chǔ)和查詢性能。在拆分表時(shí),需謹(jǐn)慎考慮數(shù)據(jù)冗余和可能引發(fā)的多表JOIN問題,以達(dá)到更高效的數(shù)據(jù)查詢和處理。
情況五:表中數(shù)據(jù)量太大
當(dāng)單個(gè)表中的數(shù)據(jù)量超過1000萬條時(shí),通常會(huì)導(dǎo)致查詢效率下降,即使使用了索引也可能變得比較緩慢。在這種情況下,單純建立索引并不能完全解決問題。因此,針對大數(shù)據(jù)量表的情況,可以考慮以下幾種解決方案:
- 數(shù)據(jù)歸檔:將歷史數(shù)據(jù)移出主表,保留只保留最近半年的數(shù)據(jù),而將半年前的數(shù)據(jù)進(jìn)行歸檔。這樣可以減少單表數(shù)據(jù)量,提升查詢效率。
- 分庫分表、分區(qū):將數(shù)據(jù)拆分到多個(gè)庫、多個(gè)表或者進(jìn)行分區(qū)存儲(chǔ)。通過分散數(shù)據(jù)存儲(chǔ)的方式,可以有效降低單表數(shù)據(jù)量,提高查詢性能。關(guān)于分庫分表和分區(qū)的詳細(xì)介紹可以在相關(guān)文檔中查閱。
- 使用第三方數(shù)據(jù)庫:將數(shù)據(jù)同步到支持大規(guī)模查詢的分布式數(shù)據(jù)庫中,例如OceanBase、TiDB,或者存儲(chǔ)到搜索引擎中,如Elasticsearch等。這些數(shù)據(jù)庫具有更好的擴(kuò)展性和處理大數(shù)據(jù)量的能力,可以提升查詢效率和系統(tǒng)性能。
所以,針對表中數(shù)據(jù)量過大的情況,除了建立索引外,還可以通過數(shù)據(jù)歸檔、分庫分表、分區(qū)和使用第三方數(shù)據(jù)庫等方式來優(yōu)化數(shù)據(jù)存儲(chǔ)和查詢性能,以應(yīng)對大數(shù)據(jù)量帶來的查詢效率問題。
情況六:數(shù)據(jù)庫連接數(shù)不夠
當(dāng)數(shù)據(jù)庫連接數(shù)不足時(shí),需要具體分析造成這種情況的原因??赡艿脑蛴袔讉€(gè):
- 業(yè)務(wù)量過大:如果業(yè)務(wù)量巨大,單個(gè)數(shù)據(jù)庫無法承載,那么最好的解決方案是進(jìn)行數(shù)據(jù)庫分庫操作,將數(shù)據(jù)分散存儲(chǔ)在多個(gè)庫中,以減輕單庫壓力。
- 慢SQL或長事務(wù):存在一些慢SQL查詢或長時(shí)間運(yùn)行的事務(wù),會(huì)占用數(shù)據(jù)庫連接資源,導(dǎo)致數(shù)據(jù)庫連接數(shù)不足。這種情況下,慢SQL會(huì)占用連接資源,導(dǎo)致其他查詢被阻塞,進(jìn)而影響整體查詢效率。
其實(shí)這種情況如果系統(tǒng)的用戶較多,其實(shí)很是較容易遇到的。比如:之前我就遇到過類似的問題,報(bào)錯(cuò)如下:
Caused by: ERR-CODE: [TDDL-4103][ERR_ATOM_CONNECTION_POOL_FULL]
Pool of DB 'cn-zhxxx_i-xxx_fin_risk_xxx_30xx:33.10.xxx.xx:30xx' is full.
Message from pool: wait millis 5000, active 10, maxActive 10.
AppName:FIN_RISK_xxx_APP, Env:ONLINE, UnitName:null.
如果發(fā)現(xiàn)上述問題,則需要去監(jiān)平臺(tái)上看一下相關(guān)SQL的耗時(shí)情況
我們的問題其實(shí)就是簡單的一個(gè)更新語句,其中使用了樂觀鎖進(jìn)行并發(fā)控制。
為什么樂觀鎖還會(huì)導(dǎo)致大量的鎖耗時(shí)呢?
雖然樂觀鎖是不需要加鎖的,通過CAS的方式進(jìn)行無鎖并發(fā)控制進(jìn)行更新的。但是InnoDB的update語句是要加鎖的。當(dāng)并發(fā)沖突比較大,發(fā)生熱點(diǎn)更新的時(shí)候,多個(gè)update語句就會(huì)排隊(duì)獲取鎖。
而這個(gè)排隊(duì)的過程就會(huì)占用數(shù)據(jù)庫鏈接,一旦排隊(duì)的事務(wù)比較多的時(shí)候,就會(huì)導(dǎo)致數(shù)據(jù)庫連接被耗盡。
當(dāng)數(shù)據(jù)庫連接被耗盡時(shí),通常是因?yàn)榕抨?duì)的事務(wù)過多導(dǎo)致的。在高并發(fā)情況下,如果排隊(duì)的事務(wù)數(shù)量很大,就會(huì)耗盡數(shù)據(jù)庫連接資源。
這類問題的解決思路有以下幾個(gè):
- 使用緩存進(jìn)行熱點(diǎn)數(shù)據(jù)更新,如Redis,以減輕數(shù)據(jù)庫壓力。
- 采用異步更新的方式,平滑處理高并發(fā)更新請求,避免峰值沖擊。
- 將熱點(diǎn)數(shù)據(jù)拆分存儲(chǔ)到不同的庫或表中,減少并發(fā)沖突。
- 合并更新請求,通過批量執(zhí)行的方式降低沖突。例如,將多個(gè)增加積分的操作合并為一次性批量執(zhí)行,減少數(shù)據(jù)庫負(fù)擔(dān)。
需要注意的是,第2和第4種方案會(huì)引入一定的延遲,將實(shí)時(shí)更新變?yōu)楫惒礁?,可能?huì)影響數(shù)據(jù)的實(shí)時(shí)性。而第1和第3種方案在實(shí)施過程中成本較高,但相對更完整。
根據(jù)實(shí)際業(yè)務(wù)場景,選擇合適的解決方案非常重要。在某些情況下,如我們的業(yè)務(wù)場景,選擇第4種方案,即合并更新操作并批量執(zhí)行,可以有效降低數(shù)據(jù)庫連接壓力。舉例來說,如果需要給100個(gè)用戶增加積分,可以將這些操作合并并在一定時(shí)間間隔內(nèi)批量執(zhí)行,以減少數(shù)據(jù)庫負(fù)擔(dān)。
情況七:數(shù)據(jù)庫表結(jié)構(gòu)不合理
當(dāng)數(shù)據(jù)庫表結(jié)構(gòu)不合理時(shí),這也是造成性能問題的關(guān)鍵原因之一。例如,某些字段存儲(chǔ)了過長的內(nèi)容,或者缺乏合理的冗余導(dǎo)致需要頻繁進(jìn)行多表關(guān)聯(lián)查詢。解決這類問題的思路通常是進(jìn)行數(shù)據(jù)庫重構(gòu)或者考慮分表操作。
情況八:數(shù)據(jù)庫IO或者CPU比較高
另外,數(shù)據(jù)庫高IO或CPU占用率也是常見的問題。當(dāng)數(shù)據(jù)庫整體IO或CPU負(fù)載過高時(shí),查詢速度可能會(huì)下降,因此需要分析背后的原因并采取相應(yīng)的解決方案。
情況九:存在長事務(wù)
長事務(wù)和慢SQL問題類似,都會(huì)占用數(shù)據(jù)庫連接,導(dǎo)致其他請求需要等待。
情況十:鎖競爭導(dǎo)致的等待
在數(shù)據(jù)庫中,鎖競爭也會(huì)導(dǎo)致等待。當(dāng)多個(gè)并發(fā)請求爭奪共享資源時(shí),會(huì)導(dǎo)致鎖等待,進(jìn)而增加執(zhí)行時(shí)間,使SQL變慢。這種情況也會(huì)類似于CPU被打滿的問題。
情況十一:數(shù)據(jù)庫參數(shù)不合理
針對具體業(yè)務(wù)場景,適當(dāng)調(diào)整數(shù)據(jù)庫參數(shù)可以顯著提升SQL效率。例如,調(diào)整內(nèi)存大小、緩存大小、線程池大小等參數(shù)都可能對數(shù)據(jù)庫性能產(chǎn)生影響。