Senior 3 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
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.
● 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?
🔥

That's Hibernate & JPA. Mark it forged?

3 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