引言
在Java開發的江湖里,MyBatis Plus(MP)早已是“效率利器”——它用極簡的API封裝了CRUD操作,讓開發者從重復的SQL編寫中解放出來。但隨著項目數據量從“萬級”躍升至“十萬級”“百萬級”,一個尷尬的現實逐漸浮現:曾經跑得飛快的MP應用,開始頻繁出現接口超時、數據庫壓力驟增的情況。問題根源往往不在MP本身,而在那些“隱形的SQL”——它們可能因索引缺失、查詢冗余或分頁邏輯欠妥,在數據洪流中淪為性能瓶頸。今天,我們就來聊聊如何用MP的特性,精準定位并解決這些SQL性能問題。
一、定位性能問題:先學會“看日志”和“讀執行計劃”
很多同學遇到慢查詢第一反應是“是不是MP的問題?”——其實MP本身只是ORM框架,SQL性能的核心還是在數據庫本身的執行效率。所以第一步,得先搞清楚“MP到底生成了什么SQL?執行了多久?”
1. 開啟MP的SQL日志:讓“隱形SQL”現形
MP的日志配置非常簡單,但90%的新手可能沒配置對。在application.yml
里加上這幾行,開發環境直接能看到完整的SQL執行過程:
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制臺打印SQL(開發環境必備)global-config:db-config:logic-delete-field: logic_delete_field # 邏輯刪除字段(自動過濾已刪數據,避免全表掃描)
輸出示例:
控制臺會打印類似這樣的信息:
==> Preparing: SELECT id,name,age FROM user WHERE age > ? AND logic_delete_field = 0
==> Parameters: 18(Integer)
<== Total: 10
重點關注Preparing
里的SQL語句和Parameters
里的參數,這是后續分析的基礎。
2. 數據庫層面:用EXPLAIN“解剖”SQL
光看MP日志還不夠,必須結合數據庫的執行計劃。以MySQL為例,拿到MP生成的SQL后,在Navicat或命令行執行EXPLAIN + SQL語句
,重點關注4個指標:
指標 | 含義 | 優化目標 |
---|---|---|
type | 訪問類型(性能從好到差:system > const > eq_ref > ref > range > index > ALL) | 至少達到range ,避免ALL (全表掃描) |
key | 實際使用的索引 | 不為NULL (未命中索引) |
rows | MySQL估計要掃描的行數 | 數值越小越好 |
Extra | 額外信息(如Using filesort 文件排序、Using temporary 臨時表) | 避免這兩種情況 |
舉個栗子:
假設MP生成了SELECT * FROM user WHERE username LIKE '%張三%'
,執行EXPLAIN
后發現type=ALL
且key=NULL
,這說明:
- 沒有給
username
字段加索引(左模糊LIKE '%xxx'
無法用普通索引); - 全表掃描導致性能極差(數據量10萬時,掃描時間可能飆升到秒級)。
3. 慢查詢日志:抓出“隱形殺手”
除了實時日志,一定要開啟數據庫的慢查詢日志,記錄執行時間超過閾值的SQL。以MySQL為例,在my.cnf
里配置:
slow_query_log = 1 # 開啟慢查詢
slow_query_log_file = /var/log/mysql/slow.log # 日志路徑
long_query_time = 1 # 超過1秒的SQL記錄
log_queries_not_using_indexes = 1 # 記錄未使用索引的SQL(關鍵!)
重啟MySQL后,所有“慢SQL”都會被記錄下來,結合MP日志的時間戳,就能精準定位到具體是哪個業務接口觸發了問題SQL。
二、MP常見的5大SQL性能“坑”
通過日志和執行計劃分析后,你會發現MP的性能問題大多源于使用不當,而不是框架本身的bug。以下是最常見的5類問題,看看你中過幾個?
1. 全表掃描:索引白加了?
典型場景:
- 對高頻查詢字段(如
age
、create_time
)未加索引; - 用
LIKE '%關鍵詞%'
做模糊查詢(左模糊無法用普通索引); - 邏輯刪除未生效(未配置
@TableLogic
,導致查詢時仍然掃描已刪除數據)。
優化建議:
- 對高頻查詢、排序、分組的字段(如
create_time
、status
)添加索引; - 左模糊查詢(
LIKE '%xxx'
)盡量改用全文索引(MySQL 5.6+支持FULLTEXT
)或搜索引擎(如Elasticsearch); - 配置
@TableLogic
標記邏輯刪除字段,MP會自動過濾已刪數據,避免全表掃描。
2. N+1查詢:循環調用數據庫,能不慢嗎?
典型場景:
查詢主表(如用戶表)后,循環調用接口查詢關聯表(如訂單表):
// 錯誤示范:1次查用戶,N次查訂單(總查詢次數=1+N)
List<User> users = userMapper.selectList(wrapper);
users.forEach(user -> {List<Order> orders = orderMapper.selectByUserId(user.getId()); // 循環調用
});
問題:
數據量1000時,總查詢次數是1001次!數據庫連接池被占滿,響應時間飆升。
優化方案:
- 批量查詢:先查所有用戶ID,再用
selectBatchIds
一次性查訂單:List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList()); List<Order> orders = orderMapper.selectBatchIds(userIds); // 1次查詢搞定
- 懶加載:用
@TableField(select = false)
標記不需要立即查詢的關聯字段,需要時手動調用; - MyBatis二級緩存:對高頻讀、低頻寫的數據(如字典表)啟用緩存(需注意緩存一致性)。
3. 批量操作:舊版本MP的“隱形殺手”
典型場景:
用saveBatch
插入1萬條數據,發現耗時5秒+。查日志發現MP生成了1萬條INSERT
語句!
原因:
MP 3.5.0之前,默認的批量插入實現是通過foreach
拼接多條INSERT
語句(非數據庫原生的批量插入),網絡IO和事務提交次數暴增。
優化方案:
- 升級MP到3.5.0+:默認支持
INSERT INTO user (name,age) VALUES (a),(b),(c)
的原生批量插入; - 手動分批處理:數據量極大時(如10萬條),按每1000條分批插入:
List<List<User>> batches = ListUtils.partition(userList, 1000); // 每批1000條 batches.forEach(batch -> userMapper.saveBatch(batch));
4. 分頁查詢:LIMIT 100000,20 等于“自殺”
典型場景:
做后臺管理系統時,用戶翻到第100頁,SQL是SELECT * FROM user LIMIT 100000,20
。數據庫需要掃描前100020行,耗時隨數據量線性增長。
問題:
LIMIT offset, size
的本質是“先掃描offset+size行,再丟棄前offset行”,數據量大時效率極低。
優化方案:
- 小數據量分頁:直接用MP的
Page
對象,底層會自動處理; - 大數據量分頁:改用
WHERE id > lastId LIMIT size
(需主鍵有序):// 假設上一頁最后一條記錄的ID是lastId QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.gt("id", lastId).last("LIMIT 20"); // 直接拼接LIMIT語句 IPage<User> page = userMapper.selectPage(new Page<>(current, size), wrapper);
5. 冗余查詢:SELECT * 害人不淺
典型場景:
用userMapper.selectById(1L)
查詢用戶,但表里有avatar
(大圖片)、remark
(長文本)等字段,結果返回了幾十KB的數據,內存和網絡都被占滿。
問題:
SELECT *
會查詢所有字段,包括大字段和不必要的字段,增加網絡傳輸和內存消耗。
優化方案:
- 指定查詢字段:用
QueryWrapper.select()
明確需要的字段:QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("id", "name", "age"); // 只查這三個字段 User user = userMapper.selectOne(wrapper);
- DTO投影:定義只包含需要字段的DTO類,避免實體類膨脹:
@Data public class UserDTO {private Long id;private String name;private Integer age; } // 查詢結果直接映射到DTO List<UserDTO> dtos = userMapper.selectList(wrapper);
三、總結:MP性能優化的“三板斧”
通過上面的案例,我們可以總結出MP SQL性能優化的核心思路:
1. 日志是“眼睛”,EXPLAIN是“放大鏡”
開啟MP日志,結合EXPLAIN
分析執行計劃,90%的性能問題都能定位到索引或SQL寫法上。
2. 索引不是萬能的,但不加索引是萬萬不能
高頻查詢字段一定要加索引,左模糊、全表掃描這類“坑”能避則避。
3. 批量和分頁要“講究”
批量操作用3.5.0+的新特性,分頁大數據量時用主鍵范圍查詢,避免LIMIT offset, size
。
最后想和大家說:MP是工具,SQL性能的核心還是在開發者對業務的理解和數據庫的掌握。多動手分析日志、執行計劃,多測試不同場景下的性能表現,才能讓項目“快人一步”!
你在開發中遇到過哪些MP的SQL性能問題?歡迎在評論區分享你的踩坑經歷和解決方案~