Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

PL/SQL & T-SQL (Oracle and SQL Server)  Module 14 – Performance: plans, PL/SQL called from SQL and table functions

Oracle execution plans with DBMS_XPLAN

Read estimated plans with EXPLAIN PLAN and actual plans with DISPLAY_CURSOR, compare E-Rows with A-Rows, and see how histograms and bind peeking change both.

  • Intermediate
  • 20 minutes
  • Examples run with oracle-free 23.26.3.0.0, mssql 17.0.5005.3, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: EXPLAIN PLAN shows the optimizer's plan before anything runs · Actual plans put measured rows beside the estimates · Which plan tools every edition has, and which need a pack. With a pass: 2 in-depth parts and 2 interview answers, about 13 minutes.

What you will learn

  • Show an estimated plan with EXPLAIN PLAN and an actual plan with DISPLAY_CURSOR
  • Compare E-Rows with A-Rows using gather_plan_statistics and ALLSTATS LAST
  • Explain how a frequency histogram changes the optimizer's row estimate
  • Predict when bind peeking reuses a plan and when adaptive cursor sharing adds a child cursor
  • Distinguish the plan tools every edition includes from those that need a management pack

Before you start

What you’ll learn · Before you start

What you will learn

  • Show an estimated plan with EXPLAIN PLAN and an actual plan with DISPLAY_CURSOR
  • Compare E-Rows with A-Rows using gather_plan_statistics and ALLSTATS LAST
  • Explain how a frequency histogram changes the optimizer's row estimate
  • Predict when bind peeking reuses a plan and when adaptive cursor sharing adds a child cursor
  • Distinguish the plan tools every edition includes from those that need a management pack

Before you start

On this page

A query for the 10 ‘new’ orders among 100,000 can be planned for 33,333 rows, because the optimizer assumes the rows are spread evenly over the three status values. An The steps Oracle's optimizer chooses to run a SQL statement: which tables and indexes to read, in what order, and how to join them, with an estimate of the rows each step returns. shows that choice, and DBMS_XPLAN prints it, first as an estimate and then with the rows the query really read.

You will read both kinds of plan, measure the gap between them, fix an estimate with a histogram, and watch a bind value decide which plan a statement keeps.

EXPLAIN PLAN shows the optimizer’s plan before anything runs

Every script in this lesson builds its own orders_skew table: 100,000 orders, of which 98,990 are ‘shipped’, 1,000 ‘paid’ and 10 ‘new’, with an index on status. EXPLAIN PLAN: An Oracle SQL statement that asks the optimizer for a statement's plan and writes its steps to PLAN_TABLE without running the statement, so every number in the result is an estimate. asks for the plan of a query without running it:

Interactive · stage by stage What EXPLAIN PLAN does, and what it skips Step through EXPLAIN PLAN for the 10 'new' orders, then DISPLAY reading the plan back.
What EXPLAIN PLAN does, and what it skips, step 5 of 5. Showing EXPLAIN PLAN FOR, Optimizer picks a plan, Rows go to PLAN_TABLE, DBMS_XPLAN.DISPLAY. In focus: skip.not runEXPLAIN PLAN FORstatus = 'new'Optimizer picks a planfrom the statisticsRows go to PLAN_TABLEone row per stepDBMS_XPLAN.DISPLAYformats those rows

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The statement is explained. EXPLAIN PLAN FOR SELECT ... FROM orders_skew WHERE status = 'new' asks for the plan of a query that would return 10 of the 100,000 rows.
  2. The optimizer estimates. With statistics but no histogram, the optimizer expects 100,000 / 3 = 33,333 rows for any status and costs the access paths for that number.
  3. Steps are written down. Each step of the chosen plan becomes a row of PLAN_TABLE: the operation, the object, the estimated rows and cost, and the predicate it applies.
  4. DISPLAY formats the rows. DBMS_XPLAN.DISPLAY turns those rows into the familiar table, with a Predicate Information section under it. TYPICAL is its default format.
  5. Nothing was measured. The query never ran, so every number is an estimate. The plan the query uses when it runs can differ, for example when a bind value or the statistics differ.

The optimizer writes one row per step into PLAN_TABLE, and DBMS_XPLAN.DISPLAY formats those rows. TYPICAL, the default format, shows the operations, the estimated rows, bytes and cost, and the predicates; BASIC shows only the operations; ALL adds the projection and alias sections:

explain_basic.sql: the estimated plan for the 10 'new' orders SQL · plans/explain_basic.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- EXPLAIN PLAN asks the optimizer for its plan without running the query; DBMS_XPLAN.DISPLAY formats the rows
-- EXPLAIN PLAN wrote to PLAN_TABLE. The table is built here: 100,000 orders, of which 10 are 'new'.
SET FEEDBACK OFF
SET LINESIZE 150
SET PAGESIZE 200
DROP TABLE IF EXISTS orders_skew PURGE;

CREATE TABLE orders_skew AS
SELECT LEVEL AS order_id,
       CASE WHEN LEVEL <= 10 THEN 'new'
            WHEN LEVEL <= 1010 THEN 'paid'
            ELSE 'shipped' END AS status,
       MOD(LEVEL, 500) + 1 AS customer_id,
       ROUND(MOD(LEVEL * 37, 9000) + 99.5, 2) AS amount
FROM   dual
CONNECT BY LEVEL <= 100000;

CREATE INDEX orders_skew_status_ix ON orders_skew (status);

-- Table and column statistics, with no histogram on any column yet.
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS_SKEW', method_opt => 'FOR ALL COLUMNS SIZE 1')

SET FEEDBACK ON
-- 1. The plan for the 10 'new' orders, estimated only: nothing runs.
EXPLAIN PLAN FOR
SELECT order_id, customer_id, amount FROM orders_skew WHERE status = 'new';

-- 2. Expected: Rows 33333 for the status step (100,000 rows / 3 distinct values), and the predicate
--    "STATUS"='new' under Predicate Information. Whether it shows INDEX RANGE SCAN or TABLE ACCESS FULL is the
--    optimizer's choice for 33,333 rows; read it on your run.
SELECT * FROM DBMS_XPLAN.DISPLAY(format => 'TYPICAL');

-- 3. The same, with only the operations.
SELECT * FROM DBMS_XPLAN.DISPLAY(format => 'BASIC');

-- EXPLAIN PLAN is a DML statement that wrote rows to PLAN_TABLE and does not commit them: roll them back. Every
-- script of this lesson builds its own copy of orders_skew, so this one drops it.
ROLLBACK;
DROP TABLE orders_skew PURGE;
explain_basic.sql, run in the lab Python · explain_basic_session.py
"""explain_basic.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("plans/explain_basic.sql")

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @plans/explain_basic.sql
Explained.


PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 3497657578

---------------------------------------------------------------------------------
| Id  | Operation         | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |             | 33333 |   716K|   105   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| ORDERS_SKEW | 33333 |   716K|   105   (0)| 00:00:01 |
---------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("STATUS"='new')

13 rows selected.


PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 3497657578

-----------------------------------------
| Id  | Operation         | Name        |
-----------------------------------------
|   0 | SELECT STATEMENT  |             |
|   1 |  TABLE ACCESS FULL| ORDERS_SKEW |
-----------------------------------------

8 rows selected.


Rollback complete.


Table dropped.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 explain_basic_session.py

The Rows column of the status step reads 33,333: the statistics know the table has 100,000 rows and 3 distinct statuses, and nothing more. EXPLAIN PLAN is a DML statement that does not commit, so the script rolls its PLAN_TABLE rows back.

Treating an explained plan as the plan that ran

Oracle’s tuning guide warns that the explained plan can differ from the one used at run time, for example when bind values, statistics or session parameters differ. Use EXPLAIN PLAN to ask questions; use the cursor cache, next, to see what happened.

Checkpoint

Check: Estimated plans with EXPLAIN PLAN

  1. Question 1 of 2 What happens to the query when you run EXPLAIN PLAN FOR it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing; the optimizer writes its chosen plan to PLAN_TABLE without running it

    EXPLAIN PLAN fills PLAN_TABLE with the optimizer's chosen steps and estimates; DBMS_XPLAN.DISPLAY formats them.

  2. Question 2 of 2 orders_skew has 100,000 rows and 3 distinct statuses, with no histogram. How many rows does EXPLAIN PLAN expect for status = 'new'?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 33333 rows (anything from 33332 to 33334 counts)

    With no histogram the optimizer assumes an even spread: 100,000 / 3 = 33,333 rows for any status, the Rows figure of explain_basic.sql's plan.

Actual plans put measured rows beside the estimates

After a query runs, its plan stays in the cursor cache, and DISPLAY_CURSOR: A DBMS_XPLAN function that reads the plan a statement really used from the cursor cache; with the gather_plan_statistics hint it shows measured rows (A-Rows) beside the estimates (E-Rows). reads it from there. Run the query with the hint gather_plan_statistics and each step also records the rows it produced and the buffers it read:

Interactive · message by message From a query that ran to its plan with real row counts Step through the 'new' query run with gather_plan_statistics, then DISPLAY_CURSOR reading its plan.
From a query that ran to its plan with real row counts, step 5 of 5. Messages so far: Your session to Cursor cache: query with the hint; Your session to Cursor cache: format 'ALLSTATS LAST'; Cursor cache to Your session: E-Rows and A-Rows.Your sessionCursor cachequery with the hintformat 'ALLSTATS LAST'E-Rows and A-Rows

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The query runs with counting on. The session runs the 'new' query with the hint gather_plan_statistics, so each step of the plan counts the rows it produces and the buffers it reads.
  2. The plan stays in the cache. The cursor, its plan and the statistics of its last execution stay in the cursor cache until the cursor ages out.
  3. DISPLAY_CURSOR asks for it. DISPLAY_CURSOR with no SQL_ID asks for the last cursor this session ran. That is why SERVEROUTPUT is off: SQL*Plus would otherwise run its own GET_LINES call after the query.
  4. Estimates meet measurements. The plan comes back with E-Rows, the estimate, beside A-Rows, what happened: 33,333 expected and 10 found on the step that filters status.
  5. A gap worth reading. A ratio of 3,333 to 1 between E-Rows and A-Rows says the optimizer planned for the wrong number of rows: here, because nothing told it that 'new' is rare.

With the format 'ALLSTATS LAST', E-Rows (the cardinality estimate: The number of rows the optimizer expects an operator to return, based on statistics; a large gap with actual rows signals a plan problem. More in “Reading SQL Server execution plans”) stands next to A-Rows, the measured rows of the last execution, with a Buffers column of logical reads. The script needs read access to V$SQL, V$SQL_PLAN, V$SQL_PLAN_STATISTICS_ALL and V$SESSION; its header gives the four grants:

display_cursor.sql: E-Rows 33,333 against A-Rows 10 and 98,990 SQL · plans/display_cursor.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- DBMS_XPLAN.DISPLAY_CURSOR reads the plan a query really used from the cursor cache. With the hint
-- gather_plan_statistics the query also records, per step, the rows it produced (A-Rows) and the buffers it read.
-- DISPLAY_CURSOR needs READ on four views the lab's LAB user does not hold; once, as SYS in FREEPDB1:
--   GRANT READ ON SYS.V_$SQL TO lab;                    GRANT READ ON SYS.V_$SQL_PLAN TO lab;
--   GRANT READ ON SYS.V_$SQL_PLAN_STATISTICS_ALL TO lab; GRANT READ ON SYS.V_$SESSION TO lab;
SET FEEDBACK OFF
SET LINESIZE 160
SET PAGESIZE 200
DROP TABLE IF EXISTS orders_skew PURGE;

CREATE TABLE orders_skew AS
SELECT LEVEL AS order_id,
       CASE WHEN LEVEL <= 10 THEN 'new'
            WHEN LEVEL <= 1010 THEN 'paid'
            ELSE 'shipped' END AS status,
       MOD(LEVEL, 500) + 1 AS customer_id,
       ROUND(MOD(LEVEL * 37, 9000) + 99.5, 2) AS amount
FROM   dual
CONNECT BY LEVEL <= 100000;

CREATE INDEX orders_skew_status_ix ON orders_skew (status);
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS_SKEW', method_opt => 'FOR ALL COLUMNS SIZE 1')

-- With SERVEROUTPUT ON, SQL*Plus runs its own call after each statement, and DISPLAY_CURSOR with no SQL_ID would
-- show that call's plan instead of the query's.
SET SERVEROUTPUT OFF
SET FEEDBACK ON

-- 1. Run the query with row counting switched on. Expected: COUNT 10, SUM of the 10 'new' amounts.
SELECT /*+ gather_plan_statistics */ COUNT(*) AS orders, SUM(amount) AS total
FROM   orders_skew
WHERE  status = 'new';

-- 2. The plan of the last statement this session ran, with its last execution's statistics. Expected: E-Rows
--    33333 against A-Rows 10 on the step that filters status, and a Buffers column per step.
SELECT * FROM DBMS_XPLAN.DISPLAY_CURSOR(format => 'ALLSTATS LAST');

-- 3. The same for 'shipped'. Expected: E-Rows 33333 against A-Rows 98990.
SELECT /*+ gather_plan_statistics */ COUNT(*) AS orders, SUM(amount) AS total
FROM   orders_skew
WHERE  status = 'shipped';
SELECT * FROM DBMS_XPLAN.DISPLAY_CURSOR(format => 'ALLSTATS LAST');

DROP TABLE orders_skew PURGE;
display_cursor.sql, run in the lab Python · display_cursor_session.py
"""display_cursor.sql in the lab as LAB, after the four grants its header names. The A-Time column, the time
each step took, changes from run to run and is shown as <time>."""

from oracle_session import Lab

with Lab(masks=[(r"\|\d\d:\d\d:\d\d\.\d\d \|", "|  <time>    |")]) as lab:
    lab.sysdba("""
GRANT READ ON SYS.V_$SQL TO lab;
GRANT READ ON SYS.V_$SQL_PLAN TO lab;
GRANT READ ON SYS.V_$SQL_PLAN_STATISTICS_ALL TO lab;
GRANT READ ON SYS.V_$SESSION TO lab;
""", note="once, as SYS in FREEPDB1")
    lab.sqlplus("plans/display_cursor.sql")

Output

-- once, as SYS in FREEPDB1
$ sqlplus -s / as sysdba <<'SQL'
ALTER SESSION SET CONTAINER = FREEPDB1;
GRANT READ ON SYS.V_$SQL TO lab;
GRANT READ ON SYS.V_$SQL_PLAN TO lab;
GRANT READ ON SYS.V_$SQL_PLAN_STATISTICS_ALL TO lab;
GRANT READ ON SYS.V_$SESSION TO lab;
SQL
Session altered.


Grant succeeded.


Grant succeeded.


Grant succeeded.


Grant succeeded.

$ sqlplus -s lab@//localhost/FREEPDB1 @plans/display_cursor.sql
    ORDERS      TOTAL
---------- ----------
        10       3030

1 row selected.


PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  a6ku484a2pcv1, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics */ COUNT(*) AS orders, SUM(amount) AS
total FROM   orders_skew WHERE  status = 'new'

Plan hash value: 1102967995

--------------------------------------------------------------------------------------------
| Id  | Operation          | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |      1 |        |      1 |  <time>    |     373 |
|   1 |  SORT AGGREGATE    |             |      1 |      1 |      1 |  <time>    |     373 |
|*  2 |   TABLE ACCESS FULL| ORDERS_SKEW |      1 |  33333 |     10 |  <time>    |     373 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("STATUS"='new')


20 rows selected.


    ORDERS      TOTAL
---------- ----------
     98990  455287970

1 row selected.


PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  1ajv1v0vfd729, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics */ COUNT(*) AS orders, SUM(amount) AS
total FROM   orders_skew WHERE  status = 'shipped'

Plan hash value: 1102967995

--------------------------------------------------------------------------------------------
| Id  | Operation          | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |      1 |        |      1 |  <time>    |     373 |
|   1 |  SORT AGGREGATE    |             |      1 |      1 |      1 |  <time>    |     373 |
|*  2 |   TABLE ACCESS FULL| ORDERS_SKEW |      1 |  33333 |  98990 |  <time>    |     373 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("STATUS"='shipped')


20 rows selected.


Table dropped.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 display_cursor_session.py

For ‘new’, 33,333 rows were expected and 10 found; for ‘shipped’, 33,333 expected and 98,990 found. A plan costed for a third of the table can be the wrong plan for either.

SERVEROUTPUT hides the query's plan

Called with no SQL_ID, DISPLAY_CURSOR shows the last cursor the session ran, and SQL*Plus calls DBMS_OUTPUT.GET_LINES after each statement while SERVEROUTPUT is on. Run SET SERVEROUTPUT OFF first, or pass the SQL_ID from V$SQL.

Checkpoint

Check: Actual plans and A-Rows

  1. Question 1 of 2 DISPLAY_CURSOR shows E-Rows 33333 and A-Rows 10 on the step that filters status. What does that tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The optimizer planned for 3,333 times more rows than the step produced

    A large gap between E-Rows and A-Rows shows the optimizer costed the wrong number of rows, which can make it choose an access path that suits neither value.

  2. Question 2 of 2 With SERVEROUTPUT ON, DISPLAY_CURSOR() right after your query shows a plan for a call to DBMS_OUTPUT. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: With no SQL_ID it shows the session's last cursor, and SQL*Plus called GET_LINES after your query

    Turn SERVEROUTPUT off before the query, or pass the SQL_ID (and child number) from V$SQL.

Which plan tools every edition has, and which need a pack

DBMS_XPLAN’s DISPLAY and DISPLAY_CURSOR read the plan table and the cursor cache, and the Licensing Information lists neither under a management pack. The workload history and the tuning advisers are different: on Enterprise Edition they belong to the Diagnostics and Tuning Packs, which are extra-cost options there.

Tool Reads Licence on Enterprise Edition
DBMS_XPLAN.DISPLAY PLAN_TABLE No pack
DBMS_XPLAN.DISPLAY_CURSOR The cursor cache (V$ views) No pack
AWR, ASH, DBA_HIST_ views Workload history Diagnostics Pack
SQL Tuning Advisor, real-time SQL monitoring Tuning features Tuning Pack (needs Diagnostics)
SQL plan baselines SQL Plan Management No pack

Oracle AI Database Free includes both packs, and Standard Edition 2 offers neither. DISPLAY_AWR, which reads AWR’s DBA_HIST_ views, is deprecated in 26ai in favour of DISPLAY_WORKLOAD_REPOSITORY.

A view you can query is not always a view you may query

Apart from six views it names, the Licensing Information places every DBA_HIST_ view in the Diagnostics Pack, and any way of reaching that data, a tool or a plain query, needs the pack’s licence on Enterprise Edition. Check what is licensed before you script against them.

Checkpoint

Check: Which plan tools need a pack

  1. Question 1 of 2 Which of these needs no management pack on Enterprise Edition?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: DBMS_XPLAN.DISPLAY_CURSOR

    DISPLAY and DISPLAY_CURSOR need no pack; AWR, ASH and the DBA_HIST_ views are Diagnostics Pack, the advisers and real-time SQL monitoring Tuning Pack.

  2. Question 2 of 2 A script on Enterprise Edition reads old plans with DBMS_XPLAN.DISPLAY_AWR. What does it need?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The Diagnostics Pack, because it reads AWR's DBA_HIST_ views; and a move to DISPLAY_WORKLOAD_REPOSITORY

    The Licensing Information places AWR and the DBA_HIST_ views in the Diagnostics Pack; DBMS_XPLAN's reference marks DISPLAY_AWR deprecated in favour of DISPLAY_WORKLOAD_REPOSITORY.

Histograms: the counts behind a row estimate Pro: opens with a Pro, Premium or Ultimate pass

Without a histogram, the optimizer misjudges the rarest status of orders_skew by a factor of 3,333. This part works out that error, builds a frequency histogram in the lab, reads the counts it stored and shows which kind of histogram a column with many values gets.

In this part:

  • A frequency histogram turns the even-spread guess into counts
  • Reading the counts the histogram stored
  • Which kind of histogram a column gets

1 diagram · 3 runnable examples (Python, SQL) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Bind peeking and adaptive cursor sharing, measured Pro: opens with a Pro, Premium or Ultimate pass

A statement with a bind variable keeps the plan of the value it was first parsed for, and on a skewed column that plan can be wrong for the next value. This part runs one such statement three times, counts the buffers each plan read, reads how adaptive cursor sharing reacted and compares the ways to give each value its plan.

In this part:

  • A bind value is peeked once, at the hard parse
  • What the shared plan cost, in buffers
  • Choosing how a skewed value gets its plan

1 diagram · 2 runnable examples (Python, SQL) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

The questions use orders_skew and the plans of the scripts above:

Practice that adapts to you

Practise this lesson

5 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 An explained plan uses an index, but the slow production run used a full scan. Which explanation fits Oracle's guide?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: The explained plan can differ from the one used at run time, for example through bind values or statistics

    To see the plan a run used, read it from the cursor cache with DISPLAY_CURSOR.

  2. Question 2 of 5 Which DBMS_XPLAN format shows only the operation id, name and option of each step?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: BASIC

    BASIC, TYPICAL (the default) and ALL set the level of detail; keywords such as PREDICATE or ALLSTATS add sections.

  3. Question 3 of 5 In display_cursor.sql, how many rows does A-Rows report for the status step of the 'shipped' query?

    Type a number.

    Show the answer: Question 3 of 5

    Answer: 98990 rows

    orders_skew holds 98,990 'shipped' orders, and A-Rows is what the step produced in the last execution.

  4. Question 4 of 5 What must happen for ALLSTATS LAST to show A-Rows for a query?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: The query ran with gather_plan_statistics, or with STATISTICS_LEVEL set to ALL

    DBMS_XPLAN's reference ties IOSTATS, and so ALLSTATS, to plan statistics collected by the hint or by STATISTICS_LEVEL = ALL.

  5. Question 5 of 5 You practise with AWR reports on the lab's Oracle AI Database Free. What carries over to an Enterprise Edition server at work?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: The skill does, the licence does not: Enterprise Edition needs the Diagnostics Pack for AWR

    The packs are included in Free and extra-cost on Enterprise Edition, so check the server's licences before you run AWR or ASH there.

Interview questions

How do DBMS_XPLAN.DISPLAY and DISPLAY_CURSOR differ, and when do you use each? (warm-up; fresher, 2–5 years)

DISPLAY formats the rows that EXPLAIN PLAN wrote to PLAN_TABLE: the plan the optimizer would choose, worked out without running the query, so every number is an estimate. It suits a statement you cannot or should not run, such as a large UPDATE. DISPLAY_CURSOR reads the plan a statement really used from the cursor cache; run the statement with gather_plan_statistics and the format ‘ALLSTATS LAST’ shows E-Rows beside A-Rows and buffers per step, which is how you see where the estimate went wrong. Neither needs a management pack.

Two longer questions follow, on a batch job that slowed down and on one statement that is fast for one value and slow for another:

  1. Mid to senior

    A nightly job that read about 10,000 rows of a claims table now takes 45 minutes instead of 2. DISPLAY_CURSOR with ALLSTATS LAST shows E-Rows 100 and A-Rows 150,000 on the step that filters claims by region, under a nested loops join. Walk through how you would diagnose and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    An application runs one query with a bind on ticket status. For 'open' (500 rows) it takes 10 ms; for 'closed' (2 million rows) 90 seconds. V$SQL shows one child cursor with IS_BIND_SENSITIVE = N after thousands of runs. Explain what is happening and what you would change.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does EXPLAIN PLAN do, and who formats its output?

    It asks for a query's plan without running it: the optimizer writes one row per step into PLAN_TABLE and DBMS_XPLAN.DISPLAY formats those rows. It is a DML statement that does not commit, so its rows can be rolled back.

  2. What do DBMS_XPLAN's BASIC, TYPICAL and ALL formats show?

    BASIC shows only the operations; TYPICAL, the default, adds the estimated rows, bytes and cost and the predicates; ALL adds the projection and alias sections.

  3. Why is the explained plan not always the plan that ran?

    Oracle's tuning guide warns it can differ when bind values, statistics or session parameters differ. Use EXPLAIN PLAN to ask questions and the cursor cache to see what actually happened.

  4. How do you put measured rows beside the estimates?

    Run the query with the gather_plan_statistics hint, so each step records the rows it produced and the buffers it read, then DBMS_XPLAN.DISPLAY_CURSOR with the format ALLSTATS LAST shows E-Rows next to A-Rows with a Buffers column of logical reads.

  5. Which two practical points come with DISPLAY_CURSOR?

    Called with no SQL_ID it shows the last cursor the session ran, and SQL*Plus calls DBMS_OUTPUT.GET_LINES after each statement while SERVEROUTPUT is on, so run SET SERVEROUTPUT OFF first or pass the SQL_ID from V$SQL. It also needs read access to V$SQL, V$SQL_PLAN, V$SQL_PLAN_STATISTICS_ALL and V$SESSION.

Key takeaways

  • EXPLAIN PLAN writes the optimizer’s plan to PLAN_TABLE without running the query, and DBMS_XPLAN.DISPLAY formats it; every number in it is an estimate.
  • DISPLAY_CURSOR reads the plan a query used from the cursor cache, and with gather_plan_statistics and ALLSTATS LAST it shows E-Rows beside A-Rows.
  • DISPLAY and DISPLAY_CURSOR need no pack, while AWR, ASH and the tuning advisers need the Diagnostics and Tuning Packs on Enterprise Edition.

Terms in this lesson

adaptive cursor sharing
Oracle's way of giving one statement several child cursors: a cursor whose plan may depend on its bind values is watched, and when executions differ greatly it gets children with plans for different row counts.
bind peeking
The Oracle optimizer's look at the value of a bind variable during a statement's hard parse, which it uses to estimate rows and choose the plan that later executions share.
DISPLAY_CURSOR
A DBMS_XPLAN function that reads the plan a statement really used from the cursor cache; with the gather_plan_statistics hint it shows measured rows (A-Rows) beside the estimates (E-Rows).
EXPLAIN PLAN
An Oracle SQL statement that asks the optimizer for a statement's plan and writes its steps to PLAN_TABLE without running the statement, so every number in the result is an estimate.
histogram
Column statistics that record how often values occur, so the optimizer can estimate the rows for a given value instead of assuming the rows are spread evenly over the distinct values.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.