skip to content

Querying & Raw SQL

Where a query is declared and in what form: behind a data-access interface, as an object-level string, a typed builder, or hand-written SQL. Asked because the form decides refactoring safety.

on this pageshow

questions

page 1 of 2

In a data-access layer, what is a set-based update, and how does it differ from load-modify-flush?

level: juniorimportance: must knowfreq 60%

answer

  1. one statement versus a read-then-write loop
  2. the database does the matching, not your code
  3. nothing is loaded, so nothing is tracked
  4. speed bought by skipping the object layer

basics

~20 s

A set-based update sends one UPDATE ... WHERE statement to the database through the layer. Load-modify-flush instead reads matching rows into objects, changes them in memory, and lets the layer write each changed object back.

solid answer

~40 s

Load-modify-flush is the object path: a query materialises every matching row as an object in the unit of work, you mutate the objects, and at flush time the layer compares each one against the snapshot it took at load and emits an `UPDATE` per changed object. A set-based update is the statement path: you express the change once as a predicate plus an assignment, and the layer issues a single `UPDATE ... WHERE ...` that the database applies to every matching row. Nothing is loaded, so nothing is tracked. The statement form is dramatically cheaper for large row counts because the cost stops scaling with the number of objects built in memory — but it also skips everything the layer would otherwise do per object.

go deeper

for a junior

Be able to name both shapes: one statement that changes many rows, versus loading rows into objects and letting the layer write them back. Know that the fast one loads nothing.

for a middle

Explain where the cost goes on each path — transfer, object construction, snapshot memory, one write per changed object — and list what the statement path skips: cascades, hooks, version checks, refreshing loaded copies.

for a senior

Show that you treat the skipped work as work you now own. Say how you would discard or reload affected in-memory state, handle children, and reproduce whatever the hooks did, before you commit to the statement path.

for a principal

Frame it as adding a second write path to the system. Argue when the volume justifies the divergence, and how you keep the two paths from developing different notions of what a valid row change is.

## Two shapes for the same change Every data-access layer offers at least two ways to change many rows, and they behave very differently. **Load-modify-flush** is the object path. You run a query; the layer materialises each matching row as an object and registers it in the **tracked set** (the unit of work's identity map, holding one object per identity). You mutate those objects in ordinary code. At **flush** time the layer compares each tracked object against the snapshot it took when the object was loaded — **dirty checking** — and emits one `UPDATE` for each object that actually changed. **A set-based write** is the statement path. You describe the change once — a predicate and an assignment — and the layer translates it into a single `UPDATE ... WHERE ...` or `DELETE ... WHERE ...` that the database executes over every matching row. No row is read into the client, no object is built, nothing enters the tracked set. ```sql UPDATE orders SET status = 'ARCHIVED' WHERE status = 'CLOSED' AND closed_at < ? ``` That one statement is what the layer sends. The database finds the rows, usually through an index on the predicate, and changes them in place. ## Where the cost goes | | Load-modify-flush | Set-based write | |---|---|---| | Rows transferred to the client | all matching rows | none | | Objects built and snapshotted | one per row | none | | Statements sent | one query plus one write per changed object (batched if the layer batches) | one | | Peak client memory | proportional to the result | constant | | Cascades to children | run | do not run | | Lifecycle hooks / callbacks | run | do not run | | Version check per row | performed | not performed | | Objects already in memory | updated, because they *are* the objects | untouched and now stale | The headline difference is that the object path pays per row on both sides of the wire — transfer, materialisation, snapshot memory, then a write per changed object — while the statement path pays once and lets the engine do the matching. ## What you are choosing to give up A set-based write is not "the same thing, faster". It steps around the object layer, and four guarantees leave with it: 1. **Already-loaded copies are not refreshed.** Any object of an affected row that is sitting in the current unit of work still holds the pre-statement values, and the layer will happily hand that stale object back to the next lookup. 2. **No cascade runs.** Children the layer would have deleted or updated along with the parent are left alone, which either orphans them or makes the database reject the statement on a foreign key. 3. **No lifecycle hook fires.** Audit stamps, derived columns maintained in code, and events published on change all silently do not happen. 4. **No per-row version check.** A per-object update carries the row's version in its `WHERE` clause so a concurrent modification is detected; a set-based statement simply overwrites whatever is there. Any cached copy of the affected rows held outside the unit of work is stale for the same reason: the layer never saw the individual rows change. ## Choosing between them Reach for the **statement path** when: - the change is uniform and expressible as a predicate plus an assignment; - the row count is large enough that materialising objects is the dominant cost; - no per-row logic in code has to run, or you are prepared to run it yourself. Stay on the **object path** when: - the new value of each row depends on logic that lives in code rather than in the data; - cascades, hooks or version checks carry invariants you rely on; - the row count is small, where the difference is not worth reasoning about. A practical rule: the statement path is a deliberate, documented shortcut for a bulk operation, not the default way to write. Use it where the volume justifies it, and treat everything it skips as work you have taken on yourself — refreshing or discarding in-memory copies, handling children, and re-establishing whatever the hooks used to do.

  • Why does a set-based write scale so much better than the object path as the row count grows?
    The object path's cost is proportional to rows on both sides of the wire: transfer, object construction, a snapshot per object for dirty checking, then a write per changed object. The statement path sends one statement whose client-side cost is constant; the only work that scales is the engine changing rows it has already located, usually through an index on the predicate.
  • Does the row count a set-based statement reports match the number of objects the layer knows about?
    No. The count comes from the database and reflects rows the predicate matched. The layer has no objects for them — it never loaded any — so the number tells you nothing about the tracked set, and objects for some of those rows may be sitting in memory holding the old values.
  • If the new value differs per row, can you still avoid load-modify-flush?
    Often yes: if the per-row value is derivable from data the database already holds, express it as an expression or a join against another table in one statement. Only when the value depends on logic that exists solely in application code does the object path — or a purpose-built staging step — become necessary.

saying these in an interview costs you the question

  • Thinks a set-based update also refreshes objects already loaded in memory
  • Believes the layer runs cascades and lifecycle hooks for a set-based statement
  • Assumes the statement's reported row count equals the number of tracked objects
  • Says bulk statements are always wrong because they bypass the object model
  • Defaults to load-modify-flush for millions of rows because it is the familiar path
open as a page

When a search filter arrives empty, why should a composed query omit its predicate instead of adding an always-true condition?

level: juniorimportance: must knowfreq 66%

basics

~20 s

An absent filter should contribute no predicate at all. A constant-true pad is only a concatenation crutch, a self-comparison silently drops rows holding no value, and a parameter-driven catch-all emits a different statement whose one plan must serve every combination.

open as a page

In a data-access layer, what is a native statement, and what result shapes can it be mapped into?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A native statement is SQL you write in the database's own language and hand to the layer unchanged, rather than an object-level query it translates. Its rows map back to entity objects, transfer models, or scalars.

open as a page

In a data-access layer, what is an object-level query string, and when are its mistakes caught?

level: juniorimportance: must knowfreq 72%

basics

~20 s

An object-level query is text naming mapped classes and fields, not tables and columns; the layer parses it and emits SQL. Being text, a typo or a renamed field surfaces only at parse time - startup or first run.

open as a page

In a data-access layer, what does it mean to project a query into a transfer type, tuple or scalar?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A projection asks the statement for only the fields a use case needs and materialises them as a scalar, a tuple or a small transfer type, so the row is never turned into a tracked mapped object.

open as a page

In a data-access layer with a unit of work, how does returning a tracked object differ from returning a transfer model?

level: juniorimportance: must knowfreq 64%

basics

~20 s

A tracked object stays registered with the open unit of work, so later edits to it are written at flush and unloaded parts can still be fetched. A transfer model is a plain snapshot that does neither.

open as a page

In a row-mapping data-access layer with no tracked set, how does a change to a loaded row reach the database?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Only through an UPDATE statement the code issues itself. The loaded object is a plain value with no link back to its row, so nothing detects the mutation, and forgetting the statement loses the change silently.

open as a page

In a data-access layer, how does a cursor-backed streaming read differ from one that materialises the whole result?

level: middleimportance: must knowfreq 58%

basics

~20 s

A materialising read buffers the whole result first, so memory scales with the result. A streaming read pulls rows in blocks from an open cursor as you consume them, keeping memory near one block — but the transaction stays open throughout.

open as a page

After a set-based update runs through a data-access layer, why can objects already loaded in the unit of work be wrong?

level: middleimportance: must knowfreq 64%

basics

~20 s

The statement changes rows in the database, but the unit of work never sees those rows. Its tracked objects still hold the earlier values, and the layer keeps serving those stale copies — or flushes them back over the change.

open as a page

Which parts of a dynamically composed query can be bound as parameters, and which must be chosen by your own code?

level: middleimportance: must knowfreq 72%

basics

~20 s

Placeholders bind values only. Column names, sort direction, comparison operators and the statement shape are text your code picks from a fixed internal map keyed by the request token. Page size and offset bind as values, but are still clamped to a ceiling.

open as a page

Which query authoring styles break the build when a mapped field is renamed, and which fail later?

level: middleimportance: must knowfreq 66%

basics

~20 s

Only styles whose field names reach the compiler as symbols break the build: a generated typed DSL, or a builder taking generated field references. Query strings, derived method names and named queries fail at startup or first run.

open as a page

Why can a projected result not be changed and saved back the way a loaded mapped object can?

level: middleimportance: must knowfreq 57%

basics

~20 s

Nothing about a projected row is registered - no snapshot, no identity-map entry, no version read - so a flush has nothing to compare or update. To write, load the mapped object by its key.

open as a page

Moving a service off a tracking mapper onto a query builder, which facilities must the team now provide by hand?

level: middleimportance: must knowfreq 55%

basics

~20 s

Identity, write ordering, batching into fewer round trips, cascade to related rows, the optimistic version check with its row-count test, and any cache invalidation. None of it disappears; it moves from the layer into application code.

open as a page

Why can a native read miss your transaction's pending changes, and what must you do after a native write?

level: seniorimportance: must knowfreq 63%

basics

~20 s

Pending changes live in memory until the layer sends them, so a native read misses them unless the layer flushes first. After a native write, the tracked copies are stale and must be discarded or refreshed.

open as a page

A data-access interface hides how its results are fetched. Which fetching decisions can it hide from callers, and which cannot?

level: seniorimportance: must knowfreq 58%

basics

~20 s

It can hide the statement text, join strategy, chosen columns and index or cache use. It cannot hide cost or timing: statement count, what was left unloaded, whether the result is tracked, volume, paging stability and scope duration.

open as a page

Which per-object behaviours does a set-based delete issued through a data-access layer skip, and who handles them instead?

level: middleimportance: should knowfreq 55%

basics

~20 s

It skips everything the layer runs per object: cascades to children, lifecycle hooks and the events or audit writes inside them, and the per-row version check. The database still enforces its own constraints, and the caller must supply the rest.

open as a page

When a layer runs a native statement, how do named and positional bindings differ, and how is a list bound?

level: middleimportance: should knowfreq 64%

basics

~20 s

Positional binding matches values to placeholders by order; named binding matches labels the layer rewrites, so a value can be reused and edits stay safe. A list cannot fill one placeholder: the layer expands it into one per element.

open as a page

When a native statement's rows are mapped to objects, do those objects join the layer's tracked set?

level: middleimportance: should knowfreq 58%

basics

~10 s

The mapping target decides. Rows mapped to an entity type usually enter the tracked set and are reconciled with any instance already loaded for that identity; transfer models and scalars stay outside it.

open as a page

How does deriving a query by parsing a data-access method's name work, and where does it stop?

level: middleimportance: should knowfreq 54%

basics

~20 s

The layer tokenizes the method name at wiring time, matches the tokens against mapped field names and comparison keywords, and generates the statement; arguments bind positionally. It fits one or two stable predicates and degrades fast beyond that.

open as a page

Why does a grouped total or a row spanning two aggregates need a projection rather than a mapped-object query?

level: middleimportance: should knowfreq 48%

basics

~20 s

A mapped-object query returns instances of one mapped class per row. A grouped row and a row combining fields from two aggregates correspond to no mapped class, so a projection is the only result the layer can build for them.

open as a page

What changes for callers when data-access interfaces are defined one per table instead of one per object graph?

level: middleimportance: should knowfreq 52%

basics

~20 s

Slicing by table pushes assembly onto callers: they combine the cluster themselves, order the writes, propagate generated keys, hold the cross-object rules, and open the transaction, so the fetch plan ends up spread across call sites.

open as a page

A nightly export streams ten million rows, yet memory climbs steadily and one connection is held for an hour. What went wrong?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Three separate faults produce this: rows materialised into tracked objects that are never released, streaming preconditions unmet so the driver buffered anyway, and slow per-row work inside the loop stretching the transaction the cursor needs.

open as a page

A filter that reaches through a one-to-many link adds a join - how can that change the rows the query returns?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Joining to the many side emits one output row per matching child, so a parent matching three children appears three times even though nothing from the child is selected. Counts, page sizes and totals then measure join rows, not entities.

open as a page

A search builder with eight optional filters emits many distinct statement shapes - what does that shape count cost you in practice?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Eight independent optional filters give 256 where-clause shapes before ordering multiplies them. Nobody has read most of them, each is compiled separately by the engine, index coverage differs per shape, and the failing combination is usually one no test ever assembled.

open as a page

Before reshaping a column, how do you find every query that touches it under mixed authoring styles?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Rank the evidence: regenerate typed symbols and let compilation list the call sites, parse every declared query in the build, search literal text and derived method names, then read the statements actually executed. Expand-migrate-contract keeps a miss survivable.

open as a page

A projected list query still runs slowly after it stopped returning mapped objects - what did projecting not fix?

level: seniorimportance: should knowfreq 53%

basics

~20 s

Projecting changes what each row is materialised into, not which rows the engine must produce. The same joins, filter, sort and row count remain, and a projection reaching across a to-many link still multiplies rows.

open as a page

A request drives a tracking mapper and a separate row-mapping layer side by side; what must be shared and what goes stale?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Both layers must run on one connection inside one transaction with a single owner of commit and rollback. Anything the mapper already loaded is stale once the other layer writes those rows, and must be discarded or re-read.

open as a page

For a mass data change, how do you decide between a set-based statement through the layer and the object path, and who then owns the skipped invariants?

level: principalimportance: should knowfreq 42%

basics

~20 s

Decide on two axes: whether the new value is derivable from data the database already holds, and whether the invariants the object path enforces are load-bearing. The statement path transfers those invariants to you — explicitly, tested and documented.

open as a page

Your filter API gains an optional parameter every quarter - how do you decide between one composed builder, named statements, or a search service?

level: principalimportance: should knowfreq 42%

basics

~20 s

Decide from the measured distribution of filter combinations, not the API surface. A composed builder suits a small orthogonal filter set; pinned statements suit a few dominant combinations; a search store suits text and facet filters, and costs a lagging copy.

open as a page

When should a team drop to native SQL, and how do you keep dialect differences from spreading through a codebase?

level: principalimportance: should knowfreq 45%

basics

~20 s

Drop below the mapper only for what the object-level language cannot express, or a plan you measured, never for taste. Contain it with one statement catalog, one implementation per engine, and tests on every supported engine.

open as a page

showing 1–30 of 36