# Query and Fetch Guide Design §23-§28. How queries are chosen, bounded, and proven. ## Named queries Every registered query carries a `QueryName`. It becomes the metric tag, the trace attribute, and the SQL comment that appears in `pg_stat_activity` and the slow-query log — which is the only thing that connects a statement on the server back to the use case that issued it. The format rejects raw SQL for a reason: a metric tag built from a query string is unbounded by construction, and one built from a parameterised value leaks row data into telemetry. ## Fetch plans, not eager mappings N+1 is solved per use case with a registered entity graph, not by making an association `EAGER` in the mapping. The eager fix repairs the one query that needed it and imposes the extra join on every other query against that entity, including the ones that only wanted the id. `fetchgraph` and `loadgraph` are different: a fetch graph is exhaustive (attributes outside it are lazy whatever the mapping says), a load graph is additive. Choosing the wrong one produces either missing data or the amplification the graph was meant to avoid. ## Measuring, not guessing `QueryMeasurement` records statements, hydrated entities, rows, fetches, and elapsed time. Statement count alone cannot distinguish the two failures that matter: - **N+1** — many statements, few rows. - **Cartesian fetch** — one statement, an enormous number of rows. A suite asserting only on statement count passes the second one every time. ## Pagination Offset pagination makes the database walk and discard `n` rows before returning any. Keyset pagination replaces it: - The predicate is lexicographic. For an ordering of `(createdAt, id)`, "after `(t, x)`" is `createdAt < t OR (createdAt = t AND id < x)` — **not** `createdAt <= t AND id < x`, which reads plausibly and silently drops rows from the middle of the result set. - The ordering must end in a unique column. Without one, a page boundary inside a run of equal values duplicates and skips rows. - `size + 1` rows are fetched and `size` returned. That extra row answers `hasNext` without a count query, which would be a second full scan whose answer is stale on arrival. Cursors are signed. An unsigned cursor is client-controlled ordering state: rewriting it lets a caller seek to arbitrary keys. ## Sorting Client sort parameters are mapped through `SafeSortRegistry`, never passed through. A sort field reaches the query as part of the ORDER BY clause rather than as a bound value, so forwarding the client's string means the client writes part of the statement. `JpaSort.unsafe` has no call site in this platform. The registry's tie-breaker is always appended, because a sort that does not end in a unique column has no total order and paging over a non-total order duplicates and skips rows. ## Streaming A JPA `Stream` is a live cursor holding a `ResultSet`, a statement, and a connection. `JpaStreamExecutor` consumes it inside a try-with-resources and never returns it, because a stream returned past the transaction boundary is a connection leak that presents as unrelated timeouts elsewhere. A read-only transaction is required: streaming inside a write transaction pins a write connection for the whole traversal. ## Batching Configuring `hibernate.jdbc.batch_size` proves nothing. `BatchExecutionResult.jdbcBatches` comes from counting real `executeBatch()` calls at the JDBC layer, because an IDENTITY generator, an interleaved select, or a mid-loop flush disables batching while the configuration still says it is on. Flush and clear are separate boundaries. Flushing alone sends the statements and keeps every entity in the Persistence Context — the classic bulk-import out-of-memory.