In meinem Beispiel sind die Tabellen gleichgroß und nicht partitioniert, da das eigentlich gar keine Rolle spielte. Die Inhalte sind auch alles andere als aufregend:
select col1 , count(*) from t_2 group by col1 COL1 COUNT(*) ----- ---------- 1 5000 0 5000 select col1 , count(*) from t_4 group by col1 COL1 COUNT(*) ----- ---------- 1 2500 2 2500 3 2500 0 2500
Zunächst die Query und der Plan:
set autot on select count(*) from t_2 , t_4 where t_2.col1 = t_4.col1; COUNT(*) ---------- 25000000 Abgelaufen: 00:00:01.29 Ausführungsplan ---------------------------------------------------------- Plan hash value: 3379375143 ---------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 6 | 25 (4)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 6 | | | |* 2 | HASH JOIN | | 1 | 6 | 25 (4)| 00:00:01 | | 3 | TABLE ACCESS FULL| T_2 | 10000 | 30000 | 12 (0)| 00:00:01 | | 4 | TABLE ACCESS FULL| T_4 | 10000 | 30000 | 12 (0)| 00:00:01 | ---------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("T_2"."COL1"="T_4"."COL1")
Laut Plan liefert der HASH JOIN in STEP 2 also nur einen Satz, tatsächlich aber sind es 25000000. Wenn ich die Standard-Formel zur Berechnung von Join-Cardinalities heranziehe, dann liefert sie:
Join Selectivity = ((num_rows(t1) - num_nulls(t1.c1))/num_rows(t1)) * ((num_rows(t2) - num_nulls(t2.c2))/num_rows(t2)) / greater (num_distinct(t1.c1), num_distinct(t2.c2)) Join Cardinality = Join Selectivity * filtered cardinality (t1) * filtered cardinality (t2) Ohne NULL-Werte ergibt sich für die Cardinality: 10000 * 10000 / greatest (2, 4) = 25000000
Das wäre also exakt richtig gewesen. Woher also die 1?
Dazu noch die Statistiken:
select table_name , num_rows from user_tables where table_name in ('T_2', 'T_4'); TABLE_NAME NUM_ROWS ---------- ---------- T_2 10000 T_4 10000 select table_name , column_name , num_distinct , density from user_tab_cols where table_name in ('T_2', 'T_4') and column_name = 'COL1'; TABLE_NAME COLUMN_NAME NUM_DISTINCT DENSITY ---------- ----------- ------------ ---------- T_2 COL1 2 ,5 T_4 COL1 4 ,25
Sieht erst mal ganz plausibel aus - ist es aber natürlich nicht. Deshalb hier noch das CREATE Script zum Fall:
drop table t_2; -- initiale Füllung von T_2 mit Daten, die nicht zu T_4 passen create table t_2 as select rownum id , mod(rownum, 2) + 4 col1 from dual connect by level <= 10000; exec dbms_stats.gather_table_stats(user, 'T_2') delete from t_2; -- erst jetzt kommen die Daten, die zu den Werten in T_4 passen insert into t_2 select rownum id , mod(rownum, 2) col1 from dual connect by level <= 10000; commit; drop table t_4; create table t_4 as select rownum id , mod(rownum, 4) col1 from dual connect by level <= 10000; exec dbms_stats.gather_table_stats(user, 'T_4') select * from user_tables where table_name in ('T_2', 'T_4'); set autot on select count(*) from t_2 , t_4 where t_2.col1 = t_4.col1;
Im praktischen Fall waren die Partitionsstatistiken akkurat, aber die globalen Statistiken mit den HIGH_VALUE und LOW_VALUE-Werten zu den Spalten der Header-Tabelle waren uralt, so dass die Überlappung mit den Spaltenwerten der Positionstabelle irgendwann aufhörte. Für die Positionstabelle hatte ich die Statistiken selbst erzeugt. Relevant waren also eher folgende Statistiken:
select table_name , column_name , last_analyzed , low_value , high_value from user_tab_cols where table_name in ('T_2', 'T_4') and column_name = 'COL1'; TABLE_NAME COLUMN_NAME LAST_ANALYZED LOW_VALUE HIGH_VALUE ---------- ----------- ------------------- -------------------- ---------- T_2 COL1 08.05.2012 18:42:40 C105 C106 T_4 COL1 08.05.2012 18:42:41 80 C104
Wobei das last_analyzed-Datum im Test nicht arg viel aussagt. Aber die Wertebereiche der Spalten sind überschneidungsfrei und das bringt den CBO unmittelbar zum - einleuchtenden - Schluss, dass der Join kein Ergebnis liefern wird (woraus er dann die 1 macht). Einmal mehr genügt es also, korrekte Statistiken zu liefern, um vom CBO korrekte Abschätzungen zu bekommen (wobei im gegebenen Fall aus der 1 dann allerlei unglückliche NL Operationen folgten):
exec dbms_stats.gather_table_stats(user, 'T_2') ---------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 6 | 30 (4)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | 6 | | | |* 2 | HASH JOIN | | 25M| 143M| 30 (4)| 00:00:01 | | 3 | TABLE ACCESS FULL| T_2 | 10000 | 30000 | 17 (0)| 00:00:01 | | 4 | TABLE ACCESS FULL| T_4 | 10000 | 30000 | 12 (0)| 00:00:01 | ---------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("T_2"."COL1"="T_4"."COL1")
Da mir die Header-Tabelle nicht gehört, habe ich allerdings nicht ihre, sondern die Statistiken der Positionstabelle angepasst und dazu dbms_stats.set_column_stats verwendet, wie Tony Hasler es in seinem Blog vor einiger Zeit vorgeführt hat.
Eine Alternative wäre übrigens wahrscheinlich die Anlage von Histogrammen in der Positionstabelle gewesen, wie man (natürlich) bei Jonathan Lewis in Kapitel 10: Join Cardinality von Cost-Based Oracle nachlesen kann (S. 280ff.). In meinem Beispiel funktioniert das allerdings nur, wenn ich für beide Tabellen Histogramme erzeuge, dann allerdings unabhängig von der Überschneidung der Wertebereiche (was vermutlich so nicht beabsichtigt ist).
Keine Kommentare:
Kommentar veröffentlichen