Sorting in Hibernate means asking the database to return rows in a fixed order with an SQL ORDER BY clause, or sorting the loaded objects in Java.
We use sorting wherever a screen shows a list, for example a hiking app that lists the longest trails first and shows the best reviews of a trail at the top. For query results, we add order by to a JPQL/HQL query or call orderBy() on a Criteria query. For a collection inside an entity, @OrderBy adds the ORDER BY to the SQL that loads the collection, while Hibernate’s @SortNatural and @SortComparator sort the elements in memory.
The following example maps a sorted reviews collection and sorts query results by length and by region.
@OneToMany(mappedBy = "trail")
@OrderBy("rating DESC, author ASC")
private List<TrailReview> reviews = new ArrayList<>();
List<Trail> byLength = em.createQuery("from Trail t order by t.lengthKm desc", Trail.class).getResultList();
// select ... from Trail t1_0 order by t1_0.lengthKm desc
// [Canyon Rim, Eagle Peak, Pine Ridge, bear creek, Lake Loop]
List<Trail> byRegion = em.createQuery("from Trail t order by t.region nulls last, t.name", Trail.class).getResultList();
// select ... from Trail t1_0 order by t1_0.region asc nulls last,t1_0.name
// [Eagle Peak, Lake Loop, Canyon Rim, bear creek, Pine Ridge]
List<TrailReview> reviews = trail.getReviews();
// select ... from TrailReview r1_0 where r1_0.trail_id=? order by r1_0.rating desc,r1_0.author
// [Lokesh 5, alex 5, Maria 3]
Notice that nulls last moves Pine Ridge, the trail without a region, to the end, and the reviews arrive sorted because @OrderBy adds the ORDER BY to the SQL of the collection.
Next, we compare database-side and in-memory sorting, and then go through each option with its SQL, including user-selected sort fields and the Hibernate 7 Order API.
1. Database-Side vs In-Memory Sorting
Every sorting option in Hibernate runs in one of two places. Database-side options add an ORDER BY to the SQL, so the rows arrive sorted and Hibernate keeps that order in a List. In-memory options load the rows without an ORDER BY and put them into a TreeSet (a SortedSet that keeps its elements sorted with a Comparator).

We prefer database-side sorting, because the database can use an index and sort before LIMIT cuts the result for pagination. In-memory sorting suits small collections and rules that SQL cannot express.
In Hibernate 7.4, each option produces its own SQL, and the last three send no ORDER BY at all.
| Option | Where it sorts | SQL Hibernate sends |
|---|---|---|
| JPQL/HQL order by | Database | … order by t1_0.lengthKm desc |
| Criteria API orderBy() | Database | … order by t1_0.region asc nulls last |
| Hibernate 7 SelectionSpecification.sort() | Database | … order by t1_0.difficulty,t1_0.id |
| Native SQL query | Database | The ORDER BY we write |
| @OrderBy on a collection | Database | … where r1_0.trail_id=? order by r1_0.rating desc,r1_0.author |
| @SQLOrder on a collection | Database | … order by lower(rba1_0.author) |
| @OrderColumn on a list | Stored position | select … w1_0.position … (no ORDER BY) |
| @SortNatural on a SortedSet | Memory | No ORDER BY, uses compareTo() |
| @SortComparator on a SortedSet | Memory | No ORDER BY, uses our Comparator |
2. Hibernate Sorting Example
The following example sorts hiking trails with Hibernate 7.4.11, Java 25 and an in-memory H2 database. The complete project on GitHub runs these queries and prints the SQL, and its 28 JUnit tests check each result (mvn -q compile exec:java, mvn test).
2.1. Trails and Reviews
A trail has a name, a region, a length in kilometers and a difficulty. It also has reviews with an author and a rating. Its waypoints form an ordered list, and its tags form a set. Some rows make the sorting interesting, because Pine Ridge has no region and “bear creek” starts with a lowercase letter, whereas several trails share a difficulty.

The @Entity holds four plain fields, and these are the sort keys for the queries.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String region;
private double lengthKm;
private String difficulty;
private String author;
private int rating;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "trail_id")
private Trail trail;
2.2. Sorting Query Results With ORDER BY
In JPQL and HQL, order by takes entity attributes, not column names. Ascending is the default and desc reverses it. A second attribute sorts the rows that tie on the first one.
List<Trail> shortest = em.createQuery("from Trail t order by t.lengthKm", Trail.class).getResultList();
// order by t1_0.lengthKm
// [Lake Loop, bear creek, Pine Ridge, Eagle Peak, Canyon Rim]
List<Trail> longest = em.createQuery("from Trail t order by t.lengthKm desc", Trail.class).getResultList();
// order by t1_0.lengthKm desc
// [Canyon Rim, Eagle Peak, Pine Ridge, bear creek, Lake Loop]
List<Trail> byRegionLength = em.createQuery("from Trail t order by t.region, t.lengthKm desc", Trail.class).getResultList();
// order by t1_0.region,t1_0.lengthKm desc
// [Pine Ridge, Eagle Peak, Lake Loop, Canyon Rim, bear creek]
In the last query, the two Alps trails come longest first. Pine Ridge comes first because H2 puts NULL before other values in ascending order.
2.3. Placing Null Values First or Last
Without an explicit rule, the database decides where NULL values go. H2 puts them first in ascending order, while PostgreSQL puts them last, so a trail list tested on H2 shows the trails without a region at the other end in production on PostgreSQL. The keywords nulls first and nulls last make the order the same on every database.
List<Trail> nullsLast = em.createQuery("from Trail t order by t.region nulls last, t.name", Trail.class).getResultList();
// order by t1_0.region asc nulls last,t1_0.name
// [Eagle Peak, Lake Loop, Canyon Rim, bear creek, Pine Ridge]
List<Trail> nullsFirst = em.createQuery("from Trail t order by t.region desc nulls first, t.name", Trail.class).getResultList();
// order by t1_0.region desc nulls first,t1_0.name
// [Pine Ridge, Canyon Rim, bear creek, Eagle Peak, Lake Loop]
NULLS FIRST and NULLS LAST are part of the JPQL grammar since Jakarta Persistence 3.2. Older Hibernate versions supported them only as an HQL extension.
2.4. Case-Insensitive Sorting With lower()
String sorting follows the database collation (the rules for comparing characters). H2’s default compares character codes, so every uppercase letter comes before every lowercase one and “bear creek” ends up last. Sorting by lower() compares the lowercase values instead.
List<Trail> byName = em.createQuery("from Trail t order by t.name", Trail.class).getResultList();
// order by t1_0.name
// [Canyon Rim, Eagle Peak, Lake Loop, Pine Ridge, bear creek]
List<Trail> byLowerName = em.createQuery("from Trail t order by lower(t.name)", Trail.class).getResultList();
// order by lower(t1_0.name)
// [bear creek, Canyon Rim, Eagle Peak, Lake Loop, Pine Ridge]
A plain index on name does not help an ORDER BY lower(name). On a large table, we use a case-insensitive collation for the column or a function-based index, if the database supports one.
2.5. Sorting With the Criteria API
The Criteria API builds the same query with Java method calls. The methods cb.asc() and cb.desc() create one Order each, and orderBy() takes them in priority order. Jakarta Persistence 3.2 added a second parameter of type Nulls for the null placement.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Trail> query = cb.createQuery(Trail.class);
Root<Trail> trail = query.from(Trail.class);
query.select(trail)
.orderBy(cb.asc(trail.get("region"), Nulls.LAST),
cb.desc(trail.get("lengthKm")));
List<Trail> trails = em.createQuery(query).getResultList();
// order by t1_0.region asc nulls last,t1_0.lengthKm desc
// [Eagle Peak, Lake Loop, Canyon Rim, bear creek, Pine Ridge]
For case-insensitive sorting, we wrap the path in cb.lower().
query.select(trail).orderBy(cb.asc(cb.lower(trail.get("name"))));
// order by lower(t1_0.name)
// [bear creek, Canyon Rim, Eagle Peak, Lake Loop, Pine Ridge]
The method orderBy() replaces any order set before, so we pass all sort keys in one call.
2.6. Sorting by a Field the User Selects
A REST endpoint such as /trails?sort=length&dir=desc lets the user choose the sort field. Never concatenate a request parameter into the query string. The parameter becomes part of the query, and Hibernate runs whatever expression it contains, including a subquery.
String sort = "id * (select count(*) from TrailReview r where r.author like 'L%')";
List<Trail> rows = em.createQuery("from Trail t order by t." + sort, Trail.class).getResultList();
// order by (t1_0.id*(select count(*) from TrailReview tr1_0 where tr1_0.author like 'L%' escape ''))
The query runs, and the row order depends on data from another table. An attacker can read that data one comparison at a time. Instead, we map the public parameter names to attribute names in an allow list and reject everything else.

Map<String, String> SORTABLE = Map.of(
"name", "name",
"region", "region",
"length", "lengthKm",
"difficulty", "difficulty");
String attribute = SORTABLE.get(sortBy);
if (attribute == null) {
throw new IllegalArgumentException("Cannot sort by: " + sortBy);
}
query.select(trail).orderBy(
descending ? cb.desc(trail.get(attribute)) : cb.asc(trail.get(attribute)),
cb.asc(trail.get("id")));
List<Trail> sorted = findAll(em, "length", true);
// order by t1_0.lengthKm desc,t1_0.id
// [Canyon Rim, Eagle Peak, Pine Ridge, bear creek, Lake Loop]
List<Trail> rejected = findAll(em, "name; drop table Trail", false);
// java.lang.IllegalArgumentException: Cannot sort by: name; drop table Trail
We always add a unique attribute such as id as the last sort key. Two trails with the same difficulty have no defined order between them, so without it a page can show a row twice or skip it.
2.7. Hibernate 7 Order and SelectionSpecification
Hibernate 7 has its own type-safe sort API. The class org.hibernate.query.Order describes one sort key, and SelectionSpecification applies it to a query. It replaces the incubating SelectionQuery.setOrder() from Hibernate 6.3 to 6.6, which Hibernate 7.0 removed.
List<Trail> trails = SelectionSpecification.create(Trail.class, "from Trail")
.sort(Order.by(Trail.class, attribute, SortDirection.ASCENDING))
.sort(Order.asc(Trail.class, "id"))
.createQuery(em)
.getResultList();
// attribute = "difficulty": order by t1_0.difficulty,t1_0.id
// [Lake Loop, bear creek, Eagle Peak, Canyon Rim, Pine Ridge]
Order also covers case-insensitive sorting and null placement.
Order<Trail> byNameIgnoringCase = Order.asc(Trail.class, "name").ignoringCase();
// order by lower(t1_0.name)
Order<Trail> byRegionNullsLast = Order.by(Trail.class, "region", SortDirection.ASCENDING, Nulls.LAST);
// order by t1_0.region asc nulls last
Order resolves the name as an entity attribute, so it cannot carry an expression like the subquery in section 2.6. An unknown name fails with org.hibernate.query.sqm.PathElementException: Could not resolve attribute ‘nope’ of ‘…Trail’. We still keep the allow list, so that users see public names and get a clear error. With the Hibernate metamodel generator, Order.asc(Trail_.name) checks the attribute at compile time.
2.8. Sorting a Collection With @OrderBy
@OrderBy on a @OneToMany, @ManyToMany or @ElementCollection adds an ORDER BY to the SQL that loads the collection. Its value lists attributes of the child entity, not columns.
@OneToMany(mappedBy = "trail")
@OrderBy("rating DESC, author ASC")
private List<TrailReview> reviews = new ArrayList<>();
Trail eagle = em.find(Trail.class, eagleId);
List<TrailReview> reviews = eagle.getReviews();
// select r1_0.trail_id,r1_0.id,r1_0.author,r1_0.rating from TrailReview r1_0
// where r1_0.trail_id=? order by r1_0.rating desc,r1_0.author
// [Lokesh 5, alex 5, Maria 3]
An empty @OrderBy sorts by the primary key of the child entity. For an element collection of basic values, such as List<String>, an empty @OrderBy sorts by the value itself.
When the order needs SQL that @OrderBy does not accept, such as a function, Hibernate’s @SQLOrder takes a native SQL fragment with column names.
@OneToMany(mappedBy = "trail")
@SQLOrder("lower(author)")
private List<TrailReview> reviewsByAuthor = new ArrayList<>();
// ... where rba1_0.trail_id=? order by lower(rba1_0.author)
// [alex 5, Lokesh 5, Maria 3]
2.9. Keeping the Insertion Order With @OrderColumn
Waypoints have no attribute to sort by, because their order is the order in which we added them. The @OrderColumn annotation stores each element’s index in an extra column and rebuilds the list from it.
@ElementCollection
@OrderColumn(name = "position")
private List<String> waypoints = new ArrayList<>();
List<String> waypoints = eagle.getWaypoints();
// select w1_0.Trail_id,w1_0.position,w1_0.waypoints from Trail_waypoints w1_0 where w1_0.Trail_id=?
// [Parking, Bridge, Hut, Summit]
eagle.getWaypoints().remove("Bridge");
// delete from Trail_waypoints where Trail_id=? and position=?
// update Trail_waypoints set waypoints=? where Trail_id=? and position=?
// update Trail_waypoints set waypoints=? where Trail_id=? and position=?
// [Parking, Hut, Summit], positions 0, 1, 2
The SQL has no ORDER BY, because Hibernate places each value at its stored position. Removing an element near the start of the list rewrites every element after it, so @OrderColumn suits short lists. When the order comes from the data, @OrderBy is the cheaper choice.
2.10. Sorting a Collection in Memory With @SortNatural and @SortComparator
Both annotations still exist in Hibernate 7.4 and need a SortedSet or SortedMap field. Hibernate loads the rows without an ORDER BY and fills a PersistentSortedSet, its own TreeSet-based collection.
The @SortNatural annotation uses the element’s compareTo(). For String, that is the same character-code order as in section 2.4.
@ElementCollection
@SortNatural
private SortedSet<String> tags = new TreeSet<>();
// select t1_0.Trail_id,t1_0.tags from Trail_tags t1_0 where t1_0.Trail_id=?
// [Forest, lake, views]
The @SortComparator annotation takes a Comparator class. Our ReviewByRating sorts by rating, highest first, and then by author.
public class ReviewByRating implements Comparator<TrailReview> {
@Override
public int compare(TrailReview a, TrailReview b) {
return Comparator.comparingInt(TrailReview::getRating).reversed()
.thenComparing(TrailReview::getAuthor)
.compare(a, b);
}
}
@OneToMany(mappedBy = "trail")
@SortComparator(ReviewByRating.class)
private SortedSet<TrailReview> sortedReviews = new TreeSet<>(new ReviewByRating());
// select ... from TrailReview sr1_0 where sr1_0.trail_id=? (no order by)
// [Lokesh 5, alex 5, Maria 3]
The result matches @OrderBy from section 2.8, but the database did no sorting. A review added to sortedReviews goes to its sorted position at once ([Lokesh 5, alex 5, Anna 4, Maria 3]). In the @OrderBy list, it goes to the end until the collection is loaded again, as FAQ 3.4 shows.
3. Hibernate Sorting FAQs
3.1. In What Order Does a Query Return Rows Without ORDER BY?
In no guaranteed order. Jakarta Persistence requires the row order to be kept only when the query has an ORDER BY clause. Without one, the database returns rows in whatever order is fastest for it, which can change after an insert, an index change or a database upgrade. Whenever the order matters, the query needs an ORDER BY.
3.2. How Do I Sort by a Custom Order?
A trail app that lists beginner trails first cannot use alphabetical order, because it puts “expert” before “intermediate”. A case expression maps each value to a number, and the query sorts by that number.
List<Trail> byDifficulty = em.createQuery("""
from Trail t
order by case t.difficulty when 'beginner' then 1 when 'intermediate' then 2 else 3 end, t.name""",
Trail.class).getResultList();
// order by case t1_0.difficulty when 'beginner' then 1 when 'intermediate' then 2 else 3 end,t1_0.name
// [Lake Loop, bear creek, Pine Ridge, Canyon Rim, Eagle Peak]
3.3. How Do I Sort by a Field of an Associated Entity?
We use the path to the attribute, and Hibernate adds the join.
List<TrailReview> byTrail = em.createQuery("from TrailReview r order by r.trail.name, r.rating desc", TrailReview.class)
.getResultList();
// select ... from TrailReview tr1_0 join Trail t1_0 on t1_0.id=tr1_0.trail_id
// order by t1_0.name,tr1_0.rating desc
// [Lokesh 5, alex 5, Maria 3]
3.4. Why Is a Newly Added Element at the End of an @OrderBy List?
The @OrderBy annotation sorts only in the SQL that loads the collection, whereas list.add() appends to the Java list.
eagle.getReviews().size(); // loads [Lokesh 5, alex 5, Maria 3]
TrailReview anna = new TrailReview("Anna", 4);
eagle.addReview(anna);
em.persist(anna);
List<TrailReview> reviews = eagle.getReviews(); // [Lokesh 5, alex 5, Maria 3, Anna 4]
// next transaction, loaded again: [Lokesh 5, alex 5, Anna 4, Maria 3]
When the order must stay correct in memory, we sort the list in Java after the change, or use a SortedSet with @SortComparator.
3.5. Does @OrderBy Work With join fetch?
Yes. When a query loads the collection with join fetch, Hibernate appends the @OrderBy columns to the query’s ORDER BY.
List<TrailReview> eagleReviews = em.createQuery("from Trail t join fetch t.reviews where t.id = :id", Trail.class)
.setParameter("id", eagleId).getSingleResult().getReviews();
// select ... from Trail t1_0 join TrailReview r1_0 on t1_0.id=r1_0.trail_id
// where t1_0.id=? order by r1_0.rating desc,r1_0.author
// [Lokesh 5, alex 5, Maria 3]
3.6. How Do I Set Where Nulls Go for Every Query?
The setting hibernate.order_by.default_null_ordering (values none, first or last) applies to every ORDER BY that has no nulls first or nulls last of its own. We set it on HibernatePersistenceConfiguration or in persistence.xml.
new HibernatePersistenceConfiguration("sorting")
.property(QuerySettings.DEFAULT_NULL_ORDERING, "last")
// ...
List<Trail> trails = em.createQuery("from Trail t order by t.region, t.name", Trail.class).getResultList();
// order by t1_0.region asc nulls last,t1_0.name asc nulls last
// [Eagle Peak, Lake Loop, Canyon Rim, bear creek, Pine Ridge]
3.7. What Replaced Hibernate’s @OrderBy Annotation and Query#setOrder()?
Hibernate 7 removed a few sorting APIs, and each one has a replacement in Hibernate 7.4.
| Removed | Use instead |
|---|---|
| @org.hibernate.annotations.OrderBy(clause = “…”) | @SQLOrder(“…”), or the JPA @OrderBy |
| SelectionQuery.setOrder() (incubating in 6.3 to 6.6) | SelectionSpecification.sort() |
| @Sort(type = SortType.COMPARATOR) (Hibernate 5) | @SortComparator / @SortNatural |
| Legacy session.createCriteria() with addOrder() | Jakarta CriteriaQuery.orderBy() |
3.8. Why Does My @SortComparator Set Lose Elements?
A TreeSet treats two elements as duplicates when the comparator returns 0 for them, and it keeps only the first one. A comparator that compares only the rating drops one of two reviews with rating 5.
SortedSet<TrailReview> byRatingOnly = new TreeSet<>(Comparator.comparingInt(TrailReview::getRating));
byRatingOnly.add(new TrailReview("Lokesh", 5));
byRatingOnly.add(new TrailReview("alex", 5));
int size = byRatingOnly.size(); // 1
A comparator for a sorted set must never return 0 for two different elements. The ReviewByRating comparator in section 2.10 compares the author when the ratings are equal.
4. Conclusion
For query results, we sort in the database, either with order by in JPQL or with the Criteria API, and Hibernate 7 adds SelectionSpecification.sort() as a type-safe option. We state nulls first or nulls last when a column can be NULL.
For collections, @OrderBy sorts the rows when the collection loads, whereas @SortNatural or @SortComparator sort in memory. When the order is the insertion order, @OrderColumn stores it in an extra column. A user-selected sort field always goes through an allow list, never through string concatenation.
5. References
- Jakarta Persistence 3.2 specification: ORDER BY Clause
- OrderBy JavaDoc (Jakarta Persistence 3.2)
- OrderColumn JavaDoc (Jakarta Persistence 3.2)
- CriteriaBuilder JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: ORDER BY clause
- Hibernate ORM 7.4 User Guide: Sorted sets
- Hibernate 7.4 JavaDoc: SelectionSpecification
- Hibernate 7.0 Migration Guide
Happy Learning !!