SQL Performance Tuning: Mastering The Oracle CBO
Performance is a feature. In this comprehensive guide, we tear down the Oracle Cost-Based Optimizer, dissect execution plans, and explore the advanced techniques required to make your queries run blazingly fast.
The Anatomy of a Slow Query
When a query takes 10 minutes to run in Oracle RMS (or any large Oracle database), the problem rarely lies in the hardware. It almost always stems from the database taking a highly inefficient path to retrieve the data.
A query can be slow for many reasons:
- Missing or Stale Statistics: The database thinks a table has 10 rows when it actually has 10 million.
- Missing Indexes: The database is forced to read every single row on the disk (Full Table Scan) to find one specific record.
- CPU-Intensive Joins: The database is joining two massive tables using an inefficient loop instead of a hash.
- Hard Parsing: The application is not using bind variables, forcing the database to recompile the query on every execution.
To fix a slow query, you must stop guessing and start measuring. You must understand how the database intends to execute your code.
The Cost-Based Optimizer (CBO)
When you submit a SQL statement to Oracle, the database doesn't just execute it blindly. It passes it to a highly sophisticated piece of software called the Cost-Based Optimizer (CBO).
The CBO's job is to evaluate hundreds or thousands of potential execution plans for your query and choose the one with the lowest "Cost."
Cost is an abstract unit of work, roughly representing the estimated time and resources (I/O, CPU, Memory) required to fetch the data.
For example, if you want to find ITEM = 12345 in the ITEM_MASTER table, the CBO might consider:
- Path A: Read the entire table from top to bottom (Cost: 5,000).
- Path B: Look up
12345in an index, then fetch that one specific row (Cost: 3).
Naturally, the CBO chooses Path B. But how does the CBO know that Path B is cheaper?
The Importance of DBMS_STATS
The CBO makes its decisions based on Optimizer Statistics. These statistics are metadata about your tables and indexes, stored in the Data Dictionary.
Statistics include:
- How many rows are in the table?
- How many distinct values are in the
DEPTcolumn? - What are the minimum and maximum values of the
CREATE_DATEcolumn? - How deep is the index B-Tree?
If your statistics are stale, the CBO makes terrible decisions.
The Stale Stats Trap
Imagine you create a temporary table, insert 5 million rows, and immediately run a complex join against it. Because the table is brand new, its statistics say it has 0 rows. The CBO will optimize the query assuming the table is empty, likely choosing an awful Nested Loops join. Your query will run for hours.
To fix this, you manually gather statistics using the DBMS_STATS package:
Hard Parsing vs. Soft Parsing (Bind Variables)
Before a query can be executed, it must be parsed.
- Hard Parse: The database has never seen this exact query string before. It must validate the syntax, check privileges, generate thousands of execution plans, and pick the best one. This consumes a massive amount of CPU and locks memory structures.
- Soft Parse: The database recognizes the query string from a previous execution. It retrieves the saved execution plan from memory (the Shared Pool) and skips the hard work.
The Bind Variable Rule
If you write dynamic SQL or application code that concatenates values into the string, you force a Hard Parse every single time.
In Oracle, always use bind variables (:id, ?) when a value changes between executions.
Reading Execution Plans
The most important tool in a performance tuner's arsenal is the EXPLAIN PLAN. It shows you exactly what the CBO intends to do.
You generate a plan using the EXPLAIN PLAN FOR command, followed by DBMS_XPLAN.DISPLAY.
The output looks like a tree structure.
How to read the tree
- Read the tree inside-out and top-to-bottom. The most indented operation happens first.
- If two operations are at the same indentation level, the top one happens first.
In the example above:
- Oracle does an
INDEX RANGE SCANonITEM_MASTER_I1to find thedept = 145. - It uses the
ROWIDfrom the index to fetch the actual rows viaTABLE ACCESS BY INDEX ROWID. - Simultaneously, it performs a
TABLE ACCESS FULLonITEM_SUPPLIER. - It joins the two result sets together using a
HASH JOIN.
Understanding Access Paths (Indexes vs. FTS)
An Access Path is how Oracle retrieves data from a table.
| Operation | Description |
|---|---|
TABLE ACCESS FULL (FTS) | Reads every single block of the table below the High Water Mark. Good if you are retrieving a large percentage of the table (>10%). Bad if you are retrieving only a few rows. |
INDEX UNIQUE SCAN | Navigates a B-Tree index to find exactly ONE row (usually via a Primary Key or Unique Constraint). The fastest possible access path. |
INDEX RANGE SCAN | Navigates a B-Tree index to find multiple rows (e.g., WHERE dept = 145). |
INDEX FAST FULL SCAN | Reads every block of the index, but doesn't bother sorting them. It treats the index like a skinny table. Used when the query only selects columns that exist in the index. |
TABLE ACCESS BY INDEX ROWID | Once an index scan finds a match, it yields a ROWID (a physical disk address). Oracle uses this address to jump directly to the table and fetch the rest of the columns. |
Understanding Join Methods
When joining two tables, Oracle chooses one of three methods:
-
Nested Loops Join:
- How it works: For every row in Table A (the driving table), it looks up the matching row in Table B.
- When to use: Excellent for joining small datasets (e.g., retrieving a single item and joining to its descriptions).
- Warning: Terrible for large datasets. Looping 1 million times takes forever.
-
Hash Join:
- How it works: Reads the smaller table into memory and builds a Hash Table. Then it scans the larger table and probes the Hash Table for matches.
- When to use: The gold standard for joining large datasets or data warehousing queries.
-
Sort Merge Join:
- How it works: Sorts Table A, sorts Table B, and then merges them together.
- When to use: Used when joining with inequalities (
<,>) or when the data is already pre-sorted via an index.
Rewriting Queries for Performance
Sometimes the CBO needs help. You can rewrite queries to give the optimizer better options.
1. Avoid Functions on Indexed Columns
If you have an index on create_date, this query will not use the index:
Rewrite it to evaluate the literal, not the column:
2. Replace OR with UNION ALL
The OR operator is notoriously difficult to optimize.
Using Optimizer Hints
If the CBO refuses to pick the right plan (perhaps because statistics are skewed), you can force it using Hints. Hints are special comments placed immediately after the SELECT, INSERT, UPDATE, or DELETE keyword.
Hints are a Last Resort
Hardcoding hints locks in an execution plan forever. As your data grows, the hinted plan might become terrible, but the CBO is forbidden from changing it. Only use hints when absolutely necessary.
Common Gotchas for Developers
Important Gotchas
- !
If
item_idis aVARCHAR2, and you queryWHERE item_id = 12345(a number), Oracle implicitly converts the column:WHERE TO_NUMBER(item_id) = 12345. This completely disables any index onitem_id. Always match data types! - !
SELECT *retrieves all columns. This prevents Oracle from doing anINDEX FAST FULL SCAN(which only works if all requested columns are inside the index). Only select the columns you actually need.
Key Takeaways
Key Takeaways
- Always gather statistics (
DBMS_STATS) after massive data loads so the Cost-Based Optimizer isn't flying blind. - Always use bind variables to prevent Hard Parsing.
- Read Execution Plans inside-out, top-to-bottom. Look for expensive
TABLE ACCESS FULLoperations on large tables. - Avoid wrapping indexed columns in functions (like
TRUNCorUPPER) unless you have specifically created a Function-Based Index.


