Recept för vÄra SQL-frÄgor

Flera mĂ„nader sedan annonserade vi explain.tensor.ru — en offentlig tjĂ€nst för att analysera och visualisera frĂ„geplaner för PostgreSQL.

Under denna tid har du redan anvĂ€nt den över 6000 gĂ„nger, men en av de praktiska funktionerna kan ha förblivit oupptĂ€ckta — det Ă€r strukturella tips, som ser ut ungefĂ€r sĂ„ hĂ€r:

Recept för vÄra SQL-frÄgor

Lyssna pĂ„ dem, och dina frĂ„gor kommer att bli "slĂ€ta och silkeslena". 🙂

Men allvarligt talat, mÄnga situationer som gör en frÄga lÄngsam och "resurskrÀvande" Àr typiska och kan kÀnnas igen genom strukturen och datan i planen..

I sĂ„dana fall behöver varje enskild utvecklare inte söka efter optimeringsalternativ pĂ„ egen hand, baserat enbart pĂ„ sin erfarenhet — vi kan ge honom en ledtrĂ„d om vad som hĂ€nder, vad som kan vara orsaken och hur man kan nĂ€rma sig en lösning. Det har vi gjort.

Recept för vÄra SQL-frÄgor

LĂ„t oss titta lite nĂ€rmare pĂ„ dessa fall — hur de identifieras och vilka rekommendationer de leder till.

För en bÀttre förstÄelse av Àmnet, kan man först lyssna pÄ motsvarande del av mitt föredrag pÄ PGConf.Russia 2020, och först sedan gÄ vidare till en detaljerad analys av varje exempel:

Spela video

#1: ĐžĐœĐŽĐ”ĐșŃĐœĐ°Ń Â«ĐœĐ”ĐŽĐŸŃĐŸŃ€Ń‚ĐžŃ€ĐŸĐČĐșа»

NÀr det uppstÄr

Visa senaste fakturan för kunden "AB Klocka".

Hur man identifierar

-> Limit
   -> Sort
      -> Index [Endast] Scan [BakÄt] | Bitmap Heap Scan

Rekommendationer

AnvÀnd index utöka med sorteringsfÀlt.

Exempel:

SKAPA TABELL tbl SOM
SELECT
  generate_series(1, 100000) pk  -- 100K "fakta"
, (random() * 1000)::integer fk_cli; -- 1K olika externa nycklar

SKAPA INDEX PÅ tbl(fk_cli); -- index för foreign key

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- urval baserat pÄ specifik relation
ORDER BY
  pk DESC -- vi vill bara ha en "senaste" post
LIMIT 1;

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Det kan genast noteras att mer Àn 100 poster lÀstes av indexet, som sedan sorteras och en enda behÄlls.

Vi ÄtgÀrdar:

DROP INDEX tbl_fk_cli_idx;
SKAPA INDEX PÅ tbl(fk_cli, pk DESC); -- tillade sorteringsnyckel

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Även pĂ„ en sĂ„ primitiv urval — 8.5 gĂ„nger snabbare och 33 gĂ„nger fĂ€rre lĂ€sningar. Effekten kommer att bli desto tydligare ju fler du har "fakta" per vĂ€rde fk.

Jag pÄpekar att detta index kommer att fungera som "prefix" lika bra som det tidigare och för andra frÄgor med fk, dÀr sorteringarna pÄ pk inte fanns och inte finns (mer om detta kan du lÀsa i min artikel om att hitta ineffektiva index). Det kommer Àven sÀkerstÀlla normal stöd för explicita foreign key för detta fÀlt.

#2: ĐżĐ”Ń€Đ”ŃĐ”Ń‡Đ”ĐœĐžĐ” ĐžĐœĐŽĐ”ĐșŃĐŸĐČ (BitmapAnd)

NÀr det uppstÄr

Visa alla avtal för kunden «OOO Kolokolchik», ingÄngna i namn av «NAO Lyutik».

Hur man identifierar

-> BitmapAnd
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Rekommendationer

Skapa kompositindex pÄ fÀlten frÄn bÄda kÀllorna eller utvidga en av de befintliga med fÀlt frÄn den andra.

Exempel:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "fakta"
, (random() *  100)::integer fk_org  -- 100 olika externa nycklar
, (random() * 1000)::integer fk_cli; -- 1K olika externa nycklar

CREATE INDEX ON tbl(fk_org); -- index för foreign key
CREATE INDEX ON tbl(fk_cli); -- index för foreign key

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- urval efter ett specifikt par

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Vi ÄtgÀrdar:

DROP INDEX tbl_fk_org_idx;
CREATE INDEX ON tbl(fk_org, fk_cli);

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

HÀr Àr vinsten mindre eftersom Bitmap Heap Scan Àr tillrÀckligt effektiv i sig. Men ÀndÄ 7 gÄnger snabbare och 2.5 gÄnger fÀrre lÀsningar.

#3: ĐŸĐ±ŃŠĐ”ĐŽĐžĐœĐ”ĐœĐžĐ” ĐžĐœĐŽĐ”ĐșŃĐŸĐČ (BitmapOr)

NÀr det uppstÄr

Visa de första 20 Àldsta "egna" eller icke tilldelade Àrendena för behandling, dÀr de egna har prioritet.

Hur man identifierar

-> BitmapOr
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Rekommendationer

AnvÀnd UNION [ALL] för att kombinera underfrÄgor för varje OR-block av villkor.

Exempel:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "fakta"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- med sannolikhet 1:16 gör posten "oavgjort"
    ELSE (random() * 100)::integer -- 100 olika externa nycklar
  END fk_own;

CREATE INDEX ON tbl(fk_own, pk); -- index med "det verkar passa" sortering

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- egna
  fk_own IS NULL -- ... eller "oavgjorda"
ORDER BY
  pk
, (fk_own = 1) DESC -- först "egna"
LIMIT 20;

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Vi ÄtgÀrdar:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- först "egna" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- sedan "oavgjorda" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- men totalt - 20, mer behövs inte

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Vi utnyttjade att alla 20 nödvĂ€ndiga poster redan erhölls i det första blocket, sĂ„ den andra, med en "dyrare" Bitmap Heap Scan, kördes inte ens — slutligen 22 gĂ„nger snabbare, 44 gĂ„nger fĂ€rre lĂ€sningar!

En mer detaljerad berÀttelse om denna optimeringsmetod med konkreta exempel kan lÀsas i artiklar PostgreSQL Antipatterns: skadliga JOIN och OR och PostgreSQL Antipatterns: berÀttelsen om iterativ förbÀttring av sökningen efter namn, eller "Optimering hit och dit".

En generaliserad version av ordnad urval efter flera nycklar (och inte bara efter paret const/NULL) diskuteras i artikeln SQL HowTo: skriver while-loop direkt i frÄgan, eller "ElementÀr trevÀgsoperation".

#4: Ń‡ĐžŃ‚Đ°Đ”ĐŒ ĐŒĐœĐŸĐłĐŸ Đ»ĐžŃˆĐœĐ”ĐłĐŸ

NÀr det uppstÄr

Det uppstÄr vanligtvis nÀr man vill "koppla till Ànnu ett filter" till en redan befintlig frÄga.

"Och har ni inte en likadan, men med pÀrlemorknappar?» film "Den diamantiska handen"

Till exempel, genom att modifiera uppgiften ovan, visa de första 20 Àldsta "kritiska" ansökningarna för behandling, oavsett deras tilldelning.

Hur man identifierar

-> Sekvensanalys | Bitmap Heap Scan | Index [Endast] Scan [BakÄt]
   && 5 × rader < RRbF -- filtrerat >80% av det lĂ€sta
   && loopar × RRbF > 100 -- och dessutom mer Ă€n 100 poster totalt

Rekommendationer

Skapa [mer] specialiserad index med WHERE-villkor eller inkludera ytterligare fÀlt i indexet.

Om filtreringsvillkoret Àr "statiskt" för dina uppgifter - det vill sÀga inte förutser en utvidgning av vÀrden i framtiden - Àr det bÀttre att anvÀnda WHERE-index. Olika boolean/enum-statusar passar bra in i denna kategori.

Om dÀremot filtreringsvillkoret kan ta olika vÀrden, sÄ Àr det bÀttre att utvidga indexet med dessa fÀlt - som i situationen med BitmapAnd ovan.

Exempel:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K "fakta"
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 olika externa nycklar
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, att ansökan Àr "kritisk"

CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);

SELECT
  *
FROM
  tbl
WHERE
  critical
ORDER BY
  pk
LIMIT 20;

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Vi ÄtgÀrdar:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- lade till "statiskt" filtreringsvillkor

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Som vi ser, försvann filtreringen frÄn planen helt, och förfrÄgan blev 5 gÄnger snabbare.

#5: Ń€Đ°Đ·Ń€Đ”Đ¶Đ”ĐœĐœĐ°Ń таблОца

NÀr det uppstÄr

Olika försök att skapa en egen kö för behandlingen av uppgifter, nÀr ett stort antal uppdateringar/borttagningar av poster i tabellen leder till en situation med ett stort antal "döda" poster.

Hur man identifierar

-> Sekvensanalys | Bitmap Heap Scan | Index [Endast] Scan [BakÄt]
   && loopar × (rader + RRbF) < (delad trĂ€ff + delad lĂ€sning) × 8
      -- lÀst mer Àn 1KB per poster
   && delad trÀff + delad lÀsning > 64

Rekommendationer

Regelbundet utföra VACUUM [FULL] eller uppnÄ tillrÀckligt frekvent bearbetning autovacuum genom att justera dess parametrar, inklusive för en specifik tabell.

I de flesta fall orsakas sÄdana problem av dÄlig sammanstÀllning av förfrÄgningar vid anrop frÄn affÀrslogiken, som de som diskuterades i PostgreSQL Antipatterns: kÀmpar med legioner av "döda".

Men man mÄste förstÄ att Àven VACUUM FULL inte alltid kan hjÀlpa. För sÄdana fall Àr det vÀrt att bekanta sig med algoritmen frÄn artikeln DBA: nÀr VACUUM inte rÀcker - stÀdar tabellen manuellt.

#6: Ń‡Ń‚Đ”ĐœĐžĐ” с Â«ŃĐ”Ń€Đ”ĐŽĐžĐœŃ‹Â» ĐžĐœĐŽĐ”Đșса

NÀr det uppstÄr

Det verkar som vi har lÀst lite, och allt i index, och filtrerade inte bort nÄgon overksamhet - men ÀndÄ har betydligt fler sidor lÀsts Àn önskat.

Hur man identifierar

-> Index [Only] Scan [Backward]
   && loops × (rows + RRbF)  64

Rekommendationer

Titta noga pĂ„ strukturen av den anvĂ€nda index och nyckelomrĂ„dena som anges i förfrĂ„gan – det Ă€r troligtvis en del av indexet Ă€r inte angivet. Antagligen behöver du skapa ett liknande index, men utan prefixfĂ€lt eller lĂ€ra dig iterera deras vĂ€rden.

Exempel:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "fakta"
, (random() *  100)::integer fk_org  -- 100 olika externa nycklar
, (random() * 1000)::integer fk_cli; -- 1K olika externa nycklar

CREATE INDEX ON tbl(fk_org, fk_cli); -- nÀstan allt som i #2
-- endast att vi redan anser att en separat index pÄ fk_cli Àr överflödig och har tagit bort den

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- och fk_org Àr inte angivet, Àven om det stÄr tidigare i indexet
LIMIT 20;

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Allt verkar bra, Ă€ven med indexet, men det verkar misstĂ€nkt – varje av de 20 lĂ€sta posterna krĂ€vde att vi lĂ€ste fyra datalag, 32KB per post – Ă€r det inte för mycket? Även namnet pĂ„ indexet tbl_fk_org_fk_cli_idx fĂ„r en att fundera.

Vi ÄtgÀrdar:

CREATE INDEX ON tbl(fk_cli);

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Plötsligt – 10 gĂ„nger snabbare, och 4 gĂ„nger mindre att lĂ€sa!

Andra exempel pÄ ineffektiv anvÀndning av index kan ses i artikeln DBA: hittar oanvÀndbara index.

#7: CTE × CTE

NÀr det uppstÄr

I förfrÄgan har vi skapat "feta" CTE frÄn olika tabeller, och sedan bestÀmde vi oss för att göra en JOIN.

Fallet Àr aktuellt för versioner under v12 eller förfrÄgningar med WITH MATERIALIZED.

Hur man identifierar

-> CTE Scan
   && loops > 10
   && loops × (rows + RRbF) > 10000
      -- för stort kartesiskt produkt av CTE

Rekommendationer

Analysera noga förfrĂ„gan – Ă€r CTE verkligen nödvĂ€ndiga hĂ€r? ЕслО ĐČсД-таĐșĐž Ўа, Ń‚ĐŸ tillĂ€mpa "ordlista" i hstore/json enligt modellen som beskrivs i PostgreSQL Antipatterns: slĂ„ till med ordboken pĂ„ tunga JOIN.

#8: swap ĐœĐ° ЎОсĐș (temp written)

NÀr det uppstÄr

EngÄngshantering (sortering eller unifiering) av ett stort antal poster fÄr inte plats i det avsatta minnet.

Hur man identifierar

-> *
   && temp written > 0

Rekommendationer

Om mÀngden minne som anvÀnds av operationen inte överstiger det angivna vÀrdet för parametern work_mem, bör det justeras. Kan göras direkt i konfigurationen för alla, eller via SET [LOCAL] för specifik förfrÄgan/transaktion.

Exempel:

SHOW work_mem;
-- "16MB"

SELECT
  random()
FROM
  generate_series(1, 1000000)
ORDER BY
  1;

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Vi ÄtgÀrdar:

SET work_mem = '128MB'; -- före utförande av förfrÄgan

Recept för vÄra SQL-frÄgor
[titta pÄ explain.tensor.ru]

Av uppenbara skÀl, om bara minne anvÀnds och inte disk, sÄ kommer ocksÄ förfrÄgan att köras mycket snabbare. Samtidigt avlastas ocksÄ en del av belastningen frÄn HDD.

Men mĂ„ste förstĂ„ att det inte alltid gĂ„r att avsĂ€tta mycket minne – det kommer helt enkelt inte att rĂ€cka till alla.

#9: ĐœĐ”Đ°ĐșŃ‚ŃƒĐ°Đ»ŃŒĐœĐ°Ń статостоĐșа

NÀr det uppstÄr

Det har pumpats in mycket i databasen, men vi hann inte köra igenom det. ANALYZE.

Hur man identifierar

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && ratio >> 10

Rekommendationer

Verkligen genomföra ANALYZE.

Mer detaljerad information om denna situation finns i PostgreSQL Antipatterns: statistik Àr allt..

#10: Â«Ń‡Ń‚ĐŸ-Ń‚ĐŸ ĐżĐŸŃˆĐ»ĐŸ ĐœĐ” таĐș»

NÀr det uppstÄr

Det uppstod en vÀntan pÄ en lÄs som satts av en konkurrerande förfrÄgan, eller sÄ fanns det inte tillrÀckligt med CPU/hypervisor resurser.

Hur man identifierar

-> *
   && (shared hit / 8K) + (shared read / 1K) < time / 1000
      -- RAM hit = 64MB/s, HDD read = 8MB/s
   && time > 100ms -- vi lÀste lite, men det tog för lÄng tid

Rekommendationer

AnvÀnd ett externt övervakningssystem för servrar för att kontrollera förekomsten av lÄsningar eller onormal resursanvÀndning. Vi har redan berÀttat om vÄr metod för att organisera denna process för hundratals servrar. ). Ett typiskt exempel pÄ anvÀndning: och ). Ett typiskt exempel pÄ anvÀndning:.

Recept för vÄra SQL-frÄgor
Recept för vÄra SQL-frÄgor

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster