Skip to content

Postgres search: accurate stats favor the ordered-index scan, regressing sparse/zero-match token & date shapes #281

Description

@mauripunzueta

Summary

PR #260 added a 3-column MCV so Postgres estimates token selectivity correctly, which fixed the high-match shapes dramatically (Encounter?status=finished p95 27s → 54ms). But accurate statistics also make the planner commit harder to the ordered-index early-stop plan (idx_resources_search, walk resources in last_updated DESC and stop at LIMIT 21). That plan is optimal when many rows match and pathological when almost none do — it then walks the entire resource type before it can return an empty page.

Net effect in the benchmark is positive (p95 1.7x better, throughput +23%), but two shapes regressed and this is the next thing to attack.

Spun off from #224 / PR #260.

Evidence (real benchmark dataset, 656,737 Observations)

shape p95 before this PR p95 after
Observation?code 5,747 ms 30,018 ms
Encounter?date 2,582 ms 17,757 ms

Both are shapes where a randomly-chosen value from the benchmark's pool matches few or no rows, so the ordered idx_resources_search scan walks the whole Observation / Encounter partition looking for 21 hits that are sparse or absent. With the old (wrong) estimate the planner instead built the match set first and probed by PK, which is faster in the sparse case.

This is the genuine two-sided nature of the ordered index: it is the 149x median win for the common case and a cliff for the sparse case. The fix is to get the planner to switch strategies based on actual selectivity of the specific value, not just the parameter.

Directions to investigate

  • idx_search_* covering/INCLUDE resource_id so the sparse pre-filter path (materialize matching ids, probe resources by PK) is cheap enough that the planner prefers it when selectivity is high — removing the incentive to gamble on the ordered scan.
  • Per-value statistics: the regression is value-dependent (code=8302-2 matches many, code=non-existent matches none). A wider MCV on value_token_code / value_date may already let the planner tell these apart — measure whether bumping SET STATISTICS further separates them.
  • A LIMIT-aware cost tweak or a planner hint is a last resort; prefer fixing the estimate.
  • Confirm the same effect on value_quantity_value range shapes.

Method note (important)

These plans cannot be A/B'd on a laptop-sized replica: the dominant cost is random heap I/O on a ~10M-row search_index, which does not exist when the table fits in cache. A small replica ranked PR #260's winning fixes backwards. Use the EXPLAIN (ANALYZE, BUFFERS) capture step added to .github/workflows/fhir-benchmark.yml (.github/scripts/pg-search-plans.sql) and read Buffers: shared read=, not Execution Time.

Acceptance

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions