SQL語句執行時間太慢,有什么優化措施?
可以從四個方面進行:
第一個是查詢是否添加了索引
如果沒有的話,為查詢字段添加索引,
還有是否存在讓索引失效的場景,像是沒有遵循最左前綴,進行了一些類型轉化
第二點是SQL語句本身的優化,
1、如避免使用SELECT *,只查詢需要的字段
2、優化JOIN操作,避免笛卡爾積
如 SELECT * FROM a JOIN b(缺少 a.id = b.a_id),會產生 “笛卡爾積”(數據量 = 表 a 行數 × 表 b 行數),瞬間耗盡數據庫資源。 優化:JOIN 必須加關聯條件,且關聯字段需建索引(如 a.id 和 b.a_id)。
3、大表與大表直接 JOIN 若兩張表均有百萬級數據,直接 JOIN 會產生大量中間結果,耗時極長。
第三點是表結構設計優化
像是采用分庫分表的方式,解決數據量過大問題
- 水平分表(按行拆分):將一張表按規則拆分為多張表,每張表結構相同,數據不同。常見規則:
- 時間范圍:
orders_2023
、orders_2024
(按年份拆分); - 哈希:
user_0
~user_31
(按?user_id % 32
?拆分)。工具:Sharding-JDBC、MyCat。
- 時間范圍:
- 垂直分庫(按業務拆分):將一個數據庫按業務模塊拆分為多個數據庫,如電商系統拆分為?
user_db
(用戶)、order_db
(訂單)、product_db
(商品),避免單庫壓力過大。
第四點是架構優化
1、像是使用redis提前存儲數據,減輕數據庫的請求壓力,避免每次查詢都訪問數據庫。
2、采用讀寫分離的方式,將 “讀操作”(如查詢)路由到從庫,“寫操作”(如插入、更新)路由到主庫,避免主庫讀壓力過大。
衍生出的問題:
1、為什么添加索引后,SQL的執行時間就變快了吶?
?首先我們要了解索引這個概念,如果將數據庫比作一本,那么索引就相當于是這本書的目錄,而如果沒有目錄的話,當查找某個內容的話,你只能一頁一頁查找,,數據量越大,翻頁時間越長;
當添加了目錄后,你就可以精準的定位到某個對應,解決無效的翻頁時間。
索引的核心原理就是將“全表掃描”轉化為“精準定位”
數據庫表的原始數據(行數據)存儲在磁盤上,默認是 “無序” 的(除非按主鍵排序)。當沒有索引時,查詢數據(如?WHERE user_id = 123
)需要做以下操作:
- 從磁盤讀取表的第一行數據,檢查?
user_id
?是否等于 123; - 不等于則繼續讀第二行、第三行…… 直到遍歷完所有行(全表掃描);
- 若表有 100 萬行數據,最壞情況需要讀取 100 萬次磁盤 —— 而磁盤 IO 是數據庫性能的 “最大瓶頸”(磁盤讀寫速度比內存慢 1000 倍以上)。
添加索引后,情況完全不同:索引會單獨創建一個 “有序的索引結構”,把 “查詢條件字段(如?user_id
)” 和 “行數據的磁盤地址” 關聯起來,并且按?user_id
?排序。此時查詢?user_id = 123
?的流程變成:
- 去索引結構中查找?
user_id = 123
—— 由于索引是有序的,可通過 “二分查找”(類似查字典)快速定位,只需 3~4 次磁盤 IO(100 萬數據的二分查找次數僅約 20 次,遠少于全表掃描的 100 萬次); - 從索引中獲取對應行數據的磁盤地址;
- 直接根據地址讀取目標行數據,無需遍歷其他行。
底層邏輯
索引的數據結構是B + 樹索引
B+樹作為索引的存儲結構。選擇B+樹的原因包括:
- 節點可以有更多子節點,路徑更短;
- 磁盤讀寫代價更低,非葉子節點只存儲鍵值和指針,葉子節點存儲數據;
- B+樹適合范圍查詢和掃描,因為葉子節點形成了一個雙向鏈表。
2、如何分析這條執行很慢的SQL語句?
采用explain命令,分析這條SQL的執行情況。通過key
和key_len
可以檢查是否命中了索引,如果已經添加了索引,也可以判斷索引是否有效。通過type
字段可以查看SQL是否有優化空間,比如是否存在全索引掃描或全表掃描。通過extra
建議可以判斷是否出現回表情況,如果出現,可以嘗試添加索引或修改返回字段來優化。
3、索引失效的場景
- 沒有遵循最左前綴原則。
- 使用了模糊查詢且
%
號在前面。 - 在索引字段上進行了運算或類型轉換。
- 使用了復合索引但在中間使用了范圍查詢,導致右邊的條件索引失效。
**擴展:**最左前綴原則
索引失效的最左前綴原則是針對聯合索引(多字段索引)的一條核心規則, 簡單來說:在聯合索引中,查詢條件必須從索引的第一個字段開始匹配,且中間不能跳過任何字段,否則跳過的字段及之后的字段無法使用索引,導致索引失效或部分失效。 底層原理:
聯合索引在底層(如 B + 樹)的存儲是 “先按第一個字段排序,第一個字段相同的再按第二個字段排序,以此類推”。
如對對(a, b, c)
?建立聯合索引
- 先按?
a
?升序排列; - 當?
a
?相等時,按?b
?升序排列; - 當?
a
?和?b
?都相等時,按?c
?升序排列。
4、讀寫分離模式下如何保證主從數據一致性
原因:由于主庫數據同步到從庫存在延遲(如網絡傳輸、SQL 執行耗時),可能導致 “主庫寫入數據后,從庫讀取不到最新數據” 的問題。
解決方式:
- 配置合適刷盤策越
- 減少binlog的日志量,避免大事務,拆分為事務。
- 寫讀后延遲等待,比如寫操作后,線程休眠一段時間,再讀從庫
- 增加重試機制,:讀從庫時若獲取到舊數據(可通過版本號或時間戳判斷),重試幾次(如 3 次,每次間隔 50ms),直到獲取最新數據或超時后讀主庫。
- 對于強一致要求的數據,像是金融-支付,可以讀主庫,弱一致性的數據,像是電商商品展示,日志查詢,允許一定的延遲,可以讀從庫。
5、如何保證緩存和數據庫的數據一致性,(如,一次大量的請求到來,如何添加緩存?)
核心原則:先操作數據庫,然后再是緩存
方案一:
最常用的方案,適合大多數業務場景(最終一致性),流程如下:
1. 讀操作
- 先查緩存:命中則直接返回;
- 緩存未命中:查數據庫,將結果寫入緩存,再返回。
2. 寫操作
- 先更新數據庫;
- 再刪除緩存(而非更新緩存)。
為什么刪除緩存,而不是更新? 主要是避免 “緩存更新邏輯與數據庫更新邏輯不一致” 導致的錯誤(如數據庫有觸發器 / 事務,緩存更新可能漏處理);
方案二:
相對于方案一做出一點改變: 更新數據庫后主動更新緩存
需要注意的點是 必須在數據庫事務內更新緩存,確保數據庫與緩存操作 “同成功同失敗”。
方案三:
延遲雙刪 在高并發場景下,可能出現 “數據庫已更新,但緩存刪除請求因網絡延遲未執行” 的情況,導致舊數據殘留。
操作原理:
- 第一次刪除:盡可能在數據庫更新前清除舊緩存;
- 第二次刪除:針對 “數據庫更新后,緩存刪除請求失敗” 或 “有其他線程在數據庫更新期間寫入了舊數據到緩存” 的場景,再次清理。
方案四:
基于 binlog 的異步更新緩存(高可用場景)
通過監聽數據庫 binlog(如 MySQL 的 binlog),異步更新緩存,適合讀寫分離、高并發場景:
- 流程:
- 數據庫更新后,binlog 記錄數據變更;
- 監聽組件解析 binlog,獲取變更數據;
- 緩存更新服務根據變更數據,異步更新或刪除緩存。