Senior 7 min · March 09, 2026

HQL vs JPQL vs Native SQL — $50k L1 Cache Pitfall

Native SQL bypasses L1 cache, causing stale entity reads and $50k errors.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • JPQL is the JPA-standard query language that works on entities, not tables
  • HQL extends JPQL with Hibernate-specific features like fetch profiles
  • Native SQL gives full database control but breaks portability
  • Performance: Native SQL can be 2–5x faster for complex aggregations
  • Production trap: Native SQL bypasses the persistence context — changes aren't tracked
  • Biggest mistake: Using Native SQL for simple CRUD when JPQL would suffice
✦ Definition~90s read
What is HQL vs JPQL vs Native SQL?

These query languages exist to bridge the gap between Java's object-oriented world and the relational database's table-oriented world. JPQL (Java Persistence Query Language) is a platform-independent language defined by the JPA specification that operates on entities rather than tables.

Think of HQL, JPQL, and Native SQL as different ways to order pizza.

HQL (Hibernate Query Language) is an extension of JPQL that offers advanced features like 'FETCH JOIN' or specific function support. Native SQL is raw SQL that bypasses the ORM abstraction to leverage database-specific features like Window Functions, CTEs (Common Table Expressions), or JSONB operations that standard JPQL cannot yet express.

By leveraging JPQL/HQL for the majority of our work, we ensure 'io.thecodeforge' applications remain portable. However, we reach for Native SQL when the abstraction becomes a cage, allowing us to squeeze every drop of performance out of engines like PostgreSQL or MySQL.

Plain-English First

Think of HQL, JPQL, and Native SQL as different ways to order pizza. JPQL is like using a standard international app that works everywhere (standardized). HQL is like using the specific features of a high-end delivery service (Hibernate-exclusive). Native SQL is like walking directly into the kitchen and speaking the chef's local dialect; it's the most powerful way to get exactly what you want, but it only works in that specific kitchen.

HQL, JPQL, and Native SQL are the three primary ways to retrieve data in a Spring Boot application using Hibernate. Choosing the right one determines your application's portability, performance, and maintainability. In modern enterprise environments, the goal is often a balance: 80% standardized abstraction for maintainability and 20% raw power for performance-critical bottlenecks.

In this guide, we'll break down exactly what each query type is, why they were designed to serve different layers of abstraction, and how to use them correctly in real projects. By the end, you'll have both the conceptual understanding and practical code examples to use HQL, JPQL, and Native SQL with confidence in any 'io.thecodeforge' project.

What Is HQL vs JPQL vs Native SQL and Why Does It Exist?

These query languages exist to bridge the gap between Java's object-oriented world and the relational database's table-oriented world. JPQL (Java Persistence Query Language) is a platform-independent language defined by the JPA specification that operates on entities rather than tables. HQL (Hibernate Query Language) is an extension of JPQL that offers advanced features like 'FETCH JOIN' or specific function support. Native SQL is raw SQL that bypasses the ORM abstraction to leverage database-specific features like Window Functions, CTEs (Common Table Expressions), or JSONB operations that standard JPQL cannot yet express.

By leveraging JPQL/HQL for the majority of our work, we ensure 'io.thecodeforge' applications remain portable. However, we reach for Native SQL when the abstraction becomes a cage, allowing us to squeeze every drop of performance out of engines like PostgreSQL or MySQL.

io/thecodeforge/repository/QueryComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.thecodeforge.repository;

import io.thecodeforge.model.Project;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import java.util.List;

public class ForgeQueryService {

    @PersistenceContext
    private EntityManager entityManager;

    public void runQueries() {
        // 1. JPQL: Standardized, entity-based. Highly portable.
        List<Project> jpql = entityManager.createQuery(
            "SELECT p FROM Project p WHERE p.status = 'ACTIVE'", Project.class)
            .getResultList();

        // 2. HQL: Hibernate-specific extensions (like specific JOIN FETCH logic)
        // Note: In modern JPA, many HQL features are now standard JPQL.
        List<Project> hql = entityManager.createQuery(
            "from Project p left join fetch p.tasks where p.id = :id", Project.class)
            .setParameter("id", 1L)
            .getResultList();

        // 3. Native SQL: Production-grade PostgreSQL specific example (JSONB query)
        // io.thecodeforge: Using Native SQL for complex DB-specific types
        Query nativeQuery = entityManager.createNativeQuery(
            "SELECT * FROM forge_projects p WHERE p.metadata->>'priority' = 'HIGH'", 
            Project.class);
        List<Project> nativeResults = nativeQuery.getResultList();
    }
}
Output
JPQL/HQL: select p0_.id... from forge_projects p0_ | Native: SELECT * FROM forge_projects p...
Key Insight
The most important thing to understand is the level of abstraction. JPQL/HQL allows you to swap databases (PostgreSQL to Oracle) without changing code. Native SQL locks you into a specific vendor but unlocks 100% of their performance optimizations.
Production Insight
JPQL queries are compiled once and cached by Hibernate's query plan cache.
Native SQL queries bypass this cache — each execution re-parses the SQL.
Rule: Use JPQL for frequent queries, native only for rare, heavy operations.
Key Takeaway
JPQL = portability, HQL = Hibernate extras, Native SQL = raw power.
Choose the abstraction that matches your need, not your comfort level.
If you don't need vendor features, don't pay the portability cost.
HQL vs JPQL vs Native SQL Query Decision Flow THECODEFORGE.IO HQL vs JPQL vs Native SQL Query Decision Flow Choosing the right query type for JPA/Hibernate performance and portability HQL/JPQL (Object-Oriented) Portable across DBs; uses entity names/fields Native SQL (DB-Specific) Full SQL power; bypasses 1st-level cache Criteria API (Dynamic Queries) Type-safe, programmatic; verbose but flexible Stored Procedures Third query type; complex logic in DB Batch Operations Avoid 10k-row loops; use bulk updates Result Mapping & Type Safety Hidden complexity; ensure correct mapping ⚠ Native SQL bypasses L1 cache — $50k pitfall Always flush/clear cache before native queries or use HQL/JPQL THECODEFORGE.IO
thecodeforge.io
HQL vs JPQL vs Native SQL Query Decision Flow
Hql Jpql Native Sql

When to Use Each Query Type — A Decision Framework

The right query type depends on what you're doing. For standard CRUD and simple filters, JPQL is the default — it's database-agnostic and safe. When you need Hibernate-specific features like query hints or complex fetch strategies, HQL adds value. For reporting, bulk operations, or vendor-specific features (e.g., PostgreSQL's JSONB operators, MySQL's FULLTEXT indexes), Native SQL is your only option.

Here's a quick decision tree that senior engineers at io.thecodeforge use to decide in under 30 seconds.

io/thecodeforge/util/QueryDecision.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package io.thecodeforge.util;

// Decision logic — not actual code, just a reference
public class QueryDecision {
    /*
    if (need_portability && no_vendor_features) {
        use JPQL;
    } else if (need_hibernate_feature) {
        use HQL;
    } else if (need_vendor_specific || need_max_performance) {
        use Native SQL;
        // always use named parameters and @SqlResultSetMapping
    }
    */
}
Output
// The decision tree guides choice in 30 seconds.
The Query Spectrum Mental Model
  • JPQL: safe, portable, abstraction. Use for 80% of queries.
  • HQL: Hibernate-only features like fetch profiles, setMaxResults with hints.
  • Native SQL: last resort for complex reporting, bulk DML, or vendor-specific power.
Production Insight
Using Native SQL for simple queries is a common premature optimisation.
It kills portability and often adds no performance gain — Hibernate's JPQL compilation is efficient.
Measure first: add explain plans before deciding native is faster.
Key Takeaway
Start with JPQL, move to HQL only when needed.
Native SQL is the escape hatch — not the default.
Performance without portability is a technical debt.
Query Type Decision Tree
IfSimple CRUD or filtering, no vendor features needed
UseUse JPQL
IfNeed Hibernate-specific fetch profile or query hints
UseUse HQL
IfComplex reporting with window functions or CTEs
UseUse Native SQL with @SqlResultSetMapping
IfBatch UPDATE/DELETE that doesn't need entity tracking
UseUse Native SQL (bypass L1 cache)
IfNeed full-text search, JSONB, or GIS queries
UseUse Native SQL (vendor-specific)

Common Mistakes and How to Avoid Them

When learning these query types, most developers hit the same set of gotchas—specifically SQL injection vulnerabilities and the loss of the 'N+1' protection provided by the L2 cache. A common mistake is using Native SQL for simple CRUD operations, which prevents Hibernate from performing automatic dirty checking on the results. Conversely, many try to force complex reporting logic into JPQL, resulting in inefficient queries that would be better handled by a fine-tuned Native SQL statement. At 'io.thecodeforge', we emphasize that Native SQL should be the 'last resort' for reads, but the 'first choice' for high-performance bulk updates and deletes.

io/thecodeforge/security/QuerySafety.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.thecodeforge.security;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import java.util.List;

public class QuerySafety {
    
    // ANTI-PATTERN: Vulnerable to SQL Injection via string concatenation
    public void badQuery(EntityManager em, String input) {
        em.createNativeQuery("SELECT * FROM forge_users WHERE name = '" + input + "'").getResultList();
    }

    /**
     * CORRECT: io.thecodeforge best practice - Use Named Parameters
     * This prevents SQLi and allows the DB to cache the execution plan.
     */
    public void goodQuery(EntityManager em, String input) {
        List<?> users = em.createNativeQuery("SELECT * FROM forge_users WHERE name = :userName")
          .setParameter("userName", input)
          .getResultList();
    }

    /**
     * PRO TIP: Native SQL for bulk deletes to bypass session overhead
     */
    public int bulkDeleteOldLogs(EntityManager em) {
        return em.createNativeQuery("DELETE FROM forge_logs WHERE created_at < NOW() - INTERVAL '30 days'")
                 .executeUpdate();
    }
}
Output
// Logs: Executing query with bound parameter: :userName -> [input]
// Logs: Native DELETE executed (Bypassing Persistence Context)
Watch Out
Native SQL queries do not automatically return managed Entities. If you select raw columns, Hibernate won't track changes to those objects. Always use 'addEntity()' or 'resultClass' if you need the persistence context to track updates.
Production Insight
SQL injection via string concatenation in native queries is still the #1 query security incident.
Names parameters prevent it and allow plan reuse — two wins.
Never build SQL strings by hand in production code.
Key Takeaway
Always use named parameters in HQL and Native SQL.
Avoid string concatenation — it's fragile and insecure.
Bulk operations are faster with native SQL, but flush the context.

Result Mapping and Type Safety — The Hidden Complexity

JPQL and HQL return typed entities automatically – the result matches the entity fields. This provides compile-time safety: if the query selects fields that don't exist in the entity, it fails early. Native SQL, however, returns Object[] unless you provide a result mapping. That means runtime casting and potential ClassCastException. For production applications, always use @SqlResultSetMapping or pass the entity class to createNativeQuery. This one step saves hours of debugging.

Here's how to map a native query to a DTO using @SqlResultSetMapping.

io/thecodeforge/entity/OrderSummaryMapping.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.thecodeforge.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;

@SqlResultSetMapping(
    name = "OrderSummaryMapping",
    classes = @ConstructorResult(
        targetClass = OrderSummaryDTO.class,
        columns = {
            @ColumnResult(name = "order_id", type = Long.class),
            @ColumnResult(name = "total", type = BigDecimal.class)
        }
    )
)
@Entity
public class Order {
    // entity fields...
}

// Usage in repository:
// List<OrderSummaryDTO> summaries = em.createNativeQuery(
//     "SELECT o.id AS order_id, SUM(oi.price) AS total FROM orders o ...",
//     "OrderSummaryMapping").getResultList();
Output
// The constructor of OrderSummaryDTO is called once per row with the mapped values.
Performance Note
Mapped native queries are still faster than JPQL for complex projections because they avoid entity hydration costs. However, keep these mappings in a dedicated @SqlResultSetMapping — don't clutter entities.
Production Insight
Without @SqlResultSetMapping, native queries return Object[] — every read requires casting.
One wrong column position causes ClassCastException in production.
Invest 5 minutes in mapping to save 5 hours of debugging.
Key Takeaway
JPQL/HQL give type-safe results automatically.
Native SQL requires @SqlResultSetMapping for typed results.
Never leave native query results untyped in production code.

Migration and Portability — The Long-Term Cost

One of the strongest arguments for JPQL is that your query code survives a database migration — from PostgreSQL to Oracle, or MySQL to CockroachDB. Native SQL ties you to a specific dialect. If you ever need to switch databases, every native query becomes a migration item. That's a hidden tech debt that grows linearly with your query count.

In practice, you can manage this by isolating native queries behind a repository interface and using profile-specific query files. io.thecodeforge uses @Query annotations in Spring Data JPA with nativeQuery=true only when absolutely necessary, and documents the vendor dependency.

io/thecodeforge/repository/ProjectRepository.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package io.thecodeforge.repository;

import io.thecodeforge.model.Project;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import java.util.List;

public interface ProjectRepository extends CrudRepository<Project, Long> {

    // JPQL — portable across databases
    @Query("SELECT p FROM Project p WHERE p.status = :status")
    List<Project> findActiveProjects(@Param("status") String status);

    // Native SQL — PostgreSQL specific, documented
    @Query(value = "SELECT * FROM forge_projects WHERE metadata->>'priority' = :priority",
           nativeQuery = true)
    List<Project> findHighPriorityProjects(@Param("priority") String priority);
}
Output
// Spring Data JPA handles result mapping automatically for native queries when return type is entity.
// Isolate native queries in dedicated methods to centralize DB dependencies.
Portability vs Performance Trade-off
  • JPQL: zero migration cost. Just change the dialect property.
  • Native SQL: each query must be reviewed, tested, and often rewritten.
  • Rule: Keep native queries < 5% of your project's queries or document them explicitly.
Production Insight
A project with 200 native queries takes weeks to migrate databases.
The migration cost often exceeds the original performance benefit.
Document each native query with the vendor feature it uses and the migration plan.
Key Takeaway
JPQL is the insurance policy for database portability.
Native SQL is fast but locks you in.
Isolate native queries behind interfaces to minimize migration pain.

Criteria API: Where Dynamic Queries Go to Live or Die

You've got a query that changes based on user input. Maybe five optional filters, maybe sorting direction. HQL string concatenation works until it doesn't. That's when you meet the Criteria API.

Criteria is type-safe query construction in Java code. No string interpolation, no SQL injection nightmares, no building WHERE clauses with boolean flags that make your teammates cry during code review. The JPA Criteria API lets you compose predicates programmatically. It's verbose, yes. But verbosity beats a production outage when someone passes a malformed sort column.

The real killer feature: dynamic joins. HQL gets awkward when you need LEFT JOIN FETCH only sometimes. Criteria handles that without reflection tricks. Just check if the collection path is populated before calling fetch().

One warning — Criteria queries are harder to debug. You can't just print the generated SQL trivially. Always log the query string during development. Production issues with Criteria usually mean someone forgot that cb.equal() on an enum field doesn't handle nulls the way you think.

DynamicFilterRepository.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — java tutorial
// Dynamic query with optional filters — no string concatenation

public List<Order> findOrders(String status, String region, LocalDate after) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Order> query = cb.createQuery(Order.class);
    Root<Order> root = query.from(Order.class);

    List<Predicate> predicates = new ArrayList<>();

    if (status != null) {
        predicates.add(cb.equal(root.get("status"), Status.valueOf(status)));
    }
    if (region != null) {
        predicates.add(cb.equal(root.get("region"), region));
    }
    if (after != null) {
        predicates.add(cb.greaterThan(root.get("createdAt"), after));
    }

    query.select(root).where(cb.and(predicates.toArray(new Predicate[0])));
    query.orderBy(cb.desc(root.get("createdAt")));

    return entityManager.createQuery(query).getResultList();
}
Output
SELECT o FROM Order o WHERE o.status = :param0 AND o.createdAt > :param1 ORDER BY o.createdAt DESC
Production Trap:
Criteria queries silently ignore null predicates unless you wrap them correctly. Always use cb.and() with a populated array — an empty predicate list produces WHERE 1=1 and kills your index usage.
Key Takeaway
Criteria API for dynamic filters, HQL for static queries, Native SQL when performance makes you throw standards overboard.

Stored Procedures: The Third Query Type Nobody Talks About

Stored procedures are the forgotten child of JPA discussions. You'll find one blog post showing how to call a simple procedure with @Procedure, and then silence. But in the real world, legacy systems run on stored procedures. Migrating a 500-line PL/SQL block to HQL is a career-limiting move.

JPA supports stored procedures via @NamedStoredProcedureQuery or EntityManager.createStoredProcedureQuery(). The catch: result set mapping. If your procedure returns multiple result sets — common in reporting — JPA chokes unless you explicitly call getResultList() for each one.

Another gotcha: Oracle's REF CURSOR. JPA handles it, but the mapping requires a @SqlResultSetMapping with a constructor result for DTOs. No auto-magic. You write the mapping, or you get ClassCastException at 3 AM.

When to use stored procedures? When a query is complex enough to warrant database-level optimization or when security requires hiding table structures. Otherwise, keep it in Java. Debugging a stored procedure means paging through SQL Developer logs — not your idea of a good time.

ProcedureCaller.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// io.thecodeforge — java tutorial
// Calling a stored procedure with multiple result sets

@NamedStoredProcedureQuery(
    name = "getCustomerReport",
    procedureName = "reporting.get_monthly_customer_summary",
    resultSetMappings = {"CustomerSummaryMapping"},
    parameters = {
        @StoredProcedureParameter(mode = ParameterMode.IN, type = String.class, name = "customerId"),
        @StoredProcedureParameter(mode = ParameterMode.IN, type = LocalDate.class, name = "reportDate")
    }
)
public class CustomerSummaryDto {
    private String name;
    private BigDecimal totalSpent;
    private int orderCount;

    public CustomerSummaryDto(String name, BigDecimal totalSpent, int orderCount) {
        this.name = name;
        this.totalSpent = totalSpent;
        this.orderCount = orderCount;
    }
}

// Usage:
StoredProcedureQuery q = entityManager
    .createNamedStoredProcedureQuery("getCustomerReport")
    .setParameter("customerId", "CUST-001")
    .setParameter("reportDate", LocalDate.now().withDayOfMonth(1));

List<CustomerSummaryDto> results = q.getResultList();
Output
Hibernate: {call reporting.get_monthly_customer_summary(?, ?)}
CustomerSummaryDto{name='Acme Corp', totalSpent=45000.00, orderCount=12}
Production Trap:
Stored procedures with multiple result sets require explicit getResultList() calls per set. JPA only maps the first one by default. You'll lose data silently if you miss this.
Key Takeaway
Stored procedures are for legacy systems and ultra-complex reporting. Prefer HQL for maintainability unless you have a verified performance need.

Batch Operations: Why Your 10,000-Row Loop Is a Performance Crime

You wrote a loop that calls flush() after every entity update. That's not a query — that's a denial-of-service attack on your own database. HQL and JPQL support bulk UPDATE and DELETE statements that execute as a single SQL round-trip. No loading entities. No dirty checking. No 10,000 individual SELECTs followed by 10,000 UPDATEs.

The syntax is simple. The performance gain is brutal. A bulk delete wipes rows at the SQL level while respecting cascade rules — but watch out: the persistence context goes stale. You must call clear() and reload any affected entities after the bulk operation. Or you can call flush() before the bulk to flush pending changes, then clear() after to avoid stale state.

Production lesson: never iterate over a result set to perform row-by-row mutations. Write one JPQL update with a WHERE clause. The database engine is better at this than you are. Your transaction log will thank you.

BulkDeleteExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — java tutorial

import jakarta.persistence.*;

public class BulkDeleteExample {
    public int removeArchivedPosts(EntityManager em, int monthsOld) {
        em.flush(); // flush pending changes first
        int deleted = em.createQuery(
            "DELETE FROM Post p WHERE p.archivedAt < :cutoff")
            .setParameter("cutoff", java.time.LocalDate.now().minusMonths(monthsOld))
            .executeUpdate();
        em.clear(); // clear persistence context — entities are stale
        return deleted;
    }
}
Output
12 rows deleted.
Stale Entity Trap:
After a bulk update or delete, your EntityManager still holds old entity snapshots. Call em.clear() immediately, or em.refresh() on specific entities. Failure to do so leads to OptimisticLockException or silent data corruption.
Key Takeaway
One bulk JPQL operation replaces N loops. Always flush pending changes before, clear context after.

Type-Safe Queries with Constructor Expressions: Stop Mapping Strings to Objects by Hand

You have a SELECT p.title, p.author.name FROM Post p and then manually loop the Object[] result calling getters in the right order? That code is brittle, unreadable, and breaks silently when the query changes. JPQL supports constructor expressions: SELECT NEW com.example.PostSummary(p.title, p.author.name). The database returns fully typed objects — no casting, no positional index hell.

The constructor must match the parameter list exactly. You get compile-time safety for the Java side, runtime failure only if the query parameter count changes. This is the standard pattern for DTO projections and report queries where you don't want full entity overhead.

Production reality: constructor expressions are faster than entity loading because they skip dirty checking and lazy-load proxies. Pair them with SELECT NEW and native SQL if you need database-specific functions. The result is a typed list you can iterate immediately. No instanceof, no @Transient workarounds.

ConstructorExpressionExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — java tutorial

import jakarta.persistence.*;
import java.util.List;

// DTO class (must be in persistence unit or fully qualified)
class PostSummary {
    private final String title;
    private final String authorName;
    public PostSummary(String title, String authorName) {
        this.title = title;
        this.authorName = authorName;
    }
    // getters...
}

public class ConstructorExpressionExample {
    public List<PostSummary> fetchSummaries(EntityManager em) {
        return em.createQuery(
            "SELECT NEW PostSummary(p.title, p.author.name) " +
            "FROM Post p WHERE p.published = true", PostSummary.class)
            .getResultList();
    }
}
Output
[PostSummary{title='HQL Tips', authorName='Jane Doe'}, PostSummary{title='JPQL Gotchas', authorName='Bob Smith'}]
Senior Shortcut:
Use SELECT NEW for every read operation that doesn't need entity lifecycle. It's half the memory of managed entities and eliminates LazyInitializationException by construction.
Key Takeaway
Constructor expressions in JPQL turn Object[] hell into typed DTOs. One line, no manual mapping.

Core Hibernate Concepts: The Engine Behind HQL and JPQL

HQL, JPQL, and Native SQL don't exist in a vacuum. They run on Hibernate's core engine: the Session, Persistence Context, and first-level cache. A Session wraps a JDBC connection and manages entity lifecycle states—transient, persistent, detached, removed. The Persistence Context acts as a write-behind cache: all changes to persistent entities are automatically flushed to the database at transaction commit or before a query that might read stale data. This means your HQL update may execute immediately, or it may queue the SQL until flush time. If you call session.update() on a detached entity, Hibernate triggers a SELECT to reload the data first, then checks dirty fields—a costly hidden query. Understanding these internals prevents surprises: your JPQL delete may fail because cascade rules await flush, or your native SQL results won't match entity state until you explicitly clear the session. Master the Session as the bridge between your code and the database.

EntityStateDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — java tutorial

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

User user = new User("Alice");
session.persist(user); // state: PERSISTENT, in context

user.setName("Bob"); // dirty, queued for flush

// This HQL triggers an automatic flush first
List<User> all = session.createQuery("FROM User", User.class).list();

// Result includes "Bob" because flush happened before SELECT
tx.commit(); // flush again, then commit
session.close(); // user becomes DETACHED
Output
// Flush ensures Bob's update is persisted before the query executes.
Production Trap:
Calling native SQL SELECT after modifying entities without flushing gives stale data. Always flush() explicitly or wrap in a transaction to trigger automatic flush.
Key Takeaway
HQL and JPQL are executed within Hibernate's Session and Persistence Context—master these to control query timing and avoid hidden SQL.

Overview: HQL, JPQL, and Native SQL in the Java Persistence Stack

Java persistence offers three query languages for different trade-offs. HQL (Hibernate Query Language) is Hibernate-native, supports proprietary features like UPDATE/DELETE with joins, and is not portable to other JPA providers. JPQL (Java Persistence Query Language) is the JPA standard subset of HQL—portable across Hibernate, EclipseLink, OpenJPA, but lacks some HQL extensions. Native SQL uses the database's own dialect (MySQL, PostgreSQL, Oracle) for maximum control and performance tuning, at the cost of losing entity mapping and portability. The JPA spec mandates a Criteria API as type-safe query builder, but its verbose nature often drives developers back to HQL strings. All three live under a single EntityManager interface: createQuery() for JPQL/HQL, createNativeQuery() for SQL, and getCriteriaBuilder() for Criteria. Choose JPQL for portability, HQL for Hibernate-only features like query hints or custom functions, and Native SQL for database-specific optimizations like window functions or complex aggregations. Your decision narrows from standard to vendor to raw power.

QueryTypesOverview.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — java tutorial

EntityManager em = emf.createEntityManager();

// JPQL — portable, standard
TypedQuery<User> jpql = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
jpql.setParameter("e", "a@b.com");
User u1 = jpql.getSingleResult();

// HQL — Hibernate-only features
Query hql = em.createQuery("FROM User u WHERE u.email LIKE :e");
hql.setParameter("e", "%@b.com");
List<User> users = hql.getResultList();

// Native SQL — raw power, no mapping
Query nativeQ = em.createNativeQuery("SELECT * FROM users WHERE email = ?", User.class);
nativeQ.setParameter(1, "a@b.com");
User u2 = (User) nativeQ.getSingleResult();
Output
// JPQL and HQL return mapped entities; Native SQL requires explicit entity class or result set mapping.
Production Trap:
Native SQL bypasses Hibernate's caching and dirty checking. Joining across tables in raw SQL returns Object[] unless you map to a DTO or entity—leading to ClassCastExceptions.
Key Takeaway
JPQL for portability, HQL for Hibernate extras, Native SQL for raw power—each trades abstraction for control.
● Production incidentPOST-MORTEMseverity: high

Native SQL bypasses L1 cache — stale data costs $50k

Symptom
After calling a native UPDATE, subsequent JPQL queries returned the old entity state. The application paid the same invoice twice because the persistence context wasn't refreshed.
Assumption
Engineers assumed all queries within a transaction see the latest database state. They didn't know Native SQL doesn't flush the persistence context automatically.
Root cause
Native SQL bypasses Hibernate's flush-at-commit strategy. The entity in the L1 cache remained unchanged, so JPQL queries returned the cached snapshot, not the updated row.
Fix
Call em.flush() after the native UPDATE, or use em.refresh(entity) before reading the entity again. Better yet, avoid mixing native DML with entity reads in the same transaction.
Key lesson
  • Mixing Native SQL DML with JPQL reads in the same transaction is a recipe for stale data.
  • Always flush the persistence context after native updates when entities are still in scope.
  • Consider using JPQL UPDATE statements for batch operations when you need entity tracking.
Production debug guideSymptom → Action guide for the most common query-related failures4 entries
Symptom · 01
QuerySyntaxException: 'Table' is not mapped
Fix
You're using table names in JPQL/HQL. Replace table names with entity names. JPQL operates on @Entity classes, not database tables.
Symptom · 02
LazyInitializationException after HQL/JPQL query
Fix
The session is closed before you access lazy associations. Either fetch them eagerly with JOIN FETCH, or wrap the access in a transactional boundary.
Symptom · 03
Native SQL query returns Object[] instead of entity
Fix
You didn't specify result mapping. Use @SqlResultSetMapping or pass the entity class to createNativeQuery(sql, EntityClass.class).
Symptom · 04
JPQL query runs slow despite indexes
Fix
Check if Hibernate is generating N+1 queries. Log SQL with spring.jpa.show-sql=true and inspect the SELECT statements. Add JOIN FETCH to fix.
★ Quick Debug Cheat Sheet for Query IssuesWhen your HQL/JPQL/Native SQL behaves wrong in production, run these steps before escalating.
Wrong results or missing data
Immediate action
Enable SQL logging to see the actual query being executed.
Commands
spring.jpa.show-sql=true (application.properties)
logging.level.org.hibernate.SQL=DEBUG (logback.xml)
Fix now
Compare the logged SQL with your intended query. Check for implicit joins or wrong column references.
Stale data after native update+
Immediate action
Flush the persistence context and refresh the affected entity.
Commands
entityManager.flush() after native DML
entityManager.refresh(entity) before next read
Fix now
If refreshing multiple entities, call clear() and re-query.
N+1 query problem+
Immediate action
Check the Hibernate statistics or count SQL statements.
Commands
spring.jpa.properties.hibernate.generate_statistics=true
Add JOIN FETCH to your JPQL/HQL query
Fix now
Alternatively, use @EntityGraph on the repository method.
Query timeout or long execution+
Immediate action
Check the database's slow query log and get the actual execution plan.
Commands
EXPLAIN ANALYZE <your query> (PostgreSQL)
Enable Hibernate's slow query log: spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=200
Fix now
Add missing indexes, rewrite the query, or consider a native query for complex reporting.
Feature Comparison: JPQL vs HQL vs Native SQL
FeatureJPQLHQLNative SQL
TargetJava Entities (@Entity)Java Entities (@Entity)Database Tables/Views
PortabilityHigh (Provider Independent)Medium (Hibernate Only)Low (Database Specific)
Case SensitivityCase-sensitive (Entities/Fields)Case-sensitive (Entities/Fields)Depends on DB Collation
StandardJPA SpecificationHibernate ProprietaryANSI SQL / Vendor Dialect
OptimizationLimited by ORM logicHibernate optimizationsFull DB-level tuning
Type SafetyStrong (checked against Entity)Strong (checked against Entity)Weak (ResultSets/Object arrays)
Query Plan CacheYes (Hibernate caches compiled JPQL)Yes (Hibernate caches compiled HQL)No (re-parsed each time)
Result MappingAutomatic to EntityAutomatic to EntityRequires @SqlResultSetMapping or entity class
Bulk DMLSupported (generates SQL internally)SupportedOptimal (bypasses session overhead)

Key takeaways

1
JPQL is a core concept in JPA that ensures your application remains database-agnostic and focused on the domain model.
2
HQL provides more power than JPQL, specifically for complex joins and Hibernate-specific optimizations, but couples your code to the provider.
3
Native SQL should be reserved for complex reporting, bulk operations, or leveraging modern vendor-specific features like PostgreSQL JSONB or GiST indexes.
4
Always use named parameters (:param) to prevent SQL injection and allow for database execution plan reuse across all query types.
5
Remember that Native SQL bypasses the Persistence Context (L1 Cache) unless you specifically map the result back to an Entity.
6
Isolate native SQL behind repository or service abstractions to contain vendor dependencies and simplify future migrations.

Common mistakes to avoid

5 patterns
×

Using Native SQL for simple updates

Symptom
Hibernate's First-Level Cache becomes stale. Later JPQL reads in the same transaction return old entity state, causing data corruption.
Fix
Either flush the session after native updates, or use JPQL UPDATE for simple changes. Only use Native SQL for complex vendor-specific operations.
×

Hardcoding table names in JPQL

Symptom
QuerySyntaxException: 'forge_projects' is not mapped. The app crashes at runtime because JPQL uses entity names, not table names.
Fix
Always use the entity class name in JPQL (e.g., 'Project' instead of 'forge_projects'). Table names belong in @Table annotation only.
×

Ignoring pagination in Native SQL

Symptom
Native SQL pagination doesn't work because OFFSET/LIMIT syntax varies by database. The query either fails or returns wrong results.
Fix
Use JPA's setFirstResult/setMaxResults on the Query object, which Hibernate translates appropriately for each dialect. Or write vendor-specific syntax if you know the target DB.
×

Using SELECT * in JPQL

Symptom
The query compiles but looks foreign to anyone reading it. It works because Hibernate allows it, but it's a sign of Native SQL thinking applied to JPQL.
Fix
Always write 'SELECT p FROM Project p' with an explicit alias. This makes the query readable and ensures Hibernate correctly maps the result.
×

Not specifying result type for Native SQL

Symptom
The query returns List<Object[]> instead of typed entities. Every access requires casting, leading to ClassCastException in production.
Fix
Always pass the entity class or use @SqlResultSetMapping. If you only need specific columns, use a DTO with constructor mapping.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between JPQL and HQL. Why would a developer choos...
Q02SENIOR
When should you use a Native Query instead of JPQL in a production envir...
Q03SENIOR
How do you handle result mapping in Native SQL when you aren't returning...
Q04SENIOR
Does JPQL support all SQL features like 'UNION' or 'INTERSECT'? How does...
Q05SENIOR
What are the performance implications of using Native SQL updates versus...
Q01 of 05SENIOR

Explain the difference between JPQL and HQL. Why would a developer choose one over the other today?

ANSWER
JPQL is the JPA standard, HQL is Hibernate's extension. In modern applications (JPA 2.x+), most HQL features have been folded into JPQL — things like FETCH JOINs, subqueries, and functions. The main reason to use HQL today is if you need Hibernate-specific features like query hints (org.hibernate.comment), fetch profiles, or custom SQL functions. For the majority of apps, JPQL is sufficient and guarantees portability. If you're locked into Hibernate anyway, HQL is fine, but I'd start with JPQL as a default.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I mix JPQL and Native SQL in the same transaction?
02
Does Native SQL support pagination the same way as JPQL?
03
How do I prevent SQL injection in Native SQL?
04
Why is my Native SQL query failing with 'column name not found'?
05
Is HQL deprecated in favor of JPQL?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Hibernate & JPA. Mark it forged?

7 min read · try the examples if you haven't

Previous
One-to-Many and Many-to-Many in Hibernate
5 / 7 · Hibernate & JPA
Next
Hibernate Caching — First and Second Level