pev reads a Postgres EXPLAIN plan as a tree, a flame chart, and a list of findings
Postgres Explain Visualizer
At a glance
- What is it?
- Postgres Explain Visualizer turns EXPLAIN output into a zoomable plan tree with per-node timings, estimate and buffer analysis, and plain-language findings. Plans live in browser localStorage, and links can carry a gzipped plan straight from a psql command.
- Who is it for?
- Use pev when you already have EXPLAIN output and want it readable on a phone or shareable as a link, and run the app locally with npm run dev to keep plans off third-party hosts. Do not expect it to query a database or to diagnose anything beyond what EXPLAIN reports.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three renderings of the same plan node
The default view is a zoomable tree in which each plan node is a card. The card shading encodes whichever metric you selected: graph time, rows, cost or buffers. Links between cards are sized by row count, so a nested loop that fans out into a million rows looks different from a single scan even before you read a number.
The second view is a flame chart of where the time went, which is the more honest picture of a plan whose total time is dominated by one operator. The third is an outline layout that fits a phone screen. That third view is not a cosmetic extra. Most plan reading happens on a phone after an alert fires, and the tree view at that width is close to unusable.
Selecting a node opens a details panel with self versus total time, estimates, filters, buffers, parallel workers, and the matching part of the SQL query. Self versus total is the distinction that matters most when reading a plan: total time includes children, so attributing a slow aggregate to the Aggregate node rather than to the scan underneath it is a common misreading.
The app is a React and TypeScript project built with Vite, using React 19 and react-router, with highlight.js for the SQL highlighting and bundled IBM Plex Sans and JetBrains Mono fonts. The last push to the repository was on 2026-10-08, and the package version in package.json is 1.0.0.
The findings engine reads spill and estimate errors, not just timings
Beyond the graphics, pev computes findings in plain language. The README lists the categories it looks for: filters that throw away most of their rows, hashes and sorts that spill to disk, bad row estimates, and cold caches. Those four are the ones a plan reader most often misses, because a disk spill shows up as time but not as an explicit marker in the text output.
The code layout shows how this is organised. `parse.ts` and `textParser.ts` turn JSON or text EXPLAIN output into a common shape, which means the same analysis path runs whether you pasted JSON or the familiar indented text. `analyze.ts` computes per-node metrics (exclusive time, buffers, estimates) and tags outliers, and `insights.ts` converts that analysis into the findings list. `layout.ts` and `flame.ts` handle the two visualisations, `highlight.ts` maps a node back to its slice of the query, and `share.ts` builds and reads plan links.
Because the parse step normalises both formats, a node you select in a text-format plan still gets a SQL highlight and a findings tag. The README credits explain.depesz.com as a strong influence, and the link-and-insights workflow follows that lineage rather than the plain-text plan dump most people work from.
Generating the plan you actually want analyzed
The README recommends a specific option set, and the flags matter to what pev can tell you. Without ANALYZE there are no timings. Without BUFFERS there are no buffer counts, so the cache findings have nothing to work with. SETTINGS exposes planner configuration that can explain a surprising choice.
EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, SETTINGS, FORMAT JSON) <your query>;FORMAT JSON is worth adding for anything you intend to paste into pev, since the text parser exists mainly for plans that arrive from elsewhere. COSTS gives you the cost column the tree can shade on, and VERBOSE brings the per-column detail that makes filters readable.
For production, `auto_explain` is the supported route. The README states pev accepts `auto_explain` log entries and notes that those entries bring the query along with the plan, which saves the annoying part of matching a plan back to the statement that produced it. Pasting works from psql, pgAdmin or DBeaver output.
Running it locally instead of pasting plans into a website
pev is a static app, so there is no server to run for local use beyond a dev server or a build. It requires Node 20 or later.
npm install
npm run devThe build script compiles with `tsc -b` and then runs vite build, producing a static bundle in `dist/`. The README states the output uses relative paths, so it works when hosted under a sub-path such as `/pev/`, which is what makes a self-hosted copy inside an internal docs site straightforward.
npm test
npm run typecheck`npm test` runs Vitest, and `npm run typecheck` runs the TypeScript project build without emitting. Both matter if you plan to fork the app, since the parsing code is where a fork is most likely to change.
The privacy claim is specific and worth reading closely: plans are stored in your browser localStorage and never leave your machine. That holds for the app's own storage. The one path where a plan can travel is a share link, and that is entirely client side as well.
Plan links carry data after the hash, so the server sees nothing
You can open a plan directly from a URL of the form `https://tatiyants.com/pev/#/import?plan=...&query=...&name=...`. Everything after the `#` is never sent to the server, so the plan travels inside the fragment rather than in a request. For large plans, `plan_gz` and `query_gz` take gzipped, base64-encoded values, which keeps the link short.
The README gives a shell one-liner that goes from psql to a shareable link.
psql -XqAt -c "EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON) SELECT ..." > plan.json
open "https://tatiyants.com/pev/#/import?plan_gz=$(gzip -c plan.json | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')"Note that this pipeline rewrites the base64 alphabet to URL-safe characters and strips padding, which is what keeps the link valid in a URL bar. A link button next to each plan name does the same thing for whatever plan is open.
There is a trade-off here that the README does not dwell on. A gzipped plan link is convenient but it also puts query text, and therefore schema and business logic hints, into anything that receives the link. If a plan contains literals from a production query, treat the link as sensitive.
What pev will not tell you, and what to use instead
The clearest boundary is that pev does not connect to a database. It reads plans you give it. If you want live monitoring of running queries, that is a different class of tool, and pasting output into a browser is not a substitute for it. pev is also a read-only analyser: it has no explain plan comparison against a previous run, and no storage of plan history beyond whatever is in localStorage.
The other limit is fidelity. The parse step is the boundary, and any plan shape it does not normalise will show up as a node without metrics, a missing highlight, or no finding. Parallel query is explicitly handled, including Gather, Gather Merge and their workers, and time is attributed to the node that ran them. CTEs, InitPlans and SubPlans are handled as well, with the same attribution rule. That list is long enough to cover common real plans, but it is a list, not a promise of full coverage.
Compared with explain.depesz.com, which the README names as its main influence, the difference in approach is that pev is a self-hostable static bundle with an offline-first localStorage store and a findings layer over spill, estimate and cache problems. Compared with reading text EXPLAIN in psql directly, the difference is visualisation plus share links. What pev does not offer is execution against a live server, which is the gap you have to accept.
Editorial conclusion
Use pev when you already have EXPLAIN output and want it readable on a phone or shareable as a link, and run the app locally with npm run dev to keep plans off third-party hosts. Do not expect it to query a database or to diagnose anything beyond what EXPLAIN reports. Before adopting, verify that your plan generator includes BUFFERS and SETTINGS, since the buffer and settings findings depend on those options.
Frequently asked questions
How do I run pev locally?
pev requires Node 20 or later. Run npm install and then npm run dev for a Vite dev server, or npm run build to produce a static bundle in dist/ that uses relative paths and can be hosted under a sub-path.
Does pev send my query or plan to a server?
Plans are stored in your browser localStorage and never leave your machine. A share link keeps its payload after the # fragment, so the server receives nothing, although the link itself then contains your query text.
Why does pev not show timings or buffer findings for my plan?
The README recommends generating plans with EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, SETTINGS, FORMAT JSON). Without ANALYZE there are no timings, and without BUFFERS the buffer and cache findings have no data to work from.
Can pev read plans from auto_explain log output?
Yes. The README states pev accepts auto_explain log entries, and that those entries bring the query along with the plan. It also accepts plans pasted from psql, pgAdmin and DBeaver, in JSON or text format.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/alextatiyants-pev)Was this useful?