# `Excessibility.QueryPlan`
[🔗](https://github.com/lessthanseventy/excessibility/blob/v0.20.0/lib/excessibility/query_plan.ex#L1)

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.

# `aggregate`

```elixir
@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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
