PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"

Az orszĂĄg összes Ă©rtĂ©kesĂ­tĂ©si irodĂĄjĂĄnak ezrei rögzĂ­tik a CRM rendszerĂŒnkben naponta több tĂ­zezer kapcsolattartĂĄst — a potenciĂĄlis vagy mĂĄr velĂŒnk dolgozĂł ĂŒgyfelekkel folytatott kommunikĂĄciĂł tĂ©nyeit. Ehhez az ĂŒgyfĂ©lhez elƑször meg kell talĂĄlni, Ă©s lehetƑleg nagyon gyorsan. Ez ĂĄltalĂĄban a nĂ©v alapjĂĄn törtĂ©nik.

EzĂ©rt nem meglepƑ, hogy amikor Ășjra megvizsgĂĄltam a „nehĂ©z” lekĂ©rdezĂ©seket az egyik legterheltebb adatbĂĄzisunkban — a sajĂĄt SBS ĂŒzleti fiĂłkunkban,, felfedeztem a „top”-on a nĂ©v alapĂș „gyors” keresĂ©shez az ĂŒzleti kĂĄrtyĂĄkhoz.

SƑt, a tovĂĄbbi nyomozĂĄs Ă©rdekes pĂ©ldĂĄt törtĂ©nt elƑször az optimalizĂĄlĂĄs, majd a teljesĂ­tmĂ©nyromlĂĄs lekĂ©rdezĂ©s teljesĂ­tmĂ©nyĂ©nek fokozatos javĂ­tĂĄsa sorĂĄn, melynek sorĂĄn több csapat mƱködött, mindegyik a lehetƑ legjobb szĂĄndĂ©kkal.

0: mit akart a felhasznĂĄlĂł

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"[KDPV innen]

Mit is jelent ĂĄltalĂĄban a felhasznĂĄlĂł szĂĄmĂĄra a „gyors” keresĂ©s a nĂ©v alapjĂĄn? Szinte soha nem jelent „Ƒszinte” keresĂ©st egy substringre, mint pĂ©ldĂĄul ... LIKE '%rĂłzsa%' — hiszen akkor a talĂĄlatok közĂ© nem csak 'RĂłzsalia' Ă©s 'Bolt RĂłzsa', hanem 'GrĂłzsa' Kapcsolja össze sajĂĄt domainjĂ©t 'TĂ©lapĂł hĂĄzarĂłzsa'.

A felhasznĂĄlĂł pedig a hĂĄztartĂĄsi szinten azt Ă©rti, hogy biztosĂ­tani fogja neki a szĂł elejĂ©re törtĂ©nƑ keresĂ©st a nĂ©vben, Ă©s relevĂĄnsabbnak fogja mutatni azt, ami kezdƑdik a beĂ­rt kifejezĂ©ssel. ÉS ezt meg fogja tenni gyakorlatilag azonnal — a substring bevitelĂ©nĂ©l.

1: korlĂĄtozzuk a feladatot

AztĂĄn a felhasznĂĄlĂł biztosan nem fogja kĂŒlön beĂ­rni 'rĂłz bolt',hogy minden egyes szĂłt prefikszĂĄlva kelljen keresnie. Nem, a felhasznĂĄlĂłnak sokkal egyszerƱbb reagĂĄlni a legutolsĂł szĂł gyors javaslatĂĄra, mint szĂĄndĂ©kosan „hĂĄtrahagyni” az elƑzƑket — nĂ©zd meg, hogy mƱködik ez bĂĄrmely keresƑnĂ©l.

AlapvetƑen, kell a követelmĂ©nyek megfogalmazĂĄsa — a megoldĂĄs több mint fele. NĂ©ha a felhasznĂĄlĂĄsi eset alapos elemzĂ©se jelentƑsen befolyĂĄsolhatja az eredmĂ©nyt..

Mit tesz egy elvont fejlesztƑ?

1.0: kĂŒlsƑ keresƑmotor

Jaj, a keresĂ©s bonyolult, valĂłjĂĄban nem is akarjuk ezzel foglalkozni — adjuk ĂĄt a devopsnak! Hadd telepĂ­tsenek nekĂŒnk egy kĂŒlsƑ keresƑ rendszert az adatbĂĄzisunkkal, mint pĂ©ldĂĄul Sphinx, ElasticSearch,


MƱködƑ, bĂĄr munkaigĂ©nyes a szinkronizĂĄciĂł Ă©s azonnali vĂĄltozĂĄsok szempontjĂĄbĂłl. De a mi esetĂŒnkben nem, mivel a keresĂ©s minden ĂŒgyfĂ©l esetĂ©ben csak az Ƒ fiĂłkjĂĄnak adatai között törtĂ©nik. Az adatok elĂ©g jelentƑs vĂĄltozĂ©konysĂĄggal bĂ­rnak — Ă©s ha most a menedzser lĂ©trehozott egy kĂĄrtyĂĄt, 'RĂłzsa Bolt', akkor 5-10 mĂĄsodperc mĂșlva mĂĄr emlĂ©kezhet arra, hogy elfelejtette megadni az emailt, Ă©s szeretnĂ© megtalĂĄlni Ă©s javĂ­tani azt.

EzĂ©rt — nĂ©zzĂŒk meg, hogyan kereshetĂŒnk "közvetlenĂŒl a adatbĂĄzis szerint". SzerencsĂ©re a PostgreSQL lehetƑvĂ© teszi szĂĄmunkra, Ă©s nem egyetlen megoldĂĄssal — ezeket fogjuk megvizsgĂĄlni.

1.1: "Ƒszinte" rĂ©szstring

Kapcsolódjunk a "részstring" szóra. Hiszen pontosan az indexelésre a részstring (és akår regulåris kifejezések!) alapjån van egy nagyszerƱ pg_trgmmodulunk! Csak aztån helyesen kell majd rendezni.

Próbåljunk meg egyszerƱség kedvéért egy ilyen tåblåt létrehozni:

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

FeltöltjĂŒk 7,8 milliĂł valĂłs szervezet rekordjĂĄval Ă©s indexeljĂŒk:

CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);

KezdjĂŒk a rĂ©szstring keresĂ©sĂ©vel, keressĂŒk az elsƑ 10 rekordot:

SELECT
  *
FROM
  firms
WHERE
  lower(name) ~ ('(^|s)' || 'rĂłzsa')
ORDER BY
  lower(name) ~ ('^' || 'rĂłzsa') DESC -- elƑször a "kezdetben ĂĄllĂł"
, lower(name) -- a többi åbécé sorrendben
LIMIT 10;

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

HĂĄt, ilyen... 26ms, 31MB olvasott adatok Ă©s több mint 1,7K szƱrt rekord — a 10 keresett közĂŒl. A költsĂ©gek tĂșl magasak, nem lehetne hatĂ©konyabb megoldĂĄs?

1.2: keresĂ©s szöveg alapĂș? ez FTS!

ValĂłban, a PostgreSQL nagyon erƑs teljes szöveges keresĂ©si (Full Text Search) mechanizmust biztosĂ­t, beleĂ©rtve a prefix keresĂ©st is. NagyszerƱ megoldĂĄs, mĂ©g kiterjesztĂ©seket sem szĂŒksĂ©ges telepĂ­teni! NĂ©zzĂŒk meg:

CREATE INDEX ON firms USING gin(to_tsvector('simple'::regconfig, lower(name)));

SELECT
  *
FROM
  firms
WHERE
  to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rĂłzsa:*')
ORDER BY
  lower(name) ~ ('^' || 'rĂłzsa') DESC
, lower(name)
LIMIT 10;

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

Itt segĂ­tett nekĂŒnk egy kicsit a lekĂ©rdezĂ©s pĂĄrhuzamosĂ­tĂĄsa, amely megfelezte az idƑt 11ms. És 1,5-ször kevesebbet kellett olvasnunk — mindössze 20MB. Itt a kevesebb jobb, hiszen minĂ©l nagyobb mennyisĂ©get olvasunk ki, annĂĄl nagyobb a cache miss lehetƑsĂ©ge, Ă©s minden egyes felesleges olvasott adatlap a lemezrƑl potenciĂĄlisan "lassĂ­tja" a lekĂ©rdezĂ©st.

1.3: vĂ©gĂŒl is LIKE?

Az elƑzƑ lekĂ©rdezĂ©s nagyszerƱ, de ha naponta szĂĄzezerszer meghĂ­vjĂĄk, akkor mĂĄr 2TB olvadt adatok. Legjobb esetben is a memĂłriĂĄbĂłl, de ha nem vagyunk szerencsĂ©sek, akkor a lemezrƑl. SzĂłval prĂłbĂĄljuk meg kisebbre venni.

EmlĂ©kezzĂŒnk, hogy a felhasznĂĄlĂł mit szeretne lĂĄtni elƑször a "...ra kezdƑdƑket". De hĂĄt ez tisztĂĄn van prefix keresĂ©s segĂ­tsĂ©gĂ©vel text_pattern_ops! És csak akkor, ha "nem elĂ©g" 10 keresett bejegyzĂ©sig, akkor a FTS-keresĂ©ssel kell folytatnunk az olvasĂĄst:

INDÍTS INDEX A firms(lower(name) text_pattern_ops);

VÁLASZD
  *
BƐVÍTSD
  firms
HOL
  lower(name) LIKE ('rĂłzsa' || '%')
LIMIT 10;

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

KivĂĄlĂł mutatĂłk — mindössze 0.05ms Ă©s kissĂ© több mint 100KB elolvasva! Csak elfelejtettĂŒk a nevek szerinti rendezĂ©st, hogy a felhasznĂĄlĂł ne tĂ©vedjen el az eredmĂ©nyek között:

VÁLASZD
  *
BƐVÍTSD
  firms
HOL
  lower(name) LIKE ('rĂłzsa' || '%')
RENDEZÉS
  lower(name)
LIMIT 10;

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

Ó, mĂĄr nem nĂ©z ki olyan jĂłl — Ășgy tƱnik, hogy van index, de a rendezĂ©s mellette elszĂĄll... Persze, mĂĄr sokkal hatĂ©konyabb, mint az elƑzƑ vĂĄltozat, de...

1.4: "finomĂ­tsuk a fĂĄjlt"

De van egy index, amely lehetƑvĂ© teszi a tartomĂĄny szerinti keresĂ©st Ă©s a rendezĂ©st is — normĂĄl btree!

INDÍTS INDEX A firms(lower(name));

Csak a lekérdezést "kézzel" kell összerakni:

VÁLASZD
  *
BƐVÍTSD
  firms
HOL
  lower(name) >= 'rózsa' ÉS
  lower(name) <= ('rózsa' || chr(65535)) -- UTF8 esetén, egybåjtosoknak - chr(255)
RENDEZÉS
   lower(name)
LIMIT 10;

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

KivĂĄlĂł — Ă©s a rendezĂ©s mƱködik, Ă©s az erƑforrĂĄs-felhasznĂĄlĂĄs "mikroszkopikus" maradt, ezerszer hatĂ©konyabb, mint a "tisztĂĄn" FTS! MĂĄr csak egy összetett lekĂ©rdezĂ©st kell kĂ©szĂ­teni:

(
  VÁLASZD
    *
  BƐVÍTSD
    firms
  HOL
    lower(name) >= 'rózsa' ÉS
    lower(name) <= ('rózsa' || chr(65535)) -- UTF8, egybåjtos kódolås esetén - chr(255)
  RENDEZÉS
     lower(name)
  LIMIT 10
)
UNION ALL
(
  VÁLASZD
    *
  BƐVÍTSD
    firms
  HOL
    to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rózsa:*') ÉS
    lower(name) NOT LIKE ('rózsa' || '%') -- "...ra kezdƑdƑket" már megtaláltuk
  RENDEZÉS
    lower(name) ~ ('^' || 'rĂłzsa') DESC -- ugyanazt a rendezĂ©st hasznĂĄljuk, hogy NE lĂ©pjĂŒnk ĂĄt a btree-indexen
  , lower(name)
  LIMIT 10
)
LIMIT 10;

Megjegyzem, hogy a mĂĄsodik alkĂ©rdezĂ©s csak akkor fut le ha az elsƑ kevesebb elvĂĄrt sorok szĂĄmĂĄt adta vissza. ErrƑl a lekĂ©rdezĂ©sek optimalizĂĄlĂĄsĂĄrĂłl korĂĄbban mĂĄr Ă­rtam LIMIT Igen, most mĂĄr egyidƑben van btree Ă©s gin a tĂĄblĂĄn, de statisztikailag kiderĂŒlt, hogy kevesebb mint 10% kĂ©rĂ©s Ă©ri el a mĂĄsodik blokk vĂ©grehajtĂĄsĂĄt.

. TehĂĄt ezekkel a hĂ­resen elƑre meghatĂĄrozott tipikus korlĂĄtokkal a feladatnĂĄl a szerver erƑforrĂĄs-felhasznĂĄlĂĄsĂĄt szinte ezerszer csökkentettĂŒk! 1.5*: megoldjuk anĂ©lkĂŒl a finomĂ­tĂłtFent

Megjegyzem, hogy

Fent LIKE Zavaradtuk a helytelen rendezĂ©s hasznĂĄlatĂĄba. De a USING operĂĄtor megadĂĄsĂĄval „rĂĄ tudjuk vezetni az igaz Ăștra”:

AlapĂ©rtelmezĂ©s szerint Ă©rtendƑ ASC. Ezen kĂ­vĂŒl megadhatunk egy specifikus rendezƑ operĂĄtor nevĂ©t a lekĂ©rdezĂ©sben USING. A rendezƑ operĂĄtornak egy „kisebb” vagy „nagyobb” B-fa operĂĄtoral kell lennie. ASC ĂĄltalĂĄban egyenĂ©rtĂ©kƱ USING < Ă©s DESC ĂĄltalĂĄban egyenĂ©rtĂ©kƱ USING >.

A mi esetĂŒnkben a „kisebb” a ~<~:

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

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"
[megtekintés az explain.tensor.ru-n]

2: ahogy a lekĂ©rdezĂ©sek „megromlanak”

Most hagyjuk, hogy a lekĂ©rdezĂ©sĂŒnk „érjen” fĂ©l Ă©vig egy Ă©vet, Ă©s meglepetĂ©sĂŒnkre Ășjra felfedezzĂŒk a „top” rĂ©szen a napi összesĂ­tett „memĂłria feltornĂĄszĂĄs” mutatĂłkkal (buffers shared hit) a 5.5TB — tehĂĄt mĂ©g több, mint amennyi eredetileg volt.

Nem, termĂ©szetesen, a vĂĄllalkozĂĄsunk is nƑtt, Ă©s a terhelĂ©s is növekedett, de nem ennyire! TehĂĄt itt valami nem stimmel — nĂ©zzĂŒk meg közelebbrƑl.

2.1: a lapozĂĄs szĂŒletĂ©se

Egy ponton a mĂĄsik fejlesztƑcsapat meg akarta valĂłsĂ­tani, hogy a gyors alsĂłkeresĂ©sbƑl „ugrani” lehessen a tĂĄrolĂłba az ugyanazokkal, de kibƑvĂ­tett eredmĂ©nyekkel. De milyen tĂĄrolĂł lapozĂĄs nĂ©lkĂŒl? Csatlakoztassuk!

( ... LIMIT  + 10)
UNION ALL
( ... LIMIT  + 10)
LIMIT 10 OFFSET ;

EzutĂĄn a fejlesztƑ könnyedĂ©n prezentĂĄlhatta a keresĂ©si eredmĂ©nyek listĂĄjĂĄt „tĂ­pusĂș-lapozĂĄsos” alĂĄmerĂ­tĂ©ssel.

Persze, valĂłjĂĄban minden egyes következƑ adatoldalhoz egyre nagyobb mennyisĂ©gƱ adatot olvasunk el (az összes az elƑzƑ alkalommal, amelyet eldobunk, plusz a szĂŒksĂ©ges „farok”) — tehĂĄt ez egyĂ©rtelmƱ antipattern. Ami helyesebb lenne — a keresĂ©st a felhasznĂĄlĂłi felĂŒleten megjegyzett kulcsrĂłl indĂ­tani, de errƑl majd egy mĂĄsik alkalommal.

2.2: vĂĄgyakozĂĄs az egzotikumra

Egy ponton a fejlesztƑ vĂĄgyott rĂĄ hogy szĂ­nesĂ­tse az eredmĂ©nyhalmazt adatokkal egy mĂĄsik tĂĄblĂĄzatbĂłl, ezĂ©rt a teljes korĂĄbbi lekĂ©rdezĂ©s CTE-be kerĂŒlt:

WITH q AS (
  ...
  LIMIT  + 10
)
SELECT
  *
, (SELECT ...) sub_query -- egy lekérdezés a kapcsolódó tåblåhoz
FROM
  q
LIMIT 10 OFFSET ;

És mĂ©g Ă­gy is — nem rossz, mivel a beĂĄgyazott lekĂ©rdezĂ©st csak a 10 visszaadott rekordra szĂĄmoljĂĄk ki, ha nem lenne


2.3: DISTINCT értelmetlen és könyörtelen

Valahol ebben a fejlƑdĂ©s sorĂĄn a 2. allekĂ©rdezĂ©sbƑl kivilĂĄgosodott NOT LIKE a feltĂ©tel. NyilvĂĄnvalĂł, hogy ezutĂĄn UNION ALL kezdett visszaadni bizonyos rekordokat duplĂĄn — elƑször a sorok elejĂ©n talĂĄlhatĂłakat, majd ismĂ©t az ezen sor elsƑ szavĂĄnĂĄl. A hatĂĄron belĂŒl a mĂĄsodik lekĂ©rdezĂ©s összes bejegyzĂ©se megegyezhetett az elsƑ bejegyzĂ©seivel.

Mit csinĂĄl a fejlesztƑ a hiba keresĂ©se helyett?.. Nem kĂ©rdĂ©s!

  • kĂ©tszeresĂ©re bƑvĂ­tjĂŒk a mĂ©retet forrĂĄsmintĂĄk
  • alkalmazzuk a DISTINCT-ot, hogy csak egyedi pĂ©ldĂĄnyok maradjanak meg az egyes sorokbĂłl

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

TehĂĄt egyĂ©rtelmƱ, hogy a vĂ©geredmĂ©ny pont ugyanaz, de a mĂĄsodik CTE lekĂ©rdezĂ©s „kiugrĂĄsĂĄnak” esĂ©lye jelentƑsen megnƑtt, Ă©s ezen kĂ­vĂŒl is, nyilvĂĄnvalĂłan többet olvas..

De ez mĂ©g nem a legrosszabb. Mivel a fejlesztƑ azt kĂ©rte, hogy ne specifikus, hanem az összes mezƑ alapjĂĄn vĂĄlogassunk DISTINCT az bejegyzĂ©sek között, Ă­gy automatikusan bekerĂŒlt a sub_query mezƑ is — a lekĂ©rdezĂ©s eredmĂ©nye. Most a vĂ©grehajtĂĄs Ă©rdekĂ©ben , a rendszernek mĂĄr DISTINCTnem 10, hanem mind + 10 lekĂ©rdezĂ©st kellett vĂ©grehajtania. 2.4: a kooperĂĄciĂł mindennĂ©l fontosabb!!

Így Ă©ltek a fejlesztƑk — nem panaszkodtak, mert a nyilvĂĄntartĂĄs „finomhangolĂĄsa” Ă©rdekĂ©ben a kritikus N Ă©rtĂ©kek elĂ©rĂ©sĂ©hez a következƑ „oldal” megjelenĂ­tĂ©sekor a felhasznĂĄlĂłnak nyilvĂĄnvalĂłan nem volt elĂ©g tĂŒrelme.

AmĂ­g nem jöttek a mĂĄsik osztĂĄly fejlesztƑi, Ă©s nem akartĂĄk hasznĂĄlni ezt a kĂ©nyelmes mĂłdszert

az iteratĂ­v keresĂ©shez. — azaz veszĂŒnk egy darabot a mintĂĄbĂłl, szƱrjĂŒk tovĂĄbbi feltĂ©telek szerint, megrajzoljuk az eredmĂ©nyt, majd a következƑ darabot (ami az esetĂŒnkben N növelĂ©sĂ©vel Ă©rhetƑ el), Ă©s Ă­gy folytatjuk, amĂ­g ki nem töltjĂŒk a kĂ©pernyƑt. ÖsszessĂ©gĂ©ben a megfogott pĂ©ldĂĄnyban

N csaknem 17K-ra nƑtt, Ă©s mindössze egy nap alatt „lĂĄncban” nem kevesebb mint 4K ilyen lekĂ©rdezĂ©st hajtottak vĂ©gre. Az utolsĂłkat mĂĄr bĂĄtran vizsgĂĄltĂĄk1 GB memĂłria minden iterĂĄciĂłnĂĄl. Ezer Ă©rtĂ©kesĂ­tĂ©si menedzser az orszĂĄg irodĂĄibĂłl naponta tucatnyi ezer kapcsolatot rögzĂ­t a CRM rendszerĂŒnkben — a kommunikĂĄciĂł tĂ©nyĂ©t.


Összesen

PostgreSQL antipatternök: a cím szerinti keresés iteratív fejlesztésének meséje, avagy "Optimalizålås ide-oda"

ForrĂĄs: habr.com

MegbĂ­zhatĂł hosting vĂĄsĂĄrlĂĄsa weboldalak szĂĄmĂĄra, DDoS vĂ©delemmel, VPS VDS szerverek. đŸ”„ MegbĂ­zhatĂł webtĂĄrhely vĂĄsĂĄrlĂĄsa DDoS vĂ©delemmel, VPS VDS szerverek | ProHoster