sql >> Database >  >> RDS >> PostgreSQL

Over het nut van expressie-indexen

Bij het geven van PostgreSQL-trainingen, zowel over basis- als over geavanceerde onderwerpen, kom ik er vaak achter dat de deelnemers heel weinig idee hebben hoe krachtig de expressie-indexen kunnen zijn (als ze zich er al van bewust zijn). Dus laat me je een kort overzicht geven.

Laten we zeggen dat we een tabel hebben met een reeks tijdstempels (ja, we hebben de functie Genereer_serie die datums kan genereren):

CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

De tafel heeft ook een opvulkolom, om hem wat groter te maken. Laten we nu een eenvoudige bereikquery doen, waarbij we slechts één maand selecteren uit de ~200 jaar die in de tabel zijn opgenomen. Als je de vraag uitlegt, zie je zoiets als dit:

EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

en op mijn laptop werkt dit in ~20ms. Niet slecht, aangezien dit de hele tafel moet doorlopen met ~75k rijen.

Maar laten we een index maken op de tijdstempelkolom (alle indexen hier zijn van het standaardtype, d.w.z. btree, tenzij expliciet vermeld):

CREATE INDEX idx_t_d ON t (d);

En laten we nu proberen de query opnieuw uit te voeren:

                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

en dit werkt in 0,5 ms, dus ongeveer 40x sneller. Maar dat was natuurlijk een eenvoudige index, rechtstreeks op de kolom gemaakt, geen expressie-index. Laten we dus aannemen dat we in plaats daarvan gegevens van elke eerste dag van elke maand moeten selecteren, door een zoekopdracht als deze uit te voeren

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

die echter de index niet kan gebruiken, omdat het een uitdrukking op de kolom moet evalueren terwijl de index op de kolom zelf is gebouwd, zoals weergegeven in de EXPLAIN ANALYZE:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Dit moet dus niet alleen een sequentiële scan uitvoeren, het moet ook de evaluatie uitvoeren, waardoor de duur van de query wordt verlengd tot 43 ms.

De database kan de index om meerdere redenen niet gebruiken. Indexen (tenminste btree-indexen) zijn afhankelijk van het doorzoeken van gesorteerde gegevens, geleverd door de boomachtige structuur, en hoewel de bereikquery daarvan kan profiteren, kan de tweede query (met `extract`-aanroep) dat niet.

Opmerking:een ander probleem is dat de reeks operatoren die door indexen wordt ondersteund (d.w.z. die rechtstreeks op indexen kunnen worden geëvalueerd) zeer beperkt is. En de functie "uitpakken" wordt niet ondersteund, dus de zoekopdracht kan het bestelprobleem niet omzeilen door een Bitmap Index Scan te gebruiken.

In theorie zou de database kunnen proberen de conditie om te zetten in bereikcondities, maar dat is buitengewoon moeilijk en specifiek voor expressie. In dit geval zouden we een oneindig aantal van dergelijke "per-dag"-bereiken moeten genereren, omdat de planner de min/max-tijdstempels in de tabel niet echt kent. Dus de database probeert het niet eens.

Maar hoewel de database niet weet hoe de omstandigheden moeten worden getransformeerd, doen ontwikkelaars dat vaak wel. Bijvoorbeeld met voorwaarden als

(column + 1) >= 1000

het is niet moeilijk om het zo te herschrijven

column >= (1000 - 1)

wat prima werkt met de indexen.

Maar wat als een dergelijke transformatie niet mogelijk is, zoals bijvoorbeeld voor de voorbeeldquery

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

In dit geval zou de ontwikkelaar hetzelfde probleem moeten hebben met onbekende min/max voor de d-kolom, en zelfs dan zou het veel bereiken genereren.

Welnu, deze blogpost gaat over expressie-indexen en tot nu toe hebben we alleen reguliere indexen gebruikt die rechtstreeks op de kolom zijn gebouwd. Laten we dus de eerste expressie-index maken:

CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

wat ons dan dit uitlegplan geeft

                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Dus hoewel dit ons niet dezelfde 40x snellere snelheid geeft als de index in het eerste voorbeeld, is dat een beetje te verwachten omdat deze zoekopdracht veel meer tuples retourneert (2401 versus 32). Bovendien zijn die verspreid over de hele tabel en niet zo gelokaliseerd als in het eerste voorbeeld. Het is dus een mooie 2x versnelling, en in veel praktijkgevallen zie je veel grotere verbeteringen.

Maar de mogelijkheid om indexen te gebruiken voor voorwaarden met complexe uitdrukkingen is hier niet de meest interessante informatie - dat is een beetje de reden waarom mensen uitdrukkingsindexen maken. Maar dat is niet het enige voordeel.

Als je kijkt naar de twee uitlegplannen die hierboven zijn gepresenteerd (zonder en met de uitdrukkingsindex), zou je dit kunnen opmerken:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Juist - het creëren van de expressie-index verbeterde schattingen aanzienlijk. Zonder de index hebben we alleen statistieken (MCV + histogram) voor onbewerkte tabelkolommen, dus de database weet niet hoe de uitdrukking moet worden geschat

EXTRACT(day FROM d) = 1

Dus in plaats daarvan past het een standaardschatting toe voor gelijkheidsvoorwaarden, die 0,5% van alle rijen is - aangezien de tabel 73050 rijen heeft, eindigen we met een schatting van slechts 365 rijen. Het is gebruikelijk om veel ergere schattingsfouten te zien in toepassingen in de echte wereld.

Met de index verzamelde de database echter ook statistieken over kolommen van de index, en in dit geval bevat de kolom resultaten van de uitdrukking. En tijdens het plannen merkt de optimizer dit op en maakt een veel betere schatting.

Dit is een enorm voordeel en kan helpen bij het oplossen van sommige gevallen van slechte queryplannen veroorzaakt door onnauwkeurige schattingen. Toch zijn de meeste mensen niet op de hoogte van deze handige tool.

En het nut van deze tool nam alleen maar toe met de introductie van JSONB-gegevenstype in 9.4, omdat het ongeveer de enige manier is om statistieken te verzamelen over de inhoud van de JSONB-documenten.

Bij het indexeren van JSONB-documenten zijn er twee basisindexeringsstrategieën. U kunt ofwel een GIN/GiST-index maken voor het hele document, b.v. zoals dit

CREATE INDEX ON t USING GIN (jsonb_column);

waarmee je willekeurige paden in de JSONB-kolom kunt opvragen, de insluitingsoperator kunt gebruiken om subdocumenten te matchen, enz. Dat is geweldig, maar je hebt nog steeds alleen de basisstatistieken per kolom, die
niet erg handig zijn als de documenten worden behandeld als scalaire waarden (en niemand komt overeen met hele documenten of gebruikt een reeks documenten).

Expressie-indexen, bijvoorbeeld als volgt gemaakt:

CREATE INDEX ON t ((jsonb_column->'id'));

zal alleen nuttig zijn voor de specifieke uitdrukking, d.w.z. deze nieuw gemaakte index zal nuttig zijn voor

SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

maar niet voor query's die toegang hebben tot andere JSON-sleutels, zoals 'value' bijvoorbeeld

SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Dit wil niet zeggen dat GIN/GiST-indexen op het hele document nutteloos zijn, maar je moet kiezen. Of u maakt een gerichte expressie-index, handig bij het opvragen van een bepaalde sleutel en met het extra voordeel van statistieken over de expressie. Of u maakt een GIN/GiST-index voor het hele document, die query's op willekeurige sleutels kan verwerken, maar zonder de statistieken.

Maar je kunt in dit geval ook een taart eten en opeten, omdat je beide indexen tegelijkertijd kunt maken, en de database zal kiezen welke van hen wordt gebruikt voor individuele zoekopdrachten. En dankzij de expressie-indexen heb je nauwkeurige statistieken.

Helaas kun je niet de hele taart opeten, omdat expressie-indexen en GIN/GiST-indexen verschillende voorwaarden gebruiken

-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

zodat de planner ze niet tegelijkertijd kan gebruiken – expressie-indexen voor schatting en GIN/GiST voor uitvoering.


  1. Zijn er opties voor een join-tafel voor veel-op-veel verenigingen?

  2. MySQL-fout omzeilen Deadlock gevonden bij het proberen te vergrendelen; probeer de transactie opnieuw te starten

  3. Achterwaartse scan van SQL Server-index:begrijpen, afstemmen

  4. Installeer PostgreSQL op Ubuntu 20.04