Ühe SQL-uuringu lugu

Eelmise aasta detsembris sain VWO tugimeeskonnalt huvitava veateate. Ühe analĂŒĂŒsiaruande laadimisaeg suure ettevĂ”tte kliendi jaoks nĂ€is olevat ĂŒlemÀÀra pikk. Kuna see kuulub minu vastutusalasse, keskendusin viivitamatult probleemi lahendamisele.

Eelalugu

Et oleks selge, millest jutt, rÀÀgin pisut VWO-st. See on platvorm, millega saab oma saitidel kĂ€ivitada erinevaid sihitud kampaaniaid: teostada A/B katseid, jĂ€lgida kĂŒlastajaid ja konversioone, teha mĂŒĂŒgivihje analĂŒĂŒse, kuvada soojuskaarte ja esitada kĂŒlastuse salvestusi.

Kuid kĂ”ige olulisem platvormi aspekt on aruannete koostamine. KĂ”ik eelnevalt mainitud funktsioonid on omavahel seotud. Suur hulk teavet oleks ettevĂ”tte klientidele lihtsalt kasutuks, kui see ei oleks esitatud analĂŒĂŒsimiseks mĂ”eldud platvormi kaudu.

Platvormi abil saab teha vabalt valitud pÀringu suure hulga andmete pealt. Siin on lihtne nÀide:

Kuva kÔik klÔpsud lehel "abc.com" 
PERIOODILT <kuupÀev d1> KUNI <kuupÀev d2> 
inimeste jaoks, kes 
kasutasid Chrome'i VÕI 
(olid Euroopas JA kasutasid iPhone'i)

Pange tÀhele loogika operaatorite olemust. Need on saadaval klientide pÀringu liideses, et luua piisavalt keerulisi pÀringuid andmestike saamiseks.

Aeglane pÀring

Kliendil, millest me rÀÀgime, oli soov teha midagi, mis intuitiivselt peaks töötama kiiresti:

Kuva kÔik seansi salvestused 
kĂŒlastajate jaoks, kes on kĂ€inud mis tahes lehel 
URL-iga, mis sisaldab "/jobs".

Saidil oli tohutult liiklust, ja me sĂ€ilitasime ĂŒle miljoni unikaalse URL-aadressi vaid selle jaoks. Nad soovisid leida ĂŒsnagi lihtsa URL-malli, mis seondub nende Ă€rimudeliga.

Eeluurimine

Vaatame, 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Ă€itmise aeg: 1431924.650 ms

PÀring lÀbis 150 tuhat rida. PÀringu planeerija nÀitas mÔningaid huvitavaid detaile, kuid mingeid ilmseid kitsaskohti ei olnud.

Vaatame pÀringut pÔhjalikumalt. Nagu nÀha, tegeleb see JOIN kolme tabeliga:

  1. sessions: seansi teabe kuvamiseks: brauser, kasutajaagent, riik jne.
  2. recording_data: salvestatud URL-id, lehekĂŒljed, visiitide kestus.
  3. urls: et vÀltida ÀÀrmiselt suurte URL-ide dubleerimist, sÀilitame need eraldi tabelis.

Pange tĂ€hele, et kĂ”ik meie tabelid on juba jagatud account_id. Seega elimineeritakse olukord, kus ĂŒhe ĂŒlemÀÀra suure konto tĂ”ttu tekivad probleemid ĂŒlejÀÀnutele.

TÔendite otsing

SĂŒvenedes, mĂ€rkame, et konkreetse pĂ€ringu korral 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 Ă€kki, kuna ILIKE kĂ”ikide nende pikkade URL-ide korral (meil on ĂŒle 1,4 miljoni unikaalse URL-aadressi, mis on selle konto jaoks kogutud) vĂ”ib jĂ”udlus langetada.

Aga ei — asi pole selles!

SELECT id FROM urls WHERE url ILIKE '%enterprise_customer.com/jobs%';
  id
--------
 ...
(198661 real)

Aeg: 5231.765 ms

OtsingupĂ€ring mallide jĂ€rgi vĂ”tab ainult 5 sekundit. Mallide pĂ”hjal otsimine ĂŒle miljoni unikaalse URL-i osas pole kindlasti probleem.

JĂ€rgmine kahtlane kahtlane — mitu JOIN. VĂ”ib-olla nende ĂŒlemÀÀrane kasutamine viis aeglustumise juurde? Tavaliselt JOIN‘id on kĂ”ige ilmsemad kandidaadid jĂ”udlusprobleemide osas, aga ma ei uskunud, et meie juhtum oleks tavaline.

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 rida)

Aeg: 147.851 ms

Ja see ei olnud ka meie juhtum. JOIN‘id osutusid olema ĂŒsna kiired.

Kitsendame kahtlusi

Olin valmis hakkama pÀringut muutma, et saavutada vÔimalikult suur jÔudluse paranemine. Meie meeskond vÀlja töötas kaks peamist ideed:

  • Kasuta EXISTS-i URLi alampĂ€ringu suhtes: Soovisime veel kord kontrollida, kas URL-i alampĂ€ringuga on probleeme. Üks vĂ”imalus selleks on lihtsalt kasutada EXISTS. EXISTS suuteline tĂ”hususe suurendamiseks, kuna see lĂ”ppeb kohe, kui leiab ĂŒksiku rea vastavalt tingimusele.

VALI
	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 =5
    AND recording_data.num_of_pages > 0 ;
 count
 32519
(1 row)
Time: 1636.637 ms

Jah, alampĂ€ring, kui see on ĂŒmber. EXISTS, muudab kĂ”ik ĂŒlikiireks. JĂ€rgmine loogiline kĂŒsimus on, miks pĂ€ringud koos JOIN-dega ja alampĂ€ringud töötavad eraldi kiiresti, aga koos on nad hirmsasti aeglased?

  • Viime alampĂ€ringu CTE-sse : kui pĂ€ring töötab kiiresti iseenesest, saame kĂ”igepealt kiire tulemuse arvutada ja seejĂ€rel esitada selle pĂ”hikĂŒsimusele.

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 =5 
    AND recording_data.num_of_pages > 0;

Aga isegi see oli endiselt vÀga aeglane.

Leidke sĂŒĂŒdlane

Kogu selle aja silme ees oli ĂŒks detail, millest ma pidevalt eemale tĂ”ukasin. Aga kuna rohkem ei olnud jÀÀnud, otsustasin sellele siiski pilgu heita. Ma rÀÀgin && operaator. Seni EXISTS lihtsalt parandas jĂ”udlust, && oli ainus veel alles jÀÀnud ĂŒhine tegur kĂ”igis aeglaste pĂ€ringute versioonides.

Vaadates dokumentatsiooni, nĂ€eme, et && kasutatakse, kui on vaja leida ĂŒhiseid elemente kahe massiivi vahel.

Originaalses pÀringus on see:

AND  (  urls &&  array(select id from acc_{account_id}.urls where url  ILIKE  '%enterprise_customer.com/jobs%')::text[]   )

See tĂ€hendab, et me teeme mustriotsingut meie URL-ide ĂŒle ja seejĂ€rel leiame lĂ”ike kĂ”igist URL-idest, millel on ĂŒhised salvestused. See on veidi segane, kuna 'urls' ei viita tabelile, mis sisaldab kĂ”iki URL-e, vaid veerule 'urls' tabelis recording_data.

Kasvatades kahtlusi &&, ĂŒritasin leida neile tĂ”estust pĂ€ringu plaani kaudu, mille genereeris EXPLAIN ANALYZE (mul oli juba salvestatud plaan, kuid mulle meeldib tavaliselt SQL-is eksperimenteerida pigem, kui pĂŒĂŒda mĂ”ista pĂ€ringute planeerijate lĂ€bipaistmatust).

Filter: ((urls && ($0)::text[]) AND (r_time > '2018-12-17 12:17:23+00'::timestamp with time zone) AND (r_time = '5'::double precision) AND (num_of_pages > 0))
                           Rows Removed by Filter: 52710

Seal oli mitmeid filtreerimisridasid ainult &&. Mis tÀhendas, et see operatsioon polnud mitte ainult kulukas, vaid toimus mitu korda.

Kontrollisin seda, isoleerides 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 töötas aeglaselt. Kuna JOIN-d on kiire ja alampÀringud kiired, jÀi alles ainult && operaator.

Kuid see on vÔtmeoperatsioon. Me peame alati otsima pealt suures URL-de tabelis, et mustri pÔhjal otsida, ja me peame alati leidma lÔike. Me ei saa otsida URL-i salvestusi otse, kuna need on lihtsalt ID-d, mis viitavad urls.

Lahenduse poole liikudes

&& aeglaseks, kuna mÔlemad komplektid on tohutud. Operatsioon on suhteliselt kiire, kui asendan urls jÀrgnevaga { "http://google.com/", "http://wingify.com/" }.

Alustasin viisi otsimist, kuidas teha Postgresis kogumite lÔikeid ilma &&, kuid ei olnud erilisi edusamme.

LĂ”puks otsustasime lihtsalt probleemi isoleeritult lahendada: andke mulle kĂ”ik urls read, mille URL vastab mustrile. 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 AND
	urls.url  ILIKE  '%jobs%'

Asetame JOIN SĂŒntaksit kasutasin lihtsalt alampĂ€ringut ja lahti keritud recording_data.urls massiiv, et saaksime otse rakendada tingimust WHERE.

KÔige olulisem on see, et && kasutatakse selle kontrollimiseks, kas antud salvestusel on vastav URL-aadress. Veidi pilku keerates vÔib nÀha, et see operatsioon liigub massiivi (vÔi tabeli ridade) elementide kaudu ja peatub, kui tingimust (vastavust) tÀidetakse. Kas see ei tuleta meelde midagi? Ah, EXISTS.

Kuna recording_data.urls vĂ”ib viidata konteksti vĂ€ljastpoolt alampĂ€ringut, kui see juhtub, saame naasta tagasi meie vanale sĂ”brale EXISTS ja ĂŒmbritseda selle alampĂ€ringuga.

KokkuvÔttes saame lÔpliku optimeeritud pÀringu:

VALI 
    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 = 5 
    AND recording_data.num_of_pages > 0
    AND EXIST(
        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%'
    );

Ja lÔplik tÀitmise aeg Aeg: 1898.717 ms Kas on pidu pidada?!?

Aga mitte nii kiiresti! Esiteks peame kontrollima korrektsust. Olin vÀga ettevaatlik EXISTS optimeerimise suhtes, kuna see muudab loogikat varasemaks lÔpetamiseks. Me peame olema kindlad, et me ei ole lisanud pettumust valmistavat viga pÀringusse.

Lihtne kontroll seisnes selles, et tegin count(*) ja aeglaste ja kiirete pÀringute kohta suure hulga erinevate andme komplektide jaoks. SeejÀrel, vÀikese andmekogumi jaoks kontrollisin kÔigi tulemuste Ôigsust kÀsitsi.

KÔik kontrollid andsid pidevalt positiivseid tulemusi. Me oleme kÔik korda teinud!

TĂ”ukude Õppetunnid

Selle looga saab vÔtta palju Ôppetunde:

  1. PÀringu plaanid ei rÀÀgi kogu lugu, kuid vÔivad anda vihjeid
  2. Peamised kahtlusalused ei ole alati tegelikud sĂŒĂŒdlased
  3. Aeglaseid pÀringuid saab jagada, et kitsaskohti isoleerida
  4. Kaugel ei ole kÔik optimeerimised oma olemuselt reduktiivsed
  5. Kasutamine EXIST, kus see on vÔimalik, vÔib viia oluliseks jÔudluse kasvuks

KokkuvÔte

Oleme liikunud pĂ€ringu ajast ~24 minutist 2 sekundini — vĂ€ga tĂ”sine jĂ”udluse kasv! Kuigi see artikkel on suur, toimusid kĂ”ik meie eksperimendid ĂŒhe pĂ€eva jooksul ja nende teostamiseks ja testimiseks kulus umbes 1,5 kuni 2 tundi.

SQL on imeline keel, kui mitte karta seda, vaid proovida mÔista ja kasutada. HÀid teadmisi, kuidas SQL pÀringud toimuvad, kuidas andmebaasid genereerivad pÀringuplaanid, kuidas indeksid töötavad ja lihtsalt andmekoguse kohta, millega tegelete, saate pÀringute optimeerimises vÀga hÀsti hakkama. Samuti on oluline jÀtkata erinevate lÀhenemiste testimist ja aeglaselt probleeme lahendada, leides kitsaskohad.

Parim osa selliste tulemuste saavutamisel on mĂ€rgatav kiirusetĂ”us — kui raport, mis varem ei launud, launeb nĂŒĂŒd peaaegu koheselt.

Eriline tĂ€nu minu kolleegidele meeskonnast Adithya Mishrale, Adithya Gaurile ja Varun Malhotrale ajurĂŒnnakute eest ja Dinkar Pandirile selle eest, et leidis meie lĂ”pppĂ€ringus olulise vea, enne kui me sellega lĂ”plikult hĂŒvasti jĂ€tsime!

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster