När jag undervisar i PostgreSQL-träningar, både om grundläggande och avancerade ämnen, får jag ofta reda på att deltagarna har väldigt liten aning om hur kraftfulla uttrycksindexen kan vara (om de överhuvudtaget är medvetna om dem). Så låt mig ge dig en kort översikt.
Så låt oss säga att vi har en tabell med ett antal tidsstämplar (ja, vi har en genereringsseriefunktion som kan generera datum):
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 innehåller även en stoppningspelare för att göra det lite större. Låt oss nu göra en enkel intervallfråga och välja bara en månad från de ~200 åren som ingår i tabellen. Om du förklarar frågan ser du något i stil med detta:
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)
och på min bärbara dator körs detta på ~20ms. Inte illa, med tanke på att detta måste gå igenom hela tabellen med ~75k rader.
Men låt oss skapa ett index i tidsstämpelkolumnen (alla index här är standardtypen, d.v.s. btree, om det inte nämns uttryckligen):
CREATE INDEX idx_t_d ON t (d);
Och låt oss nu försöka köra frågan 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)
och detta går på 0,5 ms, alltså ungefär 40 gånger snabbare. Men det var förstås ett enkelt index, skapat direkt på kolumnen, inte uttrycksindex. Så låt oss anta att vi istället behöver välja data från varje 1:a dag i varje månad och göra en fråga som denna
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
som dock inte kan använda indexet, eftersom det behöver utvärdera ett uttryck i kolumnen medan indexet är byggt på själva kolumnen, som visas 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å inte bara detta måste göra en sekventiell skanning, det måste också göra utvärderingen, vilket ökar frågelängden till 43 ms.
Databasen kan inte använda indexet av flera skäl. Index (åtminstone btree-index) förlitar sig på att söka efter sorterad data, tillhandahållen av den trädliknande strukturen, och även om intervallfrågan kan dra nytta av det, kan inte den andra frågan (med "extrahera"-anrop).
Notera:Ett annat problem är att uppsättningen operatörer som stöds av index (dvs. som kan utvärderas direkt på index) är mycket begränsad. Och funktionen "extrahera" stöds inte, så frågan kan inte lösa beställningsproblemet genom att använda en Bitmap Index Scan.
I teorin kan databasen försöka omvandla villkoret till intervallförhållanden, men det är extremt svårt och specifikt för uttryck. I det här fallet måste vi generera ett oändligt antal sådana "per-dag"-intervall, eftersom planeraren inte riktigt känner till min/max-tidsstämplarna i tabellen. Så databasen försöker inte ens.
Men även om databasen inte vet hur man förändrar förhållandena, gör utvecklare det ofta. Till exempel med villkor som
(column + 1) >= 1000
det är inte svårt att skriva om det så här
column >= (1000 - 1)
vilket fungerar bra med indexen.
Men tänk om en sådan transformation inte är möjlig, som till exempel för exempelfrågan
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
I det här fallet skulle utvecklaren behöva möta samma problem med okänt min/max för kolumnen d, och även då skulle det generera många intervall.
Jo, det här blogginlägget handlar om uttrycksindex, och hittills har vi bara använt vanliga index, byggda på kolumnen direkt. Så låt oss skapa det första uttrycksindexet:
CREATE INDEX idx_t_expr ON t ((extract(day FROM d))); ANALYZE t;
som sedan ger oss denna förklarande 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å även om detta inte ger oss samma 40x hastighet som indexet i det första exemplet, är det ganska förväntat eftersom den här frågan returnerar mycket fler tuplar (2401 mot 32). De är dessutom spridda över hela tabellen och inte lika lokaliserade som i det första exemplet. Så det är en trevlig 2x snabbare, och i många verkliga fall kommer du att se mycket större förbättringar.
Men möjligheten att använda index för förhållanden med komplexa uttryck är inte den mest intressanta informationen här - det är lite av anledningen till att människor skapar uttrycksindex. Men det är inte den enda fördelen.
Om du tittar på de två förklaringsplanerna som presenteras ovan (utan och med uttrycksindexet), kanske du märker detta:
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öger – att skapa uttrycksindex förbättrade uppskattningarna avsevärt. Utan indexet har vi bara statistik (MCV + histogram) för råtabellkolumner, så databasen vet inte hur man ska uppskatta uttrycket
EXTRACT(day FROM d) = 1
Så den tillämpar istället en standarduppskattning för jämställdhetsförhållanden, som är 0,5 % av alla rader – eftersom tabellen har 73050 rader, slutar vi med en uppskattning på bara 365 rader. Det är vanligt att se mycket värre uppskattningsfel i verkliga applikationer.
Men med indexet samlade databasen också statistik på kolumner i indexet, och i detta fall innehåller kolumnen resultat av uttrycket. Och under planering märker optimeraren detta och ger mycket bättre uppskattning.
Detta är en stor fördel och kan hjälpa till att åtgärda vissa fall av dåliga frågeplaner orsakade av felaktiga uppskattningar. Ändå är de flesta omedvetna om detta praktiska verktyg.
Och användbarheten av detta verktyg ökade bara med introduktionen av JSONB-datatypen i 9.4, eftersom det är ungefär det enda sättet att samla in statistik om innehållet i JSONB-dokumenten.
Vid indexering av JSONB-dokument finns två grundläggande indexeringsstrategier. Du kan antingen skapa ett GIN/GiST-index på hela dokumentet, t.ex. så här
CREATE INDEX ON t USING GIN (jsonb_column);
som låter dig fråga godtyckliga sökvägar i JSONB-kolumnen, använda inneslutningsoperator för att matcha underdokument, etc. Det är bra, men du har fortfarande bara den grundläggande statistiken per kolumn, som inte är särskilt användbar som dokument behandlas som skalära värden (och ingen matchar hela dokument eller använder olika dokument).
Uttrycksindex, till exempel skapade så här:
CREATE INDEX ON t ((jsonb_column->'id'));
kommer bara att vara användbar för det specifika uttrycket, dvs detta nyskapade index kommer att vara användbart för
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;
men inte för frågor som kommer åt andra JSON-nycklar, som till exempel "värde"
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';
Därmed inte sagt att GIN/GiST-index på hela dokumentet är värdelösa, men du måste välja. Antingen skapar du ett fokuserat uttrycksindex, användbart när du frågar efter en viss nyckel och med den extra fördelen av statistik om uttrycket. Eller så skapar du ett GIN/GiST-index på hela dokumentet, som kan hantera frågor på godtyckliga nycklar, men utan statistik.
Men du kan ha en kaka och äta den också, i det här fallet, eftersom du kan skapa båda indexen samtidigt, och databasen kommer att välja vilket av dem som ska användas för individuella frågor. Och du får korrekt statistik, tack vare uttrycksindexen.
Tyvärr kan du inte äta hela kakan, eftersom uttrycksindex och GIN/GiST-index använder olika villkor
-- expression (btree) SELECT * FROM t WHERE jsonb_column ->> 'id' = 123; -- GIN/GiST SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';
så att planeraren inte kan använda dem samtidigt – uttrycksindex för uppskattning och GIN/GiST för exekvering.