
Abstract: Execution plans are indispensable tools for diagnosing and improving SQL query performance in Oracle databases. This guide provides a comprehensive workflow to generate, interpret, and optimize execution plans, with practical examples, actionable insights, and advanced tuning strategies for large-scale datasets.
Execution plans in Oracle databases reveal how the SQL optimizer processes queries, exposing inefficiencies like full table scans, expensive joins, or sorting operations. With the rise of big data, optimizing these plans is critical for reducing latency, improving scalability, and minimizing resource consumption. This guide bridges theory and practice, offering a systematic approach to tuning queries using execution plans.
EXPLAIN PLAN
Generate an estimated plan without executing the query:
EXPLAIN PLAN FOR
SELECT c.customer_id, c.city, SUM(o.amount)
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'New York'
GROUP BY c.customer_id, c.city
ORDER BY SUM(o.amount) DESC;
-- Retrieve the plan
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
F10
).Include runtime metrics using GATHER_PLAN_STATISTICS
:
SELECT /*+ GATHER_PLAN_STATISTICS */
c.customer_id, c.city, SUM(o.amount) AS total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'New York'
GROUP BY c.customer_id, c.city
ORDER BY total_amount DESC;
-- Display actual execution data
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(format => 'ALLSTATS LAST'));
Execution plans are tree structures where child operations feed data to parent operations.
SELECT STATEMENT
).For the query:
SELECT c.customer_id, c.city, SUM(o.amount)
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'New York'
GROUP BY c.customer_id, c.city
ORDER BY SUM(o.amount) DESC;
Resulting Plan:
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 450K | 25468 (2)| 00:00:01 |
| 1 | SORT ORDER BY | | 10000 | 450K | 25468 (2)| 00:00:01 |
| 2 | HASH GROUP BY | | 10000 | 450K | 25467 (2)| 00:00:01 |
|* 3 | HASH JOIN | | 5000K | 214M | 24472 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| CUSTOMERS | 10000 | 300K | 423 (1)| 00:00:01 |
| 5 | TABLE ACCESS FULL| ORDERS | 5000K | 71M | 19742 (1)| 00:00:01 |
-------------------------------------------------------------------------------
SELECT STATEMENT (Id 0)
└── SORT ORDER BY (Id 1)
└── HASH GROUP BY (Id 2)
└── HASH JOIN (Id 3)
├── TABLE ACCESS FULL CUSTOMERS (Id 4)
└── TABLE ACCESS FULL ORDERS (Id 5)
CUSTOMERS
(filtered by city = 'New York'
).ORDERS
.HASH JOIN
.customer_id
and city
.total_amount DESC
.
Column |
Description |
---|---|
Id |
Step identifier. Child operations execute before parents (e.g., Id 4 → Id 3). |
Operation |
Action performed (e.g., |
Name |
Object involved (table/index name). |
Rows |
Estimated rows processed at this step (cardinality). |
Bytes |
Estimated data size processed. |
Cost (%CPU) |
Optimizer’s cost estimate (lower = better). Includes CPU and I/O overhead. |
Time |
Estimated runtime for the operation. |
The optimizer estimates cost based on:
TABLE ACCESS FULL
):
city
) or joined (customer_id
) columns.HASH JOIN
(Id 3) contributes 24,472 to the total cost (96% of total).ORDERS
with 10,000 rows from CUSTOMERS
.SORT ORDER BY
(Id 1) adds overhead.SUM(o.amount)
or materialized views.HASH JOIN
on tables with 10M+ rows.NESTED LOOPS
for indexed small tables.SORT GROUP BY
, SORT ORDER BY
.GROUP BY
/ORDER BY
columns.DBMS_STATS.GATHER_TABLE_STATS
.Covering Indexes: Include frequently accessed columns.
CREATE INDEX idx_customers_city ON customers(city) INCLUDE (customer_id, name);
Partitioning: Split tables by range, hash, or list (e.g., by order_date
).
SELECT *
with explicit columns.EXISTS
instead of IN
for subqueries.WHERE UPPER(name) = 'JOHN'
).Force specific behaviors (use sparingly):
SELECT /*+ INDEX(orders idx_orders_customer_id) */
customer_id, SUM(amount)
FROM orders
GROUP BY customer_id;
-- Customers table with 1M rows
CREATE TABLE customers (
customer_id NUMBER PRIMARY KEY,
name VARCHAR2(100),
city VARCHAR2(50)
);
-- Orders table with 5M rows
CREATE TABLE orders (
order_id NUMBER PRIMARY KEY,
customer_id NUMBER,
order_date DATE,
amount NUMBER
);
SELECT /*+ GATHER_PLAN_STATISTICS */
c.customer_id, c.city, SUM(o.amount) AS total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'New York'
GROUP BY c.customer_id, c.city
ORDER BY total_amount DESC;
Initial Execution Plan
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 450K | 25468 (2)| 00:00:01 |
| 1 | SORT ORDER BY | | 10000 | 450K | 25468 (2)| 00:00:01 |
| 2 | HASH GROUP BY | | 10000 | 450K | 25467 (2)| 00:00:01 |
|* 3 | HASH JOIN | | 5000K | 214M | 24472 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| CUSTOMERS | 10000 | 300K | 423 (1)| 00:00:01 |
| 5 | TABLE ACCESS FULL| ORDERS | 5000K | 71M | 19742 (1)| 00:00:01 |
-------------------------------------------------------------------------------
city
or customer_id
.Add Indexes:
CREATE INDEX idx_customers_city ON customers(city);
CREATE INDEX idx_orders_customer_id ON orders(customer_id, amount);
Refresh Statistics:
-- Update statistics for CUSTOMERS
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'CUSTOMERS');
-- Update statistics for ORDERS
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS');
Re-run the Query:
SELECT /*+ GATHER_PLAN_STATISTICS */
c.customer_id, c.city, SUM(o.amount) AS total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'New York'
GROUP BY c.customer_id, c.city
ORDER BY total_amount DESC;
-------------------------------------------------------------------------------
| Id | Operation | Name | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1502 (100)| 00:00:01 |
| 1 | SORT ORDER BY | | 1502 (3)| 00:00:01 |
| 2 | HASH GROUP BY | | 1498 (3)| 00:00:01 |
|* 3 | HASH JOIN | | 1412 (1)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IDX_CUSTOMERS_CITY | 40 (0)| 00:00:01 |
| 5 | INDEX FAST FULL SCAN| IDX_ORDERS_CUSTOMER_ID | 1363 (1)| 00:00:01 |
-------------------------------------------------------------------------------
Uses idx_customers_city to filter city = 'New York' efficiently.
INDEX FAST FULL SCAN on ORDERS
:
idx_orders_customer_id
to retrieve customer_id
and amount
without scanning the entire table.Cost Reduction:
DBA_HIST_SQL_PLAN
for plan regressions.Execution plans are the cornerstone of Oracle query optimization. By systematically analyzing operations like full scans, inefficient joins, and sorting steps, developers can achieve dramatic performance improvements. Key takeaways:
A/B
plan comparisons.
Further Reading