What this tool does
PostgreSQL's EXPLAIN ANALYZE tells you exactly how a query was executed — but the raw output is famously hard to read. Timings are inclusive (each node's time contains all of its children), row counts are averaged per loop, and the real bottleneck is often buried three levels deep in the tree.
This visualizer parses the plan and shows, for every node:
- Exclusive time — the time spent in the node itself, computed as its total time (× loops) minus the time of its children, with a colored bar proportional to its share of the whole query.
- Estimated vs actual rows — the planner's row estimate next to what actually came out, with the mis-estimation factor when they diverge.
- Buffers, filters, sort and hash details when they are present in the plan.
How to get a plan
Run your query with:
EXPLAIN (ANALYZE, BUFFERS)
SELECT ... your query ...;
and paste the result above — both the default plain-text format and FORMAT JSON are supported. BUFFERS is optional but recommended — it shows how much I/O each node performed. You can paste directly from psql: the QUERY PLAN header and the + line-continuation characters are cleaned up automatically.
⚠️ EXPLAIN ANALYZE really executes the query. For an INSERT/UPDATE/DELETE, wrap it in BEGIN; ... ROLLBACK;.
Sharing a plan
Copy share link encodes the whole plan, compressed, into the URL itself (the part after #). Nothing is uploaded anywhere — the fragment never even reaches our web server — so the zero-upload promise holds. The flip side is worth being explicit about: the link contains the plan, so anyone you send it to can read table names, filter values and row counts in it. Very large plans produce very long URLs; some chat tools truncate those, in which case the recipient gets a clear error instead of a wrong plan.
Warnings this tool detects
- Row estimate off by more than 10× — the planner expected a very different number of rows than it got. Bad estimates cascade into bad join strategies. Typical fixes: run
ANALYZEon the table, raise the column's statistics target, or add extended statistics for correlated columns. - Sequential scan over many rows with a filter — the executor read the whole table to keep only part of it. Often a sign that an index would help.
- Sort spilling to disk —
Sort Method: external mergemeans the sort did not fit inwork_memand used temporary files. - Hash spilling to disk — a Hash node using more than one batch had to write partitions to disk; the hash table did not fit in
work_mem. - Expensive nested loop — the inner side of a Nested Loop was executed many times and accounts for a significant share of the total runtime.
- Many rows removed by a filter — the node produced far fewer rows than it had to examine, suggesting a missing or poorly selective index.
- Node never executed is also flagged (loops = 0) so zero-time nodes don't confuse the reading.
Reading the flame graph and the tree
The flame graph at the top is the whole query at a glance: each bar is a plan node, its width is the total time spent in it (children included) and its color is how much of the query's time is the node's own work — green for cheap, red for hot. A wide red bar deep down is your bottleneck; click it to jump to the corresponding node in the tree.
In the tree, execution starts at the leaves (scans) and flows up to the root. The horizontal bar on each node is its exclusive time share: a red bar at a leaf Seq Scan means the time is really spent scanning, not in the join above it. Click a node header to expand its details — costs, row counts, filter conditions, buffer usage — and use Copy text summary to paste a compact report into a ticket or a chat.
Note on parallel queries: timing attribution across parallel workers is an approximation — children of a Gather node report loops per worker, so exclusive times there should be read as indicative.
