Excessibility.QueryPlan (Excessibility v0.19.0)

Copy Markdown View Source

Summarizes a decoded Postgres EXPLAIN (FORMAT JSON) result into a value-free plan digest — node types, relation names and row estimates only, never any row data.

The plan fingerprint hashes ONLY the structural tree (node types + relation names in traversal order), so it is stable across runs regardless of cost or row-count variance. This is what lets mix excessibility.digest.compare flag a plan change under stable SQL.

Tolerant by construction: it accepts both the raw list-wrapped Postgres form ([%{"Plan" => ...}]) and an already-unwrapped %{"Plan" => ...} map, and returns nil on anything unparseable rather than raising.

Summary

Functions

Aggregate the plan summaries of every occurrence of one query fingerprint into a single, bounded, value-free summary.

Aggregate every occurrence of one query fingerprint into the bounded set of distinct structural plan variants, one representative per structure.

Turn a decoded EXPLAIN (FORMAT JSON) result into a value-free plan summary.

Functions

aggregate(summaries)

@spec aggregate([map()]) :: map() | nil

Aggregate the plan summaries of every occurrence of one query fingerprint into a single, bounded, value-free summary.

A query fingerprint can fire many times in one journey; each occurrence carries its own plan summary, and a later occurrence can do far more row work than the first (issue #167). Selecting only the first occurrence's plan discards that magnitude before comparison ever runs, so aggregation keeps the maximum comparable row work per structural node path: estimated/actual rows, loops and rows_touched are maxed position-by-position across occurrences that share a plan structural fingerprint. max/2 is commutative, so the result is independent of occurrence order.

When occurrences carry different plan structures (e.g. the planner flipped to a different plan), they are grouped by structural fingerprint, each group is aggregated, and the group doing the most total row work is chosen as the deterministic representative (ties broken by fingerprint). Returns nil for an empty list and the sole summary unchanged for a single (or all-identical) occurrence.

aggregate_variants(summaries)

@spec aggregate_variants([map()]) :: [map()]

Aggregate every occurrence of one query fingerprint into the bounded set of distinct structural plan variants, one representative per structure.

A parameterized query can pick different plans by selectivity, so one SQL fingerprint legitimately carries several plan structures across a journey. aggregate/1 collapses them to the single heaviest representative, which discards any non-dominant variant that changed or regressed (issue #173). This keeps them all: occurrences are grouped by structural fingerprint, each group is merged by keeping the maximum comparable row work per node path (the same merge_summary/2 used by aggregate/1, so the heaviest instance of each structure survives), and the resulting representatives are returned sorted by fingerprint for determinism. Returns [] for an empty list.

summarize(arg1)

@spec summarize(term()) :: map() | nil

Turn a decoded EXPLAIN (FORMAT JSON) result into a value-free plan summary.

Returns nil for any input that does not contain a parseable "Plan" root.