Herrialde osoko salmenta bulegoetako milaka zuzendarik erregistratzen dute
Hori dela eta, ez da harritzekoa, berriro ere kontsulta "astunak" aztertzea datu-base kargatuenetako batean - gurea.
Gainera, ikerketek adibide interesgarri bat agerian utzi zuten lehenengo optimizazioa eta gero errendimenduaren degradazioa eskaera bere sekuentzialtasunarekin hainbat taldek, eta horietako bakoitzak asmo onenekin bakarrik jardun zuen.
0: zer nahi zuen erabiltzaileak?
[KDPV
Zer esan nahi du erabiltzaile batek izenaren arabera bilaketa "azkar" bati buruz hitz egiten duenean? Ia inoiz ez da antzeko azpikate baten bilaketa "zintzoa" izaten ... LIKE '%ΡΠΎΠ·Π°%'
- orduan emaitza ez ezik 'Π ΠΎΠ·Π°Π»ΠΈΡ'
ΠΈ 'ΠΠ°Π³Π°Π·ΠΈΠ½ Π ΠΎΠ·Π°'
Baina 'ΠΡΠΎΠ·Π°'
eta, are gehiago 'ΠΠΎΠΌ ΠΠ΅Π΄Π° ΠΠΎΡΠΎΠ·Π°'
.
Erabiltzaileak eguneroko mailan ematen diozula suposatzen du hitzaren hasieraren arabera bilatu izenburuan eta hori garrantzitsuagoa izan hasten da sartu. Eta egingo duzu ia berehala - linearteko sarrerarako.
1: zeregina mugatu
Eta are gehiago, pertsona bat ez da berariaz sartuko 'ΡΠΎΠ· ΠΌΠ°Π³Π°Π·'
, beraz, hitz bakoitza aurrizkiaren arabera bilatu behar duzu. Ez, askoz errazagoa da erabiltzaile batek azken hitzaren iradokizun azkar bati erantzutea aurrekoak nahita "gutxiegitzea" baino. Begira edozein bilatzailek hau nola kudeatzen duen.
Oro har, behar bezala arazoaren baldintzak formulatzea irtenbidearen erdia baino gehiago da. Batzuetan erabilera kasuen azterketa kontu handiz
Zer egiten du garatzaile abstraktuak?
1.0: kanpoko bilatzailea
Oh, bilaketa zaila da, ez dut ezer egin nahi - eman diezaiogun devopei! Utzi datu-basetik kanpoko bilatzaile bat zabaltzen: Sphinx, ElasticSearch,...
Lan-aukera, sinkronizazioari eta aldaketen abiadurari dagokionez, lan intentsiboa bada ere. Baina ez gure kasuan, bilaketa bezero bakoitzarentzat bakarrik egiten baita bere kontuaren datuen esparruan. Eta datuek nahiko aldakortasun handia dute - eta kudeatzaileak orain txartela sartu badu 'ΠΠ°Π³Π°Π·ΠΈΠ½ Π ΠΎΠ·Π°'
, orduan 5-10 segundoren buruan dagoeneko gogoratuko du bere e-posta bertan adieraztea ahaztu zaiola eta aurkitu eta zuzendu nahi duela.
Beraz - dezagun bilatu "zuzenean datu-basean". Zorionez, PostgreSQL-k hau egiteko aukera ematen digu, eta ez aukera bakarra - aztertuko ditugu.
1.1: "zintzoa" azpikatea
βAzpikateβ hitzari eusten diogu. Baina indizearen bilaketa azpikatearen bidez (eta baita adierazpen erregularren bidez ere!) bikaina dago
Saia gaitezen hurrengo plaka hartzen eredua sinplifikatzeko:
CREATE TABLE firms(
id
serial
PRIMARY KEY
, name
text
);
Benetako erakundeen 7.8 milioi erregistro kargatzen ditugu bertan eta indexatzen ditugu:
CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);
Bila ditzagun linearteko bilaketarako lehen 10 erregistroak:
SELECT
*
FROM
firms
WHERE
lower(name) ~ ('(^|s)' || 'ΡΠΎΠ·Π°')
ORDER BY
lower(name) ~ ('^' || 'ΡΠΎΠ·Π°') DESC -- ΡΠ½Π°ΡΠ°Π»Π° "Π½Π°ΡΠΈΠ½Π°ΡΡΠΈΠ΅ΡΡ Π½Π°"
, lower(name) -- ΠΎΡΡΠ°Π»ΡΠ½ΠΎΠ΅ ΠΏΠΎ Π°Π»ΡΠ°Π²ΠΈΡΡ
LIMIT 10;
Tira, hori da... 26 ms, 31 MB irakurri datuak eta iragazitako 1.7K erregistro baino gehiago - bilatutako 10entzat. Kostu orokorrak handiegiak dira, ez al dago eraginkorragorik?
1.2: bilatu testuaren arabera? FTS da!
Izan ere, PostgreSQL-k oso indartsua eskaintzen du
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', 'ΡΠΎΠ·Π°:*')
ORDER BY
lower(name) ~ ('^' || 'ΡΠΎΠ·Π°') DESC
, lower(name)
LIMIT 10;
Hemen kontsultaren exekuzioaren paralelizazioak pixka bat lagundu digu, denbora erdira murriztuz 11 ms. Eta 1.5 aldiz gutxiago irakurri behar izan genuen, guztira 20MB. Baina hemen, zenbat eta gutxiago, orduan eta hobeto, zeren zenbat eta bolumen handiagoa irakurri, orduan eta aukera handiagoak izango dira cache-a galtzeko, eta diskotik irakurritako datu-orri gehigarri bakoitza eskaeraren "balazta" potentziala da.
1.3: oraindik GUSTATZEN?
Aurreko eskaera guztiontzat ona da, baina egunean ehun mila aldiz tiratzen badiozu bakarrik, etorriko da 2TB datuak irakurri. Kasurik onenean, memoriatik, baina zorterik ez baduzu, diskotik. Beraz, saia gaitezen txikitzen.
Gogora dezagun erabiltzaileak ikusi nahi duena lehenengo "hasieran...". Beraz, hau bere forma garbienean dago text_pattern_ops
! Eta soilik bilatzen ari garen 10 erregistro "nahikoa ez" badugu, FTS bilaketa erabiliz irakurtzen amaitu beharko dugu:
CREATE INDEX ON firms(lower(name) text_pattern_ops);
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('ΡΠΎΠ·Π°' || '%')
LIMIT 10;
Errendimendu bikaina - guztira 0.05 ms eta 100 KB baino pixka bat gehiago irakurri! Bakarrik ahaztu zaigu izenaren arabera ordenatuerabiltzailea emaitzetan gal ez dadin:
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('ΡΠΎΠ·Π°' || '%')
ORDER BY
lower(name)
LIMIT 10;
Oh, zerbait jada ez da hain ederra - badirudi aurkibide bat dagoela, baina sailkapena hegan igarotzen da... Dagoeneko, noski, aurreko aukera baino askoz ere eraginkorragoa da, baina...
1.4: "fitxategi batekin amaitu"
Baina bada indize bat barrutiaren arabera bilatzeko eta ordenatzea normaltasunez erabiltzeko aukera ematen duena - btree erregularra!
CREATE INDEX ON firms(lower(name));
Eskaera soilik "eskuz bildu" beharko da:
SELECT
*
FROM
firms
WHERE
lower(name) >= 'ΡΠΎΠ·Π°' AND
lower(name) <= ('ΡΠΎΠ·Π°' || chr(65535)) -- Π΄Π»Ρ UTF8, Π΄Π»Ρ ΠΎΠ΄Π½ΠΎΠ±Π°ΠΉΡΠΎΠ²ΡΡ
- chr(255)
ORDER BY
lower(name)
LIMIT 10;
Bikaina - sailkapenak funtzionatzen du eta baliabideen kontsumoa "mikroskopikoa" izaten jarraitzen du. FTS "purua" baino milaka aldiz eraginkorragoa! Eskaera bakar batean biltzea besterik ez da geratzen:
(
SELECT
*
FROM
firms
WHERE
lower(name) >= 'ΡΠΎΠ·Π°' AND
lower(name) <= ('ΡΠΎΠ·Π°' || chr(65535)) -- Π΄Π»Ρ UTF8, Π΄Π»Ρ ΠΎΠ΄Π½ΠΎΠ±Π°ΠΉΡΠΎΠ²ΡΡ
ΠΊΠΎΠ΄ΠΈΡΠΎΠ²ΠΎΠΊ - chr(255)
ORDER BY
lower(name)
LIMIT 10
)
UNION ALL
(
SELECT
*
FROM
firms
WHERE
to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'ΡΠΎΠ·Π°:*') AND
lower(name) NOT LIKE ('ΡΠΎΠ·Π°' || '%') -- "Π½Π°ΡΠΈΠ½Π°ΡΡΠΈΠ΅ΡΡ Π½Π°" ΠΌΡ ΡΠΆΠ΅ Π½Π°ΡΠ»ΠΈ Π²ΡΡΠ΅
ORDER BY
lower(name) ~ ('^' || 'ΡΠΎΠ·Π°') DESC -- ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌ ΡΡ ΠΆΠ΅ ΡΠΎΡΡΠΈΡΠΎΠ²ΠΊΡ, ΡΡΠΎΠ±Ρ ΠΠ ΠΏΠΎΠΉΡΠΈ ΠΏΠΎ btree-ΠΈΠ½Π΄Π΅ΠΊΡΡ
, lower(name)
LIMIT 10
)
LIMIT 10;
Kontuan izan bigarren azpikontsulta exekutatzen dela lehenengoa espero baino gutxiago itzuli bazen bakarrik azken LIMIT
lerro kopurua. Kontsulten optimizazio metodo honi buruz ari naiz
Beraz, bai, orain bai btree eta bai gin dugu mahai gainean, baina estatistikoki hori ateratzen da eskaeren % 10 baino gutxiago bigarren blokearen exekuziora iristen da. Hau da, zeregin horretarako aldez aurretik ezagutzen diren muga tipikoak izanik, zerbitzariaren baliabideen guztizko kontsumoa ia mila aldiz murriztu ahal izan dugu!
1.5*: fitxategirik gabe egin dezakegu
Goi LIKE
Sailkapen okerra erabiltzea galarazi ziguten. Baina "bide egokian ezarri" daiteke USING operadorea zehaztuz:
Berez suposatzen da
ASC
. Gainera, ordenazio-operadore zehatz baten izena zehaztu dezakezu klausula bateanUSING
. Ordenatzeko operadoreak B-zuhaitz-operadoreen familia batzuen baino txikiagoa edo handiagoaren kide izan behar du.ASC
normalean baliokideaUSING <
ΠΈDESC
normalean baliokideaUSING >
.
Gure kasuan, "gutxiago" da ~<~
:
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('ΡΠΎΠ·Π°' || '%')
ORDER BY
lower(name) USING ~<~
LIMIT 10;
2: eskaerak nola garratzen diren
Orain sei hilabetez edo urtebetez "suakitzeko" eskaera uzten dugu, eta harrituta gaude berriro "goienean" aurkitzea oroimenaren eguneroko "pumping" osoaren adierazleekin (Buffers partekatutako hit) at 5.5TB - hau da, hasieran zena baino are gehiago.
Ez, noski, gure negozioa hazi egin da eta gure lan karga handitu egin da, baina ez kopuru berean! Horrek esan nahi du zerbait arraina dela hemen - asma dezagun.
2.1: orrialdearen sorrera
Noizbait, beste garapen-talde batek "jauzi" egin nahi izan zuen azpi-indizeen bilaketa azkar batetik erregistrora emaitza berdinekin, baina hedatuekin. Zer da erregistro bat orrialdeen nabigaziorik gabe? Izorratu dezagun!
( ... LIMIT <N> + 10)
UNION ALL
( ... LIMIT <N> + 10)
LIMIT 10 OFFSET <N>;
Orain bilaketa-emaitzen erregistroa "orriz orrialde" kargatuz erakustea posible zen garatzaileari inolako esfortzurik gabe.
Noski, hain zuzen ere, ondorengo datu orrialde bakoitzeko gero eta gehiago irakurtzen da (guztiak aurreko garaikoak, baztertuko ditugunak, gehi beharrezko "buztana") - hau da, antipatroi argia da. Baina zuzenagoa litzateke bilaketa hurrengo iterazioan hastea interfazean gordetako gakotik, baina horretaz beste behin.
2.2: Zerbait exotikoa nahi dut
Noizbait garatzaileak nahi zuen dibertsifikatu lortutako lagina datuekin beste mahai batetik, eta horretarako aurreko eskaera osoa CTEri bidali zitzaion:
WITH q AS (
...
LIMIT <N> + 10
)
SELECT
*
, (SELECT ...) sub_query -- ΠΊΠ°ΠΊΠΎΠΉ-ΡΠΎ Π·Π°ΠΏΡΠΎΡ ΠΊ ΡΠ²ΡΠ·Π°Π½Π½ΠΎΠΉ ΡΠ°Π±Π»ΠΈΡΠ΅
FROM
q
LIMIT 10 OFFSET <N>;
Eta hala ere, ez da txarra, azpikontsulta itzulitako 10 erregistroetarako soilik ebaluatzen baita, ez bada ...
2.3: DISTINCT zentzugabea eta errukigabea da
Nonbait halako bilakaera prozesuan 2. azpikontsultatik galdu egin zen NOT LIKE
baldintza. Argi dago honen ostean UNION ALL
itzultzen hasi zen sarrera batzuk bi aldiz - lehen lerroaren hasieran aurkitu da, eta gero berriro - lerro honen lehen hitzaren hasieran. Mugan, 2. azpikontsultako erregistro guztiak lehenaren erregistroekin bat litezke.
Zer egiten du garatzaile batek kausa bilatu beharrean?.. Ez dago zalantzarik!
- tamaina bikoiztu jatorrizko laginak
- aplikatu DISTINCTlerro bakoitzaren instantzia bakarrak lortzeko
WITH q AS (
( ... LIMIT <2 * N> + 10)
UNION ALL
( ... LIMIT <2 * N> + 10)
LIMIT <2 * N> + 10
)
SELECT DISTINCT
*
, (SELECT ...) sub_query
FROM
q
LIMIT 10 OFFSET <N>;
Hau da, argi dago emaitza, azkenean, berdina dela, baina 2. CTE azpikontsultara "hegan egiteko" aukera askoz handiagoa izan da, eta hau gabe ere, argi eta garbi irakurgarriagoa.
Baina hau ez da tristeena. Garatzaileak hautatzeko eskatu zuenetik DISTINCT
ez zehatzetarako, alor guztietarako aldi berean baizik erregistroak, orduan sub_query eremua βazpikontsultaren emaitzaβ automatikoki sartu zen bertan. Orain, exekutatzeko DISTINCT
, datu-baseak exekutatu behar zuen jada ez 10 azpikontsulta, baina guztiak <2 * N> + 10!
2.4: lankidetza batez ere!
Beraz, garatzaileek bizi izan zuten - ez zuten trabarik izan, erabiltzaileak argi eta garbi ez zuelako nahikoa pazientzia izan erregistroa N balio esanguratsuetara "egokitzeko" moteltze kronikoarekin, ondorengo "orrialde" bakoitza jasotzean.
Beste sail bateko garatzaileak etorri zitzaizkien arte eta hain metodo erosoa erabili nahi izan zuten arte bilaketa iteratiborako - hau da, lagin batetik pieza bat hartzen dugu, baldintza osagarrien arabera iragazten dugu, emaitza marrazten dugu, ondoren hurrengo pieza (gure kasuan N handituz lortzen da), eta horrela pantaila bete arte.
Oro har, harrapatutako alean N ia 17K-ko balioetara iritsi zen, eta egun bakarrean, gutxienez, eskaera horietako 4K gauzatu ziren "katean". Horietako azkenak ausardiaz eskaneatu zituzten 1 GB memoria iterazio bakoitzeko...
Guztira
Iturria: www.habr.com