Oracle EXPLAIN PLAN: SQL Performance Analysis Guide
Oracle EXPLAIN PLAN: SQL Performance Analysis Guide
Purpose
A full table scan in an execution plan is not always a fault. The optimizer can choose it on purpose. If its statistics show that most rows match, reading the whole table can be the cheapest path. The plan is where you see this choice. EXPLAIN PLAN is the statement that shows it to you.
EXPLAIN PLAN lets you examine the execution plan that the optimizer chose for a SQL statement. The output shows how the database would have run the statement at the time it was explained. It writes one row for each step of the plan into a plan table. You then read that table to see access methods, join methods, cost and cardinality.
A DBA runs this check before a new query goes to production. A DBA also runs it when a query is slow, after an index change, and when two versions of a query need a side-by-side comparison. The steps below show how to produce a plan, how to display it, and how to read what you see.
Code
1-- 1. Explain a statement (rows go to PLAN_TABLE in your schema)
2EXPLAIN PLAN FOR
3SELECT column1, column2
4FROM table1
5WHERE condition = 'value';
6
7-- 2. Tag a plan with a statement ID so you can find it later
8EXPLAIN PLAN SET STATEMENT_ID = 'bad1' FOR
9SELECT column1, column2
10FROM table1
11WHERE condition = 'value';
12
13-- 3. Display the plan with the formatted script
14SET LINES 100 PAGES 999
15@?/rdbms/admin/utlxpls
16
17-- 4. Or display it with DBMS_XPLAN
18SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());
19
20-- 5. Compare two versions of one query with two statement IDs
21EXPLAIN PLAN SET STATEMENT_ID = 'optimized_query' FOR
22SELECT /*+ INDEX(emp emp_idx) */ emp_name, salary
23FROM employees emp
24WHERE department_id = 10;
25
26EXPLAIN PLAN SET STATEMENT_ID = 'unoptimized_query' FOR
27SELECT emp_name, salary
28FROM employees
29WHERE department_id = 10;
30
31-- 6. A larger statement to analyze
32EXPLAIN PLAN SET STATEMENT_ID = 'slow_query_analysis' FOR
33SELECT o.order_id, c.customer_name, SUM(oi.quantity * oi.price)
34FROM orders o
35JOIN customers c ON o.customer_id = c.customer_id
36JOIN order_items oi ON o.order_id = oi.order_id
37WHERE o.order_date >= DATE '2025-01-01'
38GROUP BY o.order_id, c.customer_name;
Code Breakdown
Generating the plan
EXPLAIN PLAN FOR <statement>asks the optimizer for its plan. If you leave out theINTOclause, the database uses a table namedPLAN_TABLEin your own schema. The statement you explain can be aSELECT,INSERT,UPDATE,DELETEorMERGE. It can also be aCREATE TABLE,CREATE INDEXorALTER INDEX ... REBUILDstatement.SET STATEMENT_ID = 'name'puts your own label on the rows of the plan. If you omit it, the value is null. Use a label whenever the plan table holds rows from many plans. Remove old rows with the same label first, or the new plan will mix with them.
Displaying the plan
@?/rdbms/admin/utlxplsruns the Oracle script that displays the plan table output for serial processing. A second script,utlxplp.sql, shows the same output with the parallel execution columns.SET LINES 100 PAGES 999sets a 100-character line and a 999-line page, so the plan prints with few repeated headings.DBMS_XPLAN.DISPLAYis a table function that displays the contents of the plan table. You can pass a plan table name, a statement ID and a format such asTYPICAL.
Comparing two plans
Blocks 5 and 6 show the main reason for statement IDs. You explain two versions of one query under two labels. Then you display each one and compare the steps and the cost. The hint in block 5 only works if an index named emp_idx exists on the table. Replace the names with objects from your own schema.
Key Points
- The plan is a prediction. The explained plan can differ from the plan used at run time. The cause can be a difference between the explain environment and the execution environment.
- Bind variables are a special case. With bind variables in general, the
EXPLAIN PLANoutput might not represent the real execution plan. - You need privileges on both sides. You must be able to insert rows into the plan table. You must also have the privileges needed to run the statement you explain. To read the plan, you must be able to query the plan table.
- Cost is a comparison unit. The cost is an internal unit that the plan displays only so you can compare plans. You cannot tune the cost value itself. Cost does not equal time, and you cannot compare the costs of different queries with each other.
- The plan table holds more than steps. It also holds optimization data, such as the cost and cardinality of each operation.
Insights and Best Practices
Read the plan for the choices the optimizer made
The optimizer looks at many access methods, such as a full table scan or an index scan. It looks at join methods such as nested loops and hash joins. It looks at different join orders and at possible query transformations. It picks the plan with the lowest cost. When you read a plan, ask which of these choices appear and whether the statistics support them.
A full table scan is a good example. If statistics show that 80 percent of employees are managers, a full table scan can be the most efficient choice. If statistics show that very few employees are managers, reading an index and then reading the table rows by rowid can be cheaper. A scan that looks wrong may be right, and a scan that looks fine may rest on old statistics. Check the statistics before you change the query.
Look at join methods and join order
Nested loops and hash joins suit different row counts. In a nested loops join, the number of rows in the outer table sets how often the database must probe the inner table. A nested loops join is a good choice when the database can avoid scanning a large part of the inner table, because the join condition filters its rows. If few rows are filtered, scanning the right table in a hash join is the better choice. The optimizer also tries more than one join order and abandons the ones that cost more.
When you review a plan with three tables, as in block 6, check which table drives the join and which join method each step uses. Then ask if the order matches what you know about the data.
Use statement IDs as a lab notebook
Give every test its own label, such as before_index and after_index. Display both plans. Record the cost and the steps. Do this before and after each change, and you keep a written history of what each change did. Cost values are only useful inside one query, so compare the same query across versions and not one query against another.
Pick the display tool that fits
Use utlxpls for a quick view of a serial plan. Use utlxplp when the statement runs in parallel. Use DBMS_XPLAN.DISPLAY when you need to pick the plan table, the statement ID or the format. To see the plan of a statement that is already in the cursor cache, use DBMS_XPLAN.DISPLAY_CURSOR. This function displays the explain plan of any cursor loaded in the cursor cache, and it can also report plan statistics such as I/O, memory and timing.
When to Use This Check
- Before you release a new query to production.
- When a query is slow and you need to see how the optimizer runs it.
- After you add or drop an index and want to confirm the plan changed.
- When you test two versions of one query and want to compare them.
- After a statistics refresh, if a query changes speed.
- When you cannot run a heavy statement but need to know its likely cost.
Troubleshooting Common Issues
The plan table does not exist. If you omit INTO, the database expects PLAN_TABLE in your schema. The sample table is defined in the script utlxplan.sql on the distribution media. Its exact name and location depend on your operating system. Your own output table must have the same column names and data types as the sample.
You get a privilege error. Check the three rights listed in Key Points: insert on the plan table, the right to run the explained statement, and query on the plan table.
The plan looks wrong for a statement with bind variables. The explained plan may not match the real one. Compare it with the plan of the cursor that actually ran, by using DBMS_XPLAN.DISPLAY_CURSOR.
Old rows confuse the output. If a label is reused, the plan table can hold rows for the same ID from earlier runs. Remove the old rows before you explain again with that label.
Two plans show very different cost, but run time is the same. Cost does not equate directly to time. Measure the real run time before you act on a cost difference.
References
- Oracle Database SQL Language Reference 19c - EXPLAIN PLAN - The statement syntax, the INTO and STATEMENT_ID clauses, the privileges needed and the statements you can explain
- Oracle Database SQL Tuning Guide 19c - Generating and Displaying Execution Plans - Covers PLAN_TABLE, the utlxpls and utlxplp scripts, DBMS_XPLAN display functions and the bind variable caveat
- Oracle Database SQL Tuning Guide 19c - Query Optimizer Concepts - Explains cost, access methods, join methods and join order as the optimizer weighs them
- ORACLE-BASE - Explain Plan Usage - A community walkthrough of explaining a query, tagging plans with STATEMENT_ID and displaying them with DBMS_XPLAN
Posts in this series
- Oracle PLAN_TABLE Setup and EXPLAIN PLAN Usage Guide
- Oracle Autotrace Commands: SQL Execution Plan Analysis
- Oracle EXPLAIN PLAN: SQL Performance Analysis Guide
- Oracle Database: Find Query Hash Value Using V$SQLAREA
- Oracle SQL Hash Lookup: Retrieve SQL Text from V$SQLAREA
- Oracle SQL Execution Statistics Query with v$sqlarea
- Get the Actual Execution Plan with DBMS_XPLAN.DISPLAY_CURSOR
- Watch Long-Running SQL in Real Time with V$SQL_MONITOR