PostgreSQL antipaterni: štetni JOIN i OR

Bojte se operacija, buffers koje donose…
Na primjeru malog upita razmotrit ćemo neke univerzalne pristupe optimizaciji upita u PostgreSQL-u. Koristiti ih ili ne — izbor je na vama, ali vrijedi ih upoznati.

U nekim budućim verzijama PG situacija se može promijeniti s "pametnijim" planerom, ali za 9.4/9.6 izgleda otprilike jednako, kao što su primjeri ovdje.

Uzet ću sasvim realan upit:

SELECT
  TRUE
FROM
  "Dokument" d
INNER JOIN
  "DokumentProširenje" doc_ex
    USING("@Dokument")
INNER JOIN
  "TipDokumenta" t_doc ON
    t_doc."@TipDokumenta" = d."TipDokumenta"
WHERE
  (d."Lice3" = 19091 or d."Zaposlenik" = 19091) AND
  d."$Crtica" IS NULL AND
  d."Obrisano" IS NOT TRUE AND
  doc_ex."Stanje"[1] IS TRUE AND
  t_doc."TipDokumenta" = 'PlanRadova'
LIMIT 1;

o imenima tablica i poljaNa "ruske" nazive polja i tablica može se gledati na različite načine, ali to je stvar ukusa. Budući da kod nas u "Tenzoru" nemamo stranih programera, a PostgreSQL nam omogućava davanje naziva čak i hijeroglifima, ako su oni stavljeni u navodnike, preferiramo imenjavanje objekata jasno i razumljivo, kako ne bi došlo do nesporazuma.
Pogledajmo dobiveni plan:
PostgreSQL antipaterni: štetni JOIN i OR
[pogledati na explain.tensor.ru]

144ms i gotovo 53K buffers — dakle više od 400MB podataka! Imat ćemo sreće ako se svi oni nađu u predmemoriji u trenutku našeg upita, inače će trajati mnogo duže pri čitanju s diska.

Algoritam je najvažniji!

Kako bismo na neki način optimizirali bilo koji upit, prvo moramo razumjeti što on zapravo treba raditi.
Za sada ćemo izvan okvira ovog članka ostaviti razvoj same strukture Baze Podataka, i dogovoriti se da možemo relativno "jeftino" prepisati upit i/ili nanijeti na bazu neke potrebne nam indekse.

Dakle, upit:
— provjerava postoji li bilo koji dokument
— u nam stanju i određenog tipa
— gdje je autor ili izvršitelj potreban zaposlenik

JOIN + LIMIT 1

Često je programeru lakše napisati upit u kojem se prvo spoji veći broj tablica, a zatim iz te gomile ostane samo jedan zapis. No lakše za programera — ne znači učinkovitije za Bazu Podataka.
U našem slučaju bilo je samo 3 tablice — a kakav je efekt…

Hajde da se prvo riješimo spajanja s tablicom "TipDokumenta", a usput obavijestimo bazu da je tip zapisa jedinstven (mi to znamo, ali planer još nije shvatio):

WITH T AS (
  SELECT
    "@TipDokumenta"
  FROM
    "TipDokumenta"
  WHERE
    "TipDokumenta" = 'PlanPoslova'
  LIMIT 1
)
...
WHERE
  d."TipDokumenta" = (TABLE T)
...

Da, ako je tabela/CTE sastavljena samo od jednog polja jedne jedine stavke, tada se u PG može pisati i ovako, umjesto

d."TipDokumenta" = (SELECT "@TipDokumenta" FROM T LIMIT 1)

„Lenji” proračuni u upitima PostgreSQL-a

BitmapOr vs UNION

U nekim slučajevima Bitmap Heap Scan će nas koštati vrlo skupo — na primjer, u našoj situaciji, kada se dovoljan broj stavki uklapa u traženi uvjet. Dobili smo to zbog OR-uvjeta koji je postao BitmapOr-operacije u planu.
Vratimo se na izvorni zadatak — potrebno je pronaći stavku koja odgovara bilo kojem od uvjeta — što znači da nije potrebno tražiti svih 59K stavki po oba uvjeta. Postoji način da se prvo obradi jedan uvjet, a na drugi pređe samo kada po prvom ništa ne bude pronađeno. Pomoći će nam takva konstrukcija:

(
  SELECT
    ...
  LIMIT 1
)
UNION ALL
(
  SELECT
    ...
  LIMIT 1
)
LIMIT 1

„Vanjski” LIMIT 1 garantira da će pretraga završiti čim se pronađe prva stavka. I ako se ona pronađe već u prvom bloku, izvršenje drugog neće se odvijati (nikada se ne izvršava u planu).

„Skrivanje pod CASE” složenih uvjeta

U izvornoj upitu postoji izuzetno neprijatan trenutak — provjera stanja prema povezanoj tablici „DokumentProširenje”. Bez obzira na istinitost drugih uvjeta u izrazu (na primjer, d."Obrisan" IS NOT TRUE), ovo spajanje se uvijek izvršava i „košta resurse”. Više ili manje će ih biti potrošeno — ovisi o volumenu ove tablice.
Međutim, moguće je modificirati upit tako da se pretraga povezane stavke odvija samo kada je to doista potrebno:

SELECT
  ...
FROM
  "Dokument" d
WHERE
  ... /*index cond*/ AND
  CASE
    WHEN "$Nacrt" IS NULL AND "Obrisan" IS NOT TRUE THEN (
      SELECT
        "Stanje"[1] IS TRUE
      FROM
        "DokumentProširenje"
      WHERE
        "@Dokument" = d."@Dokument"
    )
  END

Budući da nam iz povezane tablice ni jedno od polja nije potrebno za rezultat, imamo mogućnost pretvoriti JOIN u uvjet koji se odnosi na podupit.
Ostavimo indeksi polja „izvan zagrada” CASE, jednostavne uvjete iz stavke prenosimo u WHEN-blok — a sada se „teški” upit izvršava samo kada dođemo do THEN.

Moje prezime je „Ukupno”

Sastavljamo rezultantni upit sa svim gore opisanim mehanizmima:

S T AS (
  SELECT
    "@TipDokumenta"
  FROM
    "TipDokumenta"
  WHERE
    "TipDokumenta" = 'PlanRadova'
)
  (
    SELECT
      TRUE
    FROM
      "Dokument" d
    WHERE
      ("Lice3", "TipDokumenta") = (19091, (TABLE T)) AND
      CASE
        WHEN "$Nacrt" IS NULL AND "Obrisan" IS NOT TRUE THEN (
          SELECT
            "Stanje"[1] IS TRUE
          FROM
            "DokumentProsirenje"
          WHERE
            "@Dokument" = d."@Dokument"
        )
      END
    LIMIT 1
  )
UNION ALL
  (
    SELECT
      TRUE
    FROM
      "Dokument" d
    WHERE
      ("TipDokumenta", "Zaposlenik") = ((TABLE T), 19091) AND
      CASE
        WHEN "$Nacrt" IS NULL AND "Obrisan" IS NOT TRUE THEN (
          SELECT
            "Stanje"[1] IS TRUE
          FROM
            "DokumentProsirenje"
          WHERE
            "@Dokument" = d."@Dokument"
        )
      END
    LIMIT 1
  )
LIMIT 1;

Prilagodba [za] indekse

Oko je primijetilo da se uvjeti koji se indeksiraju u podblokovima UNION malo razlikuju — to je zato što već imamo odgovarajuće indekse na tablici. A da ih nema — onda bi trebalo stvoriti: Dokument(Lice3, TipDokumenta) i Dokument(TipDokumenta, Zaposlenik).
o redoslijedu polja u ROW-uvjetimaGledajući sa stajališta planera, naravno, može se napisati i (A, B) = (constA, constB), i (B, A) = (constB, constA). No prilikom zapisivanja u redoslijedu polja u indeksu, takav upit je jednostavno lakše debagirati.
Što je u planu?
PostgreSQL antipaterni: štetni JOIN i OR
[pogledati na explain.tensor.ru]

Nažalost, nismo imali sreće, i u prvom UNION-bloku nije ništa pronađeno, pa je drugi ipak otišao na izvršenje. Ali čak i tada — samo 0.037ms i 11 buffer-a!
Poboljšali smo upit i smanjili „prijenos” podataka u memoriji nekoliko tisuća puta, koristeći prilično jednostavne tehnike — dobar rezultat uz malo kopipaste. 🙂

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster