Sonntag, November 24, 2013

Unsichtbare Spalten

Richard Foote erläutert in seinem Blog die grundlegenden Eigenschaften von invisible columns und weist dabei insbesondere auf einen interessanten Nebeneffekt hin: durch die Änderung der Sichtbarkeit kann man die Reihenfolge der Spalten in der Anzeige ändern. Dazu ein Beispiel:

drop table t;

create table t (
    a number
  , b number 
  , c number
);

insert into t(a, b, c) values (1, 2, 3);

select * 
  from t;

         A          B          C
---------- ---------- ----------
         1          2          3

alter table t modify a invisible;

select * 
  from t;

         B          C
---------- ----------
         2          3

alter table t modify a visible;

select * 
  from t;

         B          C          A
---------- ---------- ----------
         2          3          1

Die Spalte A erscheint nach der erneuten Setzung auf VISIBLE an letzter Stelle. Ein Blick auf die Informationen im data dictionary liefert die zugehörigen Metadaten:

select column_name
     , segment_column_id 
     , column_id
  from dba_tab_cols
 where table_name = 'T'
 order by segment_column_id;

COLUMN_NAME      SEGMENT_COLUMN_ID  COLUMN_ID
---------------- ----------------- ----------
A                                1          3
B                                2          1
C                                3          2

Während sich an der Ablage im Block (natürlich) nichts ändert (also an der segment_column_id), ergibt sich eine veränderte Reihenfolge in der Darstellung (column_id). In manchen Situationen könnte diese Möglichkeit recht nützlich sein.

Donnerstag, November 21, 2013

Suche in Tabellen ähnlicher Struktur

Wieder war es eine Frage im OTN-Forum, die mich an etwas erinnert hat, das ich fast vergessen hatte: die Möglichkeit, mit Hilfe von XML-Funktionen innerhalb einer Query auf alle Tabellen zuzugreifen, die eine Spalte mit einem bestimmten Namen haben. Wenn ich z.B. herausfinden will, wie viele Datensätze in welchen Tabellen einer Bedingung WHERE DEPTNO = 20 genügen, dann erhalte ich die Antwort über:

-- 11.1.0.7
select owner
     , table_name
     , to_number(
          extractvalue(
                    xmltype(
                            dbms_xmlgen.getxml('select count(*) c from '
                                                || owner || '.'|| table_name 
                                                || ' where deptno = 20')
                            )
                    ,'/ROWSET/ROW/C')) row_count
  from dba_tab_cols
 where column_name = 'DEPTNO'
 order by owner, table_name;
 
OWNER                          TABLE_NAME                      ROW_COUNT
------------------------------ ------------------------------ ----------
SCOTT                          DEPT                                    1
SCOTT                          EMP                                     5

Google erinnert mich gerade daran, dass ich einen Verweis auf Marco Gralikes entsprechenden Artikel schon vor zwei Jahren hier untergebracht hatte, damals allerdings ohne Beispiel. Schon damals war dbms_xmlgen anscheinend nicht mehr unbedingt das Mittel der Wahl, erfüllt aber nach wie vor seinen Zweck.

Montag, November 18, 2013

PIVOT und UNPIVOT

Gestern hatte ich hier Frank Kulashs schöne PIVOT+UNPIVOT-Lösung zu einem relativ seltsamen SQL-Problem untergebracht und passend dazu liefert heute Alex Nuijten ein umfassenderes Beispiel zum Thema.

Sonntag, November 17, 2013

Umsortierung von Werten in einem Datensatz

Wieder mal ein aussageschwacher Titel - aber in diesem Fall glaube ich, dass die Fragestellung tatsächlich schwer auf den Punkt zu bringen ist. Worum es geht, ist Folgendes: im OTN-Forum SQL and PL/SQL wurde dieser Tage die Frage gestellt, wie man die Daten der folgenden Tabelle so umordnen kann, dass die nach der ersten Spalte (col_1_rank) sortierte Kombination der ersten beiden Spalten (col_1_rank, col_1_value) mit der nach der dritten Spalte (col_2_rank) sortierten Kombination der Spalten drei und view (col_2_rank, col_2_value) verknüpft wird.

col_1_rank col_1_value col_2_rank col_2_value
1 AAA 3 HHH
2 BBB 2 GGG
3 CCC 5 JJJ
4 DDD 1 FFF
5 EEE 4 III

Als Ergebnis sollte sich also die folgende Tabelle ergeben:

col_1_rank col_1_value col_2_rank col_2_value
1 AAA 1 FFF
2 BBB 2 GGG
3 CCC 3 HHH
4 DDD 4 III
5 EEE 5 JJJ

Dabei galt die zusätzliche Einschränkung, dass zur Umsetzung kein Join verwendet werden durfte, weil nur ein Scan der Tabelle durchgeführt werden sollte.

Nun könnte man sicher einwenden, dass das eine recht bizarre Anforderung ist, und dass der einfache table scan mit komplexeren analytischen Funktionen unter Umständen teurer sein könnte, als ein Join der Daten der beiden logischen Blöcke in der Tabelle - aber als merkwürdige SQL-Fragestellung fand ich die Anforderung interessant genug, um eine Lösung zum Thema beizusteuern:

drop table t;
 
create table t (
    col_1_rank number
  , col_1_value varchar2(20)
  , col_2_rank number
  , col_2_value varchar2(20)
);
 
insert into t values (1, 'AAA', 3, 'HHH');
insert into t values (2, 'BBB', 2, 'GGG');
insert into t values (3, 'CCC', 5, 'JJJ');
insert into t values (4, 'DDD', 1, 'FFF');
insert into t values (5, 'EEE', 4, 'III');
 
with
basedata as (
select col_1_rank
     , col_1_value
     , ',' || listagg(col_2_value, ',') within group (order by col_2_rank) over() || ',' col_2_value
  from t
)
select col_1_rank
     , col_1_value
     , substr( col_2_value
             , instr(col_2_value, ',', 1, col_1_rank) + 1
             , (instr(col_2_value, ',', 1, col_1_rank + 1) - 1) - (instr(col_2_value, ',', 1, col_1_rank))
              ) col_2_value
  from basedata
order by col_1_rank;

COL_1_RANK COL_1_VALUE          COL_2_VALUE
---------- -------------------- ------------------------------
         1 AAA                  FFF
         2 BBB                  GGG
         3 CCC                  HHH
         4 DDD                  III
         5 EEE                  JJJ

Mir schien das als obskure Lösung zu einer obskuren Fragestellung ganz angemessen, aber kurz nach meiner Antwort kam Frank Kulashs Lösungsvorschlag:

SELECT   *
FROM      t
UNPIVOT   (               (mutual_rank, val)
          FOR  label IN ( (col_1_rank,  col_1_value)  AS 1
                        , (col_2_rank,  col_2_value)  AS 2
                        )
          )
PIVOT     (    MIN (val)
          FOR  label  IN ( 1  AS col_1_value
                         , 2  AS col_2_vlaue
                         )
          )
ORDER BY  mutual_rank
;

MUTUAL_RANK COL_1_VALUE          COL_2_VLAUE
----------- -------------------- --------------------
          1 AAA                  FFF
          2 BBB                  GGG
          3 CCC                  HHH
          4 DDD                  III
          5 EEE                  JJJ

-- wobei der UNPIVOT-Abschnitt die eingehenden Datensätze
-- in ihre beiden logischen Abschnitte teilt
-- und die PIVOT-Klausel für die neue Verknüpfung zuständig ist
SELECT   *
FROM      t
UNPIVOT   (               (mutual_rank, val)
          FOR  label IN ( (col_1_rank,  col_1_value)  AS 1
                        , (col_2_rank,  col_2_value)  AS 2
                        )
          );

     LABEL MUTUAL_RANK VAL
---------- ----------- --------------------
         1           1 AAA
         2           3 HHH
         1           2 BBB
         2           2 GGG
         1           3 CCC
         2           5 JJJ
         1           4 DDD
         2           1 FFF
         1           5 EEE
         2           4 III

Da die LISTAGG-Variante spätestens am 4000 Byte-Limit (= maximale Größe für VARCHAR2) des Ergebnisses an ihre Grenzen stößt, ist die PIVOT/UNPIVOT-Lösung deutlich nützlicher. Ohnehin ist es mein Prinzip im OTN-Forum nichts in Threads zu schreiben, an denen der Herr Kulash beteiligt ist, weil der normalerweise ohnehin schon die passende Antwort gegeben hat...

Freitag, November 15, 2013

Vereinfachte Administration für Partitionierte Tabellen in 12c

Gwen Lazenby zeigt im Blog der Oracle University ein paar nette Verbesserungen bei der Administration partitionierter Tabellen, die mit Release 12c eingeführt wurden. Hauptsächlich geht es dabei um die Möglichkeit, diverse Partitionen mit einem einzelnen Kommando zu behandeln - also zu erzeugen, zu splitten, zu verschmelzen oder zu löschen. Dabei enthält der Artikel recht umfangreiche Beispiele. Ich vermute, dass da relativ wenig Magie im Spiel ist - sprich: keine neue interne Logik -, sondern nur eine syntaktische Vereinfachung integriert wurde (habe mir die Details aber nicht angeschaut); aber gerade solche Vereinfachungen ersparen ermüdende Routinearbeit und reduzieren damit auch die Fehleranfälligkeit administrativer Operationen.

Montag, November 11, 2013

Reverse Indizes und Remote-Zugriffe

Jonathan Lewis zeigt ein recht erstaunliches Phänomen, das einerseits merkwürdig und andererseits dokumentiert ist - seit den Tagen von Oracle 8.1.5 gilt: "Reverse indexes on remote tables are not visible to the optimizer. This can prevent nested-loop joins from being used for remote tables if there is an equijoin using a column with only a reverse index." Was man als ein weiteres Argument dafür ansehen kann, reverse key indexes mit Bedacht einzusetzen. Im Artikel wird das Verhalten (natürlich) unter Einsatz eines Beispiels vorgeführt.

Sonntag, November 10, 2013

Bytes Angaben für View-Operatoren im Ausführungsplan

Manchmal stellt mich die Aufgabe der Wahl eines passenden Titels für einen Blog-Eintrag vor unlösbare Schwierigkeiten...

Vor knapp einem Monat hat Randolf Geist einen Artikel mit dem deutlich griffigeren Titel View Data Volume Estimates veröffentlicht, den ich an dieser Stelle kurz zusammenfasse, um die Chance zu erhöhen, mich bei Bedarf an das angesprochenen Verhalten zu erinnern. Worum es geht, ist Folgendes: normalerweise verwendet der Optimizer die column statistics zur Berechnung der bytes-Angabe im execution plan. Im Fall des Auftretens eines View-Operators im Plan für eine unmerged view ist dieses Vorgehen aber nicht möglich und der Optimizer muss stattdessen auf default-Werte ausweichen, die aus den Spaltendefinitionen abgeleitet werden - in der Regel scheint dabei ein Füllgrad von 50% angenommen zu werden (also z.B. 50 Byte für eine Spalte vom Typ VARCHAR2(100)). Im Fall der Verwendung von multi-byte-Zeichensätzen werden die Schätzung daher noch weiter erhöht.

Gefährlich können falsche Schätzungen des Datenvolumens bei allen Operationen sein, die sich auf diese Angabe beziehen, vor allem aber bei HASH JOINs, bei denen die Bestimmung von input und probe set davon abhängt. Im Artikel folgen einige Beispiele, die das Verhalten vorführen. Interessant ist dabei auch der Hinweis, dass in diesem Fall dynamic sampling keine Verbesserung bringt, da dabei ähnliche Kalkulationen ins Spiel kommen wie im Fall der Verwendung von Statistiken. Der Herr Geist schließt mit dem Hinweis, dass das Verhalten auch in 12c unverändert ist.

Mittwoch, November 06, 2013

Postgres: Database Links und Foreign Data Wrapper

Es gibt viele Dinge, die mir an postgres ausgesprochen gut gefallen: die Datenbank hat einen sehr ausgereiften SQL-Dialekt, eine erstaunliche Erweiterbarkeit, zahllose Datentypen und mit psql ein sehr schönes command-line interface. Wenn ich allerdings darüber nachdenke, was mir an diesem RDBMS weniger gut gefällt, dann gehört dazu die Tatsache, dass die Datenbanken eines postgres Clusters ganz strikt voneinander getrennt sind, da ihre data dictionaries nichts voneinander wissen. Das FAQ im PostgreSQL Wiki sagt dazu: "There is no way to directly query a database other than the current one. Because PostgreSQL loads database-specific system catalogs, it is uncertain how a cross-database query should even behave." Wahrscheinlich kann man das Verhalten also theoretisch begründen - gefallen muss es mir deshalb aber noch nicht. Immerhin gibt es aber zwei Möglichkeiten, diese Beschränkung zu umgehen: die Verwendung von database links und den Einsatz eines foreign data wrappers.

Database Link

Die ältere Lösung ist dabei der database link, der es ermöglicht, eine Query zu definieren, die in einer entfernten Datenbank ausgeführt wird. Dabei gibt man eine Connection an und ergänzt das auszuführende Statement sowie eine Typ-Beschreibung der Ergebnisspalten. Dazu ein kleines Beispiel. Zunächst lege ich drei Datenbanken an, von denen zwei die Rolle von Quellsystemen spielen, während die dritte als Reporting-Datenbank fungiert. Im Beispiel liegen alle Datenbanken im gleichen postgres-Cluster (also dem, was man in anderen RDBMS als Instanz bezeichnet), aber sie könnten auch auf entfernten Rechnern und in unterschiedlichen Clustern liegen. Aus Gründen der Vereinfachung habe ich mich auch nicht um eine plausible Benutzerzuordnung gekümmert, sondern agiere überall als postgres:

-- Anlage von Test-Datenbanken
-- Reporting-DB
postgres=# create database dbadmin;
CREATE DATABASE
-- Quell-DBs
postgres=# create database t1;
CREATE DATABASE
postgres=# create database t2;
CREATE DATABASE

-- Quell-Tabellen
t1=# create table t1_tab1 as select generate_series(1, 10) id;
SELECT 10
t2=# create table t2_tab2 as select generate_series(1, 100) id;
SELECT 100

Dazu nun ein Zugriff über database link:

dbadmin=# SELECT *
dbadmin-#   FROM dblink('dbname=t1 user=postgres password=XXXXXX'
dbadmin-#             , 'select id from t1_tab1')
dbadmin-#     AS t1(id int)
dbadmin-# ;
 id
----
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
(10 Zeilen)

In diesem kleinen Beispiel ist das Verfahren noch relativ handhabbar, aber für größere Queries kann es recht mühsam werden, die Spaltendefinitionen aufzuführen. Die Dokumentation empfiehlt, solche database links in Views zu packen, aber beim Zugriff auf eine größere Anzahl von Datenbanken wird auch diese Vorgehensweise sperrig.

Foreign Data Wrapper

Zur Vereinfachung des Vorgehens (und aus einer Reihe weiterer Gründe wie Transaktionsmanagement, Connection-Management und Performance, die in der Dokumentation detailliert erläutert werden) wurde mit postgres 9.3 die extension postgres_fdw in den Standard aufgenommen (für 9.2 gab es einen Backport). Diese Erweiterung erlaubt die Definition umfangreicherer statischer Objekte, die den Zugriff auf entfernte Datenbanken vereinfachen. Das folgende Beispiel basiert auf den Datenbank- und Tabellendefinitionen des database link Beispiels:

-- Installation der extension
dbadmin=# create extension postgres_fdw;
CREATE EXTENSION

-- Anlage von Metadaten-Objekten in der Reporting-Datenbank (DBADMIN)
-- Definition der Server
dbadmin=# CREATE SERVER fdw_t1 FOREIGN DATA WRAPPER postgres_fdw 
dbadmin=# OPTIONS (dbname 't1', host 'localhost');
CREATE SERVER
dbadmin=# CREATE SERVER fdw_t2 FOREIGN DATA WRAPPER postgres_fdw 
dbadmin=# OPTIONS (dbname 't2', host 'localhost');
CREATE SERVER

-- Definition von user-mappings
dbadmin=# CREATE USER MAPPING for postgres
dbadmin-#   SERVER fdw_t1
dbadmin-#   OPTIONS (user 'postgres', password 'XXXXXX');
CREATE USER MAPPING

dbadmin=# CREATE USER MAPPING for postgres
dbadmin-#   SERVER fdw_t2
dbadmin-#   OPTIONS (user 'postgres', password 'XXXXXX');
CREATE USER MAPPING

-- Definition von foreign tables
dbadmin=# CREATE FOREIGN TABLE t1_tab1
dbadmin-#   (
dbadmin(#     id integer
dbadmin(#   )
dbadmin-#   SERVER fdw_t1 OPTIONS (table_name 't1_tab1');
CREATE FOREIGN TABLE

dbadmin=# CREATE FOREIGN TABLE t2_tab2
dbadmin-#   (
dbadmin(#     id integer
dbadmin(#   )
dbadmin-#   SERVER fdw_t2 OPTIONS (table_name 't2_tab2');
CREATE FOREIGN TABLE

-- eine übergreifende Abfrage auf Zieltabellen 
-- aus zwei unterschiedlichen Datenbanken
dbadmin-# select 't2_tab2' table_name, count(*) cnt from t2_tab2;
 table_name | cnt
------------+-----
 t1_tab1    |  10
 t2_tab2    | 100
(2 Zeilen)

Natürlich bleibt auch in diesem Fall das Problem, dass man zur Anlage der foreign tables deren Definitionen im Quellsystem kennen muss, aber um diese Information zu bekommen, könnte man zusätzlich eine foreign table für information_schema.tables einrichten. Nützlich ist dabei auch, dass die Definition für eine foreign table nicht alle Spalten der Quelltabelle enthalten muss, so dass man sich auf die relevanten Angaben beschränken kann:

dbadmin=# CREATE FOREIGN TABLE t2_columns
dbadmin-#   (
dbadmin(#     table_name text
dbadmin(#   , column_name text
dbadmin(#   , data_type text
dbadmin(#   )
dbadmin-# SERVER fdw_t2 OPTIONS (schema_name 'information_schema', table_name 'columns');
CREATE FOREIGN TABLE
dbadmin=# select * from t2_columns limit 10;
  table_name  | column_name | data_type
--------------+-------------+-----------
 pg_statistic | starelid    | oid
 pg_statistic | staattnum   | smallint
 pg_statistic | stainherit  | boolean
 pg_statistic | stanullfrac | real
 pg_statistic | stawidth    | integer
 pg_statistic | stadistinct | real
 pg_statistic | stakind1    | smallint
 pg_statistic | stakind2    | smallint
 pg_statistic | stakind3    | smallint
 pg_statistic | stakind4    | smallint
(10 Zeilen)

Ich will nicht behaupten, dass damit alle Probleme gelöst wären - und finde immer noch, dass die entsprechenden Optionen anderer RDBMS (z.B. database links bei Oracle, linked server im SQL Server) etwas besser zu handhaben sind, aber ich denke, dass postgres_fdw einen signifikanten Fortschritt darstellt.

Übrigens fällt mir gerade noch ein Punkt ein, der mich bei postgres deutlich mehr stört als der gerade ausgeführte, nämlich das Fehlen detaillierter Informationen zu den ausgeführten Queries und ihrer Performance. Aber das ist wieder einmal eine Geschichte, die ein andermal erzählt werden soll.

Dienstag, November 05, 2013

Jonathan Lewis über Parallel Execution

Vor ein paar Wochen hat Jonathan Lewis eine Artikelserie begonnen, in der er erklärt, wie parallele Operationen durchgeführt werden und wie man parallele Pläne interpretieren kann:
  • Parallel Execution – 1
    • weist zunächst darauf hin, dass serielle Pläne in der Regel relativ harmlos bleiben und liefert ein paar Hinweise auf Fälle, in denen das nicht der Fall ist (subquery pushing, Repräsentation von scalar subqueries, Abweichende Reihenfolge für join order und order of operation).
    • entscheidend für parallele Ausführung ist, dass möglichst alle Teile des Plans sinnvoll parallelisierbar sind, damit ein Parallelisierungsgrad von N dazu führen kann, dass die Ausführungszeit auf 1/N sinkt. Dabei gibt es vier mögliche Punkte, die diese einfache Rechnung beeinflussen können:
      • der setup overhead der Parallelisierung ist nicht kostenlos.
      • für parallel query gibt es immer den Schritt der Weitergabe der Daten an das frontend, der eine finale Serialisierung erfordert (und damit ein bottleneck darstellen kann).
      • für eine Query mit Parallelisierungsgrad N werden 2 * N slaves verwendet.
      • für Exadata ergibt sich eine Parallelisierung auf Ebene der storage server unter Umständen auch für serielle Queries.
    • zunächst teilt der query coordinator die Tabelle in N Stücke (bzw. in 13 Stücke, da hier eine - in einem Kommentar des Autors erläuterte - 9:3:1-Strategie eingesetzt wird).
    • für Join-Operationen ist es nicht möglich, sicherzustellen, dass die N Teile der einen Tabelle zu den N Teilen der anderen Tabelle passen. Daher werden zwei sets von N parallel exececution slaves eingesetzt, wobei die erste Gruppe die erste Tabelle so effizient wie möglich einliest und die Ergebnisse an die zweite Gruppe übergibt, wobei ein Mechanismus verwendet wird, der einen möglichst effizienten Zugriff auf die zweite Tabelle (bzw. Datenmenge) gewährleisten soll.
    • eine Variante ist die Weitergabe der kompletten Ergebnismenge aus der ersten Tabelle an alle slaves der zweiten Gruppe. Diese "(broadcast, none)"-Operation ist dann sinnvoll, wenn nur eine sehr beschränkte Datenmenge aus der ersten Tabelle gelesen wird. Für große Datenmenge werden die Kosten der Vervielfältigung jedoch ein Problem.
    • wenn der Zugriff auf die erste Tabelle eine große Ergebnismenge liefert, wird statt der Broadcast-Strategie ein anderes Verfahren verwendet. In diesem Fall wird eine hash Funktion auf die Join-Spalten angewandt, um sicherzustellen, dass die parallel gelesenen Teilbereiche der verknüpften Tabellen zueinander passen. Diese "(hash, hash)"-Operation kann von der Verwendung von Bloom-Filtern profitieren, die eine Filterung vor dem Datentransfer ermöglichen (aber false positives zulassen).
  • Parallel Execution – 2
    • enthält ein handliches Beispiel mit einem Star Schema bestehend aus einer Fakten- und drei zugehörigen Dimensionstabellen, zu dem eine Query mit einem geeigneten seriellen Plan mit mehreren Hash Joins vorgestellt wird. Dieser Plan wird ausführlich erklärt und besteht im wesentlichen daraus, dass zunächst die Hash Maps der Dimensionstabellen im Speicher aufgebaut und dann die Sätze der Faktentabelle gelesen und gegen die Dimensionsdaten geprüft werden, so dass alle rows für die der probe step erfolgreich verläuft, unmittelbar in der Ergebnismenge landen. Der Hash Join ist in diesem Fall also keine blocking operation.
  • Parallel Execution – 3
    • liefert den parallelen Plan zur Query, die im vorangehenden Artikel vorgestellt wurde.
    • dabei werden einige zusätzliche Hints vorgestellt, die zur expliziten Definition des Parellelisierungsverfahrens verwendet werden können:
      • parallel(alias, 2) in Ergänzung zu full(alias): fordert einen parallelen FTS.
      • pq_distribute(alias none broadcast) in Ergänzung zu use_hash(alias) und swap_join_inputs(alias): definiert das parallelisierte Vorgehen beim Hash Join, in dem es die Methode der Weitergabe der slave sets festlegt.
    • es folgt eine detaillierte Analyse des parallelen Zugriffsplans:
      • die zu den FTS gehörigen steps PX BLOCK ITERATOR geben an, dass der parallele Scan über rowid ranges erfolgt.
      • Paare der Operationen PX SEND / PX RECEIVE zeigen an, dass die parallel slaves ihre Ergebnisse an den nächsten Schritt weitergeben. Die Weitergabe erfolgt dabei über virtuelle Tabellen, deren Namen TQ10000, TQ10001 etc. lauten (wobei TQ für table queue steht).
      • die Nummern in den Namen der TQ-Objekte entsprechen der Reihenfolge der Verwendung: TQ10000 wird also als erstes Objekt verwendet, dann TQ10001 usw.
      • die TQ-Spalte im Plan gibt an, welche steps für die Füllung einer virtuellen Tabelle verantwortlich sind: Q1,00 verweist dabei auf TQ10000.
      • abgesehen von der Parallelisierung entspricht die Verarbeitungsfolge im Beispiel recht exakt der Vorgehensweise der seriellen Ausführung.
    • in 12c gibt es weitere Informationen im Plan (etwa zu Bloom-Filtern) und neue Hints (pq_replicate) zur detaillierten Ablaufsteuerung.
  • Parallel Execution – 4
    • erläutert die in Teil 3 erwähnten Bloom-Filter genauer.
    • außerdem werden noch ein paar Details zu den virtual tables und zur parallel execution message size nachgeliefert.
      • wichtig ist zunächst, dass ein parallel slave nicht zur gleichen Zeit aus einer virtual table lesen und in eine andere virtual table schreiben kann.
      • dabei ist eine virtual table grundsätzlich einfach eine (relativ kleine) Menge von Pages im Speicher, deren Größe durch den Parameter _parallel_execution_message_size bestimmt wird (der unterschiedliche Default-Werte in unterschiedlichen Versionen hat und dessen Größe auch noch von anderen Parametern bestimmt wird, dabei zwischen 2K und 16K liegt).
      • Da dieser Speicherbedarf pro communication channel anfällt und jeder slave in einem slave set Daten an jeden slave der anderen slave sets weitergeben kann, ergibt sich in der Summe unter Umständen ein recht hoher Speicherbedarf (errechnet als: * * 3 (= erforderliche Pages) * _parallel_execution_message_size).
      • der Speicherplatz wird im shared pool oder in einem large pool "PX msg pool" bereitgestellt.
      • die relativ geringe Anzahl verfügbarer Pages in jedem channel führt dazu, dass es zu Wait-Events zwischen lesenden und schreibenden Zugriffen kommt. Diese Events sind: "PX Deq Credit: send blkd" und "PX Deq Credit: need buffer".
    • Bloom Filter helfen dabei, das zu transferierende Datenvolumen zu begrenzen und damit die "send blkd" und "need buffer" Waits zu minimieren.
      • besonders relevant ist dieser Filterung beim Hash Join (und dabei insbesondere beim Scan der zweiten Tabelle, deren Inhalte mit der in-memory hash table verglichen werden).
      • dabei wird der Bloom Filter während des Aufbaus der hash table bestückt und kann dann an die slaves, die den Scan der zweiten Tabelle durchführen, weitergegeben werden. Dabei kann ein Bloom Filter bekanntlich false positives enthalten (was in Christian Antogninis Artikel zum Thema genauer erklärt wird, auf den Jonathan Lewis verweist, und den ich hier auch schon gelegentlich erwähnt habe).
      • es folgt ein umfangreiches Beispiel mit Ausführungsplänen. Im Plan erscheint der Bloom Filter als JOIN FILTER CREATE Schritt auf Seite der Erzeugung der hash table und als JOIN FILTER USE beim Scan der zweiten Tabelle. In der predicate section erscheint dabei der Funktionsaufruf sys_op_bloom_filter().
  • Parallel Execution – 5
      • erläutert die Veränderungen, die sich ergeben, wenn man das Verteilungsverfahren von broadcast auf hash umstellt.
      • im Beispiel wird die Änderung des Verfahrens über pq_distribute-Hints hervorgerufen.
      • der Plan mit Hash-Verteilung ist deutlich komplizierter als der broadcast-Plan: es erscheinen sieben virtuelle Tabellen (statt vier) und die Verwendung von Bloom Filtern wird intensiver (wobei die sys_op_bloom_filter-Prädikate nicht vollständig sind).
      • ergänzend zum Plan mit rowsource-Statistiken werden die zugehörigen Informationen auf v$pq_tqstat aufgeführt.
      • Die Analyse folgt wieder der Nummerierung der virtuellen Tabellen. Ich spare mir an dieser Stelle die detaillierte Nacherzählung (die ziemlich ausufernd werden müsste) und erwähne nur, dass die Angaben zu den Bloom-Filtern im Plan nicht notwendig an den richtigen Stellen erscheinen. Zusätzlich gibt's noch eine Grafik zur Visualisierung der Operationen und ihrer Reihenfolge.