sql >> Database teknologi >  >> RDS >> PostgreSQL

Om nytten af ​​udtryksindekser

Når jeg underviser i PostgreSQL-træninger, både om grundlæggende og avancerede emner, finder jeg ofte ud af, at deltagerne har meget lidt idé om, hvor kraftfulde udtryksindekserne kan være (hvis de overhovedet er opmærksomme på dem). Så lad mig give dig et kort overblik.

Så lad os sige, at vi har en tabel med en række tidsstempler (ja, vi har en genereringsseriefunktion, der kan generere datoer):

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;

Bordet indeholder også en polstringssøjle, for at gøre det lidt større. Lad os nu lave en simpel intervalforespørgsel og vælge kun en måned fra de ~200 år, der er inkluderet i tabellen. Hvis du forklarer forespørgslen, vil du se noget som dette:

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)

og på min bærbare computer kører dette i ~20ms. Ikke dårligt, i betragtning af at dette skal gå gennem hele tabellen med ~75.000 rækker.

Men lad os oprette et indeks i tidsstempelkolonnen (alle indekser her er standardtypen, dvs. btree, medmindre det er nævnt eksplicit):

CREATE INDEX idx_t_d ON t (d);

Og lad os nu prøve at køre forespørgslen igen:

                               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)

og dette kører på 0,5 ms, så cirka 40 gange hurtigere. Men det var selvfølgelig et simpelt indeks, oprettet direkte på kolonnen, ikke udtryksindeks. Så lad os antage, at vi i stedet skal vælge data fra hver 1. dag i hver måned ved at lave en forespørgsel som denne

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

som dog ikke kan bruge indekset, da det skal evaluere et udtryk på kolonnen, mens indekset er bygget på selve kolonnen, som vist på 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)

Så dette skal ikke kun udføre en sekventiel scanning, det skal også udføre evalueringen, hvilket øger forespørgselsvarigheden til 43 ms.

Databasen kan ikke bruge indekset af flere årsager. Indekser (mindst btree-indekser) er afhængige af forespørgsel efter sorterede data, leveret af den trælignende struktur, og selvom intervalforespørgslen kan drage fordel af det, kan den anden forespørgsel (med "udtræk"-kald) ikke.

Bemærk:Et andet problem er, at det sæt af operatører, der understøttes af indekser (dvs. som kan evalueres direkte på indekser) er meget begrænset. Og "udtræk"-funktionen er ikke understøttet, så forespørgslen kan ikke omgå bestillingsproblemet ved at bruge en Bitmap Index Scan.

I teorien kan databasen forsøge at transformere tilstanden til rækkeviddebetingelser, men det er ekstremt vanskeligt og specifikt for udtryk. I dette tilfælde er vi nødt til at generere et uendeligt antal af sådanne "per-dag"-intervaller, fordi planlæggeren ikke rigtig kender min/max tidsstempler i tabellen. Så databasen forsøger ikke engang.

Men selvom databasen ikke ved, hvordan man transformerer forholdene, gør udviklere det ofte. For eksempel med forhold som

(column + 1) >= 1000

det er ikke svært at omskrive det sådan her

column >= (1000 - 1)

hvilket fungerer fint med indekserne.

Men hvad nu hvis en sådan transformation ikke er mulig, som for eksempel for eksempelforespørgslen

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

I dette tilfælde ville udvikleren skulle stå over for det samme problem med ukendt min/maks for d-kolonnen, og selv da ville det generere en masse intervaller.

Nå, dette blogindlæg handler om udtryksindekser, og indtil videre har vi kun brugt almindelige indekser, bygget på spalten direkte. Så lad os oprette det første udtryksindeks:

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

som så giver os denne forklarende plan

                               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)

Så selvom dette ikke giver os den samme 40x speedup som indekset i det første eksempel, er det lidt forventet, da denne forespørgsel returnerer langt flere tuples (2401 vs. 32). Desuden er de spredt gennem hele tabellen og ikke så lokaliserede som i det første eksempel. Så det er en pæn 2x speedup, og i mange tilfælde i den virkelige verden vil du se meget større forbedringer.

Men muligheden for at bruge indekser til forhold med komplekse udtryk er ikke den mest interessante information her - det er lidt grunden til, at folk opretter udtryksindekser. Men det er ikke den eneste fordel.

Hvis du ser på de to forklarende planer præsenteret ovenfor (uden og med udtryksindekset), vil du måske bemærke dette:

                               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)
 ...

Højre – oprettelse af udtryksindekset forbedrede estimaterne væsentligt. Uden indekset har vi kun statistik (MCV + histogram) for rå tabelkolonner, så databasen ved ikke, hvordan man estimerer udtrykket

EXTRACT(day FROM d) = 1

Så den anvender i stedet et standardestimat for lighedsforhold, som er 0,5 % af alle rækker – da tabellen har 73050 rækker, ender vi med et estimat på blot 365 rækker. Det er almindeligt at se meget værre estimeringsfejl i applikationer fra den virkelige verden.

Med indekset indsamlede databasen dog også statistik over kolonner i indekset, og i dette tilfælde indeholder kolonnen resultater af udtrykket. Og under planlægningen bemærker optimeringsværktøjet dette og producerer meget bedre skøn.

Dette er en stor fordel og kan hjælpe med at rette nogle tilfælde af dårlige forespørgselsplaner forårsaget af unøjagtige estimater. Alligevel er de fleste mennesker ikke klar over dette praktiske værktøj.

Og anvendeligheden af ​​dette værktøj steg kun med introduktionen af ​​JSONB-datatypen i 9.4, fordi det er omtrent den eneste måde at indsamle statistik om indholdet af JSONB-dokumenterne på.

Ved indeksering af JSONB-dokumenter findes der to grundlæggende indekseringsstrategier. Du kan enten oprette et GIN/GiST indeks på hele dokumentet, f.eks. sådan her

CREATE INDEX ON t USING GIN (jsonb_column);

som giver dig mulighed for at forespørge på vilkårlige stier i JSONB-kolonnen, bruge indeslutningsoperator til at matche underdokumenter osv. Det er fantastisk, men du har stadig kun den grundlæggende statistik pr. kolonne, som ikke er særlig nyttig som dokumenter. behandles som skalære værdier (og ingen matcher hele dokumenter eller bruger en række dokumenter).

Udtryksindekser, for eksempel oprettet sådan her:

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

vil kun være nyttig for det bestemte udtryk, dvs. dette nyoprettede indeks vil være nyttigt for

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

men ikke for forespørgsler, der får adgang til andre JSON-nøgler, som for eksempel 'værdi'

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

Dermed ikke sagt, at GIN/GiST-indekser på hele dokumentet er ubrugelige, men du skal vælge. Enten opretter du et fokuseret udtryksindeks, nyttigt, når du forespørger på en bestemt nøgle og med den ekstra fordel af statistik om udtrykket. Eller du opretter et GIN/GiST-indeks på hele dokumentet, der kan håndtere forespørgsler på vilkårlige nøgler, men uden statistik.

Men du kan få en kage og spise den også i dette tilfælde, fordi du kan oprette begge indekser på samme tid, og databasen vil vælge, hvilken af ​​dem der skal bruges til individuelle forespørgsler. Og du får nøjagtige statistikker takket være udtryksindekserne.

Desværre kan du ikke spise hele kagen, fordi udtryksindekser og GIN/GiST-indekser bruger forskellige betingelser

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

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

så planlæggeren ikke kan bruge dem på samme tid – udtryksindekser til estimering og GIN/GiST til udførelse.


  1. SQLite Sum() vs Total():Hvad er forskellen?

  2. Laravel Eloquent vælg alle rækker med max created_at

  3. Bedste måde at få PK Guide af indsat række

  4. sammenføj to forskellige tabeller og fjern duplikerede poster