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.

.sqlplan, .xml, .json or .txt, up to 10 MiB. UTF-8 or UTF-16.

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's Node Type, MySQL's access type or iterator), the table it reads (SQL Server's Object, PostgreSQL's Relation Name; MySQL operator names already name the table), and a rows line such as act 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, PostgreSQL Total 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, TOP or EXISTS an 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 .sqlplan file, or Show Execution Plan XML… to copy the XML.
  • T-SQL, actual plan: run the query between SET STATISTICS XML ON; and SET 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_plan and 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_stats returns the last actual plan of a cached statement when the LAST_QUERY_PLAN_STATS database 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: the query_block layout and the iterator tree that MySQL 8.3 added as explain_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 mysql client, the table borders, the \G row header, the EXPLAIN: 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

EngineEstimateActualCompared as
SQL ServerEstimateRows per execution × (1 + EstimateRebinds + EstimateRewinds) executionsActualRows summed over all threads and executionsall executions, like SSMS's Estimated Number of Rows for All Executions
PostgreSQLPlan Rows, per loopActual Rows, average per loopper loop; Actual Loops shown
MySQLrows= in (cost=…), estimated_rowsrows= in (actual time=…), per loopper 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;
Graph of the sample SQL Server plan: Sort over two Nested Loops joins, an Index Seek and a Key Lookup on dbo.Orders and a Clustered Index Seek on dbo.CustomersSortSortact 4,812 · est 120100%!2Nested LoopsNested Loopsact 4,812 · est 12098%Nested LoopsNested Loopsact 4,812 · est 12049%Index SeekIndex Seekdbo.Ordersact 4,812 · est 120<1%Key LookupKey Lookupdbo.Ordersact 4,812 · est 120 · 4,812 exec49%Clustered Index SeekClustered Index Seekdbo.Customersact 4,812 · est 120 · 4,812 exec49%
The graph the viewer draws for the sample (rendered by the same code as the tool above). The boxes name the table; the details pane names the index. The Index Seek (index 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's RunTimeCountersPerThread elements.
  • PostgreSQL: EXPLAIN (FORMAT JSON) with or without ANALYZE, including sub-plans and init-plans, and auto_explain JSON.
  • MySQL: EXPLAIN FORMAT=JSON (both layouts), EXPLAIN ANALYZE and EXPLAIN FORMAT=TREE.
  • Detection: the format is detected from the content; the Format list overrides it. PostgreSQL text EXPLAIN, SHOWPLAN_TEXT and tabular MySQL EXPLAIN get a message naming the command whose output to paste instead; Oracle DBMS_XPLAN output 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-ok when plan A is pasted, picked from a file or re-read after a Format change: format (mssql, pgsql, mysql-json or mysql-tree), mode (estimated or actual) and operators (1-10, 11-50, 51-200 or 201+).
  • tool-execution-plan-viewer-parse-error for the same actions when the input is refused: reason (an error code such as pg-text, invalid or too-large) and slot (a or b).
  • tool-execution-plan-viewer-compare when plan B is pasted or picked while plan A is loaded: format (the format code, or mismatch when the two formats differ).
  • tool-execution-plan-viewer-load-sample for the sample buttons: sample (mssql, pgsql, mysql-json, mysql-tree or compare). Loading a sample sends no parse or compare event.
  • tool-execution-plan-viewer-download-cta for the Download Free button at the bottom, with no properties.
  • download_page_click for the Download link in the site header and the Get Jam SQL Studio links (page, placement, and target_os, which is always unknown here), and tool_product_click for the Execution plans in the desktop app links (page, placement, tool and destination_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 as Filter:. MySQL JSON in the query_block layout shows a Plan Node step above each table of a nested loop, and its cost_info objects 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