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

Querydsl存在一对多关联时fetch查询过慢的优化咨询

Querydsl 关联查询分页性能优化问题

我们在项目中使用Querydsl实现动态查询、排序和分页,涉及Ticket、Payment、TicketPayment三个实体的一对多/多对一关联。执行以下代码时性能极差:

List<?> result = querydsl.applyPagination(pageable, query).fetch();

即使是offset=0、size=10的分页请求,耗时也超过10分钟。观察生成的SQL发现没有添加fetch first ? rows only片段,推测是关联加载Ticket和TicketPayment表(Ticket表10万行,TicketPayment表12万行)导致的。但在SQL Developer中执行相同逻辑的查询,返回50条数据仅需不到4秒。

请问可以通过哪些方式优化查询速度?能否定制fetch()方法提升效率?


现有QueryDSL查询代码

import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

Querydsl querydsl = new Querydsl(entityManager, (new PathBuilderFactory()).create(<EntityClass>.class));
JPQLQuery<?> query = new JPAQuery<>(entityManager);

//TODO: prepare your query here 

//获取总条数
Long totalElements = query.fetchCount();

//应用分页
List<?> result = querydsl.applyPagination(pageable, query).fetch();
//返回10条数据但耗时超10分钟

//返回分页结果
return new PageImpl<>(result, pageable, totalElements);

相关实体及表结构

Ticket实体

@Entity
@Table(name = "TICKET")
public class Ticket implements Serializable {
    
    @Id
    @Column(name = "TICKET_ID", nullable = false)
    private Long id;
    
    @OneToMany(mappedBy = "Ticket")
    private Set<TicketPayment> ticketPayments= new HashSet<>();
    //其他字段...
}

Payment实体

@Entity
@Table(name = "PAYMENT")
public class Payment implements Serializable {

    @Id
    @Column(name = "PAYMENT_ID")
    private Long id;
    @Column(name = "PAYMENT_CODE")
    private String code;
    //其他字段...
}

TicketPayment关联实体

@Entity
@Table(name = "TICKET_PAYMENT")
@IdClass(TicketPaymentPK.class)
public class TikcetPayment{

    @Id
    @Column(name = "TICKET_ID", nullable = false, precision = 0)
    private long ticketId;

    @Id
    @Column(name = "PAYMENT_ID", nullable = false, precision = 0)
    private long paymentId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TICKET_ID", referencedColumnName = "TICKET_ID", nullable = false, insertable = false, updatable = false)
    private Ticket ticket;

    @ManyToOne
    @JoinColumn(name = "PAYMENT_ID", referencedColumnName = "PAYMENT_ID", nullable = false, insertable = false, updatable = false)
    private Payment payment;
    //其他字段...
}

复合主键类

public class TicketPaymentPKimplements Serializable {
@Id
@Column(name = "TICKET_ID", nullable = false, precision = 0)
private long ticketId;
@Id
@Column(name = "PAYMENT_ID", nullable = false, precision = 0)
private long paymentId;
}

优化方案

1. 修复实体映射错误

  • 修正关联属性名:Ticket实体中@OneToMany(mappedBy = "Ticket")改为mappedBy = "ticket",对应TicketPayment实体的ticket属性,否则JPA无法正确识别关联关系,引发无效查询。
  • 修正实体类拼写:关联实体类名TikcetPayment改为TicketPayment,避免类映射异常。
  • 补充复合主键的equals/hashCode:TicketPaymentPK必须重写这两个方法,否则JPA无法正确识别主键,导致缓存或查询逻辑错误。
  • 设置Payment关联为懒加载:TicketPayment中@ManyToOne默认是EAGER加载,改为FetchType.LAZY,避免查询时自动加载所有关联Payment数据。

2. 分页逻辑优化:先查主键再加载数据

直接对关联查询应用分页会导致先执行全量关联查询再内存分页,这是性能瓶颈的核心。改为两次查询:

  1. 先分页查询Ticket的主键ID列表,此时生成的SQL会包含fetch first ? rows only,仅筛选出目标数量的ID。
  2. 通过ID批量查询完整的Ticket及关联数据。

示例代码:

// 第一步:查询分页后的Ticket ID列表
JPQLQuery<Long> idQuery = new JPAQuery<>(entityManager)
    .select(qTicket.id)
    .from(qTicket)
    // 添加原查询条件、排序规则
    .leftJoin(qTicket.ticketPayments, qTicketPayment)
    .leftJoin(qTicketPayment.payment, qPayment)
    .orderBy(/* 你的排序条件 */);

// 应用分页到ID查询
List<Long> ticketIds = querydsl.applyPagination(pageable, idQuery).fetch();

// 第二步:通过ID批量查询完整数据
List<Ticket> result = new JPAQuery<>(entityManager)
    .select(qTicket)
    .from(qTicket)
    .leftJoin(qTicket.ticketPayments, qTicketPayment).fetchJoin() // 需要立即加载则用fetchJoin,否则保持懒加载
    .leftJoin(qTicketPayment.payment, qPayment).fetchJoin()
    .where(qTicket.id.in(ticketIds))
    .orderBy(/* 和第一步一致的排序条件 */)
    .fetch();

// 获取总条数(复用ID查询的条件)
Long totalElements = idQuery.fetchCount();

return new PageImpl<>(result, pageable, totalElements);

3. 强制生成原生分页语句

确保Querydsl使用对应数据库的方言模板,比如Oracle用OracleTemplates,MySQL用MySQLTemplates,这样才能生成数据库原生的分页语句:

// 创建JPAQuery时指定模板(以Oracle为例)
JPQLQuery<?> query = new JPAQuery<>(entityManager, new OracleTemplates());

如果是Spring Data JPA集成Querydsl,需在配置中指定正确的数据库方言。

4. 数据库索引优化

给关联字段、排序字段、过滤字段添加索引,减少全表扫描:

  • 给TICKET_PAYMENT.TICKET_ID和TICKET_PAYMENT.PAYMENT_ID添加联合索引
  • 给排序字段添加单独索引
  • 给查询中常用的过滤字段添加索引

5. 避免不必要的关联加载

  • 若业务不需要返回TicketPayment或Payment数据,直接查询Ticket表即可,无需关联。
  • 若需要关联但不需要立即加载,保持FetchType.LAZY,避免EAGER加载带来的性能损耗。

关于定制fetch()方法

不建议直接定制Querydsl的fetch()方法,该方法是底层查询执行的入口,修改风险较高。性能优化应优先从查询逻辑、实体映射、数据库索引层面入手。若需自定义结果处理,可使用fetchResults()、fetchStream()或Transformers转换结果,但核心还是先优化查询逻辑本身。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 22:55:56