MybatisPlus模糊查询性能优化:当`like`遇上多值匹配,如何避免全表扫描?
MybatisPlus模糊查询性能优化:多字段匹配的深度实践
在数据驱动的现代应用中,模糊查询几乎是每个系统都无法绕开的功能需求。当数据量达到百万级甚至更高时,一个简单的LIKE '%keyword%'操作就可能成为系统性能的"阿喀琉斯之踵"。特别是当业务需求要求在多个字段上进行模糊匹配时,问题会变得更加复杂——索引失效、全表扫描、执行计划劣化等问题接踵而至。
1. 多字段模糊查询的性能陷阱
1.1 OR条件与索引失效
MybatisPlus中常见的多字段模糊查询写法是这样的:
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("username", "张") .or() .like("email", "example") .or() .like("phone", "138");这种链式OR操作生成的SQL类似于:
SELECT * FROM user WHERE username LIKE '%张%' OR email LIKE '%example%' OR phone LIKE '%138%'问题核心在于:
- 前导通配符
%导致索引失效 - OR条件使得优化器难以选择最优执行计划
- 多字段组合查询时,数据库可能选择全表扫描
1.2 执行计划分析对比
我们通过EXPLAIN分析不同写法的执行计划差异:
| 查询类型 | 索引使用情况 | 扫描行数 | 执行时间 |
|---|---|---|---|
| 单字段右模糊 | 使用索引 | 1,200 | 15ms |
| 多字段OR模糊 | 全表扫描 | 1,000,000 | 1,200ms |
| 多字段UNION ALL | 部分使用索引 | 3,600 | 45ms |
提示:在MySQL中,
LIKE 'value%'可以使用索引,但LIKE '%value%'会导致索引失效
2. 优化策略与实践方案
2.1 查询结构调整
策略一:UNION ALL替代OR
// 方案1:分别查询后合并结果 List<User> result = new ArrayList<>(); result.addAll(userMapper.selectList(new QueryWrapper<User>().like("username", "张"))); result.addAll(userMapper.selectList(new QueryWrapper<User>().like("email", "example"))); result.addAll(userMapper.selectList(new QueryWrapper<User>().like("phone", "138"))); // 方案2:使用UNION ALL SQL @Select("SELECT * FROM user WHERE username LIKE '%${keyword}%' " + "UNION ALL SELECT * FROM user WHERE email LIKE '%${keyword}%' " + "UNION ALL SELECT * FROM user WHERE phone LIKE '%${keyword}%'") List<User> searchByMultipleFields(@Param("keyword") String keyword);策略二:条件预筛选
// 先通过精确匹配缩小范围 List<Integer> userIds = userMapper.selectObjs( new QueryWrapper<User>() .select("id") .eq("status", 1) .between("create_time", startDate, endDate) ).stream().map(o -> (Integer)o).collect(Collectors.toList()); // 再在缩小后的范围内做模糊查询 if(!userIds.isEmpty()) { queryWrapper.in("id", userIds) .and(wrapper -> wrapper .like("username", "张") .or() .like("email", "example")); }2.2 MybatisPlus 3.x特性应用
Lambda表达式条件构造
// 更安全的类型安全写法 List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery() .and(qw -> { for(String keyword : keywords) { qw.or().like(User::getUsername, keyword) .or().like(User::getEmail, keyword); } }) .eq(User::getIsDeleted, 0));条件构造器优化技巧
// 动态条件构建 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); boolean hasCondition = false; if(StringUtils.isNotBlank(username)) { queryWrapper.like(rightLike(username), "username"); hasCondition = true; } if(StringUtils.isNotBlank(email)) { if(hasCondition) queryWrapper.or(); queryWrapper.like(rightLike(email), "email"); hasCondition = true; } // 辅助方法:避免前导通配符 private String rightLike(String value) { return value.endsWith("%") ? value : value + "%"; }3. 高级优化方案
3.1 引入全文索引
对于频繁的模糊查询场景,考虑使用专业搜索技术:
-- MySQL全文索引 ALTER TABLE user ADD FULLTEXT INDEX ft_search (username, email, phone); -- 使用MATCH AGAINST语法 SELECT * FROM user WHERE MATCH(username, email, phone) AGAINST('+张 +example' IN BOOLEAN MODE);MybatisPlus中集成:
@Select("SELECT * FROM user WHERE MATCH(username, email, phone) " + "AGAINST(#{query} IN BOOLEAN MODE)") List<User> fullTextSearch(@Param("query") String query);3.2 异步查询与缓存策略
实现方案:
- 用户发起搜索请求
- 系统立即返回空结果+查询ID
- 后台异步执行复杂查询
- 结果缓存到Redis并通知前端
- 前端通过WebSocket或轮询获取结果
// 伪代码示例 public SearchResult searchAsync(SearchParams params) { String searchId = UUID.randomUUID().toString(); // 放入消息队列异步处理 mqTemplate.send("search.queue", new SearchTask(searchId, params)); // 立即返回 return new SearchResult(searchId, Collections.emptyList()); } // 消费者处理 public void handleSearchTask(SearchTask task) { List<User> results = complexSearch(task.getParams()); redisTemplate.opsForValue().set("search:"+task.getSearchId(), results); notifyClient(task.getSearchId()); }4. 实战案例:电商用户搜索优化
某电商平台用户表(2000万数据)的多字段搜索优化实践:
原始方案:
queryWrapper.like("nickname", keyword) .or().like("mobile", keyword) .or().like("email", keyword);- 平均响应时间:3.2秒
- CPU使用率峰值:90%
优化方案:
- 建立(nickname, mobile, email)的组合索引
- 使用UNION ALL拆分查询
- 增加结果缓存
if(isComplexSearch(keyword)) { // 复杂查询走异步流程 return searchAsync(keyword); } else { // 简单查询使用优化后的UNION方案 return fastSearch(keyword); }优化后指标:
- 平均响应时间:480ms
- CPU使用率峰值:35%
- 99线延迟:<1秒
配置参考:
# MybatisPlus配置 mybatis-plus.global-config.db-config.logic-not-delete-value=0 mybatis-plus.global-config.db-config.logic-delete-value=1 # 连接池配置 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.connection-timeout=30000在具体实现时,我们发现当查询条件超过3个OR组合时,使用UNION ALL方案性能优势更加明显。但对于小数据量表(10万行以下),简单的OR查询可能更合适,需要根据实际业务场景进行权衡。
