You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

查询调优咨询及数据库性能决策流程合理性评审

技术问询
  1. 是否有进一步调优以下查询的方法?
  2. 请评审以下决策流程是否逻辑充分:
    • 使用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),提升数据缓存命中率。

二、决策流程评审

当前流程逻辑存在明显不足,需补充关键验证环节:

  1. getConnection()耗时高的原因未明确:
    仅归因于API占用资源,未验证连接池是否耗尽、是否存在连接泄漏、数据库连接队列是否阻塞。需通过监控工具(如慢查询日志、连接池监控)定位真实原因。
  2. 垂直扩容无效的分析缺失:
    垂直扩容后性能无提升,说明瓶颈不在硬件,而在查询效率(如全表扫描、无索引)或锁竞争。需先排查查询执行计划,优化查询后再评估扩容。
  3. 水平扩容的前提不充分:
    水平扩容是复杂方案,若低效查询未优化,即使扩容仍会占用大量资源。应先完成查询优化,再评估是否需要分库分表。
  4. 对比其他API的逻辑不严谨:
    其他APIgetConnection()耗时短,可能是查询效率更高或请求量更低,不能直接得出“该API占用大量资源”的结论,需通过执行计划、慢日志对比验证。

总结:当前决策流程逻辑不充分,需先完成查询性能分析、连接池问题定位,再决定扩容方案,而非直接从垂直扩容跳到水平扩容。

内容的提问来源于stack exchange,提问作者firefly_0

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 16:07:02