PagÄjuÅ”Ä gada decembrÄ« es saÅÄmu interesantu kļūdu ziÅojumu no VWO atbalsta komandas. Liela korporatÄ«vÄ klienta viena no analÄ«tisko pÄrskatu ielÄdes laiks Ŕķita pÄrmÄrÄ«gs. Un tÄ kÄ Å”Ä« ir mana atbildÄ«bas joma, es nekavÄjoties pievÄrsos problÄmas risinÄÅ”anai.
AizvÄsture
Lai bÅ«tu skaidrs, par ko es runÄju, pastÄstÄ«Å”u nedaudz par VWO. Å Ä« ir platforma, ar kuras palÄ«dzÄ«bu savÄs vietnÄs varat uzsÄkt dažÄdas mÄrÄ·tiecÄ«gas kampaÅas: veikt A/B eksperimentus, izsekot apmeklÄtÄjiem un reklÄmguvumiem, analizÄt pÄrdoÅ”anas piltuvi, parÄdÄ«t siltuma kartes un atskaÅot apmeklÄjumu ierakstus.
Bet vissvarÄ«gÄkÄ lieta platformÄ ir ziÅoÅ”ana. Visas iepriekÅ” minÄtÄs funkcijas ir savstarpÄji saistÄ«tas. Un korporatÄ«vajiem klientiem milzÄ«gs informÄcijas apjoms bÅ«tu vienkÄrÅ”i bezjÄdzÄ«gs bez jaudÄ«gas platformas, kas to parÄda analÄ«tiskÄ formÄ.
Izmantojot platformu, varat veikt izlases veida vaicÄjumu lielai datu kopai. Å eit ir vienkÄrÅ”s piemÄrs:
RÄdÄ«t visus klikŔķus lapÄ "abc.com" NO <datums d1> LÄŖDZ <date d2> personÄm, kuras izmantoja Chrome VAI (atrodas EiropÄ UN izmantoja iPhone)
PievÄrsiet uzmanÄ«bu BÅ«la operatoriem. Tie ir pieejami klientiem vaicÄjumu saskarnÄ, lai veiktu patvaļīgi sarežģītus vaicÄjumus, lai iegÅ«tu paraugus.
LÄns pieprasÄ«jums
AttiecÄ«gais klients mÄÄ£inÄja darÄ«t kaut ko tÄdu, kam intuitÄ«vi jÄdarbojas Ätri:
RÄdÄ«t visus sesijas ierakstus lietotÄjiem, kuri apmeklÄja jebkuru lapu, kuras URL satur "/jobs"
Å ai vietnei bija daudz trafika, un mÄs tikai tai saglabÄjÄm vairÄk nekÄ miljonu unikÄlu URL. Un viÅi vÄlÄjÄs atrast diezgan vienkÄrÅ”u URL veidni, kas bÅ«tu saistÄ«ta ar viÅu uzÅÄmÄjdarbÄ«bas modeli.
IepriekÅ”Äja izmeklÄÅ”ana
PaskatÄ«simies, kas notiek datu bÄzÄ. ZemÄk ir sÄkotnÄjais lÄnais SQL vaicÄjums:
SELECT
count(*)
FROM
acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data as recording_data,
acc_{account_id}.sessions as sessions
WHERE
recording_data.usp_id = sessions.usp_id
AND sessions.referrer_id = recordings_urls.id
AND ( urls && array(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%')::text[] )
AND r_time > to_timestamp(1542585600)
AND r_time < to_timestamp(1545177599)
AND recording_data.duration >=5
AND recording_data.num_of_pages > 0 ;
Un Ŕeit ir laiki:
PlÄnotais laiks: 1.480 ms Izpildes laiks: 1431924.650 ms
VaicÄjums pÄrmeklÄja 150 tÅ«kstoÅ”us rindu. VaicÄjumu plÄnotÄjs parÄdÄ«ja dažas interesantas detaļas, bet nekÄdu acÄ«mredzamu vÄjo vietu.
IzpÄtÄ«sim pieprasÄ«jumu tÄlÄk. KÄ redzat, viÅÅ” to dara JOIN trÄ«s tabulas:
sesijas: lai parÄdÄ«tu informÄciju par sesiju: āāpÄrlÅ«kprogramma, lietotÄja aÄ£ents, valsts un tÄ tÄlÄk.
ierakstÄ«Å”anas_dati: ierakstÄ«tie URL, lapas, apmeklÄjumu ilgums
URL: lai izvairÄ«tos no ÄrkÄrtÄ«gi lielu URL dublÄÅ”anas, mÄs tos saglabÄjam atseviÅ”Ä·Ä tabulÄ.
Å emiet vÄrÄ arÄ« to, ka visas mÅ«su tabulas jau ir sadalÄ«tas account_id. TÄdÄjÄdi tiek izslÄgta situÄcija, kad viens Ä«paÅ”i liels konts rada problÄmas citiem.
MeklÄ norÄdes
Paskatoties tuvÄk, redzam, ka ar konkrÄto pieprasÄ«jumu kaut kas nav kÄrtÄ«bÄ. Ir vÄrts sÄ«kÄk aplÅ«kot Å”o rindu:
urls && array(
select id from acc_{account_id}.urls
where url ILIKE '%enterprise_customer.com/jobs%'
)::text[]
PirmÄ doma bija, ka varbÅ«t tÄpÄc ILIKE visos Å”ajos garajos URL (mums ir vairÄk nekÄ 1,4 miljoni unikÄls Å”im kontam apkopotie URL) veiktspÄja var ciest.
Bet nÄ, tas nav galvenais!
SELECT id FROM urls WHERE url ILIKE '%enterprise_customer.com/jobs%';
id
--------
...
(198661 rows)
Time: 5231.765 ms
Pats veidnes meklÄÅ”anas pieprasÄ«jums aizÅem tikai 5 sekundes. Rakstura meklÄÅ”ana miljonos unikÄlu URL noteikti nav problÄma.
NÄkamie aizdomÄs turamie sarakstÄ ir vairÄki JOIN. VarbÅ«t to pÄrmÄrÄ«ga izmantoÅ”ana ir izraisÄ«jusi palÄninÄÅ”anos? Parasti JOIN's ir visredzamÄkie veiktspÄjas problÄmu kandidÄti, taÄu es neticÄju, ka mÅ«su gadÄ«jums ir tipisks.
analytics_db=# SELECT
count(*)
FROM
acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data_0 as recording_data,
acc_{account_id}.sessions_0 as sessions
WHERE
recording_data.usp_id = sessions.usp_id
AND sessions.referrer_id = recordings_urls.id
AND r_time > to_timestamp(1542585600)
AND r_time < to_timestamp(1545177599)
AND recording_data.duration >=5
AND recording_data.num_of_pages > 0 ;
count
-------
8086
(1 row)
Time: 147.851 ms
Un tas arÄ« nebija mÅ«su gadÄ«jums. JOIN's izrÄdÄ«jÄs diezgan Ätrs.
AizdomÄs turamo loka saÅ”aurinÄÅ”anÄs
Es biju gatavs sÄkt mainÄ«t vaicÄjumu, lai panÄktu iespÄjamos veiktspÄjas uzlabojumus. Es un mana komanda izstrÄdÄjÄm 2 galvenÄs idejas:
ApakÅ”vaicÄjuma URL izmantojiet EXISTS: mÄs vÄlÄjÄmies vÄlreiz pÄrbaudÄ«t, vai nav raduÅ”Äs problÄmas ar vietrÄžu URL apakÅ”vaicÄjumu. Viens veids, kÄ to panÄkt, ir vienkÄrÅ”i izmantot EXISTS. EXISTSvar ievÄrojami uzlabo veiktspÄju, jo tas beidzas uzreiz, tiklÄ«dz tiek atrasta vienÄ«gÄ virkne, kas atbilst nosacÄ«jumam.
SELECT
count(*)
FROM
acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data as recording_data,
acc_{account_id}.sessions as sessions
WHERE
recording_data.usp_id = sessions.usp_id
AND ( 1 = 1 )
AND sessions.referrer_id = recordings_urls.id
AND (exists(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%'))
AND r_time > to_timestamp(1547585600)
AND r_time < to_timestamp(1549177599)
AND recording_data.duration >=5
AND recording_data.num_of_pages > 0 ;
count
32519
(1 row)
Time: 1636.637 ms
Nu jÄ. ApakÅ”vaicÄjums, kad tas ir iesaiÅots EXISTS, padara visu super Ätru. NÄkamais loÄ£iskais jautÄjums, kÄpÄc pieprasÄ«jums ar JOIN-ami un pats apakÅ”vaicÄjums ir Ätrs atseviŔķi, bet kopÄ Å”ausmÄ«gi lÄni?
ApakÅ”vaicÄjuma pÄrvietoÅ”ana uz CTE : ja vaicÄjums ir Ätrs pats par sevi, mÄs varam vienkÄrÅ”i vispirms aprÄÄ·inÄt Ätro rezultÄtu un pÄc tam nodroÅ”inÄt to galvenajam vaicÄjumam
WITH matching_urls AS (
select id::text from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%'
)
SELECT
count(*) FROM acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data as recording_data,
acc_{account_id}.sessions as sessions,
matching_urls
WHERE
recording_data.usp_id = sessions.usp_id
AND ( 1 = 1 )
AND sessions.referrer_id = recordings_urls.id
AND (urls && array(SELECT id from matching_urls)::text[])
AND r_time > to_timestamp(1542585600)
AND r_time < to_timestamp(1545107599)
AND recording_data.duration >=5
AND recording_data.num_of_pages > 0;
Bet tas joprojÄm bija ļoti lÄns.
VainÄ«gÄ atraÅ”ana
Visu Å”o laiku manu acu priekÅ”Ä pazibÄja viens sÄ«kums, ko nemitÄ«gi sviedu malÄ. Bet tÄ kÄ nekas cits neatlika, nolÄmu paskatÄ«ties arÄ« uz viÅu. Es runÄju par && operators. Uz redzÄÅ”anos EXISTS tikai uzlabota veiktspÄja && bija vienÄ«gais atlikuÅ”ais kopÄ«gais faktors visÄs lÄnÄ vaicÄjuma versijÄs.
Skatoties uz dokumentÄcija, mÄs to redzam && izmanto, ja jÄatrod kopÄ«gi elementi starp diviem masÄ«viem.
SÄkotnÄjÄ pieprasÄ«jumÄ tas ir:
AND ( urls && array(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%')::text[] )
Tas nozÄ«mÄ, ka mÄs savos vietrÄžos URL veicam raksta meklÄÅ”anu, pÄc tam atrodam krustojumu ar visiem URL, kuros ir izplatÄ«tas ziÅas. Tas ir nedaudz mulsinoÅ”i, jo Å”eit "urls" attiecas nevis uz tabulu, kurÄ ir visi URL, bet gan uz tabulas kolonnu "urls". recording_data.
Ar pieaugoÅ”Äm aizdomÄm par &&, mÄÄ£inÄju tiem atrast apstiprinÄjumu izveidotajÄ vaicÄjumu plÄnÄ EXPLAIN ANALYZE (Man jau bija saglabÄts plÄns, bet man parasti ir ÄrtÄk eksperimentÄt ar SQL, nevis mÄÄ£inÄt saprast vaicÄjumu plÄnotÄju necaurredzamÄ«bu).
Filter: ((urls && ($0)::text[]) AND (r_time > '2018-12-17 12:17:23+00'::timestamp with time zone) AND (r_time < '2018-12-18 23:59:59+00'::timestamp with time zone) AND (duration >= '5'::double precision) AND (num_of_pages > 0))
Rows Removed by Filter: 52710
Bija vairÄkas filtru rindas tikai no &&. Kas nozÄ«mÄja, ka Ŕī operÄcija bija ne tikai dÄrga, bet arÄ« veikta vairÄkas reizes.
Es to pÄrbaudÄ«ju, izolÄjot stÄvokli
SELECT 1
FROM
acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data_30 as recording_data_30,
acc_{account_id}.sessions_30 as sessions_30
WHERE
urls && array(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%')::text[]
Å is vaicÄjums bija lÄns. TÄpÄc ka JOIN-s ir Ätri un apakÅ”vaicÄjumi ir Ätri, vienÄ«gais, kas palicis, bija && operators.
Å Ä« ir tikai galvenÄ darbÄ«ba. Mums vienmÄr ir jÄmeklÄ visa pamatÄ esoÅ”Ä URL tabula, lai meklÄtu modeli, un mums vienmÄr ir jÄatrod krustojumi. MÄs nevaram meklÄt tieÅ”i pÄc URL ierakstiem, jo āātie ir tikai ID, kas attiecas uz urls.
CeÄ¼Ä uz risinÄjumu
&& lÄni, jo abi komplekti ir milzÄ«gi. Ja nomainÄ«Å”u, operÄcija bÅ«s salÄ«dzinoÅ”i Ätra urls par { "http://google.com/", "http://wingify.com/" }.
Es sÄku meklÄt veidu, kÄ iestatÄ«t krustojumu PostgresÄ, neizmantojot &&, bet bez Ä«paÅ”iem panÄkumiem.
Galu galÄ mÄs nolÄmÄm problÄmu atrisinÄt atseviŔķi: dodiet man visu urls rindas, kuru URL atbilst modelim. Bez papildu nosacÄ«jumiem tas bÅ«s -
SELECT urls.url
FROM
acc_{account_id}.urls as urls,
(SELECT unnest(recording_data.urls) AS id) AS unrolled_urls
WHERE
urls.id = unrolled_urls.id AND
urls.url ILIKE '%jobs%'
TÄ vietÄ, lai JOIN sintakse Es tikko izmantoju apakÅ”vaicÄjumu un paplaÅ”inÄju recording_data.urls masÄ«vs, lai jÅ«s varÄtu tieÅ”i lietot nosacÄ«jumu WHERE.
VissvarÄ«gÄkais Å”eit ir tas && izmanto, lai pÄrbaudÄ«tu, vai konkrÄtais ieraksts satur atbilstoÅ”u URL. Ja nedaudz paskatÄs, jÅ«s varat redzÄt, ka Ŕī darbÄ«ba pÄrvietojas pa masÄ«va elementiem (vai tabulas rindÄm) un apstÄjas, kad ir izpildÄ«ts nosacÄ«jums (atbilstÄ«ba). Tev neko neatgÄdina? JÄ, EXISTS.
KopÅ” tÄ laika recording_data.urls var atsaukties Ärpus apakÅ”vaicÄjuma konteksta, kad tas notiek, mÄs varam atsaukties uz savu veco draugu EXISTS un iesaiÅojiet ar to apakÅ”vaicÄjumu.
Saliekot visu kopÄ, mÄs iegÅ«stam galÄ«go optimizÄto vaicÄjumu:
SELECT
count(*)
FROM
acc_{account_id}.urls as recordings_urls,
acc_{account_id}.recording_data as recording_data,
acc_{account_id}.sessions as sessions
WHERE
recording_data.usp_id = sessions.usp_id
AND ( 1 = 1 )
AND sessions.referrer_id = recordings_urls.id
AND r_time > to_timestamp(1542585600)
AND r_time < to_timestamp(1545177599)
AND recording_data.duration >=5
AND recording_data.num_of_pages > 0
AND EXISTS(
SELECT urls.url
FROM
acc_{account_id}.urls as urls,
(SELECT unnest(urls) AS rec_url_id FROM acc_{account_id}.recording_data)
AS unrolled_urls
WHERE
urls.id = unrolled_urls.rec_url_id AND
urls.url ILIKE '%enterprise_customer.com/jobs%'
);
Un pÄdÄjais izpildes laiks Time: 1898.717 ms Laiks svinÄt?!?
Ne tik Ätri! Vispirms jums ir jÄpÄrbauda pareizÄ«ba. Es biju ÄrkÄrtÄ«gi aizdomÄ«gs EXISTS optimizÄcija, jo tÄ maina loÄ£iku, lai pÄrtrauktu agrÄk. Mums ir jÄpÄrliecinÄs, ka pieprasÄ«jumam neesam pievienojuÅ”i nepÄrprotamu kļūdu.
VienkÄrÅ”s tests bija palaist count(*) gan lÄniem, gan Ätrajiem vaicÄjumiem lielam skaitam dažÄdu datu kopu. PÄc tam nelielai datu apakÅ”kopai es manuÄli pÄrbaudÄ«ju, vai visi rezultÄti ir pareizi.
Visi testi sniedza nemainÄ«gi pozitÄ«vus rezultÄtus. MÄs visu salabojÄm!
GÅ«tÄs mÄcÄ«bas
No Ŕī stÄsta var mÄcÄ«ties daudzas mÄcÄ«bas:
VaicÄjumu plÄni neizstÄsta visu, taÄu tie var sniegt norÄdes
Galvenie aizdomÄs turamie ne vienmÄr ir patiesie vainÄ«gie
LÄnus vaicÄjumus var sadalÄ«t, lai izolÄtu vÄjÄs vietas
Ne visas optimizÄcijas ir reducÄjoÅ”as
Izmantot EXIST, ja iespÄjams, var izraisÄ«t dramatisku produktivitÄtes pieaugumu
secinÄjums
MÄs pÄrgÄjÄm no vaicÄjuma laika ~24 minÅ«tes uz 2 sekundÄm ā diezgan ievÄrojams veiktspÄjas pieaugums! Lai gan Å”is raksts iznÄca liels, visi mÅ«su veiktie eksperimenti notika vienas dienas laikÄ, un tika lÄsts, ka optimizÄcijai un testÄÅ”anai bija nepiecieÅ”amas 1,5ā2 stundas.
SQL ir brÄ«niŔķīga valoda, ja jÅ«s no tÄs nebaidÄties, bet mÄÄ£iniet to iemÄcÄ«ties un lietot. Ja jums ir laba izpratne par to, kÄ tiek izpildÄ«ti SQL vaicÄjumi, kÄ datu bÄze Ä£enerÄ vaicÄjumu plÄnus, kÄ darbojas indeksi, un vienkÄrÅ”i apstrÄdÄjamo datu lielumu, jÅ«s varat ļoti veiksmÄ«gi optimizÄt vaicÄjumus. TomÄr vienlÄ«dz svarÄ«gi ir turpinÄt izmÄÄ£inÄt dažÄdas pieejas un lÄnÄm nojaukt problÄmu, atrodot vÄjÄs vietas.
LabÄkÄ daļa Å”Ädu rezultÄtu sasniegÅ”anÄ ir pamanÄms, redzams Ätruma uzlabojums ā tagad pÄrskats, kas iepriekÅ” pat netika ielÄdÄts, tagad tiek ielÄdÄts gandrÄ«z uzreiz.
ÄŖpaÅ”s paldies mani biedri pÄc Aditjas MiÅ”ras pavÄles, Aditja Gauru Šø Varuns Malhotra prÄta vÄtrai un Dinkars Pandirs par to, ka atradÄm svarÄ«gu kļūdu mÅ«su pÄdÄjÄ pieprasÄ«jumÄ, pirms mÄs beidzot atvadÄ«jÄmies no tÄs!