Firebird 5.0.1 Verbesserungen im Optimierer
(c) D.Simonov, IBSurgeon, 21-Aug-2024
Kürzlich wurde ein Point-Release des Firebird 5.0 DBMS veröffentlicht: Firebird 5.0.1. Neben der Behebung von Fehlern wurde eine neue experimentelle Optimierungsfunktion hinzugefügt, die in diesem Artikel besprochen wird.
Konvertierung von Unterabfragen in ANY/SOME/IN/EXISTS in Semi-Join
Ein Semi-Join ist eine Operation, die zwei Relationen verbindet und nur Zeilen aus einer der Relationen zurückgibt, ohne den gesamten Join auszuführen. Im Gegensatz zu anderen Join-Operatoren gibt es keine explizite Syntax, um anzugeben, ob ein Semi-Join durchgeführt werden soll. Sie können jedoch einen Semi-Join mithilfe von Unterabfragen in ANY/SOME/IN/EXISTS durchführen.
Traditionell transformiert Firebird Unterabfragen in ANY/SOME/IN-Prädikaten in korrelierte Unterabfragen im EXISTS-Prädikat und führt die Unterabfrage in EXISTS für jeden Datensatz der äußeren Abfrage aus. Bei der Ausführung einer Unterabfrage innerhalb eines EXISTS-Prädikats wird die FIRST-ROWS-Strategie verwendet, und die Ausführung stoppt sofort, nachdem der erste Datensatz zurückgegeben wurde.
Ab Firebird 5.0.1 können Unterabfragen in ANY/SOME/IN/EXISTS-Prädikaten in Semi-Joins konvertiert werden. Diese Funktion ist standardmäßig deaktiviert und kann durch Setzen des Konfigurationsparameters SubQueryConversion auf true in der Datei firebird.conf oder database.conf aktiviert werden.
Diese Funktion ist experimentell und daher standardmäßig deaktiviert. Sie können sie aktivieren und Ihre Abfragen mit Unterabfragen in ANY/SOME/IN/EXISTS-Prädikaten testen. Wenn die Leistung besser ist, lassen Sie sie aktiviert; andernfalls setzen Sie den Parameter SubQueryConversion auf den Standardwert ( false) zurück.Der Standardwert für den Konfigurationsparameter SubQueryConversion kann in Zukunft geändert oder der Parameter vollständig entfernt werden. Dies wird geschehen, sobald die neue Vorgehensweise sich in den meisten Fällen als optimaler erwiesen hat. |
Im Gegensatz zur direkten Ausführung von ANY/SOME/IN/EXISTS auf Unterabfragen, d.h. als korrelierte Unterabfragen, bietet die Ausführung als Semi-Joins mehr Raum für Optimierungen. Semi-Joins können mit verschiedenen Algorithmen wie Hash Join (semi) oder Nested Loop Join (semi) durchgeführt werden, während korrelierte Unterabfragen immer für jeden Datensatz der äußeren Abfrage ausgeführt werden.
Lassen Sie uns versuchen, diese Funktion zu aktivieren, indem wir den Parameter SubQueryConversion in der Datei firebird.conf auf true setzen. Jetzt führen wir einige Experimente durch.
Führen wir die folgende Abfrage aus:
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND H.CODE_SEX = 2
AND H.CODE_HORSE IN (
SELECT COVER.CODE_FATHER
FROM COVER
WHERE COVER.CODE_DEPARTURE = 1
AND EXTRACT(YEAR FROM COVER.BYDATE) = 2023
)
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Filter
-> Table "HORSE" as "H" Access By ID
-> Bitmap And
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Range Scan (full match)
-> Bitmap
-> Index "FK_HORSE_SEX" Range Scan (full match)
-> Record Buffer (record length: 41)
-> Filter
-> Table "COVER" Access By ID
-> Bitmap And
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Range Scan (full match)
-> Bitmap
-> Index "FK_COVER_DEPARTURE" Range Scan (full match)
COUNT
=====================
297
Current memory = 552356752
Delta memory = 352
Max memory = 552567920
Elapsed time = 0.045 sec
Buffers = 32768
Reads = 0
Writes = 0
Fetches = 43984
Per table statistics:
--------------------------------+---------+---------+---------+---------+---------+
Table name | Natural | Index | Insert | Update | Delete |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 1516| | | |
HORSE | | 37069| | | |
--------------------------------+---------+---------+---------+---------+---------+
Im Ausführungsplan sehen wir eine neue Join-Methode Hash Join (semi). Das Ergebnis der Unterabfrage in IN wurde gepuffert, was im Plan als Record Buffer (record length: 41) sichtbar ist. Das heißt, in diesem Fall wurde die Unterabfrage in IN einmal ausgeführt, ihr Ergebnis wurde im Speicher der Hash-Tabelle gespeichert, und dann suchte die äußere Abfrage einfach in dieser Hash-Tabelle.
Zum Vergleich führen wir dieselbe Abfrage mit deaktivierter Unterabfrage-zu-Semi-Join-Konvertierung aus.
Sub-query
-> Filter
-> Filter
-> Table "COVER" Access By ID
-> Bitmap And
-> Bitmap
-> Index "FK_COVER_FATHER" Range Scan (full match)
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Range Scan (full match)
Select Expression
-> Aggregate
-> Filter
-> Table "HORSE" as "H" Access By ID
-> Bitmap And
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Range Scan (full match)
-> Bitmap
-> Index "FK_HORSE_SEX" Range Scan (full match)
COUNT
=====================
297
Current memory = 552046496
Delta memory = 352
Max memory = 552135600
Elapsed time = 0.395 sec
Buffers = 32768
Reads = 0
Writes = 0
Fetches = 186891
Per table statistics:
--------------------------------+---------+---------+---------+---------+---------+
Table name | Natural | Index | Insert | Update | Delete |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 297| | | |
HORSE | | 37069| | | |
--------------------------------+---------+---------+---------+---------+---------+
Der Ausführungsplan zeigt, dass die Unterabfrage für jeden Datensatz der Hauptabfrage ausgeführt wird, dabei aber einen zusätzlichen Index FK_COVER_FATHER verwendet. Dies ist auch in den Ausführungsstatistiken sichtbar: Die Anzahl der Fetches ist 4-mal größer, die Ausführungszeit ist fast 4-mal schlechter.
| Der Leser könnte fragen: Warum zeigt der Hash-Semi-Join 5-mal mehr Index-Lesevorgänge der COVER-Tabelle, ist aber ansonsten besser? Tatsache ist, dass Index-Lesevorgänge in den Statistiken die Anzahl der mit dem Index gelesenen Datensätze anzeigen, nicht die Gesamtzahl der Indexzugriffe, von denen einige überhaupt keine Datensätze abrufen - aber diese Zugriffe sind nicht kostenlos. |
Was ist passiert? Um die Transformation von Unterabfragen besser zu verstehen, führen wir einen imaginären Semi-Join-Operator “SEMI JOIN” ein. Wie bereits erwähnt, ist diese Art von Join in der SQL-Sprache nicht repräsentiert. Unsere Abfrage mit dem IN-Operator wurde in eine äquivalente Form transformiert, die wie folgt geschrieben werden kann:
SELECT
COUNT(*)
FROM
HORSE H
SEMI JOIN (
SELECT COVER.CODE_FATHER
FROM COVER
WHERE COVER.CODE_DEPARTURE = 1
AND EXTRACT(YEAR FROM COVER.BYDATE) = 2023
) TMP ON TMP.CODE_FATHER = H.CODE_HORSE
WHERE H.CODE_DEPARTURE = 1
AND H.CODE_SEX = 2
Jetzt ist es klarer. Dasselbe gilt für Unterabfragen mit EXISTS. Schauen wir uns ein weiteres Beispiel an:
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.CODE_DEPARTURE = 1
AND COVER.CODE_FATHER = H.CODE_FATHER
AND COVER.CODE_MOTHER = H.CODE_MOTHER
)
Derzeit ist es nicht möglich, ein solches EXISTS mit IN zu schreiben. Sehen wir uns an, wie es ohne Transformation in einen Semi-Join implementiert wird.
Sub-query
-> Filter
-> Table "COVER" Access By ID
-> Bitmap And
-> Bitmap
-> Index "FK_COVER_MOTHER" Range Scan (full match)
-> Bitmap
-> Index "FK_COVER_FATHER" Range Scan (full match)
Select Expression
-> Aggregate
-> Filter
-> Table "HORSE" as "H" Access By ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Range Scan (full match)
COUNT
=====================
91908
Current memory = 552240400
Delta memory = 352
Max memory = 554680016
Elapsed time = 19.083 sec
Buffers = 32768
Reads = 0
Writes = 0
Fetches = 935679
Per table statistics:
--------------------------------+---------+---------+---------+---------+---------+
Table name | Natural | Index | Insert | Update | Delete |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 91908| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Sehr langsam. Jetzt setzen wir SubQueryConversion = true und führen die Abfrage erneut aus.
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Filter
-> Table "HORSE" as "H" Access By ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Range Scan (full match)
-> Record Buffer (record length: 49)
-> Filter
-> Table "COVER" Access By ID
-> Bitmap
-> Index "FK_COVER_DEPARTURE" Range Scan (full match)
COUNT
=====================
91908
Current memory = 552102000
Delta memory = 352
Max memory = 561520736
Elapsed time = 0.208 sec
Buffers = 32768
Reads = 0
Writes = 0
Fetches = 248009
Per table statistics:
--------------------------------+---------+---------+---------+---------+---------+
Table name | Natural | Index | Insert | Update | Delete |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 140254| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Die Abfrage wurde 100-mal schneller ausgeführt! Wenn wir sie mit unserem fiktiven SEMI-JOIN-Operator umschreiben, sieht die Abfrage wie folgt aus:
SELECT
COUNT(*)
FROM
HORSE H
SEMI JOIN (
SELECT
COVER.CODE_FATHER,
COVER.CODE_MOTHER
FROM COVER
) TMP ON TMP.CODE_FATHER = H.CODE_FATHER AND TMP.CODE_MOTHER = H.CODE_MOTHER
WHERE H.CODE_DEPARTURE = 1
Kann jede korrelierte Unterabfrage in IN/EXISTS in einen Semi-Join konvertiert werden? Nein, nicht jede. Wenn die Unterabfrage beispielsweise FETCH/FIRST/SKIP/ROWS-Filter enthält, kann die Unterabfrage nicht in einen Semi-Join konvertiert werden und wird als korrelierte Unterabfrage ausgeführt. Hier ist ein Beispiel für eine solche Abfrage:
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.CODE_FATHER = H.CODE_HORSE
OFFSET 0 ROWS
)
Hier ändert die Phrase OFFSET 0 ROWS die Semantik der Abfrage nicht, und das Ergebnis ihrer Ausführung ist dasselbe wie ohne sie. Schauen wir uns den Plan und die Statistiken dieser Abfrage an.
Sub-query
-> Skip N Records
-> Filter
-> Table "COVER" Access By ID
-> Bitmap
-> Index "FK_COVER_FATHER" Range Scan (full match)
Select Expression
-> Aggregate
-> Filter
-> Table "HORSE" as "H" Access By ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Range Scan (full match)
COUNT
=====================
10971
Aktueller Speicher = 551912944
Delta-Speicher = 288
Maximaler Speicher = 552002112
Verstrichene Zeit = 0,201 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 408988
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 10971| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Wie Sie sehen können, erfolgte die Umwandlung in einen Semi-Join nicht. Entfernen wir nun OFFSET 0 ROWS und erfassen die Statistiken erneut.
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereichsscan (volle Übereinstimmung)
-> Datensatzpuffer (Datensatzlänge: 33)
-> Tabelle "COVER" Vollscan
COUNT
=====================
10971
Aktueller Speicher = 552112128
Delta-Speicher = 288
Maximaler Speicher = 585044592
Verstrichene Zeit = 0,405 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 854841
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | 722465| | | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Hier erfolgte die Umwandlung in einen Semi-Join, und wie wir sehen können, ist die Ausführungszeit schlechter geworden. Der Grund dafür ist, dass der Optimierer derzeit keine Kostenschätzung zwischen den Join-Algorithmen Hash Join (semi) und Nested Loop Join (semi) unter Verwendung eines Indexes hat. Daher gilt die Regel: Wenn die Join-Bedingung nur Gleichheit enthält, wird der Algorithmus Hash Join (semi) gewählt, andernfalls werden die IN/EXISTS-Unterabfragen wie gewohnt ausgeführt.
Nun deaktivieren wir die Semi-Join-Umwandlung und betrachten die Ausführungsstatistiken.
Sub-query
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap
-> Index "FK_COVER_FATHER" Bereichsscan (volle Übereinstimmung)
Select Expression
-> Aggregate
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereichsscan (volle Übereinstimmung)
COUNT
=====================
10971
Aktueller Speicher = 551912752
Delta-Speicher = 288
Maximaler Speicher = 552001920
Verstrichene Zeit = 0,193 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 408988
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 10971| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Wie Sie sehen können, sind die Abrufe exakt gleich wie im Fall, wenn die Unterabfrage die Klausel OFFSET 0 ROWS enthielt, und die Ausführungszeit unterscheidet sich innerhalb der Fehlertoleranz. Das bedeutet, dass Sie die Klausel OFFSET 0 ROWS als Hinweis verwenden können, um die Semi-Join-Umwandlung zu deaktivieren.
Nun betrachten wir Fälle, in denen in Unterabfragen eine andere korrelierte Bedingung als Gleichheit und IS NOT DISTINCT FROM verwendet wird.
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.BYDATE > H.BIRTHDAY
)
Sub-query
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap
-> Index "COVER_IDX_BYDATE" Bereichsscan (untere Grenze: 1/1)
Select Expression
-> Aggregate
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereichsscan (volle Übereinstimmung)
Wie ich oben sagte, erfolgte keine Umwandlung in einen Semi-Join; die Unterabfrage wird für jeden Datensatz der Hauptabfrage ausgeführt.
Setzen wir die Experimente fort und schreiben eine Abfrage mit Gleichheit und einem weiteren Prädikat außer Gleichheit.
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.CODE_FATHER = H.CODE_FATHER
AND COVER.BYDATE > H.BIRTHDAY
)
Select Expression
-> Aggregate
-> Nested Loop Join (semi)
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereichsscan (volle Übereinstimmung)
-> Filter
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap
-> Index "COVER_IDX_BYDATE" Bereichsscan (untere Grenze: 1/1)
Hier sehen wir im Plan die erste Verwendung der Join-Methode Nested Loop Join (semi), aber leider ist dieser Plan schlecht, weil der Index FK_COVER_FATHER nicht verwendet wird. Sie werden mit einer solchen Abfrage keine Ergebnisse erhalten. Dies kann mit dem Hinweis OFFSET 0 ROWS behoben werden.
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.CODE_FATHER = H.CODE_FATHER
AND COVER.BYDATE > H.BIRTHDAY
OFFSET 0 ROWS
)
Sub-query
-> Überspringe N Datensätze
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap
-> Index "FK_COVER_FATHER" Bereichsscan (volle Übereinstimmung)
Select Expression
-> Aggregate
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereichsscan (volle Übereinstimmung)
COUNT
=====================
72199
Aktueller Speicher = 554017824
Delta-Speicher = 320
Maximaler Speicher = 554284480
Verstrichene Zeit = 45,548 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 84145713
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 75894621| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Nicht die beste Ausführungszeit, aber in diesem Fall haben wir zumindest ein Ergebnis erhalten.
Somit ermöglicht die Umwandlung von Unterabfragen in ANY/SOME/IN/EXISTS in Semi-Joins in einigen Fällen eine erhebliche Beschleunigung der Abfrageausführung, aber derzeit ist diese Funktion noch unvollkommen und daher standardmäßig deaktiviert. In Firebird 6.0 wird versucht, eine Kostenschätzung für diese Funktion hinzuzufügen sowie eine Reihe anderer Mängel zu beheben. Darüber hinaus plant Firebird 6.0 die Umwandlung von Unterabfragen ALL/NOT IN/NOT EXISTS in Anti-Joins.
Zum Abschluss der Betrachtung der Ausführung von Unterabfragen in IN/EXISTS möchte ich anmerken, dass, wenn Sie eine Abfrage der Form haben
SELECT ...
FROM T1
WHERE IN (SELECT field FROM T2 ...)
oder
SELECT ...
FROM T1
WHERE EXISTS (SELECT ... FROM T2 WHERE T1. = T2.field)
dann sind solche Abfragen fast immer effizienter auszuführen als
SELECT ...
FROM
T1
JOIN (SELECT DISTINCT field FROM T2) tmp ON tmp.field = T1.
Lassen Sie mich ein klares Beispiel geben:
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_HORSE IN (
SELECT
CODE_FATHER
FROM COVER
WHERE EXTRACT(YEAR FROM COVER.BYDATE) = 2022
)
Ausführungsplan und Statistiken unter Verwendung von Hash Join (semi)
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Tabelle "HORSE" als "H" Vollscan
-> Datensatzpuffer (Datensatzlänge: 41)
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereichsscan (volle Übereinstimmung)
COUNT
=====================
1616
Aktueller Speicher = 554176768
Delta-Speicher = 288
Maximaler Speicher = 555531328
Verstrichene Zeit = 0,229 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 569683
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 6695| | | |
HORSE | 525875| | | | |
--------------------------------+---------+---------+---------+---------+---------+
Ziemlich schnell, aber die Tabelle HORSE wird vollständig gelesen.
Ausführungsplan und Statistiken mit klassischer Unterabfrageausführung
Sub-query
-> Filter
-> Filter
-> Tabelle "COVER" Zugriff per ID
-> Bitmap Und
-> Bitmap
-> Index "FK_COVER_FATHER" Bereichsscan (volle Übereinstimmung)
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereichsscan (volle Übereinstimmung)
Select Expression
-> Aggregate
-> Filter
-> Tabelle "HORSE" als "H" Vollscan
COUNT
=====================
1616
Aktueller Speicher = 553472512
Delta-Speicher = 288
Maximaler Speicher = 553966592
Verstrichene Zeit = 6,862 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 2462726
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 1616| | | |
HORSE | 525875| | | | |
--------------------------------+---------+---------+---------+---------+---------+
Sehr langsam. Die Tabelle HORSE wird vollständig gescannt, und die Unterabfrage wird mehrfach ausgeführt - für jeden Datensatz in der Tabelle HORSE.
Und nun eine schnelle Option mit DISTINCT
SELECT
COUNT(*)
FROM
HORSE H
JOIN (
SELECT
DISTINCT
CODE_FATHER
FROM COVER
WHERE EXTRACT(YEAR FROM COVER.BYDATE) = 2022
) TMP ON TMP.CODE_FATHER = H.CODE_HORSE
Select Expression
-> Aggregate
-> Nested Loop Join (inner)
-> Unique Sort (Datensatzlänge: 44, Schlüssellänge: 12)
-> Filter
-> Tabelle "COVER" als "TMP COVER" Zugriff per ID
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereichsscan (volle Übereinstimmung)
-> Filter
-> Tabelle "HORSE" als "H" Zugriff per ID
-> Bitmap
-> Index "PK_HORSE" Eindeutiger Scan
COUNT
=====================
1616
Aktueller Speicher = 554349728
Delta-Speicher = 320
Maximaler Speicher = 555531328
Verstrichene Zeit = 0,011 Sek.
Puffer = 32768
Lesevorgänge = 0
Schreibvorgänge = 0
Abrufe = 14954
Statistik pro Tabelle:
--------------------------------+---------+---------+---------+---------+---------+
Tabellenname | Natürlich | Index | Einfügen | Aktualis. | Löschen |
--------------------------------+---------+---------+---------+---------+---------+
```markdown
COVER | | 6695| | | |
HORSE | | 1616| | | |
--------------------------------+---------+---------+---------+---------+---------+
Keine unnötigen Lesevorgänge, die Abfrage wird sehr schnell ausgeführt. Daraus folgt die Schlussfolgerung - schauen Sie sich immer den Ausführungsplan von Unterabfragen in IN/EXISTS/ANY/SOME an und prüfen Sie alternative Varianten der Abfrageformulierung.