The root is always slowest until you subtract its children
Planflare parses query plans into one tree, subtracts child runtime, and points at the database operation doing the work instead of blaming the root.
The top node in a query plan usually has the largest runtime. That does not make it the slowest operation. Its clock includes every child below it, so blaming the root is a bit like blaming a restaurant bill on the person who carried it to the table.
Planflare turns Postgres, MySQL, and SQLite query
plans into a collapsible tree and marks the operation consuming the most time. It is for the
moment when I already know what EXPLAIN does, but do not want to decode 40 indentation-sensitive
lines during an incident.
The input can be Postgres text or JSON, MySQL EXPLAIN FORMAT=JSON, or SQLite EXPLAIN QUERY
PLAN output. There is no database connection and no upload. The parser runs as WebAssembly in
the browser, which matters because a plan can contain literal filter values copied from the
query.
The useful time is the time left over
Query-plan timing is inclusive. Suppose a hash join reports 120 ms, with a sequential scan below it reporting 95 ms and a hash build reporting 20 ms. Highlighting total time paints the join red, but most of its 120 ms was spent waiting for those children to finish.
Planflare instead computes self-time: the node's total time minus the totals of its direct children. In this example, the join owns roughly 5 ms, while the sequential scan owns 95 ms. That subtraction stops the root from winning merely because it contains the entire query and points at the step worth investigating first.
Runtime is only half the diagnosis. The planner chooses an algorithm using estimated row counts. If it expects 100 rows and receives 100,000, a nested loop that looked cheap can become painful. Planflare shows estimated and actual rows together, then flags a mismatch greater than 10 times. The red timing mark identifies where the query paid; the blue estimate mark can explain why the planner chose that route.
Three formats, one tree
I did not split the input with a pile of regular expressions. These formats have structure,
even when that structure is informal. Postgres uses variable-width indentation and can
interleave Buffers: details. MySQL uses arbitrarily nested JSON. SQLite emits numeric node
and parent IDs.
The Rust parser handles those rules in separate engine modules, then normalizes every result
into the same PlanNode tree: operation type, relation, estimated cost and rows, measured time
and rows, loop count, and children. The TypeScript interface never needs a special rendering
branch for MySQL or SQLite. It receives one shape, computes the annotations, and draws it.
That boundary is also a guard against a dangerous failure mode for a diagnostic tool: a plausible-looking wrong answer. Malformed or unrecognized input produces an inline error instead of a partial tree assembled from whatever lines happened to match. Property tests feed arbitrary input to the parser to check that odd text returns an error rather than a crash.
There are honest limits. Self-time is a lead, not a complete diagnosis. It cannot tell me whether a scan needs an index, whether an estimate is stale because statistics need attention, or whether cold storage distorted one run. SQLite plans also lack Postgres-style measured timing, so there is no runtime hotspot to compute there. Planflare shortens the search; it does not replace knowing the schema and workload.
Try it
Open Planflare, load the built-in Postgres example,
and jump to the red node. Compare its total time with its self-time, then look for a blue row
estimate warning nearby. After that, paste a plan from EXPLAIN (ANALYZE, BUFFERS) and expand
the hottest branch. The parsers, tests, and browser interface are on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…