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

























