Firebird 5.0.1-verbeteringen in de optimizer
(c) D.Simonov, IBSurgeon, 21-aug-2024
Onlangs is een point release van het Firebird 5.0 DBMS uitgebracht Firebird 5.0.1. Naast het oplossen van fouten is er een nieuwe experimentele optimalisatiefunctie toegevoegd, die in dit artikel wordt besproken.
Subquery’s converteren naar ANY/SOME/IN/EXISTS in semi-join
Een semi-join is een bewerking die twee relaties samenvoegt en alleen rijen uit één van de relaties retourneert zonder de volledige join uit te voeren. In tegenstelling tot andere join-operators is er geen expliciete syntaxis om aan te geven of een semi-join moet worden uitgevoerd. U kunt echter een semi-join uitvoeren met behulp van subquery’s in ANY/SOME/IN/EXISTS.
Traditioneel transformeert Firebird subquery’s in ANY/SOME/IN-predicaten naar gecorreleerde subquery’s in het EXISTS-predicaat en voert de subquery in EXISTS uit voor elk record van de buitenste query. Bij het uitvoeren van een subquery binnen een EXISTS-predicaat wordt de FIRST ROWS-strategie gebruikt en stopt de uitvoering onmiddellijk nadat het eerste record is geretourneerd.
Vanaf Firebird 5.0.1 kunnen subquery’s in ANY/SOME/IN/EXISTS-predicaten worden geconverteerd naar semi-joins. Deze functie is standaard uitgeschakeld en kan worden ingeschakeld door de configuratieparameter SubQueryConversion op true te zetten in het bestand firebird.conf of database.conf.
Deze functie is experimenteel en daarom standaard uitgeschakeld. U kunt deze inschakelen en uw query’s met subquery’s in ANY/SOME/IN/EXISTS-predicaten testen. Als de prestaties beter zijn, laat u deze ingeschakeld; anders zet u de parameter SubQueryConversion terug naar de standaardwaarde ( false).De standaardwaarde voor de configuratieparameter SubQueryConversion kan in de toekomst worden gewijzigd, of de parameter kan volledig worden verwijderd. Dit zal gebeuren zodra de nieuwe werkwijze in de meeste gevallen bewezen optimaler te zijn. |
In tegenstelling tot het direct uitvoeren van ANY/SOME/IN/EXISTS op subquery’s, d.w.z. als gecorreleerde subquery’s, biedt het uitvoeren ervan als semi-joins meer ruimte voor optimalisatie. Semi-joins kunnen worden uitgevoerd met verschillende Hash Join (semi)- of Nested Loop Join (semi)-algoritmen, terwijl gecorreleerde subquery’s altijd voor elk record van de buitenste query worden uitgevoerd.
Laten we proberen deze functie in te schakelen door de parameter SubQueryConversion op true te zetten in het bestand firebird.conf. Laten we nu wat experimenten doen.
Laten we de volgende query uitvoeren:
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| | | |
--------------------------------+---------+---------+---------+---------+---------+
In het uitvoeringsplan zien we een nieuwe join-methode Hash Join (semi). Het resultaat van de subquery in IN werd gebufferd, wat in het plan zichtbaar is als Record Buffer (record length: 41). Dat wil zeggen dat in dit geval de subquery in IN één keer werd uitgevoerd, het resultaat in het hashtabelgeheugen werd opgeslagen en de buitenste query vervolgens eenvoudigweg in deze hashtabel zocht.
Ter vergelijking voeren we dezelfde query uit met subquery-naar-semi-join-conversie uitgeschakeld.
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| | | |
--------------------------------+---------+---------+---------+---------+---------+
Het uitvoeringsplan laat zien dat de subquery voor elk record van de hoofdquery wordt uitgevoerd, maar gebruikmaakt van een extra index FK_COVER_FATHER. Dit is ook zichtbaar in de uitvoeringsstatistieken: het aantal Fetches is 4 keer groter en de uitvoeringstijd is bijna 4 keer slechter.
| De lezer vraagt zich misschien af: waarom toont hash semi-join 5 keer meer indexlezingen van de COVER-tabel, maar is het verder beter? Het feit is dat indexlezingen in statistieken het aantal records tonen dat met behulp van de index is gelezen; ze tonen niet het totale aantal index-toegangen, waarvan sommige helemaal geen records ophalen, maar deze toegangen zijn niet gratis. |
Wat is er gebeurd? Om de transformatie van subquery’s beter te begrijpen, introduceren we een denkbeeldige semi-join-operator “SEMI JOIN”. Zoals ik al zei, wordt dit type join niet weergegeven in de SQL-taal. Onze query met de IN-operator werd getransformeerd naar een equivalente vorm, die als volgt kan worden geschreven:
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
Nu is het duidelijker. Hetzelfde gebeurt voor subquery’s die EXISTS gebruiken. Laten we naar een ander voorbeeld kijken:
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
)
Momenteel is het niet mogelijk om zo’n EXISTS met IN te schrijven. Laten we zien hoe het wordt geïmplementeerd zonder transformatie naar een semi-join.
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| | | |
--------------------------------+---------+---------+---------+---------+---------+
Zeer traag. Laten we nu SubQueryConversion = true instellen en de query opnieuw uitvoeren.
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| | | |
--------------------------------+---------+---------+---------+---------+---------+
De query werd 100 keer sneller uitgevoerd! Als we het herschrijven met onze fictieve SEMI JOIN-operator, ziet de query er als volgt uit:
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
Kan elke gecorreleerde subquery in IN/EXISTS worden geconverteerd naar een semi-join? Nee, niet elke; bijvoorbeeld als de subquery FETCH/FIRST/SKIP/ROWS-filters bevat, kan de subquery niet worden geconverteerd naar een semi-join en wordt deze uitgevoerd als een gecorreleerde subquery. Hier is een voorbeeld van zo’n query:
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 verandert de zin OFFSET 0 ROWS de semantiek van de query niet en het resultaat van de uitvoering is hetzelfde als zonder. Laten we naar het plan en de statistieken van deze query kijken.
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
Huidig geheugen = 551912944
Delta geheugen = 288
Max geheugen = 552002112
Verstreken tijd = 0.201 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 408988
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 10971| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Zoals u kunt zien, heeft de transformatie naar een semi-join niet plaatsgevonden. Laten we nu OFFSET 0 ROWS verwijderen en opnieuw statistieken opnemen.
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Filter
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereikscan (volledige match)
-> Record Buffer (recordlengte: 33)
-> Tabel "COVER" Volledige scan
COUNT
=====================
10971
Huidig geheugen = 552112128
Delta geheugen = 288
Max geheugen = 585044592
Verstreken tijd = 0.405 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 854841
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | 722465| | | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Hier heeft de conversie naar semi-join plaatsgevonden, en zoals we kunnen zien is de uitvoeringstijd slechter geworden. De reden is dat de optimizer momenteel geen kostenraming heeft tussen de Hash Join (semi) en Nested Loop Join (semi) join-algoritmen met behulp van een index, dus de regel is: als de join-voorwaarde alleen gelijkheid bevat, wordt het Hash Join (semi)-algoritme gekozen, anders worden de IN/EXISTS-subquery’s op de gebruikelijke manier uitgevoerd.
Laten we nu de semi-join-conversie uitschakelen en naar de uitvoeringsstatistieken kijken.
Sub-query
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap
-> Index "FK_COVER_FATHER" Bereikscan (volledige match)
Select Expression
-> Aggregate
-> Filter
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereikscan (volledige match)
COUNT
=====================
10971
Huidig geheugen = 551912752
Delta geheugen = 288
Max geheugen = 552001920
Verstreken tijd = 0.193 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 408988
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 10971| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Zoals u kunt zien, is Ophaalbewerkingen exact gelijk aan het geval waarin de subquery de clausule OFFSET 0 ROWS bevatte, en de uitvoeringstijd verschilt binnen de foutmarge. Dit betekent dat u de clausule OFFSET 0 ROWS kunt gebruiken als een hint om de semi-join-conversie uit te schakelen.
Laten we nu kijken naar gevallen waarin een andere gecorreleerde voorwaarde dan gelijkheid en IS NOT DISTINCT FROM in subquery’s wordt gebruikt.
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_DEPARTURE = 1
AND EXISTS (
SELECT *
FROM COVER
WHERE COVER.BYDATE > H.BIRTHDAY
)
Sub-query
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap
-> Index "COVER_IDX_BYDATE" Bereikscan (ondergrens: 1/1)
Select Expression
-> Aggregate
-> Filter
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereikscan (volledige match)
Zoals ik hierboven al zei, heeft er geen transformatie naar een semi-join plaatsgevonden; de subquery wordt voor elk record van de hoofdquery uitgevoerd.
Laten we de experimenten voortzetten en een query schrijven met gelijkheid en nog een predicaat naast gelijkheid.
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
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereikscan (volledige match)
-> Filter
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap
-> Index "COVER_IDX_BYDATE" Bereikscan (ondergrens: 1/1)
Hier zien we in het plan het eerste gebruik van de Nested Loop Join (semi)-joinmethode, maar helaas is dit plan slecht, omdat de FK_COVER_FATHER-index niet wordt gebruikt. U zult geen resultaten krijgen van een dergelijke query. Dit kan worden verholpen met de OFFSET 0 ROWS-hint.
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
-> Skip N Records
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap
-> Index "FK_COVER_FATHER" Bereikscan (volledige match)
Select Expression
-> Aggregate
-> Filter
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "FK_HORSE_DEPARTURE" Bereikscan (volledige match)
COUNT
=====================
72199
Huidig geheugen = 554017824
Delta geheugen = 320
Max geheugen = 554284480
Verstreken tijd = 45.548 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 84145713
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 75894621| | | |
HORSE | | 96021| | | |
--------------------------------+---------+---------+---------+---------+---------+
Niet de beste uitvoeringstijd, maar in dit geval hebben we tenminste het resultaat gekregen.
Het omzetten van subquery’s naar ANY/SOME/IN/EXISTS naar semi-join kan dus in sommige gevallen de query-uitvoering aanzienlijk versnellen, maar momenteel is deze functie nog onvolmaakt en daarom standaard uitgeschakeld. In Firebird 6.0 zullen ze proberen kostenraming voor deze functie toe te voegen, evenals een aantal andere tekortkomingen te verhelpen. Bovendien zijn er in Firebird 6.0 plannen om conversie van subquery’s ALL/NOT IN/NOT EXISTS naar anti-join toe te voegen.
Ter afsluiting van de bespreking van de uitvoering van subquery’s in IN/EXISTS, wil ik opmerken dat als u een query van de vorm heeft
SELECT ...
FROM T1
WHERE IN (SELECT field FROM T2 ...)
of
SELECT ...
FROM T1
WHERE EXISTS (SELECT ... FROM T2 WHERE T1. = T2.field)
dergelijke query’s bijna altijd efficiënter zijn om uit te voeren als
SELECT ...
FROM
T1
JOIN (SELECT DISTINCT field FROM T2) tmp ON tmp.field = T1.
Laat me een duidelijk voorbeeld geven:
SELECT
COUNT(*)
FROM
HORSE H
WHERE H.CODE_HORSE IN (
SELECT
CODE_FATHER
FROM COVER
WHERE EXTRACT(YEAR FROM COVER.BYDATE) = 2022
)
Uitvoeringsplan en statistieken met behulp van Hash Join (semi)
Select Expression
-> Aggregate
-> Filter
-> Hash Join (semi)
-> Tabel "HORSE" als "H" Volledige scan
-> Record Buffer (recordlengte: 41)
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereikscan (volledige match)
COUNT
=====================
1616
Huidig geheugen = 554176768
Delta geheugen = 288
Max geheugen = 555531328
Verstreken tijd = 0.229 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 569683
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 6695| | | |
HORSE | 525875| | | | |
--------------------------------+---------+---------+---------+---------+---------+
Redelijk snel, maar de HORSE-tabel wordt volledig gelezen.
Uitvoeringsplan en statistieken met klassieke subquery-uitvoering
Sub-query
-> Filter
-> Filter
-> Tabel "COVER" Toegang op ID
-> Bitmap En
-> Bitmap
-> Index "FK_COVER_FATHER" Bereikscan (volledige match)
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereikscan (volledige match)
Select Expression
-> Aggregate
-> Filter
-> Tabel "HORSE" als "H" Volledige scan
COUNT
=====================
1616
Huidig geheugen = 553472512
Delta geheugen = 288
Max geheugen = 553966592
Verstreken tijd = 6.862 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 2462726
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 1616| | | |
HORSE | 525875| | | | |
--------------------------------+---------+---------+---------+---------+---------+
Zeer traag. De HORSE-tabel wordt volledig gescand en de subquery wordt meerdere keren uitgevoerd - voor elk record in de HORSE-tabel.
En nu een snelle optie met 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 (recordlengte: 44, sleutellengte: 12)
-> Filter
-> Tabel "COVER" als "TMP COVER" Toegang op ID
-> Bitmap
-> Index "IDX_COVER_BYYEAR" Bereikscan (volledige match)
-> Filter
-> Tabel "HORSE" als "H" Toegang op ID
-> Bitmap
-> Index "PK_HORSE" Unieke scan
COUNT
=====================
1616
Huidig geheugen = 554349728
Delta geheugen = 320
Max geheugen = 555531328
Verstreken tijd = 0.011 sec
Buffers = 32768
Leesbewerkingen = 0
Schrijfbewerkingen = 0
Ophaalbewerkingen = 14954
Statistieken per tabel:
--------------------------------+---------+---------+---------+---------+---------+
Tabelnaam | Natuurlijk | Index | Invoegen | Bijwerken | Verwijderen |
--------------------------------+---------+---------+---------+---------+---------+
COVER | | 6695| | | | HORSE | | 1616| | | | ——————————–+———+———+———+———+———+
Geen onnodige reads, de query wordt zeer snel uitgevoerd. Vandaar de conclusie - kijk altijd naar het uitvoeringsplan van subquery's in IN/EXISTS/ANY/SOME, en controleer alternatieve varianten van het schrijven van query's.