xb.sql and xbi.sql - Explain Oracle Execution Plans Better! (Part 2)

2022-03-17

In the previous post in this series, I showed how my scripts xb.sql and xbi.sql (eXplain Better and eXplain Better, by sqlId) help you see Oracle execution plan details in a more complete way.

Here are the features that I currently plan to cover:

  1. More detailed access/filter predicate info next to plan tree data (Part 1)
  2. More intelligent row-source level execution statistics presentation (Part 2, this post)
  3. Calculate Optimizer Cost misestimation factor and demo its effects (Part 3, coming soon)

More intelligent row-source level execution statistics presentation

Look into the following example output by DBMS_XPLAN.DISPLAY_CURSOR, I ran it on a random benchmark query (that had rowsource level metrics collection enabled using statistics_level=all). I have truncated some output columns below, so that the output wouldn’t be that wide, but you still may need to scroll right to see the A-Time, Buffers, Reads columns:

SQL> @xi czxpthmzk8nnd 0
eXplain the execution plan for sqlid czxpthmzk8nnd child 0...
...

-------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name               | Starts | A-Rows |   A-Time   | Buffers | Reads  |
-------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                    |      1 |     20 |00:00:03.22 |     125K|   9391 |
|   1 |  SORT ORDER BY               |                    |      1 |     20 |00:00:03.22 |     125K|   9391 |
|   2 |   HASH GROUP BY              |                    |      1 |    245 |00:00:03.22 |     125K|   9391 |
|*  3 |    HASH JOIN                 |                    |      1 |   9056 |00:00:03.22 |     125K|   9391 |
|*  4 |     TABLE ACCESS FULL        | DATE_DIM           |      1 |   6000 |00:00:00.01 |    1456 |      0 |
|   5 |     NESTED LOOPS             |                    |      1 |    112K|00:00:03.18 |     123K|   9391 |
|   6 |      NESTED LOOPS            |                    |      1 |    112K|00:00:00.09 |   11240 |     77 |
|*  7 |       TABLE ACCESS FULL      | ITEM               |      1 |    160 |00:00:00.03 |   10374 |      0 |
|*  8 |       INDEX RANGE SCAN       | STORE_SALES_PK_IDX |    160 |    112K|00:00:00.04 |     866 |     77 |
|   9 |      TABLE ACCESS BY GLOBAL I| STORE_SALES        |    112K|    112K|00:00:03.06 |     112K|   9314 |
-------------------------------------------------------------------------------------------------------------

I intentionally omitted the predicate section of this plan, as I covered predicates in the previous post and want to focus only on the metrics here.

The highlighted metrics above are cumulative. This means a few things:

  1. The parent node in the execution plan includes whatever time and resources its children/grandchildren have consumed (A-Time, Buffers, Reads).
  2. If you want to find out the time consumed by an execution plan line alone (self-time, self-value), you will have to do some math, deduct the sum of all direct children’s metrics from the cumulative metrics of their parent to compute the number that the parent itself was using.
  3. The root node of a plan tree shows the plan totals, cumulatively including all time and resource consumption of the plan steps.
  4. The leaf nodes of the plan tree show only the self-times of these leaves, as they don’t have any child nodes whose metrics to cumulatively add up.

With simple plans like above, you can just visually follow the “tree” structure and see where the biggest numbers in the plan totals originate from. The plan total A-Time for the root node SELECT STATEMENT is 3.22 seconds and its immediate child node also reports 3.22 seconds. Therefore, there was no (noticeable) extra time spent in the SELECT STATEMENT step itself.

We descend deeper into the tree and see that plan line #3, HASH JOIN reports 3.22 seconds spent in+under it, but when we add the direct child times together 0.01 seconds from #4 - TABLE ACCESS FULL [DATE_DIM] and 3.18 seconds from #5 - NESTED LOOPS, we get 0.01 + 3.18 = 3.19 seconds only. We are “missing” 30 milliseconds and this delta is the time spent inside the parent HASH JOIN step itself.

You can follow this logic step by step throughout all child nodes in the tree - or just eyeball and see what are the deepest areas of the tree with the biggest cumulative values. This becomes error-prone (and not-so-systematic), especially with plans where siblings at a certain level don’t fit onto the same screen anymore, etc.

XB-scripts to the rescue!

That’s why my xb scripts do this math for you! Now you can see the self-values consumed on each plan line immediatelly, without having to do math-eyeballing all the time.

Warning, the output is very wide, you will need either a horizontally scrolling terminal (or properly configured cmd.exe on Windows), use SQL*Developer (that can horizontally scroll script output) or just spool your sqlplus output to a file and keep it open in a text editor.

Here’s the xbi.sql output (it takes SQL_ID and child# as arguments) - scroll right to see the whole story:

SQL> @xbi czxpthmzk8nnd 0
-- xbi.sql: eXplain Better v1.01 for sql_id=czxpthmzk8nnd child=0 - by Tanel Poder (https://blog.tanelpoder.com)

         SQL_ID         CHLD ADDRESS          Plan Hash Value First Load Time
-------- ------------- ----- ---------------- --------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------
Cursor:  czxpthmzk8nnd     0 000000049451AFB8      2501019660 Statement first parsed at: 2022-03-17/01:21:20 - 37 seconds ago

 Pred    Op  Par.  #Sib                                              Query Block             ms spent in Consistent  Rowsource  Real #rows     Est. rows      Opt. Card.    Current   Physical   Physical        Memory Workarea        Optimizer
 #Col    ID    ID  ling Row Source                                   name                 this operation       gets     starts    returned         total     misestimate       gets  read blks write blks     used (MB) Passes               Cost
----- ----- ----- ----- -------------------------------------------- -------------------- -------------- ---------- ---------- ----------- ------------- --------------- ---------- ---------- ---------- ------------- ------------- -----------
          0             SELECT STATEMENT                             >>> Plan totals >>>        3,220.09     125211          1          20                                        0       9391          0                                  121263
          1     0     1  SORT ORDER BY                               SEL$1                           .07          0          1          20        114030           5701x          0          0          0           .00 OPTIMAL            121263
          2     1     1   HASH GROUP BY                                                             3.89          0          1         245        114030            465x          0          0          0          5.54 OPTIMAL            121263
    A     3     2     1    HASH JOIN                                                               28.78          0          1        9056        114030             13x          0          0          0          1.63 OPTIMAL            117113
    F     4     3     1     TABLE ACCESS FULL [DATE_DIM]             SEL$1                          4.02       1456          1        6000          6000              1x          0          0          0                                     229
          5     3     2     NESTED LOOPS                                                           39.05          0          1      112576        114030              1x          0          0          0                                  116883
          6     5     1      NESTED LOOPS                                                          13.67          0          1      112576        114345              1x          0          0          0                                  116883
    F     7     6     1       TABLE ACCESS FULL [ITEM]               SEL$1                         31.71      10374          1         160           165              1x          0          0          0                                    1670
  A#1     8     6     2       INDEX RANGE SCAN [STORE_SALES_PK_IDX]  SEL$1                         43.44        866        160      112576        110880             -1x          0         77          0                                       5
          9     5     2      TABLE ACCESS BY GLOBAL INDEX ROWID [ST  SEL$1                      3,055.46     112515     112576      112576      78015168            693x          0       9314          0                                     698

The >>> Plan totals >>> line (root of the tree) is the only line where I still show the total, cumulative metrics of the entire plan (so you’d know how much the complete plan execution took). On all other lines, the highlighted columns have some basic math applied and show the non-cumulative self-time and self-values of the metrics:

Where do I get the raw numbers from? I do not use DBMS_XPLAN at all, but go straight to the source - V$SQL_PLAN and V$SQL_PLAN_STATISTICS_ALL.

So, this makes your life easier, you can just scroll through these highlighted columns and see where the biggest numbers are, in our case operation #9 - TABLE ACCESS BY GLOBAL INDEX ROWID that used 3,055.46 ms of A-Time out of the Plan total of 3,220.09ms. Other steps in the plan clearly consumed some time too, but not significantly compared to the main troublemaker. Similarly, you can scroll right to other cumulative metrics (consistent gets, physical reads) and easily identify where most of the I/Os come from.

Here’s another slightly edited example. Let’s say you want to quickly identify where most of the logical I/Os (Buffers) come from. Scroll right and try to follow the plan tree with your eyes and parse the numbers (where some are in “Kilo” units and some not):

--------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                | Starts | A-Rows |   A-Time   | Buffers | Reads  |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                     |      1 |     20 |00:00:02.90 |     170K|    153K|
|   1 |  TEMP TABLE TRANSFORMATION   |                     |      1 |     20 |00:00:02.90 |     170K|    153K|
|   2 |   LOAD AS SELECT (CURSOR DURA| SYS_TEMP_0FD9D66BC_ |      1 |      0 |00:00:02.79 |     155K|    153K|
|   3 |    HASH GROUP BY             |                     |      1 |  64004 |00:00:02.77 |     155K|    153K|
|*  4 |     HASH JOIN                |                     |      1 |   2057K|00:00:02.52 |     155K|    153K|
|   5 |      VIEW                    | VW_GBF_7            |      1 |    365 |00:00:00.01 |    1456 |      0 |
|*  6 |       TABLE ACCESS FULL      | DATE_DIM            |      1 |    365 |00:00:00.01 |    1456 |      0 |
|   7 |      PARTITION RANGE ALL     |                     |      1 |     11M|00:00:02.27 |     153K|    153K|
|   8 |       TABLE ACCESS FULL      | STORE_RETURNS       |     22 |     11M|00:00:02.25 |     153K|    153K|
|   9 |   SORT ORDER BY              |                     |      1 |     20 |00:00:00.11 |   14987 |      0 |
|* 10 |    HASH JOIN                 |                     |      1 |  25597 |00:00:00.10 |   14987 |      0 |
|* 11 |     TABLE ACCESS FULL        | STORE               |      1 |     12 |00:00:00.01 |      18 |      0 |
|* 12 |     HASH JOIN                |                     |      1 |  25597 |00:00:00.10 |   14968 |      0 |
|* 13 |      HASH JOIN               |                     |      1 |  25597 |00:00:00.01 |       1 |      0 |
|  14 |       VIEW                   | VW_SQ_1             |      1 |     11 |00:00:00.01 |       0 |      0 |
|  15 |        HASH GROUP BY         |                     |      1 |     11 |00:00:00.01 |       0 |      0 |
|  16 |         VIEW                 |                     |      1 |  64004 |00:00:00.01 |       0 |      0 |
|  17 |          TABLE ACCESS FULL   | SYS_TEMP_0FD9D66BC_ |      1 |  64004 |00:00:00.01 |       0 |      0 |
|  18 |       VIEW                   |                     |      1 |  64004 |00:00:00.01 |       0 |      0 |
|  19 |        TABLE ACCESS FULL     | SYS_TEMP_0FD9D66BC_ |      1 |  64004 |00:00:00.01 |       0 |      0 |
|  20 |      TABLE ACCESS FULL       | CUSTOMER            |      1 |    709K|00:00:00.05 |   14966 |      0 |
--------------------------------------------------------------------------------------------------------------

And now look at the same plan using the xbi.sql tool:

SQL> @xbi bpt7vm7wbsn42 0

 Pred    Op  Par.  #Sib                                           Query Block             ms spent in Consistent  Rowsource  Real #rows     Est. rows      Opt. Card.    Current   Physical   Physical        Memory Workarea        Optimizer
 #Col    ID    ID  ling Row Source                                name                 this operation       gets     starts    returned         total     misestimate       gets  read blks write blks     used (MB) Passes               Cost
----- ----- ----- ----- ----------------------------------------- -------------------- -------------- ---------- ---------- ----------- ------------- --------------- ---------- ---------- ---------- ------------- ------------- -----------
          0             SELECT STATEMENT                          >>> Plan totals >>>        2,901.67     170271          1          20                                        2     153524          0                                   44144
          1     0     1  TEMP TABLE TRANSFORMATION                SEL$2980E977                    .06          0          1          20                                        1          0          0
          2     1     1   LOAD AS SELECT (CURSOR DURATION MEMORY) SEL$F6D5D92C                  22.31          0          1           0                                        1          0          0           .00 OPTIMAL
          3     2     1    HASH GROUP BY                                                       246.10          0          1       64004       2096755             33x          0          0          0         75.58 OPTIMAL             37556
    A     4     3     1     HASH JOIN                                                          253.99          1          1     2057505       2096755              1x          0          0          0          1.69 OPTIMAL             23776
          5     4     1      VIEW  [VW_GBF_7]                     SEL$7502D05D                    .02          0          1         365           365              1x          0          0          0                                     229
    F     6     5     1       TABLE ACCESS FULL [DATE_DIM]        SEL$7502D05D                   4.35       1456          1         365           365              1x          0          0          0                                     229
          7     4     2      PARTITION RANGE ALL                                                10.82          0          1    11500560      11500560              1x          0          0          0                                   23491
          8     7     1       TABLE ACCESS FULL [STORE_RETURNS]   SEL$F6D5D92C               2,254.94     153827         22    11500560     253012320             22x          0     153524          0                                   23491
          9     1     2   SORT ORDER BY                                                          5.03          0          1          20        100470           5024x          0          0          0           .84 OPTIMAL              6588
    A    10     9     1    HASH JOIN                                                             7.16          1          1       25597        100470              4x          0          0          0          1.22 OPTIMAL              5271
    F    11    10     1     TABLE ACCESS FULL [STORE]             SEL$2980E977                    .04         18          1          12            12              1x          0          0          0                                      20
    A    12    10     2     HASH JOIN                                                           31.20          1          1       25597        104838              4x          0          0          0          4.00 OPTIMAL              5251
   FA    13    12     1      HASH JOIN                                                           4.19          1          1       25597        104838              4x          0          0          0          1.21 OPTIMAL              2535
         14    13     1       VIEW  [VW_SQ_1]                     SEL$8F9407EC                    .02          0          1          11            11              1x          0          0          0                                    1311
         15    14     1        HASH GROUP BY                      SEL$8F9407EC                   6.07          0          1          11            11              1x          0          0          0          1.01 OPTIMAL              1311
         16    15     1         VIEW                              SEL$D67CB2D2                    .05          0          1       64004       2096755             33x          0          0          0                                    1213
         17    16     1          TABLE ACCESS FULL [SYS_TEMP_0FD9 SEL$D67CB2D2                   1.86          0          1       64004       2096755             33x          0          0          0                                    1213
         18    13     2       VIEW                                SEL$D67CB2D3                    .05          0          1       64004       2096755             33x          0          0          0                                    1213
         19    18     1        TABLE ACCESS FULL [SYS_TEMP_0FD9D6 SEL$D67CB2D3                   1.79          0          1       64004       2096755             33x          0          0          0                                    1213
         20    12     2      TABLE ACCESS FULL [CUSTOMER]         SEL$2980E977                  51.62      14966          1      709015        709015              1x          0          0          0                                    2284

It’s easy to spot whatever is the biggest number in a column. I typically highlight the biggest value by triple-clicking it in my terminal (so the entire plan line is highlighted and I can scroll left and right without losing the context).

Oracle requirements/limitations

Oracle requires you to use the gather_plan_statistics hint or use statistics_level=all to see these row-source level metrics (DBMS_XPLAN.DISPLAY_CURSOR is no different). You then need to run your query and either wait for it to complete (get full execution metrics) or CTRL+C or kill the session when you don’t want to wait anymore. This way you will get partial metrics up to however far the plan execution had progressed by then.

If you kill/cancel the query, then thanks to the abrupt exit, some of the top level node metrics & A-Time values do not get populated and the parent nodes will show less time/resources used compared to their children combined. The XB scripts in this case may show negative values for some operations. That’s just an Oracle instrumentation’s shortcoming when killing/canceling queries. Nevertheless, looking for the biggest (positive) values in the output still points you to the right direction.

For long enough running queries, you could just use Oracle’s SQL Monitoring feature (I have a few hacking session videos about it on YouTube) and you don’t have to deal with re-running your queries with proper instrumentation enabled. However, SQL Monitoring does not always kick in (it’s designed for long-running queries), so one more “always-on” plan-line level profiling tool in your toolbelt should be my asqlmon.sql script.

That’s all for today!


  1. I am finally close to launching the completely rebuilt 2024 versions of my Linux & AOT classes in my Learning Platform! (Updates to SQL Tuning class in H2 2024):
    Advanced Oracle SQL Tuning training. Advanced Oracle Troubleshooting training, Linux Performance & Troubleshooting training. Check them out!
  2. Get randomly timed updates by email or follow Social/RSS