db-query-patterns (M13)
How queries are written decides whether the indexes from M11 can even be used. This module inspects
query shape in ORM source and raw SQL. Feeds the Performance & Scale score (axis performance,
relational Query w18, shared with M3/M19).
What it checks
SELECT * — fetching all columns defeats covering indexes, bloats network/cache, and couples
code to column order. Flag in hot paths.
- Structural N+1 — a query inside a loop / per-row lazy relation load that should be a single
join or batched
IN. Static detection is directional (the loop's runtime cardinality is
unknown) — it points at the structure, never claims a row count.
- OFFSET pagination —
LIMIT n OFFSET m degrades linearly with depth; deep pagination should use
keyset/seek (WHERE id > $last ORDER BY id LIMIT n).
- Non-SARGable predicates — wrapping the indexed column in a function (
WHERE lower(email)=…,
WHERE date(created_at)=…, leading-wildcard LIKE '%x', implicit type cast) so the index can't be
used. Recommend an expression index or rewriting the predicate. For the leading-wildcard /
LIKE '%x%' case a B-tree can never help — name the remedy: a pg_trgm GIN/GiST index on the
column (or a dedicated search engine for heavy full-text search).
Score / axis
Feeds performance only (relational Query w18; Query category in document/time-series/graph
profiles).
Tier-0 (static)
Grep ORM call sites and raw SQL for SELECT *, function-wrapped indexed columns, leading-wildcard
LIKE, and OFFSET; detect query calls inside loops/.map/per-item relation access for N+1. All
query-pattern findings are at most directional from source — confirming the actual plan/cost needs
runtime (needs_api / Tier-2).
Tier-1/2 (verification query)
EXPLAIN (ANALYZE, BUFFERS) <the suspect query>;
Method explain_plan. A Seq Scan where an index exists confirms a non-SARGable predicate; the
actual rows × loops confirms an N+1 amplification. Tier-2 pg_stat_statements (ordered by
total_exec_time) surfaces the real hot queries — without it, hotness is directional.
Findings
Emit findings per schema/finding.schema.json. Examples:
M13.orders.select_star_hot_path — SELECT * in a frequent read (severity:2, warn, axis
performance, confidence directional, fixable: proposed).
M13.users.n_plus_one_posts — per-row relation load in a loop (severity:3, warn, directional,
fixable: advisory — requires app-side eager load / batching).
M13.feed.offset_deep_pagination — OFFSET deep paging (severity:2, warn, directional,
fixable: proposed — keyset rewrite).
M13.users.non_sargable_lower_email — function-wrapped indexed column (severity:3, warn,
directional, fixable: proposed — expression index or predicate rewrite).
Each finding: evidence.observed quotes the query / call site verbatim (secrets redacted);
verification.reproduce is the EXPLAIN above referencing $DATABASE_URL, or a grep for the
pattern; expected_impact is banded + confidence-tagged (no naked %).
Honesty
- N+1 is directional by design: a loop running twice is fine, the same loop over 10k rows is not —
static analysis cannot tell which, so it never caps and never quotes a multiplier.
SELECT * is harmless on a small lookup or a one-shot admin query — scope severity to hot paths.
- Rewrites that change result semantics (keyset, predicate restructure) are
proposed/advisory,
never auto; expression-index additions can be proposed.
1---2name: db-query-patterns3description: Audit query-shape anti-patterns — SELECT *, structural N+1 (directional), OFFSET pagination vs keyset, and non-SARGable predicates that defeat indexes. Module M13. Feeds the Performance & Scale score.4---56# db-query-patterns (M13)78How queries are *written* decides whether the indexes from M11 can even be used. This module inspects9query shape in ORM source and raw SQL. Feeds the **Performance & Scale** score (axis `performance`,10relational *Query* w18, shared with M3/M19).1112## What it checks13141. **`SELECT *`** — fetching all columns defeats covering indexes, bloats network/cache, and couples15 code to column order. Flag in hot paths.162. **Structural N+1** — a query inside a loop / per-row lazy relation load that should be a single17 join or batched `IN`. Static detection is **directional** (the loop's runtime cardinality is18 unknown) — it points at the structure, never claims a row count.193. **OFFSET pagination** — `LIMIT n OFFSET m` degrades linearly with depth; deep pagination should use20 **keyset/seek** (`WHERE id > $last ORDER BY id LIMIT n`).214. **Non-SARGable predicates** — wrapping the indexed column in a function (`WHERE lower(email)=…`,22 `WHERE date(created_at)=…`, leading-wildcard `LIKE '%x'`, implicit type cast) so the index can't be23 used. Recommend an expression index or rewriting the predicate. For the leading-wildcard /24 `LIKE '%x%'` case a B-tree can never help — name the remedy: a `pg_trgm` **GIN/GiST** index on the25 column (or a dedicated search engine for heavy full-text search).2627## Score / axis2829Feeds **performance** only (relational *Query* w18; *Query* category in document/time-series/graph30profiles).3132## Tier-0 (static)3334Grep ORM call sites and raw SQL for `SELECT *`, function-wrapped indexed columns, leading-wildcard35`LIKE`, and `OFFSET`; detect query calls inside loops/`.map`/per-item relation access for N+1. All36query-pattern findings are at most `directional` from source — confirming the actual plan/cost needs37runtime (`needs_api` / Tier-2).3839## Tier-1/2 (verification query)4041```sql42EXPLAIN (ANALYZE, BUFFERS) <the suspect query>;43```44Method `explain_plan`. A `Seq Scan` where an index exists confirms a non-SARGable predicate; the45`actual rows` × `loops` confirms an N+1 amplification. Tier-2 `pg_stat_statements` (ordered by46`total_exec_time`) surfaces the real hot queries — without it, hotness is `directional`.4748## Findings4950Emit findings per `schema/finding.schema.json`. Examples:51- `M13.orders.select_star_hot_path` — `SELECT *` in a frequent read (`severity:2`, `warn`, axis52 `performance`, confidence `directional`, `fixable: proposed`).53- `M13.users.n_plus_one_posts` — per-row relation load in a loop (`severity:3`, `warn`, `directional`,54 `fixable: advisory` — requires app-side eager load / batching).55- `M13.feed.offset_deep_pagination` — `OFFSET` deep paging (`severity:2`, `warn`, `directional`,56 `fixable: proposed` — keyset rewrite).57- `M13.users.non_sargable_lower_email` — function-wrapped indexed column (`severity:3`, `warn`,58 `directional`, `fixable: proposed` — expression index or predicate rewrite).5960Each finding: `evidence.observed` quotes the query / call site **verbatim** (secrets redacted);61`verification.reproduce` is the `EXPLAIN` above referencing `$DATABASE_URL`, or a `grep` for the62pattern; `expected_impact` is banded + confidence-tagged (no naked %).6364## Honesty6566- N+1 is **directional by design**: a loop running twice is fine, the same loop over 10k rows is not —67 static analysis cannot tell which, so it never caps and never quotes a multiplier.68- `SELECT *` is harmless on a small lookup or a one-shot admin query — scope severity to hot paths.69- Rewrites that change result semantics (keyset, predicate restructure) are `proposed`/`advisory`,70 never `auto`; expression-index additions can be `proposed`.