Skip to main content

EXPLAIN & index audit — large retrieval paths

Evidence that the hot read paths use indexes, never a Seq Scan on a large table on the bounded path, and apply filters + privacy BEFORE the LIMIT. Generated by apps/api/src/topics/explain-audit.integration.spec.ts (run under the integration config against a migrated Postgres seeded with ~4k topics and ~4k broker operations across 20 providers, ANALYZEd). Direct Postgres — no cache layer.

How to read this

At fixture scale (a few thousand rows) Postgres often prefers a Seq Scan because the table is tiny and a scan is genuinely cheaper than an index — that does not mean the index is missing. To audit that each index is valid and serves the query (the behavior that holds at 50k+ rows where the scan is no longer free), the index-usage checks run with SET LOCAL enable_seqscan = off and confirm the planner switches to the index. The structural checks (filter/privacy before LIMIT) hold regardless of scale.

Indexes backing the paths

The broker SEARCH index (0092) is the gap this card closed: the cross-provider intent search ILIKEs name/description with a leading wildcard (%query%), which a B-tree cannot serve — the trigram GIN makes it index-assisted instead of a row-by-row filter over every operation in the workspace at scale.

Verdicts (captured plans)

Broker per-provider list — clean index path:
Broker cross-provider search (seqscan off) — served by an index, no Seq Scan; the (workspace_id, active, ILIKE) filter is in the WHERE, beneath the LIMIT:
Topic FTS search (seqscan off) — index path, no Seq Scan; search_tsv @@ plainto_tsquery(...) and status <> 'archived' filter on the scan node, under the LIMIT. Topic list — privacy before LIMIT (the load-bearing invariant): the draft- privacy predicate (status <> 'draft' OR is_private IS NOT TRUE OR created_by IS NULL OR created_by = :principal) is a WHERE clause on the scan/Filter node, which EXPLAIN prints beneath the Limit node — proof it is evaluated before the bound (no privacy-after-limit leak). 686 of 4000 rows removed by the filter before the 40-row LIMIT in the captured run.

Acceptance

  • Filters + privacy run before LIMIT on every audited path ✓ (structural, EXPLAIN-confirmed).
  • Keyset/cursor paths use composite indexes; no deep-offset scan is required for the bounded reads ✓.
  • The one missing index (broker cross-provider search) was added via an explicit TypeORM migration (0092), never an ad-hoc CREATE INDEX ✓.
  • Reproducible: pnpm --filter @driftless/api test:integration (with a Postgres) runs the audit; the two card-validate specs (retrieve-eval.integration, collections-perf.service.integration) gate the correctness of these paths.