Eelmise aasta detsembris sain huvitava veaaruande VWO tugimeeskonnalt. Ăhe analĂŒĂŒsiraporti laadimisaeg suurtele ettevĂ”tte klientidele tundus olema ĂŒlemÀÀra pikk. Kuna see on minu vastutusala, keskendusin kohe probleemi lahendamisele.
Eellugu
Et selgem oleks, millest jutt kĂ€ib, rÀÀgin veidi VWO-st. See on platvorm, mille abil saab kĂ€ivitada erinevaid sihitud kampaaniaid oma veebilehtedel: lĂ€bi viia A/B katseid, jĂ€lgida kĂŒlastajaid ja konversioone, analĂŒĂŒsida mĂŒĂŒgivoolu, kuvada soojuskaarte ja esitada kĂŒlastuste salvestusi.
Aga kĂ”ige tĂ€htsam platvormi juures on aruannete koostamine. KĂ”ik eelnevalt nimetatud funktsioonid on omavahel seotud. Ja ettevĂ”tte klientide jaoks oleks tohutu informatsioonihulk lihtsalt kasutuks, kui see ei oleks korraliku analĂŒĂŒsiplatvormi esitlemine.
Platvormi kasutades saab teha vaba pÀringu suurel andmekogumil. Siin on lihtne nÀide:
Kuva kĂ”ik klĂ”psud lehel "abc.com" KUNI <kuupĂ€ev d1> kuni <kuupĂ€ev d2> inimeste jaoks, kes kasutasid Chrome'i VĂI (olid Euroopas JA kasutasid iPhone'i)
Pange tÀhele loogilisi operaatorite. Need on klientide jaoks pÀringute liideses, et luua keerulisemaid pÀringuid valikute saamiseks.
Aeglane pÀring
Client, mille kohta jutt kĂ€ib, ĂŒritas teha midagi, mis peaks intuitiivselt toimuma kiiresti:
Kuva kĂ”ik sessioonide salvestused kasutajate jaoks, kes kĂŒlastasid mis tahes lehte, kus on url, mis sisaldab "\/jobs"
Sellel veebisaidil oli tohutu liiklus ja me hoidsime ĂŒle miljoni ainulaadse URL-aadressi ainult selle jaoks. Ja nad soovisid leida ĂŒsna lihtsa url-malli, mis oleks seotud nende Ă€ri mudeliga.
Eeluurimine
Vaadakem, mis toimub andmebaasis. Allpool on algne aeglane SQL-pÀring:
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 ;Siin on ajad:
Eeldatav aeg: 1.480 ms TĂ€ideviimise aeg: 1431924.650 ms
PÀring hÔlmas 150 000 rida. PÀringu planeerija nÀitas paar huvitavat detaili, kuid ei mingit ilmselget kitsaskohta.
Uurime pÀringut edasi. Nagu nÀha, see teeb JOIN kolm tabelit:
- sessions: seansi teabe nÀitamiseks: brauser, kasutajaagent, riik jne.
- recording_data: salvestatud URLid, lehed, kĂŒlastuste kestus
- urls: et vÀltida ÀÀrmiselt suurte URLide dubleerimist, hoiame neid eraldi tabelis.
Pange tĂ€hele, et kĂ”ik meie tabelid on juba jagatud account_id. Nii on vĂ€listatud olukord, kus ĂŒhe eriti suure konto tĂ”ttu tekivad probleemid ĂŒlejÀÀnutel.
TÔendite otsing
LÀhemal vaatlemisel nÀeme, et konkreetse pÀringuga on midagi valesti. Tasub tÀhelepanu pöörata sellele reale:
urls && array(
select id from acc_{account_id}.urls
where url ILIKE '%enterprise_customer.com/jobs%'
)::text[]Esimene mÔte oli, et vÔib-olla on selle tÔttu, et ILIKE kÔikide nende pikkade URLide puhul (meil on rohkem kui 1,4 miljonit unikaalset URLi, mis on selle konto jaoks kogutud) jÔudlus vÔib olla problemaatiline.
Aga ei â asi pole selles!
SELECT id FROM urls WHERE url ILIKE '%enterprise_customer.com/jobs%';
id
--------
...
(198661 rida)
Aeg: 5231.765 msIsegi mustriotsingu pÀring vÔtab vaid 5 sekundit. Mustriotsing miljonis unikaalses URLis pole kindlasti probleem.
JĂ€rgmine kahtlane isik nimekirjas â mĂ”ned JOIN. VĂ”ib-olla nende ĂŒlemÀÀrane kasutamine viis aeglustumiseni? Tavalised JOINon kĂ”ige ilmsesemaid kahtlusaluseid jĂ”udlusprobleemide osas, kuid ma ei uskunud, et meie juhtum oleks tĂŒĂŒpiline.
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 = 5
AND recording_data.num_of_pages > 0 ;
count
-------
8086
(1 rida)
Aeg: 147.851 msJa see polnud samuti meie juhtum. JOINon osutunud ĂŒsna kiireks.
Kitsendame kahtlusaluste ringi
Olin valmis alustama pÀringu muutmist, et saavutada vÔimalikult palju jÔudluse parandusi. Meie meeskond töötas vÀlja kaks peamist ideed:
- Kasutada EXISTS URL alampĂ€ringut: Soovisime veel kord kontrollida, kas URLide alampĂ€ringuga on probleeme. Ăks viis selle saavutamiseks on lihtsalt kasutada
EXISTS.EXISTStÔhusalt parandada jÔudlust, kuna see lÔppeb kohe, kui see leiab ainulaadse rea tingimuse alusel.
VALI
count(*)
KUST
acc_{account_id}.urls kui recordings_urls,
acc_{account_id}.recording_data kui recording_data,
acc_{account_id}.sessions kui sessions
KUS
recording_data.usp_id = sessions.usp_id
JA ( 1 = 1 )
JA sessions.referrer_id = recordings_urls.id
JA (exists(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%'))
JA r_time > to_timestamp(1547585600)
JA r_time =5
JA recording_data.num_of_pages > 0 ;
count
32519
(1 rida)
Aeg: 1636.637 msJah, alamkĂŒsitlus, kui see on pakendatud EXISTS, muudab kĂ”ik super kiireks. JĂ€rgmine loogiline kĂŒsimus on, miks pĂ€ringud koos JOIN-idega ja alamkĂŒsitlus on eraldi kiiresti, kuid koos aeglustavad need kohutavalt?
- Viime alamkĂŒsitluse CTE-sse : kui pĂ€ring on iseenesest kiire, saame kĂ”igepealt lihtsalt kiiresti tulemuse arvutada ja seejĂ€rel anda selle pĂ”hipĂ€ringule
WITH matching_urls AS (
vali id::text from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%'
)
VALI
count(*) FROM acc_{account_id}.urls kui recordings_urls,
acc_{account_id}.recording_data kui recording_data,
acc_{account_id}.sessions kui sessions,
matching_urls
KUS
recording_data.usp_id = sessions.usp_id
JA ( 1 = 1 )
JA sessions.referrer_id = recordings_urls.id
JA (urls && array(VALI id from matching_urls)::text[])
JA r_time > to_timestamp(1542585600)
JA r_time =5
JA recording_data.num_of_pages > 0;Kuid isegi see oli endiselt vÀga aeglane.
Leiame sĂŒĂŒdlase
Kogu selle aja silme ees vilkus ĂŒks pisiasi, millest ma pidevalt kĂ”rvale tĂ”ukasin. Kuid kuna ei jÀÀnud enam midagi muud, otsustasin sellele pilku heita. Ma rÀÀgin && operaatorist. Seni EXISTS lihtsalt parandas jĂ”udlust, && oli ainus allesjÀÀnud ĂŒhine tegur kĂ”igis aeglase pĂ€ringu versioonides.
Vaadates , nĂ€eme, et && kasutatakse, kui on vaja leida ĂŒhiseid elemente kahe massiivi vahel.
Originaalses pÀringus see:
JA ( urls && array(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com/jobs%')::text[] )Mis tĂ€hendab, et me teeme mustri pĂ”hjal otsingu meie URLe, seejĂ€rel leiame ristmiku kĂ”ikide URL-idega, millel on ĂŒhised kirjed. See on natuke segane, kuna 'urls' siin ei viita tabelile, kus on kĂ”ik URL-id, vaid veerg 'urls' tabelis recording_data.
Seoses suureneva kahtlusega &&, ĂŒritasin leida kinnitust nendele kĂŒsimustele genereeritud pĂ€ringu plaanis. EXPLAIN ANALYZE (mul on juba olnud salvestatud plaan, kuid tavaliselt on mul mugavam eksperimentida SQL-is kui proovida aru saada pĂ€ringute optimeerijate lĂ€bipaistmatust).
Filter: ((urls && ($0)::text[]) JA (r_time > '2018-12-17 12:17:23+00'::timestamp with time zone) JA (r_time = '5'::double precision) JA (num_of_pages > 0))
Ridu eemaldatud filtri kaudu: 52710Seal oli mitu filtririda ainult &&. Mis tÀhendas, et see operatsioon oli mitte ainult kulukas, vaid jÔudis ka mitu korda toime.
Kontrollisin seda, isolerides tingimuse
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[]See pĂ€ring kĂ€idi aeglaselt. Kuna JOIN-id on kiired ja alam-pĂ€ringud on kiired, jĂ€i ĂŒle ainult && operaator.
Aga see on ainult see peamine operatsioon. Me peame alati otsima kogu pÔhilaudade URL-addresside kaudu, et otsida mallide jÀrgi ja me peame alati leidma ristumisi. Me ei saa otsida URL-ide salvestiste kaudu otse, sest need on lihtsalt ID, mis viitavad urls.
Teel lahendusele
&& aeglane, kuna mÔlemad kogumid on suured. Operatsioon oleks suhteliselt kiire, kui ma asendan urls . Tundub, et { "http://google.com/", "http://wingify.com/" }.
Hakkasin otsima viisi, kuidas teha Postgresis kogumite ristumist ilma &&, kuid ilma eriliste edusammudeta.
LĂ”puks otsustasime lihtsalt lahendada probleemi isoleeritult: anna mulle kĂ”ik urls read, mille jaoks URL vastab mallile. Ilma tĂ€iendavate tingimusteta oleks see âÂ
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 JA
urls.url ILIKE '%jobs%'Asenda JOIN sĂŒntaksis kasutasin lihtsalt alam-pĂ€ringut ja avasin recording_data.urls massiiv, et saaks otse rakendada tingimust KUS.
Siin on kĂ”ige olulisem see, et && kasutatakse, et kontrollida, kas antud salvestis sisaldab vastavat URL- аЎŃĐ”Ńа. Veidi tĂ”mmates vĂ”ib nĂ€ha, kuidas selles operatsioonis liikuda massiivi elementide (vĂ”i tabeli ridade) vahel ja peatuda, kui toimub tingimus (vastavus). Kas midagi ei meenuta? Ah, EXISTS.
Kuna recording_data.urls vĂ”ib viidata alt konteksti alam-pĂ€ringu kontekstis, kui see juhtub, saame tagasi pöörduda meie vanade sĂ”prade poole EXISTS ja ĂŒmbritseda seda alam-pĂ€ringut.
Kokku sidudes saame me lÔpuks optimeeritud pÀringu:
VALI
count(*)
KUSTA
acc_{account_id}.urls kui recordings_urls,
acc_{account_id}.recording_data kui recording_data,
acc_{account_id}.sessions kui sessions
KUS
recording_data.usp_id = sessions.usp_id
JA ( 1 = 1 )
JA sessions.referrer_id = recordings_urls.id
JA r_time > to_timestamp(1542585600)
JA r_time =5
JA recording_data.num_of_pages > 0
JA EXITS(
VALI urls.url
KUSTA
acc_{account_id}.urls kui urls,
(VALI unnest(urls) AS rec_url_id KUSTA acc_{account_id}.recording_data)
KUI unrolled_urls
KUS
urls.id = unrolled_urls.rec_url_id JA
urls.url ILIKE '%enterprise_customer.com/jobs%'
);
Ja lÔplik tÀitmise aeg Aeg: 1898.717 ms Kas on aeg tÀhistamiseks?!?
Ăra nii kiiresti! Esiteks peame kontrollima Ă”igsust. Ma olin ÀÀrmiselt kahtlev EXISTS optimeerimise osas, kuna see muudab loogikat varasemaks lĂ”petamiseks. Me peame olema kindlad, et me ei ole lisanud nĂ€htamatut viga pĂ€ringusse.
Lihtne kontroll seisnes count(*) ja aeglastes ja kiiretes pÀringutes erinevate andmekogumite jaoks. SeejÀrel kontrollisin vÀikese andmekogumi puhul kÔiki tulemusi kÀsitsi.
KÔik kontrollid andsid stabiilselt positiivseid tulemusi. Me oleme kÔik korda teinud!
TĂ”mmatud Ăppetunnid
Sellest loost saab palju Ôppetunde:
- KĂŒsi plaanid ei rÀÀgi kogu lugu, kuid vĂ”ivad anda vihjeid
- Peamised kahtlusalused ei ole alati tĂ”elised sĂŒĂŒdlased
- Aeglaseid pÀringuid saab jagada kitsaste kohtade isoleerimiseks
- KÔik optimeerimised ei ole loomulikult reduktiivsed
- Kasutamine
EXIST, kus see on vÔimalik, vÔib viia drastilise jÔudluse kasvuni
KokkuvÔte
Me liigutasime pĂ€ringu aega ~24 minutist 2 sekundini - ĂŒsna mĂ€rkimisvÀÀrne jĂ”udluse kasv! Kuigi see artikkel on suur, toimusid kĂ”ik eksperimendid ĂŒhe pĂ€eva jooksul ja hinnanguliselt kulus optimeerimise ja testimise jaoks 1,5â2 tundi.
SQL on imeline keel, kui mitte karta seda, vaid pĂŒĂŒda aru saada ja kasutada. Hea arusaam sellest, kuidas SQL-pĂ€ringuid töötatakse, kuidas andmebaas genereerib plaanid, kuidas indeksid töötavad ja lihtsalt andmete suurusest, millega on tegemist, aitab teil pĂ€ringute optimeerimisel vĂ€ga hĂ€sti edasi jĂ”uda. Samuti on oluline jĂ€tkata erinevate lĂ€henemiste proovimist ja aeglaselt probleemi lahendamist kitsaste kohtade leidmiseks.
Parim osa sarnaste tulemuste saavutamisel on silmapaistev nĂ€htav kiirusetĂ”us â kui aruanne, mis varem isegi ei laadinud, laadib nĂŒĂŒd peaaegu koheselt.
Eriline tĂ€nu minu kolleegidele tiimist Aditya Mishra, Aditya Gaur ja ajurĂŒnnaku ja Dinkar Pandirile selle eest, et leidis meie lĂ”pp-pĂ€ringus olulise vea, enne kui me selle lĂ”plikult lĂ”petasime!
Allikas: habr.com
