Today I faced a performance problem caused by a bad cardinality estimation involving a CDB view in a 12.1.0.2 multitenant environment. While solving the problem I did a number of observations that I try to summarize in this blog post.

First of all, when checking the execution plan of a query already running for more than two hours, I noticed that, in the execution plan, neither the referenced CDB view nor one of its underlying objects were referenced. The following query (and its execution plan) executed while connect to the CDB illustrates (I also added the 12.2.0.1 output to show you the difference it that area):

12.1.0.2

SQL> EXPLAIN PLAN FOR SELECT * FROM cdb_tables;
 
SQL> SELECT * FROM table(dbms_xplan.display);
 
Plan hash value: 1439328272
 
---------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |    TQ  |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |          | 20000 |    16M|     1 (100)| 00:00:01 |       |       |        |      |            |
|   1 |  PX COORDINATOR         |          |       |       |            |          |       |       |        |      |            |
|   2 |   PX SEND QC (RANDOM)   | :TQ10000 | 20000 |    16M|     1 (100)| 00:00:01 |       |       |  Q1,00 | P->S | QC (RAND)  |
|   3 |    PX PARTITION LIST ALL|          | 20000 |    16M|     1 (100)| 00:00:01 |     1 |   254 |  Q1,00 | PCWC |            |
|   4 |     FIXED TABLE FULL    | X$CDBVW$ | 20000 |    16M|     1 (100)| 00:00:01 |       |       |  Q1,00 | PCWP |            |
---------------------------------------------------------------------------------------------------------------------------------

12.2.0.1

SQL> EXPLAIN PLAN FOR SELECT * FROM cdb_tables;
 
SQL> SELECT * FROM table(dbms_xplan.display);
 
Plan hash value: 1043806087
 
-------------------------------------------------------------------------------------------------
| Id  | Operation          | Name       | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |            | 20000 |    28M|     1 (100)| 00:00:01 |       |       |
|   1 |  PARTITION LIST ALL|            | 20000 |    28M|     1 (100)| 00:00:01 |     1 |     3 |
|   2 |   CONTAINERS FULL  | DBA_TABLES | 20000 |    28M|     1 (100)| 00:00:01 |       |       |
-------------------------------------------------------------------------------------------------

As you can see, the 12.1.0.2 execution plan doesn’t reference any object related to the CDB_TABLES view or one of its underlying tables. Instead, it uses the generic fixed table X$CDBVW$. Simply put, X$CDBVW$ is a fixed table that gives access to data stored in PDBs. To know more, I advise you to read Laurent Leturgez’s blog post entitled Oracle Database 12c CDB$VIEW function.

In the real query, the CDB view was joined to a number of V$ views. Unfortunately, the query optimizer selected the wrong join method (no surprise, it was a nested loops join instead of a hash join…) and the performance was abysmal. When I saw that the cardinality estimations were wrong, I checked whether the involved objects had statistics. But, because of its particular behavior, the fixed table X$CDBVW$ had no statistics. And, by the way, statistics on it can’t be gathered. If you try, you get the following error:

12.1.0.2

SQL> exec dbms_stats.gather_table_stats('SYS','X$CDBVW$')
BEGIN dbms_stats.gather_table_stats('SYS','X$CDBVW$'); END;
 
*
ERROR at line 1:
ORA-20000: Unable to analyze TABLE "SYS"."X$CDBVW$", analyzing the table is not supported
ORA-06512: at "SYS.DBMS_STATS", line 35464
ORA-06512: at line 1

12.2.0.1

SQL> exec dbms_stats.gather_table_stats('SYS','X$CDBVW$')
BEGIN dbms_stats.gather_table_stats('SYS','X$CDBVW$'); END;
 
*
ERROR at line 1:
ORA-20000: Unable to analyze TABLE "SYS"."X$CDBVW$", insufficient privileges or does not exist
ORA-06512: at "SYS.DBMS_STATS", line 36873
ORA-06512: at "SYS.DBMS_STATS", line 36496
ORA-06512: at "SYS.DBMS_STATS", line 36716
ORA-06512: at line 1

As Laurent mentioned in his blog post, the query optimizer uses a default value instead. However, what I noticed is that the estimation wasn’t 10000 rows as he mentioned. In my case it was 30000 rows. The difference is probably due to the version. In fact, he wrote his blog post when only 12.1.0.1 was available, but my customer is using 12.1.0.2. So, I did a couple of test on my own test environment and found out that as of and including 12.1.0.2 the number of rows increases proportionally as the number of open PDBs increases. The following example illustrates:

12.1.0.2 / 12.2.0.1

SQL> SELECT con_id, name, open_mode FROM v$pdbs WHERE open_mode LIKE 'READ%';
 
    CON_ID NAME                           OPEN_MODE
---------- ------------------------------ ----------
         2 PDB$SEED                       READ ONLY
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'seed' FOR SELECT * FROM cdb_tables;
 
SQL> ALTER PLUGGABLE DATABASE test1 OPEN;
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'test1' FOR SELECT * FROM cdb_tables;
 
SQL> ALTER PLUGGABLE DATABASE test2 OPEN;
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'test2' FOR SELECT * FROM cdb_tables;
 
SQL> ALTER PLUGGABLE DATABASE test3 OPEN;
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'test3' FOR SELECT * FROM cdb_tables;
 
SQL> SELECT con_id, name, open_mode FROM v$pdbs WHERE open_mode LIKE 'READ%';
 
    CON_ID NAME                           OPEN_MODE
---------- ------------------------------ ----------
         2 PDB$SEED                       READ ONLY
         4 TEST1                          READ WRITE
         5 TEST2                          READ WRITE
         6 TEST3                          READ WRITE
 
SQL> SELECT statement_id, cardinality FROM plan_table WHERE id = 0;
 
STATEMENT_ID                   CARDINALITY
------------------------------ -----------
seed                                 20000
test1                                30000
test2                                40000
test3                                50000

Finally, in the real query, because of the join condition based on the CON_ID column, the query optimizer incorrectly adjusted the number of rows returned through the fixed table X$CDBVW$. That led me doing few tests related to the selectivity estimations related to the CON_ID column. As the following example illustrates, the query optimizer uses a default selectivity of 1% for equality predicates, and 5% for range predicates.

12.1.0.2

SQL> EXPLAIN PLAN SET STATEMENT_ID = 'all' FOR SELECT * FROM cdb_tables;
 
Explained.
 
SQL> SELECT * FROM table(dbms_xplan.display);
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 1439328272
 
---------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |    TQ  |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |          | 50000 |    40M|     2 (100)| 00:00:01 |       |       |        |      |            |
|   1 |  PX COORDINATOR         |          |       |       |            |          |       |       |        |      |            |
|   2 |   PX SEND QC (RANDOM)   | :TQ10000 | 50000 |    40M|     2 (100)| 00:00:01 |       |       |  Q1,00 | P->S | QC (RAND)  |
|   3 |    PX PARTITION LIST ALL|          | 50000 |    40M|     2 (100)| 00:00:01 |     1 |   254 |  Q1,00 | PCWC |            |
|   4 |     FIXED TABLE FULL    | X$CDBVW$ | 50000 |    40M|     2 (100)| 00:00:01 |       |       |  Q1,00 | PCWP |            |
---------------------------------------------------------------------------------------------------------------------------------
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'eq' FOR SELECT * FROM cdb_tables WHERE con_id = 0;
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'gt' FOR SELECT * FROM cdb_tables WHERE con_id > 0;
 
SQL> SELECT statement_id, cardinality FROM plan_table WHERE id = 0;
 
STATEMENT_ID                   CARDINALITY
------------------------------ -----------
all                                  50000
eq                                     500
gt                                    2500

12.2.0.1

SQL> EXPLAIN PLAN SET STATEMENT_ID = 'all' FOR SELECT * FROM cdb_tables;
 
Explained.
 
SQL> SELECT * FROM table(dbms_xplan.display);
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 1281079049
 
-----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name       | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |    TQ  |IN-OUT| PQ Distrib |
-----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |            | 50000 |    42M|     1 (100)| 00:00:01 |       |       |        |      |            |
|   1 |  PX COORDINATOR         |            |       |       |            |          |       |       |        |      |            |
|   2 |   PX SEND QC (RANDOM)   | :TQ10000   | 50000 |    42M|     1 (100)| 00:00:01 |       |       |  Q1,00 | P->S | QC (RAND)  |
|   3 |    PX PARTITION LIST ALL|            | 50000 |    42M|     1 (100)| 00:00:01 |     1 |     3 |  Q1,00 | PCWC |            |
|   4 |     CONTAINERS FULL     | DBA_TABLES | 50000 |    42M|     1 (100)| 00:00:01 |       |       |  Q1,00 | PCWP |            |
-----------------------------------------------------------------------------------------------------------------------------------
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'eq' FOR SELECT * FROM cdb_tables WHERE con_id = 0;
 
SQL> EXPLAIN PLAN SET STATEMENT_ID = 'gt' FOR SELECT * FROM cdb_tables WHERE con_id > 0;
 
SQL> SELECT statement_id, cardinality FROM plan_table WHERE id = 0;
 
STATEMENT_ID                   CARDINALITY
------------------------------ -----------
all                                  50000
eq                                     500
gt                                    2500

It goes without saying that such estimates are way off. Good estimates should consider the number of open PDBs….

In summary, if you see wrong estimates related to CDB views, don’t be surprised. In fact, the query optimizer bases its estimations on a number of default values.