Mittwoch, September 12, 2012

Join Cardinality und explizite Typ-Konvertierung

Gerade habe ich im OTN-Forum einen Fall vorgestellt, bei dem der CBO zu einer seltsamen Fehleinschätzung der Join Cardinality kommt (und der den Kern eines Problems darstellt, dem ich gestern bei einer großen DWH-Query begegnet bin):

Zunächst der harmlose Versuchsaufbau:

drop table t1;
drop table t2;

create table t1
as
select rownum col1
  from dual
connect by level <= 1000;

create table t2
as
select to_char(rownum) col1
  from dual
connect by level <= 1000;

exec dbms_stats.gather_table_stats(user, 't1')
exec dbms_stats.gather_table_stats(user, 't2')

Also zwei Tabellen, die jeweils die Werte 1 bis 1000 enthalten und deren einziger Unterschied der Datentyp ist: einmal number, einmal varchar2. Wenn ich jetzt versuche, diesen Unterschied (man könnte es wohl auch Modellierungsfehler nennen) in einer inline View zu beheben, bekomme ich ein überraschendes Ergebnis:

explain plan for
select t.col1
     , t2.col1
  from (select /*+ no_merge */
               to_char(col1) col1
          from t1) t
     , t2
 where t.col1 = t2.col1;

select * from table(dbms_xplan.display('', ''));

Plan hash value: 1373845765

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     1 |    25 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN          |      |     1 |    25 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL | T2   |  1000 |  3000 |     3   (0)| 00:00:01 |
|   3 |   VIEW              |      |  1000 | 22000 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T1   |  1000 |  3000 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   1 - access("T"."COL1"="T2"."COL1")

Der CBO erwartet in Step 1 also nur einen Satz! Mit der Standardformel für die Berechnung der Join Cardinality (ohne Berücksichtigung von NULL-Werten und Filterungen) bekomme ich Folgendes:

  • join selectivity = 1 / greater(num_distinct(t1.col1), num_distinct(t2.col1))
  • join cardinality = join selectivity * cardinality t1 * cardinality t2
  • So I get 1/1000 * 1000 * 1000 = 1000

Und dieses - korrekte - Ergebnis von 1000 bekomme ich auch in nahezu jedem anderen Fall:

-- ohne no_merge
select t.col1
     , t2.col1
  from (select to_char(col1) col1
          from t1) t
     , t2
 where t.col1 = t2.col1

Plan hash value: 1838229974

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |  1000 |  6000 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN         |      |  1000 |  6000 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| T1   |  1000 |  3000 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| T2   |  1000 |  3000 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------

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

   1 - access("T2"."COL1"=TO_CHAR("COL1"))

-- ohne explizite Typ-Umwandlung in der inline View
select t.col1
     , t2.col1
  from (select /*+ no_merge */
               col1
          from t1) t
     , t2
 where t.col1 = t2.col1

Plan hash value: 1373845765

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |  1000 | 16000 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN          |      |  1000 | 16000 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL | T2   |  1000 |  3000 |     3   (0)| 00:00:01 |
|   3 |   VIEW              |      |  1000 | 13000 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T1   |  1000 |  3000 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   1 - access("T"."COL1"=TO_NUMBER("T2"."COL1"))

-- mit zusätzlicher expliziter Typ-Umwandlung im Join
select t.col1
     , t2.col1
  from (select /*+ no_merge */
               to_char(col1) col1
          from t1) t
     , t2
 where to_char(t.col1) = t2.col1

Plan hash value: 1373845765

----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |  1000 | 25000 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN          |      |  1000 | 25000 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL | T2   |  1000 |  3000 |     3   (0)| 00:00:01 |
|   3 |   VIEW              |      |  1000 | 22000 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| T1   |  1000 |  3000 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------

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

   1 - access("T2"."COL1"=INTERNAL_FUNCTION("T"."COL1"))

In allen diesen Fällen erfolgt (laut access-Prädikat der Join-Operation) eine zusätzlich Typ-Umwandlung, die den CBO offenbar von der Fehleinschätzung der Cardinality abbringt. Aber warum der CBO durch die explizite Typumwandlung so massiv gestört wird, bleibt mir zunächst ein Rätsel.

Nachtrag: ein CBO-Trace zeigt für die Varianten mit und ohne to_char in der inline-View sehr ähnliche Ergebnisse. Zu den signifikanten Unterschieden gehören die folgenden Angaben:

-- mit to_char
Join Card:  0.000000 = = outer (1000.000000) * inner (1000.000000) * sel (0.000000)
Join Card - Rounded: 1 Computed: 0.00

-- ohne to_char
Join Card:  1000.000000 = = outer (1000.000000) * inner (1000.000000) * sel (0.001000)
Join Card - Rounded: 1000 Computed: 1000.00

Aber woher die sel-Angabe (0.000000) im ersten Fall kommt, sehe ich nicht.

Nachtrag 14.09.2012: in der OTN-Diskussion wurde klar, dass das Problem der unbrauchbaren Join-Cardinality-Schätzungen bei Verwendung inadäquater Datentypen auch andere Join-Syntax-Varianten betreffen kann, was den Fall noch unerfreulicher macht - und die Bedeutung einer soliden Modellierung unterstreicht.

Keine Kommentare:

Kommentar veröffentlichen