Ühe SQL uurimise lugu

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:

  1. sessions: seansi teabe nÀitamiseks: brauser, kasutajaagent, riik jne.
  2. recording_data: salvestatud URLid, lehed, kĂŒlastuste kestus
  3. 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 ms

Isegi 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 ms

Ja 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. EXISTS vĂ”ib tĂ”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 ms

Jah, 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 dokumentatsioon, 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: 52710

Seal 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:

  1. KĂŒsi plaanid ei rÀÀgi kogu lugu, kuid vĂ”ivad anda vihjeid
  2. Peamised kahtlusalused ei ole alati tĂ”elised sĂŒĂŒdlased
  3. Aeglaseid pÀringuid saab jagada kitsaste kohtade isoleerimiseks
  4. KÔik optimeerimised ei ole loomulikult reduktiivsed
  5. 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 Varun Malhotra 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

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster