PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"

Tuhanded mĂŒĂŒgibĂŒroode juhid ĂŒle kogu riigi registreerivad meie CRM-sĂŒsteemis iga pĂ€ev kĂŒmneid tuhandeid kontakte — suhtlemise fakte potentsiaalsete vĂ”i juba koostööd tegevaid kliente. Ja selle kliendi leidmiseks tuleb seda kĂ”igepealt teha, ja soovitavalt vĂ€ga kiiresti. See toimub kĂ”ige sagedamini nime jĂ€rgi.

Seega ei ole ĂŒllatav, et, arutades taas "raskete" pĂ€ringute ĂŒle ĂŒhes meie kĂ”ige koormatud andmebaasidest — meie enda korporatiivkonto SBiSi, avastasin "tipus" pĂ€ringu "kiire" nimepĂ”hise otsingu jaoks organisatsioonide kaartide jaoks.

Lisaks paljastas edasine uurimine huvitava nĂ€ite algsest optimeerimisest ja seejĂ€rel jĂ”udluse halvenemisest pĂ€ringu jĂ€rjestikuse tĂ€iustamise kĂ€igus, mida viisid lĂ€bi mitmed meeskonnad, kellest igaĂŒks tegutses ĂŒksnes parimatest kavatsustest.

0: mida tahtis kasutaja

PostgreSQL antipattern'id: jutt iteratiivsetest tĂ€iustustest nimepĂ”hise otsingu osas ehk "Optimeerimine edasi-tagasi"[КДПВ siit]

Mida mĂ”tleb kasutaja ĂŒldiselt, kui rÀÀgib "kiirest" otsingust pealkirja jĂ€rgi? Harva, kui see osutub "ausaks" otsinguks alamsĂ”nna tĂŒĂŒpi ... LIKE '%roos%' — sest siis jÀÀvad tulemused mitte ainult 'Rooslia' ja 'Pood Roos', vaid ka 'Groos' ja isegi 'Vanaema Roos'.

Kasutaja mĂ”istab igapĂ€evaelus, et te tagate talle otsingu sĂ”na alguse jĂ€rgi pealkirjas ja nĂ€itate enam asjakohaseid tulemusi, mis algab sisestatud. Ja teete seda praktiliselt koheselt — alamsĂ”nade sisestamise korral.

1: kitsendame ĂŒlesande

Ja kindlasti ei hakka inimene spetsiaalselt sisestama 'roos pood',et iga sĂ”na sooviksite otsida prefiksina. Ei, kasutajal on palju lihtsam reageerida kiiresti esitatud vihjele viimasele sĂ”nale, kui tahtlikult "puuduvad" eelnevad — vaadake, kuidas see toimib igas otsingumootoris.

Üldiselt, Ă”igesti formuleerida ĂŒlesande nĂ”uded — enam kui pooled lahendusest. MĂ”nikord vĂ”ib tĂ€helepanelik use case'i analĂŒĂŒs oluliselt mĂ”jutada tulemust.

Mida teeb abstraktne arendaja?

1.0: vÀline otsingumootor

Oi, otsimine on keeruline, ei viitsi sellega tegeleda — lasta devops'il sellega tegeleda! Las nad seadistavad meile vĂ€lise otsingusĂŒsteemi, nagu Sphinx, ElasticSearch,


Töötav, kuigi töömahukas sĂŒnkroniseerimise ja muudatuste kiirusest. Kuid mitte meie puhul, kuna otsing toimub iga kliendi jaoks ainult tema konto andmete ulatuses. Andmed on piisavalt muutlikud — ja kui praegu haldur sisestab kaardi 'Poekett Roosa', siis 5-10 sekundit hiljem vĂ”ib ta juba meeles pidada, et unustas seal e-posti aadressi ning soovib seda leida ja parandada.

SeetĂ”ttu — laseme otsida „otse andmebaasist”. Õnneks vĂ”imaldab PostgreSQL meil seda teha, ja mitte ĂŒhe versiooniga — vaatame neid lĂ€bi.

1.1: „aus” alamstring

Haakume sĂ”na „alamstring” kĂŒlge. Just indekseeritud otsingu jaoks alamstringide jĂ€rgi (ja isegi regulaaravaldiste jĂ€rgi!) on olemas suurepĂ€rane moodul pg_trgm! Ainult pĂ€rast seda tuleb Ă”igesti jĂ€rjestada.

Proovime vÔtta lihtsuse mÔttes sellise tabeli:

CREATE TABLE firms(
  id
    serial
      PRIMARY KEY
, name
    text
);

Laadime sinna 7.8 miljonit reaalselt olemasolevat organisatsiooni ja indekseerime:

LOOJA LAIA pg_trgm;
LOOJA INDICE FIRMADE ÜLEGINA gin(lower(nimi) gin_trgm_ops);

Otsime alamsÔna otsimiseks esimesed 10 salvestust:

VALI
  *
KUST
  firmad
KUS
  lower(nimi) ~ ('(^|s)' || 'roos')
KOOS
  lower(nimi) ~ ('^' || 'roos') DESC -- esmalt "alustavad"
, lower(nimi) -- ĂŒlejÀÀnud tĂ€hestikujĂ€rjekorras
PIIRANG 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

No, selline
 26ms, 31 MB loetud andmeid ja rohkem kui 1.7K filtreeritud salvestust — 10 otsitava jaoks. Kulud on liiga suured, kas ei saaks kuidagi efektiivsemalt?

1.2: tekstipÔhine otsing? see on ju FTS!

TÔepoolest, PostgreSQL pakub vÀga vÔimsat tÀisteksti otsingu mehhanismi (Full Text Search), sealhulgas prefiksotsingu vÔimalusega. SuurepÀrane valik, isegi laiendusi pole vaja installida! Proovime:

LOOJA INDICE FIRMADE ÜLEGINA gin(to_tsvector('simple'::regconfig, lower(nimi)));

VALI
  *
KUST
  firmad
KUS
  to_tsvector('simple'::regconfig, lower(nimi)) @@ to_tsquery('simple', 'roos:*')
KOOS
  lower(nimi) ~ ('^' || 'roos') DESC
, lower(nimi)
PIIRANG 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

Siin aitas meile veidi pĂ€ringute tĂ€itmise paralleelisus, vĂ€hendades aega kahe korda kuni 11ms. Ja lugeda tuli meil 1.5 korda vĂ€hem — ainult 20MB. Siin kehtib vĂ€hem on rohkem, kuna mida suurem on meie loetav maht, seda suurem on chance saada cache miss, ja iga ĂŒlearune andmeleht, mis loetakse kettalt, on potentsiaalne "peatus" pĂ€ringu jaoks.

1.3: siiski LIKE?

KÔik eelnevad pÀringud on head, aga kui seda tÔmmata sada tuhat korda pÀevas, siis juba koguneb 2TB loetud andmeid. Parimal juhul mÀlu kaudu, aga kui Ônne pole, siis kettalt. Nii et proovime seda vÀiksemaks teha.

KĂ€ime ĂŒle, mida kasutaja tahab nĂ€ha esiteks "mis algavad ...". See on ju puhtalt prefiksipĂ€ring kasutades text_pattern_ops! Ja ainult siis, kui meil on "puudu" kuni 10 otsitavat kirjet, peame need lugema FTS-pĂ€ringuga:

CREATE INDEX ON firms(lower(name) text_pattern_ops);

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('roosa' || '%')
LIMIT 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

SuurepĂ€rased nĂ€itajad — vaid 0.05ms ja veidi ĂŒle 100KB loetud! Ainult et me unustasime sorteerimise nime jĂ€rgi, et kasutaja ei eksiks tulemustes:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('roosa' || '%')
ORDER BY
  lower(name)
LIMIT 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

Oi, midagi on juba vale — katsed, et indeks olemas, aga sortimine kĂ€ib mööda
 See on muidugi palju efektiivsem kui eelmine versioon, kuid


1.4: "töötama viimistlusfailiga"

Aga on ju olemas indeks, mis vĂ”imaldab otsida vahemiku jĂ€rgi ja sortimist ka normaalselt kasutada — tavaline btree!

Loo indeks: CREATE INDEX ON firms(lower(name));

Kuid pÀring tuleb "ise kokku panna":

SELECT
  *
FROM
  firms
WHERE
  lower(name) >= 'roos' AND
  lower(name) <= ('roos' || chr(65535)) -- UTF8 jaoks, ĂŒhesĂŒteme jaoks - chr(255)
ORDER BY
  lower(name)
LIMIT 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

SuurepĂ€rane — nii sortimine töötab, kui ka ressursitarbimine on jÀÀnud "mikroskoopiliseks", tuhandeid kordi efektiivsem kui "puhas" FTS! JÀÀnud on kokku panna ĂŒhte pĂ€ringusse:

(
  SELECT
    *
  FROM
    firms
  WHERE
    lower(name) >= 'roos' AND
    lower(name) <= ('roos' || chr(65535)) -- UTF8 jaoks, ĂŒhesĂŒteme jaoks - chr(255)
  ORDER BY
    lower(name)
  LIMIT 10
)
UNION ALL
(
  SELECT
    *
  FROM
    firms
  WHERE
    to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'roos:*') AND
    lower(name) NOT LIKE ('roos' || '%') -- "alustavad" leidsime juba ĂŒlal
  ORDER BY
    lower(name) ~ ('^' || 'roos') DESC -- kasutame sama sortimist, et MITTE minna btree-indekse
  , lower(name)
  LIMIT 10
)
LIMIT 10;

TÔden, et teine alampÀring tÀidetakse ainult juhul, kui esimene tagastas vÀhem, kui oodati viimasena LIMIT ridade arvu kohta. Sellest optimeerimismeetodist olen ma juba varem kirjutanud.

Noh, meil on nĂŒĂŒd tabelis samaaegselt nii btree kui ka gin, kuid statistika pĂ”hjal selgus, et alla 10% pĂ€ringutest jĂ”uab teise ploki tĂ€itmisele. See tĂ€hendab, et selliste tuttavate tĂŒĂŒpide piirangute korral suutsime serveri ressursitarbimist vĂ€hendada praktiliselt tuhandete kordadeni!

1.5*: saame hakkama ilma viiliga

Ülal LIKE meil takistas vale sortimine. Aga selle saab "Ă”igele teele suunata" USING-operatoori abil:

Vaikimisi eeldatakse ASC. Samuti saab sortimisoperatoori nime mÀÀrata lauses USING. Sortimisoperatoor peab olema „vĂ€iksem“ vĂ”i „suurem“ mĂ”ne B-puu operatsiooni perekonna liige. ASC tavaliselt samavÀÀrne USING < ja DESC tavaliselt samavÀÀrne USING >.

Meie puhul on „vĂ€iksem“ ~<~:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('roosa' || '%')
ORDER BY
  lower(name) USING ~<~
LIMIT 10;

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"
[vaata explain.tensor.ru]

2: kuidas pÀringud "hapnevad"

NĂŒĂŒd jĂ€tame oma pĂ€ringu „setitama“ pooleks aastaks kuni aastaks ja avastame ĂŒllatusega, et see on taas „tipus“ kogupĂ€evase „mĂ€lupotentsiaali“ nĂ€itajatega (jagatud puhvri tabamused) 5.5TB — see on veelgi rohkem kui algselt.

Ei, loomulikult on meie Ă€ri kasvanud ja koormus suurenenud, kuid mitte nii palju! See tĂ€hendab, et midagi on siin valesti — lĂ€hme vaatame.

2.1: lehekĂŒljenduse sĂŒnn

MĂ”nes jĂ€rgnevas hetkes soovis teise arendustiimi liige luua vĂ”imaluse kiirelt subtekstist „hĂŒppamiseks“ registrisse, mis sisaldab samu, kuid laiendatud tulemusi. Aga mis register on lehekĂŒljendamata? LĂ€hme lisame!

( ... LIMIT <N> + 10)
UNION ALL
( ... LIMIT <N> + 10)
LIMIT 10 OFFSET <N>;

NĂŒĂŒd sai arendajale muretseda, et nĂ€idata otsingu tulemuste registrit „lehekĂŒljendusega“ allalaadimiseks.

Muidugi, tegelikult loetakse iga jĂ€rgmise andmelehe jaoks ĂŒha rohkem ja rohkem. (eelmiste kordade kĂ”ik, mida jĂ€tame kĂ”rvale, pluss vajalik „sabake”) — see tĂ€hendab, et see on ĂŒhemĂ”tteliselt vale muster. Õigem oleks algatada otsing jĂ€rgmise iteratsiooni juures mĂ€letatud vĂ”tme pĂ”hjal, aga sellest rÀÀgime mĂ”ni teine kord.

2.2: tahaks natuke eksootikat

MÔnel hetkel soovis arendaja mitmekesistada tulemustes olevat valikut teise tabeli andmetega, mille jaoks kogu eelmine pÀring saadeti CTE-sse:

WITH q AS (
  ...
  LIMIT  + 10
)
SELECT
  *
, (SELECT ...) sub_query -- mingi pÀring seotud tabelisse
FROM
  q
LIMIT 10 OFFSET ;

Ja isegi sel moel — pole paha, kuna sisemine pĂ€ring arvutatakse vaid 10 tagastatud kirje jaoks, kui mitte


2.3: DISTINCT mÔttetu ja halastamatu

Kusagil selle evolutsiooni kĂ€igus teise alampĂ€ringu kadus Ă€ra NOT LIKE tingimus. On selge, et pĂ€rast seda UNION ALL hakkas tagastama mĂ”ningaid kirjeid kaks korda — esmalt leitud stringi alguse jĂ€rgi, seejĂ€rel veel kord — selle stringi esimese sĂ”na alguse jĂ€rgi. LĂ”pptulemusena vĂ”isid kĂ”ik teise alampĂ€ringu kirjed kattuda esimese kirjetega.

Mida arendaja teeb, selle asemel et otsida pĂ”hjust?.. Ei mingit kĂŒsimust!

  • kaks korda suurendame algsete valikute mahtu
  • rakendame DISTINCT, et ainult ĂŒhe eksemplari iga rea jaoks sobiks

WITH q AS (
  ( ... LIMIT  + 10)
  UNION ALL
  ( ... LIMIT  + 10)
  LIMIT  + 10
)
SELECT DISTINCT
  *
, (SELECT ...) alampÀring
FROM
  q
LIMIT 10 OFFSET ;

See tÀhendab, et tulemus on lÔppkokkuvÔttes tÀpselt sama, aga vÔimalus "mööda lasta" 2. alampÀringus CTE on oluliselt kÔrgem, ja ilma selleta ka loetakse kindlasti rohkem.

Aga see ei ole kĂ”ige hullem. Kuna arendaja palus valida DISTINCT mitte konkreetsetelt, vaid kohe kĂ”ikidelt vĂ€ljadelt kirjed, siis sinna automaatselt sattus ka vĂ€li alampĂ€ring — alampĂ€ringu tulemus. NĂŒĂŒd, et teostada DISTINCT, pidi baasis juba tegema mitte 10 alampĂ€ringut, vaid kĂ”ik + 10!

2.4: koostöö on kÔik!

Nii magasid arendajad - ei kurtnud, sest registris "kohandada" tÀhtsate vÀÀrtuste N saavutamiseks, kui iga jÀrgmise "lehe" hankimise aeglasemaks muutmine oli selgelt liiga palju kannatust nÔudnud.

Kuni neid tulid arendajad teisest osakonnast ja soovisid kasutada sellist mugavat meetodit iteratiivseks otsimiseks — see, we take a piece from a sample, filter it by additional conditions, draw the result, then the next piece (which in our case is achieved by increasing N), and so on until we fill the screen.

In general, in the instance caught N reached values of nearly 17K, and in total over the day, at least 4K such requests were executed ‘in sequence’. The last of them were confidently scanned already by 1GB of memory at each iteration


Kokku

PostgreSQL antipattern'id: jutt iteratiivsetest tÀiustustest nimepÔhise otsingu osas ehk "Optimeerimine edasi-tagasi"

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster