Native SQL Queries in Hibernate
Learn how to run native SQL from Hibernate with createNativeQuery, and when dropping down to raw SQL makes sense for cases HQL cannot express cleanly.
Introduction
HQL covers the vast majority of queries you will write against your entities, but it is not the only option. Sometimes you need a database-specific function, a complex analytical query, or a hand-tuned statement that HQL simply cannot express cleanly. For those cases, Hibernate lets you drop down to native SQL while still getting entity mapping and Session management.
- When native SQL is the right tool over HQL
- How to run native SQL with createNativeQuery
- Mapping native query results back to entities
- Returning scalar values and DTOs from native queries
- Defining reusable named native queries
Why Native SQL?
HQL is deliberately database-agnostic and object-oriented, which means it does not expose every feature a specific database offers. Common reasons to reach for native SQL include vendor-specific functions (like MySQL's GROUP_CONCAT), complex window functions, stored procedure calls, or a query that is already tuned and tested as raw SQL that you do not want to rewrite.
createNativeQuery Basics
Session.createNativeQuery() accepts a plain SQL string, written exactly as your database expects it — real table and column names, not entity and field names.
import org.hibernate.query.NativeQuery;import java.util.List;
Session session = factory.openSession();
NativeQuery<Object[]> query = session.createNativeQuery( "SELECT id, name, salary FROM employees WHERE salary > :minSalary");query.setParameter("minSalary", 50000);
List<Object[]> rows = query.getResultList();
for (Object[] row : rows) { System.out.println("ID: " + row[0] + ", Name: " + row[1] + ", Salary: " + row[2]);}
session.close();Click Run to see what this code prints.
Mapping to Entities
If you select every column of a mapped entity, Hibernate can return fully-managed entity instances instead of raw Object[] rows, by telling createNativeQuery which entity class to map onto.
NativeQuery<Employee> query = session.createNativeQuery( "SELECT * FROM employees WHERE department = :dept", Employee.class);query.setParameter("dept", "Engineering");
List<Employee> employees = query.getResultList();
for (Employee e : employees) { System.out.println(e.getName() + " - " + e.getSalary());}Click Run to see what this code prints.
Because the results are mapped to Employee, these objects are attached to the persistence context just like an HQL or Criteria API result — changes to them are tracked and flushed on commit.
Scalar and DTO Results
For queries that do not map cleanly onto one entity — like aggregations across a join — you can select individual columns and map them into a plain constructor, similar to HQL's constructor expressions.
NativeQuery<Object[]> query = session.createNativeQuery( "SELECT department, COUNT(*), AVG(salary) FROM employees GROUP BY department");
List<Object[]> results = query.getResultList();
for (Object[] row : results) { String department = (String) row[0]; Number count = (Number) row[1]; Number avgSalary = (Number) row[2]; System.out.println(department + ": " + count + " employees, avg salary " + avgSalary);}Click Run to see what this code prints.
Named Native Queries
Just like named HQL queries, a native query can be declared once on an entity with @NamedNativeQuery and reused by name, keeping the raw SQL out of your business logic.
import javax.persistence.*;
@Entity@Table(name = "employees")@NamedNativeQuery( name = "Employee.findHighEarners", query = "SELECT * FROM employees WHERE salary > :minSalary", resultClass = Employee.class)public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name; private Double salary;
// Getters and setters}List<Employee> highEarners = session .createNamedQuery("Employee.findHighEarners", Employee.class) .setParameter("minSalary", 60000) .getResultList();HQL vs Native SQL
| Aspect | HQL | Native SQL |
|---|---|---|
| Portability | Database-agnostic | Tied to a specific database dialect |
| Syntax targets | Entity names and fields | Actual table and column names |
| Entity mapping | Automatic | Automatic only if you specify the entity class |
| Vendor-specific features | Not accessible | Fully accessible |
| Best for | Everyday CRUD and object-oriented queries | Complex reports, vendor functions, tuned legacy SQL |
Common Mistakes
- Concatenating user input directly into the native SQL string instead of using named/positional parameters, opening the door to SQL injection.
- Forgetting to select every column an entity needs when mapping results to that entity class, causing mapping errors.
- Reaching for native SQL out of habit for queries HQL could express just as well, losing database portability for no reason.
- Assuming native query results are always managed entities — they are Object[] rows unless you specify a result class.
- Hardcoding table/column names that later drift from the entity mapping, silently breaking the query.
Best Practices
- Reach for HQL first; use native SQL only when HQL genuinely cannot express what you need.
- Always use setParameter() for dynamic values in native queries — never string concatenation.
- Use @NamedNativeQuery to keep native SQL declared alongside the entity rather than scattered through business logic.
- Document why a native query was necessary, so future maintainers know it was a deliberate choice.
- Keep native SQL as portable as reasonably possible if the application may need to support more than one database.
Frequently Asked Questions
Only if you use setParameter() with named or positional placeholders. Concatenating raw strings into the query text defeats that protection entirely.
Yes, using a SqlResultSetMapping to describe how columns from a joined query map onto more than one entity or DTO.
Yes, setFirstResult() and setMaxResults() work on native queries the same way they do on HQL queries.
Not inherently — both ultimately execute as SQL against the database. Native SQL just gives you direct control over exactly what SQL runs.
Key Takeaways
- createNativeQuery() runs raw SQL while still going through the Session and persistence context.
- Passing an entity class maps results to managed entities; otherwise you get Object[] rows.
- @NamedNativeQuery keeps reusable native SQL declared on the entity itself.
- Always parameterize dynamic values — never concatenate user input into native SQL.
- Prefer HQL for everyday queries; reserve native SQL for cases it genuinely cannot handle.
Summary
Native SQL is Hibernate's escape hatch for the queries HQL was never meant to express — vendor-specific functions, tuned reports, and complex aggregations — while still benefiting from parameter binding and, where possible, entity mapping. Use it deliberately, and keep HQL as your default.