Receptes dziedējoÅ”iem SQL vaicājumiem

Pirms dažiem mēneÅ”iem mēs paziņojām explain.tensor.ru — publisks pakalpojums vaicājumu plānu analÄ«zei un vizualizācijai PostgreSQL.

Pa Å”o laiku jÅ«s jau esat to izmantojis vairāk nekā 6000 reizes, taču viena no ērtajām funkcijām varēja palikt nepamanÄ«ta — tas ir strukturālie ieteikumi, kuri izskatās aptuveni Ŕādi:

Receptes dziedējoÅ”iem SQL vaicājumiem

Ieklausieties tajos, un jÅ«su vaicājumi ā€œkļūs gludi un zÄ«dainÄ«giā€. šŸ™‚

Bet, runājot nopietni, daudzas situācijas, kas padara vaicājumu lēnu un ā€œresursu izsalkuÅ”uā€, ir tipiskas un var tikt atpazÄ«tas pēc struktÅ«ras un datiem plānā.

Tādā gadÄ«jumā katram izstrādātājam nebÅ«s jāmeklē optimizācijas variants paÅ”am, paļaujoties tikai uz savu pieredzi — mēs varam viņam norādÄ«t, kas Å”eit notiek, kāda varētu bÅ«t iemesls, un kā var pievērsties risinājumam. Ko mēs arÄ« izdarÄ«jām.

Receptes dziedējoÅ”iem SQL vaicājumiem

ApskatÄ«sim nedaudz sÄ«kāk Å”os gadÄ«jumus — kā tie tiek identificēti un pie kādām rekomendācijām tie noved.

Lai labāk iegrimtu tēma, vispirms var paklausÄ«ties atbilstoÅ”o sadaļu no mana referāta PGConf.Russia 2020, un tikai pēc tam pāriet pie detalizētas katra piemēra analÄ«zes:

Atskaņot video

#1: ŠøŠ½Š“ŠµŠŗŃŠ½Š°Ń «неГосортировка»

Kad rodas

ParādÄ«t pēdējo rēķinu klientam ā€œSIA ZvanÄ«tājsā€.

Kā atpazīt

-> Limit
   -> Sort
      -> Index [Only] Scan [Backward] | Bitmap Heap Scan

Ieteikumi

Izmantojamais indekss paplaŔināt ar kārtoŔanas laukiem.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K ā€œfaktiā€
, (random() * 1000)::integer fk_cli; -- 1K dažādi ārējie atslēgas

CREATE INDEX ON tbl(fk_cli); -- indekss ārējai atslēgai

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- atlase pēc konkrētas saites
ORDER BY
  pk DESC -- vēlamies tikai vienu ā€œpēdējoā€ ierakstu
LIMIT 1;

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Jau uzreiz varam pamanīt, ka pēc indeksa tika izņemtas vairāk nekā 100 ierakstu, kuri vēlāk visi tika kārtoti, un pēc tam tika atstātas vienīgā.

Mēs labojam:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- pievienojām kārtoÅ”anas atslēgu

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Pat uz Ŕādas primitÄ«vas izlases — 8.5 reizes ātrāk un 33 reizes mazāk lasÄ«jumu. Efekts bÅ«s vēl acÄ«mredzamāks, jo vairāk jums bÅ«s ā€œfaktuā€ par katru vērtÄ«bu fk.

PiezÄ«mēju, ka Ŕāds indekss darbosies kā ā€œprefiksētsā€ ne sliktāk nekā iepriekŔējais arÄ« citiem vaicājumiem ar fk, kur kārtoÅ”anas pēc pk nebija un nav (vairāk par to var izlasÄ«t manā rakstā par neefektÄ«vu indeksu meklēŔanu). Ieskaitot to, tas nodroÅ”inās arÄ« normālu atbalstu skaidrai ārējai atslēgai Å”ajā laukā.

#2: пересечение инГексов (BitmapAnd)

Kad rodas

ParādÄ«t visus lÄ«gumus ar klientu Ā«SIA KolokolčiksĀ», kas noslēgti no Ā«NAO LjutikĀ».

Kā atpazīt

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

Ieteikumi

Izveidot kompozÄ«tmateriāla indeksi pēc laukiem no abām avota tabulām vai paplaÅ”ināt vienu no esoÅ”ajiem laukiem no otra.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

IZVEIDOT TABULU tbl KĀ
IZVĒLĒTIES
  ģenerēt_sēriju(1, 100000) pk      -- 100K "faktu"
, (nejauÅ”s() *  100)::integer fk_org  -- 100 dažādi ārējie atslēgas
, (nejauÅ”s() * 1000)::integer fk_cli; -- 1K dažādi ārējie atslēgas

IZVEIDOT INDEKS uz tbl(fk_org); -- indekss ārējai atslēgai
IZVEIDOT INDEKS uz tbl(fk_cli); -- indekss ārējai atslēgai

IZVĒLĒTIES
  *
NO
  tbl
KUR
  (fk_org, fk_cli) = (1, 999); -- atlase pēc konkrētas pāra

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Mēs labojam:

DZĒST INDEKS tbl_fk_org_idx;
IZVEIDOT INDEKS uz tbl(fk_org, fk_cli);

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Šeit ieguvums ir mazāks, jo Bitmap Heap Scan ir pietiekami efektīvs pats par sevi. Bet tomēr ir 7 reizes ātrāk un par 2.5 reizēm mazāk lasījumu.

#3: объеГинение инГексов (BitmapOr)

Kad rodas

Parādīt pirmos 20 visvecākos «savas» vai neizmantotās pieteikumus apstrādei, kur savas ir prioritāte.

Kā atpazīt

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

Ieteikumi

Izmantot UNION [VISU] lai apvienotu apakŔvaicājumus katram no OR bloka nosacījumiem.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

IZVEIDOT TABULU tbl KĀ
IZVĒLĒTIES
  ģenerēt_sēriju(1, 100000) pk  -- 100K "faktu"
, PIEQRITOT
    KAD nejauÅ”s() < 1::real/16 TAD NULL -- ar iespēju 1:16 ieraksts "neizlemts"
    CITAIS (nejauÅ”s() * 100)::integer -- 100 dažādi ārējie atslēgas
  GALA fk_own;

IZVEIDOT INDEKS uz tbl(fk_own, pk); -- indekss ar "it kā piemērotu" secību

IZVĒLĒTIES
  *
NO
  tbl
KUR
  fk_own = 1 VAI -- savi
  fk_own IR NULL -- ... vai "neizlemti"
KĀRTOT
  pk
, (fk_own = 1) DESC -- vispirms "savi"
IERBEŢINĀT 20;

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Mēs labojam:

(
  IZVĒLĒTIES
    *
  NO
    tbl
  KUR
    fk_own = 1 -- vispirms "savi" 20
  KĀRTOT
    pk
  IERBEŢINĀT 20
)
UNION VISU
(
  IZVĒLĒTIES
    *
  NO
    tbl
  KUR
    fk_own IR NULL -- tad "neizlemti" 20
  KĀRTOT
    pk
  IERBEŢINĀT 20
)
IERBEÅ¢INĀT 20; -- bet kopā - 20, vairāk nav nepiecieÅ”ams

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Mēs izmantojām to, ka visi 20 nepiecieÅ”amie ieraksti tika iegÅ«ti jau pirmajā blokā, tāpēc otrs, ar dārgāku Bitmap Heap Scan, pat netika izpildÄ«ts — rezultātā ir 22 reizes ātrāk, par 44 reizēm mazāk lasÄ«jumu!

Detalizētāks stāstÄ«jums par Å”o optimizācijas metodi uz konkrētiem piemēriem var izlasÄ«t rakstos PostgreSQL Antipatterns: kaitÄ«gi JOIN un OR un PostgreSQL Antipatterns: stāsts par iteratÄ«vu izstrādi meklēŔanā pēc nosaukuma, vai "Optimizācija tur un atpakaļ".

Vispārināta versija sakārtotas atlases pēc vairākiem atslēgas (nevis tikai pēc pāra const/NULL) aplÅ«kota rakstā SQL HowTo: rakstām while-cikli tieÅ”i vaicājumā, vai "Elementāra trÄ«ssoļotā".

#4: читаем много лишнего

Kad rodas

Parasti rodas, kad ir vēlme "pievienot vēl vienu filtru" jau esoÅ”ajam vaicājumam.

«Un jums nav tāda paŔa, bet ar perlamutra pogām?» h/f «Dimanta roka»

Piemēram, modificējot iepriekÅ” minēto uzdevumu, parādÄ«t pirmos 20 visvecākās "kritiskās" pieprasÄ«jumu apstrādei, neatkarÄ«gi no to mērÄ·a.

Kā atpazīt

-> SecÄ«gas skenēŔanas | Bitmaņu kaudzes skenēŔana | Indekss [Tikai] skenēŔana [Atpakaļ]
   && 5 Ɨ rindas < RRbF -- filtrēts >80% izlasÄ«tā
   && cikli Ɨ RRbF > 100 -- un tajā paŔā laikā vairāk par 100 ierakstiem kopumā

Ieteikumi

Izveidot [vairāk] specializētu indeksu ar WHERE-nosacījumu vai iekļaut indeksā papildu laukus.

Ja filtrēŔanas nosacÄ«jums ir "statiskas" jÅ«su uzdevumiem — proti, neparedz paplaÅ”ināŔanos vērtÄ«bu saraksta nākotnē — vislabāk izmantot WHERE-indeksu. Å ajā kategorijā lieliski iederas dažādi boolean/enum statusi.

Ja filtrēŔanas nosacÄ«jums var pieņemt dažādas vērtÄ«bas, tad labāk paplaÅ”ināt indeksu ar Å”iem laukiem — kā situācijā ar BitmapAnd iepriekÅ”.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

IZVEIDOT TABULU tbl KĀ
IZVILKT
  ģenerēt_sēriju(1, 100000) pk -- 100K "fakti"
, KAS
    JA nejauŔi() < 1::reāls/16 TAD NULL
    CITĀDI (nejauÅ”i() * 100)::vesels -- 100 dažādas ārējās atsauces
  BEIGĀ fk_own
, (nejauŔi() < 1::reāls/50) kritisks; -- 1:50, ka pieprasījums "kritisks"

IZVEIDOT INDEX uz tbl(pk);
IZVEIDOT INDEX uz tbl(fk_own, pk);

IZVILKT
  *
NO
  tbl
KUR
  kritisks
PASÅŖTÄŖJUMS PĒC
  pk
IEROBEŽOT 20;

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Mēs labojam:

IZVEIDOT INDEX uz tbl(pk)
  KUR kritisks; -- pievienojām "statisku" filtrēŔanas nosacÄ«jumu

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Kā redzam, filtrēŔana no plāna pilnÄ«bā aizgāja, un pieprasÄ«jums kļuva 5 reizes ātrāks.

#5: Ń€Š°Š·Ń€ŠµŠ¶ŠµŠ½Š½Š°Ń таблица

Kad rodas

Dažādi mēģinājumi izveidot savu uzdevumu apstrādes rindu, kad lielais atjauninājumu/izdzēŔanas ierakstu daudzums tabulā izraisa lielu "miruÅ”o" ierakstu skaitu.

Kā atpazīt

-> SecÄ«gas skenēŔanas | Bitmaņu kaudzes skenēŔana | Indekss [Tikai] skenēŔana [Atpakaļ]
   && cikli Ɨ (rindas + RRbF) < (dalÄ«ts hits + dalÄ«ts lasÄ«jums) Ɨ 8
      -- izlasīts vairāk nekā 1KB katram ierakstam
   && dalīts hits + dalīts lasījums > 64

Ieteikumi

Regulāri manuāli veikt VACUUM [FULL] vai sasniegt adekvāti biežu apstrādi autovacuum ar tā parametru precÄ«zu pielāgoÅ”anu, tostarp konkrētai tabulai.

Visvairāk Ŕādas problēmas izrādās izraisa slikta vaicājumu kompozÄ«cija, kad tiek izsaukti biznesa loÄ£ikā, piemēram, tiem, kas tika aplÅ«koti PostgreSQL Antipatterns: cīņa ar miruÅ”o masām.

Bet jāizprot, ka pat VACUUM FULL nevar palÄ«dzēt vienmēr. Šādiem gadÄ«jumiem vērts iepazÄ«ties ar algoritmu no raksta DBA: kad VACUUM nepalÄ«dz — notÄ«rām tabulu manuāli.

#6: чтение с «сереГины» инГекса

Kad rodas

Izskatās, ka esam izlasÄ«juÅ”i nedaudz, un viss pēc indeksa, un nevienu nevajadzÄ«gu nefiltrējām — bet tomēr izlasÄ«tas ievērojami vairāk lapas nekā vēlētos.

Kā atpazīt

-> Indekss [Tikai] Skatīt [Atpakaļ]
   && cikli Ɨ (rindiņas + RRbF)  64

Ieteikumi

RÅ«pÄ«gi aplÅ«kojiet izmantotā indeksa struktÅ«ru un atslēgas laukus, kas norādÄ«ti pieprasÄ«jumā — visticamāk, daļa indeksa nav norādÄ«ta. Visticamāk, jums bÅ«s jāizveido lÄ«dzÄ«gs indekss, bet bez prefiksa laukiem vai jāmācās iterēt to vērtÄ«bas.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

IZVEIDOT TABULU tbl KĀ
IZVĒLĒTIES
  generate_series(1, 100000) pk      -- 100K "fakti"
, (random() *  100)::integer fk_org  -- 100 dažādi ārējie atslēgas
, (random() * 1000)::integer fk_cli; -- 1K dažādi ārējie atslēgas

IZVEIDOT INDEKSU uz tbl(fk_org, fk_cli); -- viss gandrīz kā #2
-- tikai atseviŔķs indekss par fk_cli mēs jau esam uzskatÄ«juÅ”i par lieku un izdzēsuÅ”i

IZVĒLĒTIES
  *
NO
  tbl
KUR
  fk_cli = 999 -- un fk_org nav norādīts, lai gan tas tika norādīts indeksā agrāk
IEROBEŽOT 20;

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Izskatās, ka viss ir labi, pat pēc indeksa, bet kaut kā aizdomÄ«gi — katram no 20 nolasÄ«tajiem ierakstiem bija jālasās pa 4 datu lapām, 32KB uz ierakstu — vai nav pārāk daudz? Un indeksa nosaukums tbl_fk_org_fk_cli_idx aicina uz pārdomām.

Mēs labojam:

IZVEIDOT INDEKSU uz tbl(fk_cli);

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

PēkŔņi — 10 reizes ātrāk, un 4 reizes mazāk lasÄ«t!

Citus neefektÄ«vas indeksu izmantoÅ”anas piemērus var redzēt rakstā DBA: atrodam bezjēdzÄ«gus indeksus.

#7: CTE Ɨ CTE

Kad rodas

Pieprasījumā nobriedām "treknos" CTE no dažādām tabulām, un pēc tam nolēmām veikt starp tiem JOIN.

Situācija ir aktuāla versijām zem v12 vai pieprasījumiem ar WITH MATERIALIZED.

Kā atpazīt

-> CTE SkatīŔana
   && cikli > 10
   && cikli Ɨ (rindiņas + RRbF) > 10000
      -- pārāk liela dekarta reize CTE

Ieteikumi

RÅ«pÄ«gi analizējiet pieprasÄ«jumu — vai te vispār ir vajadzÄ«gi CTE? Если все-таки Га, то uztvert "vārdnÄ«cu" hstore/json pēc modeļa, kas aprakstÄ«ts PostgreSQL antipatterns: trāpÄ«jums ar vārdnÄ«cu pa smagu JOIN.

#8: swap на Гиск (temp written)

Kad rodas

Vienreizēja apstrāde (ŔķiroÅ”ana vai unikālizācija) liela skaita ierakstu neievietojas atvēlētajā atmiņā.

Kā atpazīt

-> *
   && temp uzrakstīts > 0

Ieteikumi

Ja operācijā izmantotā atmiņas daudzums stipri pārsniedz iestatītā parametra work_mem, tā jāpielāgo. Var uzreiz konfigurēt visiem, vai arī caur SET [LOCAL] konkrētam pieprasījumam/transakcijai.

IsClosed = 0 AND Canceled = 0 AND Enabled = 0

PARĀDÄŖT work_mem;
-- "16MB"

IZVĒLĒTIES
  random()
NO
  generate_series(1, 1000000)
KĀRTOT
  1;

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Mēs labojam:

SET work_mem = '128MB'; -- pirms pieprasījuma izpildes

Receptes dziedējoÅ”iem SQL vaicājumiem
[apskatīt uz explain.tensor.ru]

Skaidru iemeslu dēļ, ja tiek izmantota tikai atmiņa, nevis disks, pieprasÄ«jums tiks izpildÄ«ts daudz ātrāk. Tajā paŔā laikā, daļa slodzes no HDD tiek noņemta.

Tomēr jāņem vērā, ka nevar vienmēr pieŔķirt daudz atmiņas — tās vienkārÅ”i nepietiks visiem.

#9: Š½ŠµŠ°ŠŗŃ‚ŃƒŠ°Š»ŃŒŠ½Š°Ń статистика

Kad rodas

Bāzē tika iekļauti daudzi dati, bet nepaspēja tos pārbaudīt. ANALYZE.

Kā atpazīt

-> SecÄ«ba skenēŔana | Bitmap heap skenēŔana | Indekss [Tikai] skenēŔana [Atpakaļ]
   && attiecība >> 10

Ieteikumi

Paveikt ANALYZE.

Sīkāk Ŕī situācija aprakstīta PostgreSQL antipatterns: statistika ir visu pamats.

#10: «что-то пошло не так»

Kad rodas

Notika gaidīŔana bloķēŔanai, ko uzlika konkurējoÅ”s pieprasÄ«jums, vai nebija pietiekami CPU/hipervizora resursi.

Kā atpazīt

-> *
   && (shared hit / 8K) + (shared read / 1K) < laiks / 1000
      -- RAM hit = 64MB/s, HDD lasījums = 8MB/s
   && laiks > 100ms -- lasījām maz, bet pārāk ilgi

Ieteikumi

Izmantojiet ārēju sistēmu monitorēŔanai serveriem attiecÄ«bā uz bloķējumiem vai neparastu resursu patēriņu. Par mÅ«su varianta organizēŔanu Ŕā procesa ietvaros simtiem serveru mēs jau stāstÄ«jām Å”eit un Å”eit.

Receptes dziedējoÅ”iem SQL vaicājumiem
Receptes dziedējoÅ”iem SQL vaicājumiem

Avots: habr.com

Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem šŸ”„ Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem | ProHoster