Execution Plan Viewer for SQL Server, PostgreSQL and MySQL
Open a SQL Server .sqlplan file or paste showplan XML, PostgreSQL EXPLAIN (FORMAT JSON) output, or MySQL EXPLAIN FORMAT=JSON / EXPLAIN ANALYZE output, and see the operators as a graph with each operator's subtree cost as a share of the statement cost, estimated vs actual rows and warnings, and the operator's properties in a details pane. Add a second plan to see what changed. The plan is parsed in your browser; nothing is uploaded.
42% subtree cost as a share of the statement act · est actual and estimated rows orange: actual rows 10× or more above the estimate and at least 10 rows !2 warnings on the operator
Pick a file, paste a plan or click Load sample. The operator graph, the statement list for multi-statement plans and the details pane appear here.
Compare with a second plan
Paste or open plan B in the same format — for example the plan after adding an index or rewriting the query. The list below counts matched, added and removed operators and, for SQL Server and PostgreSQL plans, the cost and row changes.
What the viewer shows
An execution plan is the tree of operators the database chose to run a statement: scans and seeks that read tables and indexes at the bottom, joins, sorts and aggregates above them, and the operator that returns rows at the top. The viewer draws that tree top-down with the root first, the same layout the Jam SQL Studio desktop app uses. Rows flow upwards, from the leaves to the root.
- Operator box: the operator name (SQL Server's
PhysicalOp, PostgreSQL'sNode Type, MySQL's access type or iterator), the table it reads (SQL Server'sObject, PostgreSQL'sRelation Name; MySQL operator names already name the table), and a rows line such asact 4,812 · est 120. Index names are in the details pane. - Percentage badge: the operator's estimated subtree cost as a share of the statement's root operator (SQL Server
EstimatedTotalSubtreeCost, PostgreSQLTotal Cost). - Orange rows line: actual rows are 10 or more times the estimate and at least 10, so an estimate of 0.1 against 3 actual rows is not marked. Fewer rows than estimated is not marked either: below a
LIMIT,TOPorEXISTSan operator is stopped early, so that is expected. - Warning badge (
!2): SQL Server warnings recorded on that operator, such as a sort or hash spill to tempdb. - Details pane: with nothing selected, the statement overview (plan type, operator and warning counts, statement text, PostgreSQL planning and execution time, and links to the underestimated and warned operators). Select an operator to see its row figures, warnings and the properties the parser reads for it. For SQL Server these are the
<RelOp>attributes (estimated rows, I/O, CPU and subtree cost, parallelism, execution mode), the object and index, the warnings and the actual rows and executions summed from the runtime counters; predicates, output columns and per-operator I/O and timing counters stay in the XML. PostgreSQL operators list every field of the plan node, MySQL JSON operators their scalar and list fields, and MySQL tree output the cost, time, rows and loops of each line. - Tree view: the same operators as an indented list, which reads better on a phone and is the only view for statements with more than 1,500 operators.
How to get an execution plan
SQL Server and Azure SQL
- SSMS, actual plan: Query → Include Actual Execution Plan (Ctrl+M), run the query, open the Execution plan tab, right-click the graph and choose Save Execution Plan As… to write a
.sqlplanfile, or Show Execution Plan XML… to copy the XML. - T-SQL, actual plan: run the query between
SET STATISTICS XML ON;andSET STATISTICS XML OFF;. The plan comes back as an extra result set with one XML value. - T-SQL, estimated plan:
SET SHOWPLAN_XML ON;in its own batch, then the query. The statement is compiled but not executed. - Plan cache and Query Store:
sys.dm_exec_query_planand Query Store return compiled plans without runtime counts, so they open as estimated plans. On SQL Server 2019 and later,sys.dm_exec_query_plan_statsreturns the last actual plan of a cached statement when theLAST_QUERY_PLAN_STATSdatabase scoped configuration (or trace flag 2451) is on.
-- Plan of a cached statement (estimated: no runtime counters)
SELECT TOP (20) qs.total_worker_time, qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
ORDER BY qs.total_worker_time DESC;
-- Last actual plan of a cached statement (SQL Server 2019+), after
-- ALTER DATABASE SCOPED CONFIGURATION SET LAST_QUERY_PLAN_STATS = ON;
SELECT TOP (20) qs.total_worker_time, ps.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_query_plan_stats(qs.plan_handle) AS ps
ORDER BY qs.total_worker_time DESC;
-- Query Store keeps the plan as text; cast it to xml to open it as a link in SSMS
SELECT p.plan_id, TRY_CAST(p.query_plan AS xml) AS query_plan
FROM sys.query_store_plan AS p
WHERE p.query_id = 42;PostgreSQL
The viewer reads PostgreSQL's JSON plan format. ANALYZE executes the statement and adds actual rows, loops and timings; BUFFERS adds shared-buffer hits and reads per operator. Leave ANALYZE out for an estimated plan.
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT c.email, count(*) AS open_orders
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'open'
GROUP BY c.email
ORDER BY open_orders DESC
LIMIT 20;
-- For INSERT / UPDATE / DELETE, ANALYZE really changes data:
BEGIN;
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) UPDATE orders SET status = 'closed' WHERE id = 1;
ROLLBACK;In psql, psql -XqAt -f query.sql -o plan.json writes the bare JSON to a file. Copying psql's default output with the QUERY PLAN header and the + continuation marks works too: the viewer strips them, as it does the one-column result grid ([{"QUERY PLAN": …}]) that GUI clients export. Plans logged by auto_explain with auto_explain.log_format = json open as well — copy the JSON object from the log line.
MySQL
EXPLAIN FORMAT=JSON SELECT …— estimated plan. Both JSON layouts are read: thequery_blocklayout and the iterator tree that MySQL 8.3 added asexplain_json_format_version = 2.EXPLAIN ANALYZE SELECT …— actual plan as an indented tree of->lines with estimated and actual rows, loops and timings. It runs the query.EXPLAIN FORMAT=TREE SELECT …— the same tree with estimates only.- From the
mysqlclient, the table borders, the\Grow header, theEXPLAIN:label and the typed statement are removed automatically, so a straight copy of the terminal output works.
Oracle
Not supported on this page. Pasted DBMS_XPLAN output is recognised and explained, not drawn. The desktop app captures estimated and actual plans from an Oracle connection and shows them in the same graph as the other engines.
How to read cost, rows and warnings
Cost percentage
Each badge is the operator's subtree cost — the operator plus everything below it — divided by the root operator's cost. The root is always 100%, and the operator that carries the cost is the one whose percentage is much higher than the percentages of its children. Under a PostgreSQL LIMIT the child operators can show 100% too, because the Limit's own cost covers only the rows it fetches. This differs from the per-operator Cost: N% that SSMS prints under each icon, which excludes the children. Costs are the optimizer's estimate in its own units, not milliseconds, even in an actual plan. MySQL plans get no badge. Tree output (EXPLAIN ANALYZE, FORMAT=TREE) shows each line's cost= figure in the details pane, and the version 2 JSON layout lists its cost fields there, such as estimated_total_cost. The query_block JSON layout keeps its costs in cost_info objects, which the parser does not list, so those plans show no cost.
Estimated vs actual rows
| Engine | Estimate | Actual | Compared as |
|---|---|---|---|
| SQL Server | EstimateRows per execution × (1 + EstimateRebinds + EstimateRewinds) executions | ActualRows summed over all threads and executions | all executions, like SSMS's Estimated Number of Rows for All Executions |
| PostgreSQL | Plan Rows, per loop | Actual Rows, average per loop | per loop; Actual Loops shown |
| MySQL | rows= in (cost=…), estimated_rows | rows= in (actual time=…), per loop | per loop; loops= shown |
A Key Lookup or an inner index seek under Nested Loops runs once per outer row. On such an operator, act 4,812 · est 120 · 4,812 exec means one row per execution, as planned, but 4,812 executions instead of the 120 the optimizer expected — the details pane lists the per-execution estimate and both execution counts. The cause is the operator that produced the outer rows, so look for the lowest orange operator in a branch: misestimates propagate upwards.
Warnings
The warning badge counts what SQL Server writes into the <Warnings> element of an operator's <RelOp>: spills (SpillToTempDb, SortSpillDetails, HashSpillDetails), NoJoinPredicate, ColumnsWithNoStatistics and the other operator-level entries. Statement-level items that SQL Server writes under <QueryPlan> instead — PlanAffectingConvert for implicit conversions, MemoryGrantWarning and MissingIndexes suggestions — are not shown; they stay in the XML. PostgreSQL and MySQL plans have no warnings element: read spills and filters from the properties instead, for example PostgreSQL's Sort Method: external merge with Sort Space Type: Disk, or Rows Removed by Filter.
Example: a stale estimate, a Key Lookup and a sort spill
The SQL Server sample (Load sample) is the actual plan of this query on a 120,000-row dbo.Orders table whose Status statistics are out of date:
SELECT o.OrderId, o.OrderDate, o.TotalDue, c.Email
FROM dbo.Orders AS o
JOIN dbo.Customers AS c ON c.CustomerId = o.CustomerId
WHERE o.Status = N'Open'
ORDER BY o.TotalDue DESC;IX_Orders_Status) estimated 120 rows and returned 4,812 — 40× the estimate — and every operator above it inherits the gap. The Key Lookup (PK_Orders) and the Clustered Index Seek (PK_Customers) return one row per execution, as estimated, but ran 4,812 times instead of 120. The Sort was granted memory for 120 rows, so it spilled: its two warnings are Spill To Temp Db (SpillLevel=1) and Sort Spill Details (WritesToTempDb=232). Object names are invented.Load sample comparison puts that plan in A and the plan after CREATE INDEX IX_Orders_Status_Covering ON dbo.Orders (Status) INCLUDE (CustomerId, OrderDate, TotalDue) and a statistics update in B. The comparison reads:
- 4 operators matched by NodeId; 2 only in plan A, 0 only in plan B.
- 2 matched operators have a different operator type in plan B.
- 4 matched operators have a different subtree cost.
- 1 matched operator has a different row count.
- Statement cost (root subtree cost): 0.757143 → 0.187955 (−75%).
- Rows returned by the root operator: 4,812 → 4,812 (unchanged).
and the operator counts that differ: Key Lookup, Clustered Index Seek and two Nested Loops are gone; a Hash Match and a Clustered Index Scan of dbo.Customers are new. With a correct estimate of 4,800 rows the optimizer reads the covering index once and hash-joins it to the 3,000 customers, and the Sort no longer spills. SQL Server renumbers NodeIds when the plan shape changes, which is why NodeId matching reports changed operator types at NodeIds 1 and 2; the operator counts show the same change in plain terms.
Supported formats and limits
- SQL Server: showplan XML (
.sqlplan, SSMS XML,SET STATISTICS XML,SET SHOWPLAN_XML, plan cache and Query Store XML), estimated or actual, one<ShowPlanXML>document with one or many statements per paste or file. Text around the document, such as the Completion time line SSMS prints under results, is ignored. Actual row counts are summed from each operator'sRunTimeCountersPerThreadelements. - PostgreSQL:
EXPLAIN (FORMAT JSON)with or withoutANALYZE, including sub-plans and init-plans, andauto_explainJSON. - MySQL:
EXPLAIN FORMAT=JSON(both layouts),EXPLAIN ANALYZEandEXPLAIN FORMAT=TREE. - Detection: the format is detected from the content; the Format list overrides it. PostgreSQL text EXPLAIN,
SHOWPLAN_TEXTand tabular MySQL EXPLAIN get a message naming the command whose output to paste instead; OracleDBMS_XPLANoutput gets a message pointing to the desktop app, and deadlock XML one pointing to the deadlock viewer. - Size: 10 MiB per plan, checked before a file is read. Files may be UTF-8 or UTF-16 (with or without a byte-order mark). The graph is drawn for statements with up to 1,500 operators; larger ones are listed in the Tree view.
- XML safety: documents with a
<!DOCTYPE>or<!ENTITY>declaration are rejected; showplan XML never contains one.
Everything stays in your browser
The tool is one script, /js/tools/execution-plan-viewer.js, with the plan parsers, graph layout and plan comparison from the Jam SQL Studio desktop app. Your file or paste is parsed in memory and rendered as text and SVG. It is not sent to any server, not placed in the URL and not written to localStorage or sessionStorage. Clear drops both plans from memory.
Like every page on this site, the page also loads the Umami analytics script, the Google Ads tag and the site's link-click script (/js/jam-events.js). They record the page view and the events below; the plan is never part of a URL or an event, so it is not in what they send. These are all the events this page sends:
tool-execution-plan-viewer-parse-okwhen plan A is pasted, picked from a file or re-read after a Format change:format(mssql,pgsql,mysql-jsonormysql-tree),mode(estimatedoractual) andoperators(1-10,11-50,51-200or201+).tool-execution-plan-viewer-parse-errorfor the same actions when the input is refused:reason(an error code such aspg-text,invalidortoo-large) andslot(aorb).tool-execution-plan-viewer-comparewhen plan B is pasted or picked while plan A is loaded:format(the format code, ormismatchwhen the two formats differ).tool-execution-plan-viewer-load-samplefor the sample buttons:sample(mssql,pgsql,mysql-json,mysql-treeorcompare). Loading a sample sends no parse or compare event.tool-execution-plan-viewer-download-ctafor the Download Free button at the bottom, with no properties.download_page_clickfor the Download link in the site header and the Get Jam SQL Studio links (page,placement, andtarget_os, which is alwaysunknownhere), andtool_product_clickfor the Execution plans in the desktop app links (page,placement,toolanddestination_kind).
Limitations
- The viewer shows what the plan records. It does not suggest indexes or rewrites, and it does not show SQL Server's statement-level warnings or missing-index suggestions.
- MySQL tree output: text in parentheses is dropped from the operator name, so a
Filter: (o.status = 'open')line is shown asFilter:. MySQL JSON in thequery_blocklayout shows a Plan Node step above each table of a nested loop, and itscost_infoobjects are not listed, so those plans show no cost. - Comparison is structural: operators are matched by SQL Server NodeId or by position in the tree, and the result is a set of counts, not an operator-by-operator explanation. MySQL plans carry none of the cost and row fields the comparison reads, so for them only operators are compared.
- PostgreSQL text-format EXPLAIN and Oracle plans are not drawn.
Frequently asked questions
How do I open a .sqlplan file without SSMS?
Pick the .sqlplan file with the Plan file button or paste its XML into the box. A .sqlplan file is showplan XML, so this page reads it in any current browser on macOS, Windows or Linux and draws the operator graph with costs, row counts and warnings. SSMS is not needed and nothing is installed.
Does this upload my execution plan?
No. The plan is parsed by JavaScript running in this page and kept in memory only; it is not sent to a server, put in the URL or written to local or session storage. The page also loads the site's Umami and Google Ads tags, which record the page view, and the tool's own analytics events carry only fixed values such as the plan format, estimated or actual, an operator-count range or an error code, never the plan text, SQL, object names or file names.
What does the percentage on each operator mean?
It is the operator's estimated subtree cost (the operator plus everything below it) as a share of the statement's root operator. The root is therefore always 100%, and an operator's own share is roughly its percentage minus its children's. It is the optimizer's estimate in its own units, not measured time, and it is shown for SQL Server and PostgreSQL plans.
Why are estimated and actual rows so different?
The optimizer estimates rows from statistics; the actual count is what the operator really produced. Large gaps usually come from stale or missing statistics, correlated predicates, or a parameter value that differs from the one the plan was compiled for. The viewer marks an operator in orange when its actual rows are at least 10 times the estimate and at least 10 rows, compared over all executions for SQL Server and per loop for PostgreSQL and MySQL.
Can I view PostgreSQL EXPLAIN ANALYZE output here?
Yes, in JSON form. Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) followed by your query and paste the result; psql's default table framing and a result grid exported as JSON are unwrapped automatically. The default text output of EXPLAIN is recognised but not drawn, because the parser reads the JSON format. EXPLAIN ANALYZE runs the statement, so wrap data changes in a transaction you roll back.
Can it compare two execution plans?
Yes. Put the first plan in the main box and the second one, in the same format, in the Compare box below it; the list shows how many operators matched, how many are only in one plan or changed type, and the operator counts that differ. Cost and row changes are compared for SQL Server and PostgreSQL plans, on the estimates when one plan is actual and the other estimated. SQL Server operators are matched by NodeId, the others by their position in the tree.
Does it read Oracle execution plans?
No. DBMS_XPLAN text is recognised and explained, but not drawn. The Jam SQL Studio desktop app captures estimated and actual Oracle plans from an Oracle connection and shows them in the same graph as the other engines.
Tuning queries against a live database?
Jam SQL Studio is a free desktop SQL client for SQL Server, PostgreSQL, MySQL, Oracle and SQLite. On SQL Server, PostgreSQL, MySQL and Oracle connections it captures estimated and actual plans from the query editor; it also opens and exports plan files and compares two plans.
Free for personal use • No account required • Mac, Windows, Linux
More free SQL tools
SQL Server Deadlock XML Viewer
Open an .xdl or Extended Events deadlock report and see the victim, processes, locks and owner/waiter graph.
SQL Formatter
Format T-SQL, PostgreSQL, MySQL, PL/SQL, and SQLite queries.
SQLite Playground
Run SQL against a sample SQLite database in your browser, with no signup or install.
Jam SQL Studio