Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Query Tuning with HQL and Criteria: A Field Guide

Posted on January 19, 2007 By Luis Fernandez

Dialogue

Dev: My page takes five seconds. The query looks innocent.

DBA: Your logs show 412 selects. Nothing innocent there.

Dev: But I am using Hibernate with HQL and Criteria. It is supposed to help.

DBA: Tools help if you aim them. Right now you are spraying the database.

If that chat sounds familiar, keep reading. With everyone shipping apps on Hibernate 3 and JPA 1.0, we are all juggling lazy loading, caching, and SQL that we did not hand craft. The trick is learning how to steer it.

Evidence

Let me show what I have been seeing this week while tuning a Spring plus Hibernate stack talking to MySQL 5 and Oracle 10g. Same patterns keep popping up.

  • N+1 selects when walking associations in a loop. One query for parents, then one per child collection.
  • Pagination mixed with join fetch that explodes rows and ruins the page query.
  • Criteria that joins where we only need IDs, because we forgot to use Projections.
  • Caching switched on with no thought about the shape of keys or eviction, which turns into stale reads.

Here is a real slice from the logs. One page render, 50 orders with their last two items:

// HQL
List<Order> orders = session.createQuery(
  "select o from Order o order by o.created desc"
)
.setMaxResults(50)
.list();

for (Order o : orders) {
  // Needs last 2 items
  List<Item> items = o.getItems(); // LAZY
  int size = Math.min(items.size(), 2);
  for (int i = 0; i < size; i++) {
    render(items.get(i));
  }
}

Hibernate happily fires one query for orders, then up to 50 more for items. That is the classic N+1 pattern. Time goes away fast when round trips pile up.

Now look at Criteria, same shape different paint:

Criteria c = session.createCriteria(Order.class)
  .addOrder(Order.desc("created"))
  .setMaxResults(50);

List<Order> orders = c.list(); // Items still lazy

Criteria does not magically fix it. We still need to ask for the right data in one go or in sane batches.

Notes from the trenches

1. Kill N+1 with fetch join for read pages

// HQL with fetch join
List<Order> orders = session.createQuery(
  "select distinct o from Order o " +
  "left join fetch o.items i " +
  "where o.status = :s " +
  "order by o.created desc"
)
.setParameter("s", Status.OPEN)
.setMaxResults(50) // see risk notes below
.list();

Use join fetch when you will touch the collection on the page. Add distinct to counter row multiplication.

2. Or batch the collection loads

// Mapping
<set name="items" batch-size="20"> ... </set>

// Or annotation
@BatchSize(size = 20)

Batch size tells Hibernate to load child collections for several parents in one query per batch. It keeps the select count down without changing your HQL.

3. Use projections when you only need a slice

// Criteria projection for summary rows
Criteria c = session.createCriteria(Order.class)
  .setProjection(Projections.projectionList()
    .add(Projections.property("id"))
    .add(Projections.property("created"))
    .add(Projections.property("total")))
  .addOrder(Order.desc("created"))
  .setFirstResult(0)
  .setMaxResults(50);

List<Object[]> rows = c.list();

Pull only the columns your table needs to show. You save memory and time on hydration. In HQL you can also use select new to map directly to a DTO.

// HQL DTO projection
List<OrderSummary> xs = session.createQuery(
  "select new com.acme.OrderSummary(o.id, o.created, o.total) " +
  "from Order o order by o.created desc"
).setMaxResults(50).list();

4. Control fetch mode in Criteria

Criteria c = session.createCriteria(Order.class)
  .setFetchMode("items", FetchMode.JOIN) // same as join fetch
  .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

5. Paginate with care

// Always pair both
query.setFirstResult(page * size);
query.setMaxResults(size);

Page at the database. Pair both bounds so the driver does not read silly amounts under the hood.

6. Index for your filters and sorts

-- MySQL
create index idx_order_status_created on orders(status, created desc);

-- Oracle
create index idx_order_status_created on orders(status, created);

Match the where fields first. If you sort often by created, include that column too. It pays back quickly.

7. Second level cache and query cache

// hibernate.cfg.xml
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>
<property name="cache.provider_class">org.hibernate.cache.EhCacheProvider</property>

// Java
Query q = session.createQuery("from Country order by name");
q.setCacheable(true);

Cache reference data like countries and states. Be picky with query cache. Only turn it on for stable lists or small filters. Pair it with clear eviction rules on writes.

Risks

  • Join fetch with paging: many databases page after join. A join that multiplies rows can kick out legit parents. Page on a subquery of IDs first, then join to hydrate.
  • Distinct is not free: it can force sorting or temp storage. Use it only when the join inflates rows.
  • Query cache surprises: it keys by SQL plus parameters plus the session factory state. One var in the HQL and you blow the region. Also watch invalidation storms on write heavy tables.
  • Count queries: count with joins can over count. Write select count(distinct o.id) or count a subquery of IDs.
  • Lazy loading in views: JSP tag calls that touch collections can open the fire hose. Either fetch before you hit the view or use Open Session in View with a clear contract on what the view can touch.
  • Driver fetch size: set hibernate.jdbc.fetch_size for large reads. Default can be tiny and cause many trips.

Graceful exit

We are shipping apps on Spring, Hibernate, and friends while the browser wars get spicy again and everyone is playing with Ajax. The database still calls the shots. If your page is slow, start by counting queries and rows, then shape the plan with smart HQL or Criteria. Small habits beat heroic rewrites.

Quick checklist

  • Turn on SQL logging and row count in dev.
  • Spot N+1 and fix with join fetch or batch size.
  • Project to DTOs for lists.
  • Set both bounds when paging.
  • Index by filter and sort columns.
  • Cache only stable stuff. Be explicit.

If you have a gnarly case, send me the HQL and the plan. I will happily trade war stories. Meanwhile, aim the tool. Let Hibernate do the lifting you asked for, not the lifting you forgot you asked for.

General Software Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes