Querydsl存在一对多关联时fetch查询过慢的优化咨询
我们在项目中使用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. 分页逻辑优化:先查主键再加载数据
直接对关联查询应用分页会导致先执行全量关联查询再内存分页,这是性能瓶颈的核心。改为两次查询:
- 先分页查询Ticket的主键ID列表,此时生成的SQL会包含
fetch first ? rows only,仅筛选出目标数量的ID。 - 通过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

