Samstag, Januar 25, 2014

Optimierung durch Umformulierung

Mein Kollege Christian hat mir dieser Tage den Hinweis auf eine neue Webseite zum Thema SQL Optimierung geschickt, den High-Performance SQL Blog, dessen erster Artikel sich mit der Möglichkeit der Optimierung von Count Distinct Operationen durch Auslagerung in In-Line Views beschäftigt. Vorgeführt werden die Effekte am Beispiel Postgres und führen dort nach Aussage der Autoren zu einer 50 fachen Beschleunigung der Operation (was sich im etwas reißerischen Titel Use Subqueries to Count Distinct 50X Faster wiederfindet).

Optimierung durch Umformulierung ist wahrscheinlich die am häufigsten verwendete Optimierungstechnik in relationalen Datenbanken - insbesondere in solchen, deren Instrumentierung nicht besonders umfassend ist, und in RDBMS, deren Optimizer nicht besonders geschickt in der Transformation von Queries zu syntaktisch äquivalenten Varianten ist, die sich besser optimieren lassen. Eine schöne Einführung zu solchen Transformationen für den Fall Oracle hat Stefan Köhler kürzlich in seinem SAP on Oracle Blog veröffentlicht. Dass auch Oracles Optimizer (der das vermutlich besser macht als die meisten Artgenossen in anderen RDBMS) in solchen Fällen nicht unbedingt immer extrem erfolgreich ist, hat Iggy Fernandez gelegentlich gezeigt (anhand eines uralten Beispiels von Fabian Pascal).

Angesichts des Postgres-Beispiels und der dramatischen Performance-Unterschiede habe ich mir jedenfalls die Frage gestellt, was Oracle daraus macht - wobei meine Vermutung war, dass sich alle Varianten gleich verhalten. Dazu der folgende kleine Test:

-- 11.2.0.1
drop table t1;
drop table t2;

create table t1
as
select rownum dashboard_id
     , 'Dashboard ' || rownum name
  from dual
connect by level <= 100;

create table t2
as
with basedata as (
select mod(rownum, 100) dashboard_id
     , mod(rownum, 10000) user_id
  from dual
connect by level < 1000000
)
,
generator as (
select *
  from dual
connect by level <= 10
)
select basedata.*
  from basedata
     , generator;

exec dbms_stats.gather_table_stats(user, 'T1')
exec dbms_stats.gather_table_stats(user, 'T2')

set autot trace

select t1.name
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
 order by 2 desc;
 
select t1.name
     , log_counts.ct
  from t1
  join (select dashboard_id
             , count(distinct user_id) as ct
          from t2 time_on_site_logs
         group by dashboard_id
        ) log_counts
    on log_counts.dashboard_id = t1.dashboard_id
 order by log_counts.ct desc ;
 
select
  t1.name,
  log_counts.ct
from t1
join (
  select distinct_logs.dashboard_id,
  count(1) as ct
  from (
    select distinct dashboard_id, user_id
    from t2
  ) distinct_logs
  group by distinct_logs.dashboard_id
)  log_counts
on log_counts.dashboard_id = t1.dashboard_id
order by log_counts.ct desc ;

set autot off

Nun ein Blick auf die Pläne und Laufzeiten:

SQL> select t1.name
  2       , count(distinct t2.user_id)
  3    from t1
  4    join t2
  5      on t1.dashboard_id = t2.dashboard_id
  6   group by t1.name
  7   order by 2 desc;

99 Zeilen ausgewählt.

Abgelaufen: 00:00:03.34

Ausführungsplan
----------------------------------------------------------
Plan hash value: 344611024

--------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |   100 |  2600 |       | 17232   (1)| 00:01:27 |
|   1 |  SORT ORDER BY         |           |   100 |  2600 |       | 17232   (1)| 00:01:27 |
|   2 |   HASH GROUP BY        |           |   100 |  2600 |       | 17232   (1)| 00:01:27 |
|   3 |    VIEW                | VM_NWVW_1 |   707K|    17M|       | 17229   (1)| 00:01:27 |
|   4 |     HASH GROUP BY      |           |   707K|    15M|   306M| 17229   (1)| 00:01:27 |
|*  5 |      HASH JOIN         |           |  9999K|   219M|       |   659   (1)| 00:00:04 |
|   6 |       TABLE ACCESS FULL| T1        |   100 |  1600 |       |     2   (0)| 00:00:01 |
|   7 |       TABLE ACCESS FULL| T2        |  9999K|    66M|       |   656   (0)| 00:00:04 |
--------------------------------------------------------------------------------------------

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

   5 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")


Statistiken
----------------------------------------------------------
          0  recursive calls
          0  db block gets
      16595  consistent gets
      16389  physical reads
          0  redo size
       3531  bytes sent via SQL*Net to client
        566  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         99  rows processed

SQL> select t1.name
  2       , log_counts.ct
  3    from t1
  4    join (select dashboard_id
  5               , count(distinct user_id) as ct
  6            from t2 time_on_site_logs
  7           group by dashboard_id
  8          ) log_counts
  9      on log_counts.dashboard_id = t1.dashboard_id
 10   order by log_counts.ct desc ;

99 Zeilen ausgewählt.

Abgelaufen: 00:00:02.24

Ausführungsplan
----------------------------------------------------------
Plan hash value: 2847152331

---------------------------------------------------------------------------------------------
| Id  | Operation               | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |           |   100 |  3200 |       |  8948   (1)| 00:00:45 |
|   1 |  SORT ORDER BY          |           |   100 |  3200 |       |  8948   (1)| 00:00:45 |
|*  2 |   HASH JOIN             |           |   100 |  3200 |       |  8947   (1)| 00:00:45 |
|   3 |    TABLE ACCESS FULL    | T1        |   100 |  1600 |       |     2   (0)| 00:00:01 |
|   4 |    VIEW                 |           |   100 |  1600 |       |  8945   (1)| 00:00:45 |
|   5 |     HASH GROUP BY       |           |   100 |  1600 |       |  8945   (1)| 00:00:45 |
|   6 |      VIEW               | VM_NWVW_1 |   707K|    10M|       |  8945   (1)| 00:00:45 |
|   7 |       HASH GROUP BY     |           |   707K|  4833K|   153M|  8945   (1)| 00:00:45 |
|   8 |        TABLE ACCESS FULL| T2        |  9999K|    66M|       |   656   (0)| 00:00:04 |
---------------------------------------------------------------------------------------------

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

   2 - access("LOG_COUNTS"."DASHBOARD_ID"="T1"."DASHBOARD_ID")


Statistiken
----------------------------------------------------------
          0  recursive calls
          0  db block gets
      16595  consistent gets
      16422  physical reads
          0  redo size
       3508  bytes sent via SQL*Net to client
        566  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         99  rows processed

SQL> select
  2    t1.name,
  3    log_counts.ct
  4  from t1
  5  join (
  6    select distinct_logs.dashboard_id,
  7    count(1) as ct
  8    from (
  9      select distinct dashboard_id, user_id
 10      from t2
 11    ) distinct_logs
 12    group by distinct_logs.dashboard_id
 13  )  log_counts
 14  on log_counts.dashboard_id = t1.dashboard_id
 15  order by log_counts.ct desc ;

99 Zeilen ausgewählt.

Abgelaufen: 00:00:02.12

Ausführungsplan
----------------------------------------------------------
Plan hash value: 2377635410

----------------------------------------------------------------------------------------
| Id  | Operation               | Name | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |      |   100 |  3200 |       |  8948   (1)| 00:00:45 |
|   1 |  SORT ORDER BY          |      |   100 |  3200 |       |  8948   (1)| 00:00:45 |
|*  2 |   HASH JOIN             |      |   100 |  3200 |       |  8947   (1)| 00:00:45 |
|   3 |    TABLE ACCESS FULL    | T1   |   100 |  1600 |       |     2   (0)| 00:00:01 |
|   4 |    VIEW                 |      |   100 |  1600 |       |  8945   (1)| 00:00:45 |
|   5 |     HASH GROUP BY       |      |   100 |   300 |       |  8945   (1)| 00:00:45 |
|   6 |      VIEW               |      |   707K|  2071K|       |  8945   (1)| 00:00:45 |
|   7 |       HASH UNIQUE       |      |   707K|  4833K|   153M|  8945   (1)| 00:00:45 |
|   8 |        TABLE ACCESS FULL| T2   |  9999K|    66M|       |   656   (0)| 00:00:04 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("LOG_COUNTS"."DASHBOARD_ID"="T1"."DASHBOARD_ID")

Statistiken
----------------------------------------------------------
          0  recursive calls
          0  db block gets
      16595  consistent gets
      16362  physical reads
          0  redo size
       3508  bytes sent via SQL*Net to client
        566  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         99  rows processed

Die Pläne zeigen: auch Oracle schafft es nicht, die unterschiedlichen Varianten in eine Version zu transformieren - allerdings gibt es keinen großen Unterschied in der Performance: die Version, in der der Join vor der distinkten Sortierung ausgeführt wird, ist etwas langsamer (3,34 sec. gegenüber 2,24 sec. bzw. 2,12 sec.), aber offenbar ist das HASH GROUP BY bei der Sortierung in allen Fällen sehr effizient. Dabei liegen die Unterschiede erwartungsgemäß nicht auf der IO-Seite (also bei den consistent gets), sondern bei der CPU-Nutzung (wobei ich eine genauere Untersuchung der Session-Statistiken ausklammere).

Immerhin ergeben sich folgende Erkenntnisse:
  • die Möglichkeiten der Transformationen sind in Oracle groß, aber es ist nicht schwer, Fälle zu finden, in denen sie nicht genügen.
  • es bleibt eine gute Idee, Filterungen und (eindeutige) Sortierungen möglichst früh durchzuführen, um Zwischenergebnisse klein zu halten.
  • durch HASH GROUP BY sind Gruppierungsoperationen in Oracle sehr effizient (und das gilt sicher auch für den Fall von HASH UNIQUE und Sortierungen) - andere RDBMS haben damit unter Umständen größere Schwierigkeiten.
Was meinem Test fehlt (insbesondere um den dritten Punkt zu untermauern), ist ein Vergleich mit einem identischen Postgres-Beispiel auf gleicher Hardware, aber dazu fühle ich mich gerade nicht verpflichtet (zumal auch die gleiche Hardware nur ein erster Schritt auf dem Weg zur Herstellung vergleichbarer Voraussetzungen wäre - die Konfigurationen der Datenbanken wäre ein zweiter).

Nachtrag 26.01.2014: wahrscheinlich sollte ich gelegentlich damit anfangen, meine Blog-Einträge vollständig zu durchdenken, ehe ich sie veröffentliche. Beim erneuten Nachdenken über mögliche Fälle, in denen die Variante "eindeutige Sortierung vor dem Join" deutlich schneller sein könnte als die "eindeutige Sortierung nach dem Join" fiel mir jedenfalls ein, dass eine deutlich größere Projektion (also Spaltenauswahl im Ergebnis) aus der kleinen Tabelle T1 das Datenvolumen des Joins deutlich vergrößert. Im Beispiel wäre dazu in T1 eine weitere sehr breite Spalte (padding) zu ergänzen:

create table t1
as
select rownum dashboard_id
     , 'Dashboard ' || rownum name
     , lpad('*', 1000, '*') padding
  from dual
connect by level <= 1000;

Dazu gehören dann die folgenden geringfügig angepassten Queries für die Fälle 1 und 2 mit den zugehörigen Autotrace-Statistiken:

select t1.name
     , t1.padding
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
        , t1.padding
 order by 3 desc;

99 Zeilen ausgewählt.

Abgelaufen: 00:00:26.18

Ausführungsplan
----------------------------------------------------------
Plan hash value: 344611024

--------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |    71 | 72917 |       |   495K  (1)| 00:41:15 |
|   1 |  SORT ORDER BY         |           |    71 | 72917 |       |   495K  (1)| 00:41:15 |
|   2 |   HASH GROUP BY        |           |    71 | 72917 |       |   495K  (1)| 00:41:15 |
|   3 |    VIEW                | VM_NWVW_1 |   500K|   489M|       |   494K  (1)| 00:41:15 |
|   4 |     HASH GROUP BY      |           |   500K|   488M|    10G|   494K  (1)| 00:41:15 |
|*  5 |      HASH JOIN         |           |  9999K|  9765M|       |   659   (1)| 00:00:04 |
|   6 |       TABLE ACCESS FULL| T1        |   100 |    99K|       |     2   (0)| 00:00:01 |
|   7 |       TABLE ACCESS FULL| T2        |  9999K|    66M|       |   656   (0)| 00:00:04 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   5 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")

Statistiken
----------------------------------------------------------
          1  recursive calls
          0  db block gets
      16611  consistent gets
      15969  physical reads
          0  redo size
       5612  bytes sent via SQL*Net to client
        566  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         99  rows processed
 
select t1.name
     , t1.padding
     , log_counts.ct
  from t1
  join (select dashboard_id
             , count(distinct user_id) as ct
          from t2 time_on_site_logs
         group by dashboard_id
        ) log_counts
    on log_counts.dashboard_id = t1.dashboard_id
 order by log_counts.ct desc ;

99 Zeilen ausgewählt.

Abgelaufen: 00:00:02.22

Ausführungsplan
----------------------------------------------------------
Plan hash value: 1087308416

---------------------------------------------------------------------------------------------
| Id  | Operation               | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |           |   100 |   100K|       |  8948   (1)| 00:00:45 |
|   1 |  SORT ORDER BY          |           |   100 |   100K|       |  8948   (1)| 00:00:45 |
|*  2 |   HASH JOIN             |           |   100 |   100K|       |  8947   (1)| 00:00:45 |
|   3 |    VIEW                 |           |   100 |  1600 |       |  8945   (1)| 00:00:45 |
|   4 |     HASH GROUP BY       |           |   100 |  1600 |       |  8945   (1)| 00:00:45 |
|   5 |      VIEW               | VM_NWVW_1 |   707K|    10M|       |  8945   (1)| 00:00:45 |
|   6 |       HASH GROUP BY     |           |   707K|  4833K|   153M|  8945   (1)| 00:00:45 |
|   7 |        TABLE ACCESS FULL| T2        |  9999K|    66M|       |   656   (0)| 00:00:04 |
|   8 |    TABLE ACCESS FULL    | T1        |   100 |    99K|       |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("LOG_COUNTS"."DASHBOARD_ID"="T1"."DASHBOARD_ID")

Statistiken
----------------------------------------------------------
         24  recursive calls
          0  db block gets
      16613  consistent gets
      16168  physical reads
          0  redo size
       5589  bytes sent via SQL*Net to client
        566  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         99  rows processed

Während sich die zweite Query nahezu unverändert verhält, wird die erste Query massiv kostspieliger. Der Grund dafür ist, dass der Hash Join in Step 5 ein Zwischenergebnis liefert, das diesmal nicht nur sehr viele Datensätze enthält (knapp 10M rows), sondern auch sehr breite (pro Datensatz kommen die 1000 Byte für padding hinzu). Auf der Basis dieses großen Ergebnisses wird die HASH GROUP BY Operation in Step 4 dann deutlich aufwändiger und erfordert eine sehr viel umfangreichere Zwischenspeicherung während der gruppierenden Sortierung (TempSpc = 10G statt 306M in der ursprünglichen Variante). Im Ergebnis führt diese Mehrarbeit zu einer Erhöhung der Laufzeit über 25 Sekunden - was gegenüber den 2 Sekunden der umformulierten Version dann doch schon recht dramatisch ist. Um die Untersuchung diesmal nicht zu früh abzuschließen hier noch der Plan der beiden Varianten der ersten Query mit rowsource Statistiken:

select /*+ gather_plan_statistics */ 
       t1.name
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
 order by 2 desc;


----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |     99 |00:00:04.31 |   16611 |  16104 |       |       |          |
|   1 |  SORT ORDER BY         |           |      1 |    100 |     99 |00:00:04.31 |   16611 |  16104 |  9216 |  9216 | 8192  (0)|
|   2 |   HASH GROUP BY        |           |      1 |    100 |     99 |00:00:04.31 |   16611 |  16104 |   991K|   991K| 1341K (0)|
|   3 |    VIEW                | VM_NWVW_1 |      1 |    707K|   9900 |00:00:04.31 |   16611 |  16104 |       |       |          |
|   4 |     HASH GROUP BY      |           |      1 |    707K|   9900 |00:00:04.31 |   16611 |  16104 |  1224K|  1224K|   22M (0)|
|*  5 |      HASH JOIN         |           |      1 |   9999K|   9900K|00:00:02.86 |   16611 |  16104 |  1079K|  1079K| 1255K (0)|
|   6 |       TABLE ACCESS FULL| T1        |      1 |    100 |    100 |00:00:00.01 |      18 |      0 |       |       |          |
|   7 |       TABLE ACCESS FULL| T2        |      1 |   9999K|   9999K|00:00:00.92 |   16593 |  16104 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------

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

   5 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")


select /*+ gather_plan_statistics */
       t1.name
     , t1.padding
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
        , t1.padding
 order by 3 desc

----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |     99 |00:00:27.90 |   16611 |  16169 |       |       |          |
|   1 |  SORT ORDER BY         |           |      1 |     71 |     99 |00:00:27.90 |   16611 |  16169 |   124K|   124K|  110K (0)|
|   2 |   HASH GROUP BY        |           |      1 |     71 |     99 |00:00:27.90 |   16611 |  16169 |   684K|   684K| 1310K (0)|
|   3 |    VIEW                | VM_NWVW_1 |      1 |    500K|   9900 |00:00:27.87 |   16611 |  16169 |       |       |          |
|   4 |     HASH GROUP BY      |           |      1 |    500K|   9900 |00:00:27.87 |   16611 |  16169 |    11M|  1824K|   34M (0)|
|*  5 |      HASH JOIN         |           |      1 |   9999K|   9900K|00:00:04.59 |   16611 |  16169 |   684K|   684K| 1269K (0)|
|   6 |       TABLE ACCESS FULL| T1        |      1 |    100 |    100 |00:00:00.01 |      18 |      0 |       |       |          |
|   7 |       TABLE ACCESS FULL| T2        |      1 |   9999K|   9999K|00:00:01.84 |   16593 |  16169 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------

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

   5 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")

Hier bestätigt sich, dass die HASH GROUP BY Operation mit den deutlich vergrößerten Datensätzen mehr Arbeit hat, was sich auf die Laufzeit der Queries auswirkt. Bleibt die Frage, ob das auch meine Behauptung der Effektivität von HASH GROUP BY gegenüber anderen Gruppierungsverfahren in Frage stellt? Dazu ein letzter Versuch: ich deaktiviere HASH GROUP BY für die Session und vergleiche das Verhalten der gerade untersuchten Zugriffe noch einmal:

select /*+ gather_plan_statistics */ t1.name
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
 order by 2 desc;

---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation            | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |      1 |        |     99 |00:00:12.16 |   16611 |  16140 |       |       |          |
|   1 |  SORT ORDER BY       |      |      1 |    100 |     99 |00:00:12.16 |   16611 |  16140 |  9216 |  9216 | 8192  (0)|
|   2 |   SORT GROUP BY      |      |      1 |    100 |     99 |00:00:12.16 |   16611 |  16140 |   761K|   761K|  676K (0)|
|*  3 |    HASH JOIN         |      |      1 |   9999K|   9900K|00:00:03.47 |   16611 |  16140 |  1079K|  1079K| 1236K (0)|
|   4 |     TABLE ACCESS FULL| T1   |      1 |    100 |    100 |00:00:00.01 |      18 |      0 |       |       |          |
|   5 |     TABLE ACCESS FULL| T2   |      1 |   9999K|   9999K|00:00:01.62 |   16593 |  16140 |       |       |          |
---------------------------------------------------------------------------------------------------------------------------

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

   3 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")

select /*+ gather_plan_statistics */
       t1.name
     , t1.padding
     , count(distinct t2.user_id)
  from t1
  join t2
    on t1.dashboard_id = t2.dashboard_id
 group by t1.name
        , t1.padding
 order by 3 desc;

---------------------------------------------------------------------------------------------------------------------------
| Id  | Operation            | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |      1 |        |     99 |00:01:33.00 |   16611 |  16078 |       |       |          |
|   1 |  SORT ORDER BY       |      |      1 |     71 |     99 |00:01:33.00 |   16611 |  16078 |   124K|   124K|  110K (0)|
|   2 |   SORT GROUP BY      |      |      1 |     71 |     99 |00:01:33.00 |   16611 |  16078 |    11M|  2942K|   10M (0)|
|*  3 |    HASH JOIN         |      |      1 |   9999K|   9900K|00:00:03.26 |   16611 |  16078 |   684K|   684K| 1233K (0)|
|   4 |     TABLE ACCESS FULL| T1   |      1 |    100 |    100 |00:00:00.01 |      18 |      0 |       |       |          |
|   5 |     TABLE ACCESS FULL| T2   |      1 |   9999K|   9999K|00:00:01.44 |   16593 |  16078 |       |       |          |
---------------------------------------------------------------------------------------------------------------------------

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

   3 - access("T1"."DASHBOARD_ID"="T2"."DASHBOARD_ID")

Zunächst das Positive: mit dem alten SORT GROUP BY kommt der Optimizer zu akkurateren Schätzungen der cardinalities (jedenfalls bei der Query mit geringer Satzbreite). Dafür wird die Gruppierung aber deutlich langsamer: für den Fall der geringen Satzbreite steigt die Laufzeit von 4,31 sec. auf 12,16 sec. (dass die Laufzeiten bei Erzeugung von rowsource Statistiken höher werden, liegt übrigens an den verwendeten OS-Timer-Funktionen, wie hier erläutert wird). Für die breiteren Datensätze ergeben sich 1:33 min statt 27,9 sec. Im gegebenen Fall ist das also eine Verdreifachung der Laufzeit bei Verzicht auf das HASH GROUP BY.

Was bleibt also festzuhalten?
  • HASH GROUP BY ist ein sehr effektives Gruppierungsverfahren.
  • die bewusste Steuerung von SQL-Verabeitungsschritten bleibt ein wichtiges Mittel der Optimierung. Transformationen sind in ihren Möglichkeiten noch immer beschränkt.
  • Für Performance-Vergleiche ist es immer sinnvoll, die Testszenarien detailliert zu beschreiben, da eine relativ geringfügige Änderung der Versuchsanordnung zu massiven Verhaltensänderungen führen kann. Der im Namen des zugrunde liegenden Artikels erscheinende Faktor 50 mag unter bestimmten Umständen erreichbar sein, ist aber stark vom Szenario abhängig.
Nachtrag 28.01.2014: inzwischen gibt es auch im High Performance SQL Blog einen Nachtrag zum Thema, der sich mit dem Verhalten anderer RDBMS (MySQL, SQL Server, Oracle) beschäftigt und zu ähnlichen Ergebnissen kommt, wie denen, die ich hier veröffentlicht habe: Oracle und der SQL Server sind bei diesen Fragen grundsätzlich sehr viel flotter als die postgres und MySQL, was aber nicht an einer klugen Transformation liegt. Auch für die kommerziellen Systeme bringt die Umformulierung eine - eher moderate - Beschleunigung. Bemerkenswert ist allerdings, dass Postgres bei der Ursprungs-Query dramatisch schlechter abschneidet  als alle anderen RDBMS.

Nachtrag 03.11.2014: dass man auch mit postgres eine sehr viel bessere Performance für den speziellen Fall des ursprünglichen Beispiels erreichen kann, hat Hubert Lubaczewski gelegentlich gezeigt.

Keine Kommentare:

Kommentar veröffentlichen