查询调优咨询及数据库性能决策流程合理性评审
技术问询
- 是否有进一步调优以下查询的方法?
- 请评审以下决策流程是否逻辑充分:
- 使用Postman测试时,下方查询的执行时间约为100ms。
- 但达到一定流量后,响应时间超过10秒(大部分时间消耗在
getConnection()上),调整连接池大小后效果相近。 - 发现数据库服务器的CPU及内存占用率较高。
- 其他API对数据库服务器的CPU/内存占用较低,因此
getConnection()耗时更短。 - 假设该API占用大量服务器资源,尝试垂直扩容(scaling up)但未显著提升性能。
- 计划尝试水平扩容(scaling out)。
尽管下方查询占用大量CPU/内存,但不确定如何优化:
Hibernate: select store0_.store_id as col_0_0_, store0_.name as col_1_0_, store0_.address as col_2_0_, store0_.avg_review_rating as col_3_0_, store0_.like_count as col_4_0_ from store store0_ inner join category category1_ on store0_.category_id=category1_.category_id left outer join store_keyword storekeywo2_ on store0_.store_id=storekeywo2_.store_id left outer join keyword keyword3_ on storekeywo2_.keyword_id=keyword3_.keyword_id where store0_.status=? and ?=? and keyword3_.name=? and ?=? and ( store0_.name like ? escape '!' ) order by store0_.avg_review_rating desc limit ? Hibernate: select store1_.store_id as col_0_0_ from store_keyword storekeywo0_ inner join store store1_ on storekeywo0_.store_id=store1_.store_id inner join keyword keyword2_ on storekeywo0_.keyword_id=keyword2_.keyword_id where store1_.status=? and ?=? and keyword2_.name=? and ?=? and ( store1_.name like ? escape '!' ) Hibernate: select store0_.store_id as col_0_0_, storeimage1_.url as col_1_0_, keyword3_.name as col_2_0_ from store store0_ left outer join image storeimage1_ on store0_.store_id=storeimage1_.store_id left outer join store_keyword storekeywo2_ on store0_.store_id=storekeywo2_.store_id inner join keyword keyword3_ on storekeywo2_.keyword_id=keyword3_.keyword_id where store0_.store_id in ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
@Override public Page<StoreResponseWithKeyword> findStoresByCondition(Pageable pageable, String category, String keyword, String location, String storeName) { List<StoreResponseWithKeyword> storeQuery = null; int count=0; if(!keyword.isBlank()){ storeQuery = jpaQueryFactory .select( Projections.constructor( StoreResponseWithKeyword.class, store.storeId, store.name, store.address, store.avgReviewRating, //keywordlist //image.url, store.likeCount )) .from(store) .where(store.status.eq(StoreStatus.APPROVED)) .where( categoryEq(category), keywordContain(keyword), addressContain(location), storeNameContain(storeName) ) .innerJoin(store.category, QCategory.category) .leftJoin(store.storeKeywordList, storeKeyword) .leftJoin(storeKeyword.keyword, QKeyword.keyword) .orderBy(createOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); count = jpaQueryFactory .select(store.storeId) .from(storeKeyword) .where(store.status.eq(StoreStatus.APPROVED)) .where( categoryEq(category), keywordContain(keyword), addressContain(location), storeNameContain(storeName) ) .innerJoin(storeKeyword.store, store) .innerJoin(storeKeyword.keyword, QKeyword.keyword) .fetch().size(); }else{ storeQuery = jpaQueryFactory .select( Projections.constructor( StoreResponseWithKeyword.class, store.storeId, store.name, store.address, store.avgReviewRating, store.likeCount )) .from(store) .where(store.status.eq(StoreStatus.APPROVED)) .where( categoryEq(category), addressContain(location), storeNameContain(storeName) ) .innerJoin(store.category, QCategory.category) .orderBy(createOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); count = jpaQueryFactory .select(store.storeId) .from(store) .where(store.status.eq(StoreStatus.APPROVED)) .where( categoryEq(category), keywordContain(keyword), addressContain(location), storeNameContain(storeName) ) .fetch().size(); } List<Long> storeIds = storeQuery.stream().map(StoreResponseWithKeyword::getStoreId).collect(Collectors.toList()); // Image URL 및 Keyword 한 번에 가져오기 List<Tuple> combinedData = jpaQueryFactory .select(store.storeId, image.url, QKeyword.keyword.name) .from(store) .leftJoin(store.storeImageList, image) .leftJoin(store.storeKeywordList, storeKeyword) .leftJoin(storeKeyword.keyword, QKeyword.keyword) .where(store.storeId.in(storeIds)) .fetch(); Map<Long, String> storeIdToImageUrl = new HashMap<>(); Map<Long, Set<String>> storeIdToKeywordNames = new HashMap<>(); for (Tuple tuple : combinedData) { Long storeId = tuple.get(store.storeId); String imageUrl = tuple.get(image.url); String keywordName = tuple.get(QKeyword.keyword.name); storeIdToImageUrl.putIfAbsent(storeId, imageUrl); if (keywordName != null) { storeIdToKeywordNames.computeIfAbsent(storeId, k -> new HashSet<>()).add(keywordName); } } for (StoreResponseWithKeyword response : storeQuery) { String storedImageUrl = storeIdToImageUrl.getOrDefault(response.getStoreId(), ""); response.setStoreImageUrl(storedImageUrl); Set<String> keywordList = storeIdToKeywordNames.getOrDefault(response.getStoreId(), Collections.emptySet()); response.setKeywordList(new ArrayList<>(keywordList)); } return new PageImpl<>(storeQuery, pageable, count); }
public class StoreResponseWithKeyword { private Long storeId; private String storeName; private String address; private Double avgScore; private List<String> keywordList = new ArrayList<>(); private String storeImageUrl; private Long likeCount; }
一、查询调优方案
1. 减少数据库交互次数
当前代码存在多次冗余查询,可大幅合并:
- 合并主查询与关联数据查询:将store基础信息、image、keyword通过一次查询获取,利用
fetch join或内存去重处理关联数据膨胀问题,避免多次DB往返。 - 优化总数统计逻辑:将统计总数的
select storeId.fetch().size()改为select count(distinct store.storeId),避免全量数据加载到内存再统计。
2. 优化SQL执行效率
针对生成的SQL,重点优化以下点:
- 索引补全:
- 给
store(status, category_id)创建联合索引,覆盖主查询的过滤条件; - 给
keyword(name)加索引,优化keyword.name=?的过滤; - 给
store_keyword(store_id, keyword_id)加联合索引,加速关联查询; - 给
store(status, avg_review_rating)创建联合索引,避免排序时的文件排序。
- 给
- 调整关联类型:当
keyword不为空时,left join store_keyword/keyword会被where keyword.name=?转为内连接,直接改为inner join减少查询开销。 - 优化模糊查询:若
store.name like ?是后缀或全模糊匹配,普通索引无法生效,可改为前缀匹配并加索引,或引入全文索引/搜索引擎替代。
3. 代码逻辑优化
- 复用查询条件:将
categoryEq、addressContain等条件封装为可复用Predicate,避免主查询和统计查询重复定义; - 使用Querydsl原生分页:直接利用Querydsl的
page()方法返回Page对象,无需手动封装PageImpl; - 内存数据处理优化:查询关联数据时通过
group by store.storeId减少返回的Tuple数量,降低内存遍历开销。
4. 连接池与数据库配置
- 确认连接池最大连接数未超过数据库
max_connections限制,配置合理的空闲连接回收策略,避免连接泄漏; - 调整数据库缓存参数(如MySQL的
innodb_buffer_pool_size),提升数据缓存命中率。
二、决策流程评审
当前流程逻辑存在明显不足,需补充关键验证环节:
getConnection()耗时高的原因未明确:
仅归因于API占用资源,未验证连接池是否耗尽、是否存在连接泄漏、数据库连接队列是否阻塞。需通过监控工具(如慢查询日志、连接池监控)定位真实原因。- 垂直扩容无效的分析缺失:
垂直扩容后性能无提升,说明瓶颈不在硬件,而在查询效率(如全表扫描、无索引)或锁竞争。需先排查查询执行计划,优化查询后再评估扩容。 - 水平扩容的前提不充分:
水平扩容是复杂方案,若低效查询未优化,即使扩容仍会占用大量资源。应先完成查询优化,再评估是否需要分库分表。 - 对比其他API的逻辑不严谨:
其他APIgetConnection()耗时短,可能是查询效率更高或请求量更低,不能直接得出“该API占用大量资源”的结论,需通过执行计划、慢日志对比验证。
总结:当前决策流程逻辑不充分,需先完成查询性能分析、连接池问题定位,再决定扩容方案,而非直接从垂直扩容跳到水平扩容。
内容的提问来源于stack exchange,提问作者firefly_0
相关产品推荐
相关产品推荐

