Oracle Database - Compare Execution Plans
Tags: oracle, oci, database, sql
Published: 2024-04-16 17:25:54

[](
      <style>
        .libutton {
          display: flex;
          flex-direction: column;
          justify-content: center;
          padding: 7px;
          text-align: center;
          outline: none;
          text-decoration: none !important;
          color: #ffffff !important;
          width: 200px;
          height: 32px;
          border-radius: 16px;
          background-color: #0A66C2;
          font-family: "SF Pro Text", Helvetica, sans-serif;
        { Abhilash Kumar Bhattaram : </style> <a class="libutton" href="https://www.linkedin.com/comm/mynetwork/discovery-see-all?usecase=PEOPLE_FOLLOWS&followMember=abhilash-kumar-85b92918" target="_blank">Follow on LinkedIn</a>) }


Understanding SQL Plans is a very complex subject for DBA's , it is complex because of two reasons , oneis that most DBA's have no idea on what the SQL is intended to do and second is deciphering the SQL Plan output and understanding if they are are actually meaning anything.

I have noticed multiple cases where there are many SQL Plan's for a particular SQL but DBA's do not really have the necessary tools to actually compare the plans. Most DBA's would pin the SQL which takes the least time but they never had the opportunity to compare SQL Plans before doing that.

## Compare SQL Plans ( from Oracle 19c ) 

Let's say an SQL **0pvp4726mvx5u** with an multiple execution plans has been identified with child numbers 0 and 1 respectively each having a particular plan_hash_value.

From Oracle 19c DBA's have the power compare how the child numbers 0 and 1 behave with each of its execution

An example below to compare execution plans using **dbms_xplan.compare_plans**

The Oracle documentation for this package is [here](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_XPLAN.html#GUID-5A80E12C-F126-413D-8FF7-FC04B382FA92)


```
var l_cmp clob;
begin
  				:l_cmp := dbms_xplan.compare_plans(
                 reference_plan    => cursor_cache_object('0pvp4726mvx5u', 0),
                 compare_plan_list => plan_object_list(cursor_cache_object('0pvp4726mvx5u', 1)),
                 type              => 'TEXT',
                 level             => 'TYPICAL',
                 section           => 'ALL'
               ); 
end;
/
print l_cmp
```

The above code snippet would generate the compare sql plan report as below 

```

COMPARE PLANS REPORT
---------------------------------------------------------------------------------------------
  Current user           : SYS
  Total number of plans  : 2
  Number of findings     : 1
---------------------------------------------------------------------------------------------

COMPARISON DETAILS
---------------------------------------------------------------------------------------------
 Plan Number            : 1 (Reference Plan)
 Plan Found             : Yes
 Plan Source            : Cursor Cache
 SQL ID                 : 0pvp4726mvx5u
 Child Number           : 0
 Plan Database Version  : 19.0.0.0
 Parsing Schema         : "UAT1DBUSER"
 SQL Text               : /* SQL Analyze(12,1) */ SELECT a.STAT1,
                        a.STATUS, COUNT(a.STAT1) AS RECORDCOUNT FROM
                        XYZ1_TABLE4567 a inner join ABCD_EFGHIJKL_TABLE_123 b
                        on a.re2id = b.re1id or a.re2id = b.xe2id
                        where a.TYPE ='01' AND
                        TRUNC(a.LASTUPDATEON) BETWEEN TO_DATE('01-01-2023',
                        'DD/MM/YYYY') AND TO_DATE('15-04-2024', 'DD/MM/YYYY')
                        GROUP BY a.STAT1, a.STATUS ORDER BY
                        a.STAT1

Plan
-----------------------------

 Plan Hash Value  : 1774313814

----------------------------------------------------------------------------------------------------------
| Id   | Operation                         | Name                    | Rows  | Bytes  | Cost  | Time     |
----------------------------------------------------------------------------------------------------------
|    0 | SELECT STATEMENT                  |                         |       |        | 78128 |          |
|    1 |   SORT GROUP BY                   |                         |    86 |   1806 | 78128 | 00:00:04 |
|    2 |    VIEW                           | VW_ORE_4C2BA79E         |  2865 |  60165 | 78127 | 00:00:04 |
|    3 |     UNION-ALL                     |                         |       |        |       |          |
|  * 4 |      HASH JOIN                    |                         |  2472 | 111240 | 76720 | 00:00:03 |
|  * 5 |       TABLE ACCESS FULL           | XYZ1_TABLE4567          |  2472 | 101352 | 76100 | 00:00:03 |
|  * 6 |       TABLE ACCESS FULL           | ABCD_EFGHIJKL_TABLE_123 | 53391 | 213564 |   620 | 00:00:01 |
|    7 |      NESTED LOOPS                 |                         |   393 |  18471 |  1407 | 00:00:01 |
|    8 |       NESTED LOOPS                |                         |   393 |  18471 |  1407 | 00:00:01 |
|  * 9 |        TABLE ACCESS FULL          | ABCD_EFGHIJKL_TABLE_123 |   393 |   2358 |   620 | 00:00:01 |
| * 10 |        INDEX UNIQUE SCAN          | XYZ1_TABLE4567_PK       |     1 |        |     1 | 00:00:01 |
| * 11 |       TABLE ACCESS BY INDEX ROWID | XYZ1_TABLE4567          |     1 |     41 |     2 | 00:00:01 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 4 - access("A"."re2id"="B"."re1id")
* 5 - filter(("A"."TYPE"='01' AND TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))>=TO_DATE(' 2023-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
  TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))<=TO_DATE(' 2024-04-15 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
* 6 - filter("B"."re1id" IS NOT NULL)
* 9 - filter("B"."xe2id" IS NOT NULL)
* 10 - access("A"."re2id"="B"."xe2id")
* 10 - filter(LNNVL("A"."re2id"="B"."re1id"))
* 11 - filter(("A"."TYPE"='01' AND TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))>=TO_DATE(' 2023-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
  TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))<=TO_DATE(' 2024-04-15 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))

---------------------------------------------------------------------------------------------
 Plan Number            : 2
 Plan Found             : Yes
 Plan Source            : Cursor Cache
 SQL ID                 : 0pvp4726mvx5u
 Child Number           : 1
 Plan Database Version  : 19.0.0.0
 Parsing Schema         : "UAT1DBUSER"
 SQL Text               : /* SQL Analyze(12,1) */ SELECT a.STAT1,
                        a.STATUS, COUNT(a.STAT1) AS RECORDCOUNT FROM
                        XYZ1_TABLE4567 a inner join ABCD_EFGHIJKL_TABLE_123 b
                        on a.re2id = b.re1id or a.re2id = b.xe2id
                        where a.TYPE ='01' AND
                        TRUNC(a.LASTUPDATEON) BETWEEN TO_DATE('01-01-2023',
                        'DD/MM/YYYY') AND TO_DATE('15-04-2024', 'DD/MM/YYYY')
                        GROUP BY a.STAT1, a.STATUS ORDER BY
                        a.STAT1

Plan
-----------------------------

 Plan Hash Value  : 2833413552

----------------------------------------------------------------------------------------------------------
| Id   | Operation                         | Name                    | Rows  | Bytes  | Cost  | Time     |
----------------------------------------------------------------------------------------------------------
|    0 | SELECT STATEMENT                  |                         |       |        | 78128 |          |
|    1 |   SORT GROUP BY                   |                         |    86 |   1806 | 78128 | 00:00:04 |
|    2 |    VIEW                           | VW_ORE_4C2BA79E         |  2865 |  60165 | 78127 | 00:00:04 |
|    3 |     UNION-ALL                     |                         |       |        |       |          |
|  * 4 |      HASH JOIN                    |                         |  2472 | 111240 | 76720 | 00:00:03 |
|  * 5 |       TABLE ACCESS FULL           | ABCD_EFGHIJKL_TABLE_123 | 53391 | 213564 |   620 | 00:00:01 |
|  * 6 |       TABLE ACCESS FULL           | XYZ1_TABLE4567          |  2472 | 101352 | 76100 | 00:00:03 |
|    7 |      NESTED LOOPS                 |                         |   393 |  18471 |  1407 | 00:00:01 |
|    8 |       NESTED LOOPS                |                         |   393 |  18471 |  1407 | 00:00:01 |
|  * 9 |        TABLE ACCESS FULL          | ABCD_EFGHIJKL_TABLE_123 |   393 |   2358 |   620 | 00:00:01 |
| * 10 |        INDEX UNIQUE SCAN          | XYZ1_TABLE4567_PK       |     1 |        |     1 | 00:00:01 |
| * 11 |       TABLE ACCESS BY INDEX ROWID | XYZ1_TABLE4567          |     1 |     41 |     2 | 00:00:01 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 4 - access("A"."re2id"="B"."re1id")
* 5 - filter("B"."re1id" IS NOT NULL)
* 6 - filter(("A"."TYPE"='01' AND TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))>=TO_DATE(' 2023-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
  TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))<=TO_DATE(' 2024-04-15 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
* 9 - filter("B"."xe2id" IS NOT NULL)
* 10 - access("A"."re2id"="B"."xe2id")
* 10 - filter(LNNVL("A"."re2id"="B"."re1id"))
* 11 - filter(("A"."TYPE"='01' AND TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))>=TO_DATE(' 2023-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
  TRUNC(INTERNAL_FUNCTION("A"."LASTUPDATEON"))<=TO_DATE(' 2024-04-15 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))


Comparison Results (1):
-----------------------------
 1. Query block SET$669E0EF5_1: Join order is different at position 1
    (reference plan: "A"@"SET$669E0EF5_1", current plan: "B"@"SET$669E0EF5_1").


---------------------------------------------------------------------------------------------

```

In this case though we do not see much of a difference as the cost of both the sql's are 78128. But it gives us the ability to compare plans , identify what is different betwen them . These are especially useful for complex SQL's and DBA's can provide relevant updates to the devlopment teams.


The importantaspect is the the **Comparison results ** section which shows how the SQL query plans are different

```
Comparison Results (1):
-----------------------------
 1. Query block SET$669E0EF5_1: Join order is different at position 1
    (reference plan: "A"@"SET$669E0EF5_1", current plan: "B"@"SET$669E0EF5_1").
```

The above example does not tune any SQL but helps compare 2 SQL plans a very handy tool that DBA's can use , this can be used as a check to do a comparison before pinning the SQL plan with the lowest execution times.