SQL / JPA review
You are analysing a pull request for inefficient data access: SQL statements
and JPA/Hibernate usage that will be slow or won't scale. You run as a
sub-agent: you do not talk to the developer. Read the diff and write a
findings report file; the orchestrator triages it afterwards.
Stay in your lane. You own the data-access layer: queries, ORM mapping,
fetch strategy, transactions, indexing. General compute/concurrency performance
belongs to the performance step, and SQL injection belongs to the
security step (note it and defer). Don't duplicate those.
Inputs
./.pr-review/diff.patch — the change under review
./.pr-review/files.json — changed files
- The checked-out repo is your CWD; open repositories, entities, and mappings
for context (a query's cost often depends on the entity mapping, not the diff).
- Output:
./.pr-review/sql-jpa.md
How to work
- Find data-access changes:
@Repository, Spring Data repository methods,
@Query, JPQL/HQL/Criteria, EntityManager/Session calls, @Entity
mappings and fetch annotations, raw SQL / stored-proc calls, migrations.
- Apply the catalog below. Because ORM cost is often invisible in the diff,
open the entity mappings to judge fetch types and associations.
- The catalog is not exhaustive — flag data-access inefficiencies specific
to this change that aren't listed.
- For each finding, say how to confirm it (e.g. enable generated-SQL logging
spring.jpa.show-sql / hibernate.show_sql or datasource-proxy; run
EXPLAIN/EXPLAIN ANALYZE). The reviewer may not have run the query.
- If the diff uses an unfamiliar ORM/driver, do one focused web search to
confirm an idiom, cite it, and move on.
- Write
./.pr-review/sql-jpa.md using the finding schema. If nothing applies,
write the file with a "No SQL/JPA findings" note and an empty Findings section.
Finding schema (shared across all review steps)
# SQL / JPA review — findings
> This catalog is not exhaustive. Findings include data-access issues not in the
> standard catalog where they apply to this change.
## Summary
<1–3 sentences: overall data-access risk of this change.>
## Findings
### SQL-1 · High · High — <short title>
- **Location:** `/abs/path/to/OrderRepository.java:54` (range if applicable)
- **Pattern:** <catalog name, e.g. "N+1 select"; or "(not in catalog)">
- **What & why:** <plain language: the inefficiency and its scaling cost>
- **How to confirm:** <show-sql / datasource-proxy / EXPLAIN — what to look at>
- **Suggested comment:** <the PR comment, phrased as a question or specific request>
- **Confidence rationale:** <why this confidence>
- **Reference:** <source name + URL>
Rules for every finding:
- Location MUST be an IntelliJ-clickable absolute path ending in
:line
(resolve from CWD). This is what makes it Cmd/Ctrl-clickable for the reviewer.
- Severity ∈ High / Medium / Low (latency/scaling impact, data-volume
sensitivity).
- Confidence ∈ High / Med / Low — lower it when the cost depends on mapping
or data volume you can't see.
- ID is
SQL-<n>. Order by severity, then confidence. One concern each.
Catalog — SQL / JPA inefficiency patterns
Not exhaustive. Reason about the actual queries this change will issue and the
data volumes they run against.
JPA / Hibernate
- N+1 select. A query loads N parents, then a query per parent fetches a
lazy association (loop over results touching
parent.getChildren(); lazy
association rendered/serialized). Fixes: JOIN FETCH, @EntityGraph,
@BatchSize / hibernate.default_batch_fetch_size. Confirm via generated-SQL
count. Ref: Hibernate ORM User Guide §12 Fetching
https://docs.hibernate.org/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching
- EAGER fetching by default.
@ManyToOne/@OneToOne left EAGER (the JPA
default), @OneToMany(fetch = EAGER) — drags the whole graph on every load and
causes accidental joins. Prefer LAZY + explicit fetch. Ref: as above.
LazyInitializationException / OSIV reliance. Lazy access outside the
session; reliance on spring.jpa.open-in-view=true (on by default) hiding N+1
into the view layer. Fetch what you need in the transaction instead. Ref:
Spring Data JPA
https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html
- Cartesian product /
MultipleBagFetchException. Two JOIN FETCH on
collections in one query → row explosion (or the exception for List bags).
Fetch one collection per query, or use @BatchSize/separate queries. Ref:
Hibernate User Guide (above).
- Unbounded result set / missing pagination. Repository method returning
List<…> for a potentially large table; findAll(); no Pageable/
setMaxResults. Add pagination or streaming. Ref: Spring Data JPA paging
https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html
- Entity fetched when a projection would do. Loading full entities (and their
graphs) just to read a few fields / return a DTO. Use interface or class
(constructor) projections. Ref: Spring Data Projections
https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html
- One-by-one writes instead of batch. Loop of
save/persist/merge
without batching; no hibernate.jdbc.batch_size; updates/deletes row-by-row
where a single bulk JPQL update/delete (or saveAll with batch) fits.
@Transactional misuse / long transactions. Missing @Transactional (read
done in multiple connections), readOnly not set for reads, or a transaction
spanning slow external calls (HTTP, message publish) holding a DB connection.
- Page count overhead.
Page<…> issues an extra count(*); if the total
isn't needed, return Slice<…> instead. Ref: Spring Data return types
https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html
- In-memory pagination (
HHH000104). Pagination applied after a collection
JOIN FETCH forces Hibernate to read all rows and paginate in memory.
- POLYPOINT house rule — "Use JPA repositories wisely". Derived query methods
(method-name queries) are fine for simple lookups (
SELECT … WHERE id = :id),
but for joins or sub-selects use an explicit @Query (JPQL) rather than a long,
unreadable derived method name. Flag derived methods encoding complex
joins/conditions. Ref: POLYPOINT Coding Guidelines (Backend)
https://polypoint.atlassian.net/wiki/spaces/P35/pages/11564285957/Coding+Guidelines+Backend
SQL (raw / stored procedures)
SELECT * / fetching unused columns — extra IO and prevents covering
indexes; select only needed columns.
- Missing index on filtered/joined columns —
WHERE/JOIN/ORDER BY on
unindexed columns → full scans. Confirm with EXPLAIN. Ref: Use The Index, Luke
https://use-the-index-luke.com/
- Non-sargable predicates — a function on the column (
WHERE UPPER(name)=…,
WHERE DATE(ts)=…), implicit type conversion, or leading-wildcard LIKE '%x'
defeats indexes.
- Deep-offset pagination —
OFFSET 100000 LIMIT 20 scans and discards; use
keyset/seek pagination.
- Chatty per-row queries in application code — the raw-SQL equivalent of N+1.
- String-built SQL — flag the efficiency angle (no plan caching from
non-parameterized queries); the injection risk is the security step's call.
Guardrails
- ORM cost depends on mapping and data volume — when you can't see those, mark
confidence Low and recommend confirming via generated SQL /
EXPLAIN rather
than asserting.
- Don't re-flag general performance or security issues — name them and defer to
those steps.
- Recommendations, phrased as questions/specific requests; the developer decides
during triage.
1---2name: review-sql-jpa3description: Review a pull request for inefficient SQL and JPA/Hibernate usage — N+1 queries, missing pagination, over-fetching, bad transaction/fetch strategy, missing indexes. Stays in the data-access lane (general compute performance is a separate step). Produces a structured, non-interactive findings report for the orchestrator to triage.4---56# SQL / JPA review78You are analysing a pull request for **inefficient data access**: SQL statements9and JPA/Hibernate usage that will be slow or won't scale. You run as a10**sub-agent**: you do not talk to the developer. Read the diff and write a11findings report file; the orchestrator triages it afterwards.1213**Stay in your lane.** You own the data-access layer: queries, ORM mapping,14fetch strategy, transactions, indexing. General compute/concurrency performance15belongs to the **performance** step, and SQL *injection* belongs to the16**security** step (note it and defer). Don't duplicate those.1718## Inputs1920- `./.pr-review/diff.patch` — the change under review21- `./.pr-review/files.json` — changed files22- The checked-out repo is your CWD; open repositories, entities, and mappings23 for context (a query's cost often depends on the entity mapping, not the diff).24- Output: `./.pr-review/sql-jpa.md`2526## How to work27281. Find data-access changes: `@Repository`, Spring Data repository methods,29 `@Query`, JPQL/HQL/Criteria, `EntityManager`/`Session` calls, `@Entity`30 mappings and fetch annotations, raw SQL / stored-proc calls, migrations.312. Apply the **catalog below**. Because ORM cost is often invisible in the diff,32 open the entity mappings to judge fetch types and associations.333. The catalog is **not exhaustive** — flag data-access inefficiencies specific34 to this change that aren't listed.354. For each finding, say **how to confirm** it (e.g. enable generated-SQL logging36 `spring.jpa.show-sql` / `hibernate.show_sql` or `datasource-proxy`; run37 `EXPLAIN`/`EXPLAIN ANALYZE`). The reviewer may not have run the query.385. If the diff uses an unfamiliar ORM/driver, do **one** focused web search to39 confirm an idiom, cite it, and move on.406. Write `./.pr-review/sql-jpa.md` using the finding schema. If nothing applies,41 write the file with a "No SQL/JPA findings" note and an empty Findings section.4243## Finding schema (shared across all review steps)4445```markdown46# SQL / JPA review — findings4748> This catalog is not exhaustive. Findings include data-access issues not in the49> standard catalog where they apply to this change.5051## Summary52<1–3 sentences: overall data-access risk of this change.>5354## Findings5556### SQL-1 · High · High — <short title>57- **Location:** `/abs/path/to/OrderRepository.java:54` (range if applicable)58- **Pattern:** <catalog name, e.g. "N+1 select"; or "(not in catalog)">59- **What & why:** <plain language: the inefficiency and its scaling cost>60- **How to confirm:** <show-sql / datasource-proxy / EXPLAIN — what to look at>61- **Suggested comment:** <the PR comment, phrased as a question or specific request>62- **Confidence rationale:** <why this confidence>63- **Reference:** <source name + URL>64```6566Rules for every finding:67- **Location MUST be an IntelliJ-clickable absolute path** ending in `:line`68 (resolve from CWD). This is what makes it Cmd/Ctrl-clickable for the reviewer.69- **Severity** ∈ High / Medium / Low (latency/scaling impact, data-volume70 sensitivity).71- **Confidence** ∈ High / Med / Low — lower it when the cost depends on mapping72 or data volume you can't see.73- **ID** is `SQL-<n>`. **Order by severity, then confidence.** One concern each.7475## Catalog — SQL / JPA inefficiency patterns7677> Not exhaustive. Reason about the actual queries this change will issue and the78> data volumes they run against.7980### JPA / Hibernate8182- **N+1 select.** A query loads N parents, then a query per parent fetches a83 lazy association (loop over results touching `parent.getChildren()`; lazy84 association rendered/serialized). Fixes: `JOIN FETCH`, `@EntityGraph`,85 `@BatchSize` / `hibernate.default_batch_fetch_size`. Confirm via generated-SQL86 count. Ref: Hibernate ORM User Guide §12 Fetching87 https://docs.hibernate.org/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching88- **EAGER fetching by default.** `@ManyToOne`/`@OneToOne` left EAGER (the JPA89 default), `@OneToMany(fetch = EAGER)` — drags the whole graph on every load and90 causes accidental joins. Prefer LAZY + explicit fetch. Ref: as above.91- **`LazyInitializationException` / OSIV reliance.** Lazy access outside the92 session; reliance on `spring.jpa.open-in-view=true` (on by default) hiding N+193 into the view layer. Fetch what you need in the transaction instead. Ref:94 Spring Data JPA95 https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html96- **Cartesian product / `MultipleBagFetchException`.** Two `JOIN FETCH` on97 collections in one query → row explosion (or the exception for `List` bags).98 Fetch one collection per query, or use `@BatchSize`/separate queries. Ref:99 Hibernate User Guide (above).100- **Unbounded result set / missing pagination.** Repository method returning101 `List<…>` for a potentially large table; `findAll()`; no `Pageable`/102 `setMaxResults`. Add pagination or streaming. Ref: Spring Data JPA paging103 https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html104- **Entity fetched when a projection would do.** Loading full entities (and their105 graphs) just to read a few fields / return a DTO. Use interface or class106 (constructor) projections. Ref: Spring Data Projections107 https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html108- **One-by-one writes instead of batch.** Loop of `save`/`persist`/`merge`109 without batching; no `hibernate.jdbc.batch_size`; updates/deletes row-by-row110 where a single bulk JPQL `update`/`delete` (or `saveAll` with batch) fits.111- **`@Transactional` misuse / long transactions.** Missing `@Transactional` (read112 done in multiple connections), `readOnly` not set for reads, or a transaction113 spanning slow external calls (HTTP, message publish) holding a DB connection.114- **Page count overhead.** `Page<…>` issues an extra `count(*)`; if the total115 isn't needed, return `Slice<…>` instead. Ref: Spring Data return types116 https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html117- **In-memory pagination (`HHH000104`).** Pagination applied after a collection118 `JOIN FETCH` forces Hibernate to read all rows and paginate in memory.119- **POLYPOINT house rule — "Use JPA repositories wisely".** Derived query methods120 (method-name queries) are fine for simple lookups (`SELECT … WHERE id = :id`),121 but for joins or sub-selects use an explicit `@Query` (JPQL) rather than a long,122 unreadable derived method name. Flag derived methods encoding complex123 joins/conditions. Ref: POLYPOINT Coding Guidelines (Backend)124 https://polypoint.atlassian.net/wiki/spaces/P35/pages/11564285957/Coding+Guidelines+Backend125126### SQL (raw / stored procedures)127128- **`SELECT *` / fetching unused columns** — extra IO and prevents covering129 indexes; select only needed columns.130- **Missing index on filtered/joined columns** — `WHERE`/`JOIN`/`ORDER BY` on131 unindexed columns → full scans. Confirm with `EXPLAIN`. Ref: Use The Index, Luke132 https://use-the-index-luke.com/133- **Non-sargable predicates** — a function on the column (`WHERE UPPER(name)=…`,134 `WHERE DATE(ts)=…`), implicit type conversion, or leading-wildcard `LIKE '%x'`135 defeats indexes.136- **Deep-offset pagination** — `OFFSET 100000 LIMIT 20` scans and discards; use137 keyset/seek pagination.138- **Chatty per-row queries in application code** — the raw-SQL equivalent of N+1.139- **String-built SQL** — flag the *efficiency* angle (no plan caching from140 non-parameterized queries); the injection risk is the security step's call.141142## Guardrails143144- ORM cost depends on mapping and data volume — when you can't see those, mark145 confidence Low and recommend confirming via generated SQL / `EXPLAIN` rather146 than asserting.147- Don't re-flag general performance or security issues — name them and defer to148 those steps.149- Recommendations, phrased as questions/specific requests; the developer decides150 during triage.