Native SQL Queries
Native SQL Queries
JPQL and the Criteria API cover the overwhelming majority of query needs in a JPA application. But every layer of abstraction has a ceiling, and JPA is no different. When you need a window function, a recursive CTE, a full-text search expression, a database-specific hint, or a bulk operation that bypasses the ORM entirely, you drop down to native SQL. Spring Boot 3 and Hibernate 6 make this transition smooth while keeping result mapping firmly under your control.
When to Reach for Native SQL
Before writing a native query, ask whether JPQL or a Criteria query can do the job. If any of the following apply, native SQL is the right tool:
- You need a vendor-specific feature — PostgreSQL's
DISTINCT ON, MySQL'sGROUP_CONCAT, SQL Server'sCROSS APPLY. - The query uses window functions (
ROW_NUMBER(),RANK(),LEAD()/LAG()) that JPQL cannot express. - You need a recursive CTE to walk a tree or hierarchy stored in a self-referencing table.
- A DBA-provided query with optimizer hints (
USE INDEX,NOLOCK) must be executed verbatim. - Bulk
UPDATEorDELETEacross millions of rows where loading entities would be prohibitive. - A report query that joins many tables into a flat projection — faster to write in SQL and faster to run.
Creating a Native Query with EntityManager
The EntityManager exposes createNativeQuery(sql). In its simplest form, results are returned as Object[] rows:
Each element of the list is an Object[] whose indices correspond to the SELECT columns in order. Always prefer named parameters (:lim) over positional ones (?1) for readability and to avoid off-by-one mistakes.
Mapping Results to an Entity Class
When the native query selects every column of a single table, pass the entity class as the second argument to createNativeQuery and Hibernate maps the columns to managed entity instances automatically:
The returned objects are fully managed: lazy associations can be initialised, @Version fields are respected, and any modifications made inside the same transaction are flushed. This is the cleanest form of native query when your SQL touches a single mapped table.
SqlResultSetMapping — Custom Column-to-Field Mapping
When the native query joins multiple tables or uses computed columns, you need a @SqlResultSetMapping to tell Hibernate how to build the result. Place it on any entity class (it is global by name):
Now pass the mapping name as the third argument:
The @ConstructorResult calls new OrderSummaryDTO(id, fullName, total, itemCount) for each row. OrderSummaryDTO is a plain Java record or class — it does not need to be a JPA entity.
Named Native Queries
Just like JPQL, native SQL can be declared and pre-compiled at startup with @NamedNativeQuery. This moves the SQL out of Java string literals and into a centrally visible location:
Native Queries in Spring Data JPA with @Query
If you are using Spring Data repositories, add nativeQuery = true to the @Query annotation. Spring Data handles the rest:
The return type OrderSummaryProjection is a Spring Data interface-based projection — an interface whose getter names match the column aliases in the SELECT:
Spring Data generates a proxy at runtime that reads the corresponding columns. This is the most concise approach and avoids @SqlResultSetMapping entirely.
Pagination with Native Queries
Spring Data's automatic Pageable support does not work with native queries because the framework cannot reliably wrap arbitrary SQL in a count subquery. Supply a countQuery explicitly:
The First-Level Cache and Native Queries
A critical Hibernate detail: native queries bypass the first-level cache (persistence context). If you load an entity, then execute a native UPDATE on the same row without flushing, Hibernate's in-memory state is now stale. Always flush the EntityManager before running a native write query, or work inside a fresh transaction:
em.clear(). If you do not, any entity already in the persistence context will still reflect the old state, leading to silent data inconsistencies within the same transaction.
Performance Trade-offs
- No dirty checking overhead — entities returned from native queries are not automatically tracked unless you use the entity-class overload and they are full-column rows.
- Exact SQL control — you can add index hints, avoid implicit joins the ORM would generate, and select only the columns you actually need.
- Portability cost — database-specific SQL ties you to a vendor. Document each native query with the reason it cannot be expressed in JPQL.
- Schema drift risk — native SQL refers to table and column names directly. A rename migration that updates entity annotations does not automatically update native query strings.
Summary
Native SQL queries are your escape hatch when JPQL reaches its limits. Use createNativeQuery on the EntityManager directly, or annotate repository methods with @Query(nativeQuery = true). Map results to entity classes for single-table queries, to interface-based projections for multi-table aggregations, and to @ConstructorResult mappings when you need maximum control. Always flush before native writes, clear the cache afterwards, and document why each native query exists so future maintainers understand the intent.