Files
hibernate-demo/docs/21-aggregate-functions.md
T

4.2 KiB

21 — Aggregate functions

← Previous: 20 — Hibernate Validator CDI integration | Next: 22 — Sorting → | Back to README →

Backs the rewrite of ankurm.com post 4888 (aggregate functions).

What an aggregate query returns over zero rows

count, sum, avg, min, max all still return exactly one result row when the underlying table (or the filtered subset) has zero matching rows — SQL's aggregate functions are defined over the whole result set, not per-row, so there is always one row to return. count(*) on an empty set is 0; every other aggregate is NULL. Neither case throws NoResultException — getSingleResult() on an aggregate query only throws that when the query itself is malformed, never because the aggregate happened to be computed over nothing.

Long count = em.createQuery("select count(p) from Product p where p.category = :c", Long.class)
        .setParameter("c", "no-such-category-zzz").getSingleResult();
Double sum = em.createQuery("select sum(p.price) from Product p where p.category = :c", Double.class)
        .setParameter("c", "no-such-category-zzz").getSingleResult();

AggregateFunctionsTest.java — captured output

select new with a Java record

Hibernate 7 accepts a Java record's canonical constructor as a select new target exactly like it accepts a class constructor — no special annotation, no adapter, just a record whose constructor parameter types and order match the query's projection:

public record CategorySummary(String category, long productCount, double averagePrice) {}

CategorySummary.java

select new com.ankurm.hibernatedemo.aggregate.CategorySummary(p.category, count(p), avg(p.price))
from Product p group by p.category having count(p) > 1 order by p.category

HAVING count(p) > 1 filters on the aggregated group, after GROUP BY has collapsed the rows — a category with exactly one product is excluded by HAVING, not merely left ungrouped. Test — output

The Criteria API equivalent

CriteriaBuilder.avg(...), .sum(...), .count(...) and friends build the same aggregate SQL without a string query:

cq.multiselect(root.get("category"), cb.avg(root.get("price")))
        .where(cb.equal(root.get("category"), "Cables"))
        .groupBy(root.get("category"));

Test — output

Window functions — not a Hibernate 7 feature

HQL's OVER clause (row_number(), rank(), lead(), lag(), and the ordered-set / inverse distribution aggregates) is real and works exactly as SQL's window functions do. It is not new in Hibernate 7, though it is sometimes described that way: javap against hibernate-core-7.4.5.Final.jar's CommonFunctionFactory shows a windowFunctions() registration method that has existed since Hibernate 6.2, when HQL's window-function support was first added. Hibernate 7 continues to support it; it did not introduce it.

select p.name, p.price, row_number() over (partition by p.category order by p.price desc)
from Product p where p.category = 'Mice' order by p.price desc

Test — output

Going deeper

  • getSingleResult() vs getResultList() on an aggregate query: prefer getSingleResult() only when the query has no GROUP BY — a grouped aggregate can legitimately return many rows.
  • StatelessSession skips the persistence context entirely for a pure read-and-aggregate workload, avoiding the memory overhead of first-level cache entries the aggregate result itself never needs.
  • Hibernate ORM Criteria API documentation