Soovitan tutvuda Vladimir Sitnikovi 2016. aasta alguses peetud ettekande "PostgreSQL ja JDBC pigistame vÀlja iga tilka" kokkuvÔttega


Tere pĂ€eva! Minu nimi on Vladimir Sitnikov. Töösklen ettevĂ”ttes NetCracker juba 10 aastat. Ja peamiselt tegelen tootlikkuse kĂŒsimustega. KĂ”ik, mis on seotud Java ja SQL-iga, on see, mida ma armastan.
Ja tÀna rÀÀgin teile sellest, millega me ettevÔttes silmitsi seisime, kui hakkasime PostgreSQL kasutama andmebaasiserverina. Peamiselt töötame Java'ga. Kuid see, millest ma tÀna rÀÀgin, ei seondu vaid Java'ga. Kogemus on nÀidanud, et sama juhtub ka teistes keeltes.

RÀÀgime jÀrgmistest asjadest:
- andmete valimisest.
- Andmete salvestamisest.
- Ning tootlikkusest.
- Ja ka peidetud kaevandustest, mis seal on.

Alustame lihtsast kĂŒsimusest. Valime tabelist ĂŒhe rea primaarvĂ”tme alusel.

Andmebaas asub samal hostil. Ja kogu see tegevus kestab 20 millisekundit.

Need 20 millisekundit on vÀga palju. Kui teil on 100 sellist pÀringut, siis kulutate aega sekundites, et neid pÀringuid töödeldada, st raiskate aega.
Me ei soovi seda teha ja vaatame, mida andmebaas meile selleks pakub. Andmebaas pakub meile kahte valikut pÀringute tÀitmiseks.

Esimene variant on lihtne pÀring. Miks see on hea? Sellega on see, et me vÔtame selle ja saadame, ja ei midagi enamat.

Andmebaasil on veel ka tĂ€iustatud pĂ€ring, mis on keerulisem, kuid funktsionaalsem. VĂ”ib eraldi saata pĂ€ringu parsimiseks, tĂ€itmiseks, muutujate ĂŒhendamiseks jne.
Super tĂ€iustatud pĂ€ring â seda me ei kĂ€sitle praeguses ettekandes. VĂ”ib-olla tahame andmebaasilt midagi ja meil on soovide nimekiri, mis on mingil kujul koostatud, st see, mida me soovime, kuid ei ole praegu ega ka lĂ€hima aasta jooksul vĂ”imalik. SeetĂ”ttu kirjutasime selle lihtsalt ĂŒles ja kĂ€ime peamisi inimesi pommitamas.

Aga see, mida me saame teha, on lihtne pÀring ja tÀiustatud pÀring.
Mis on iga lÀhenemise eripÀra?
Lihtsat pĂ€ringut on hea kasutada ĂŒhekordseks tĂ€itmiseks. TĂ€idetud ja unustatud. Probleem on selles, et see ei toeta binaarset andmevormingut, st mĂ”ned kĂ”rge jĂ”udlusega sĂŒsteemid ei sobi selleks.

Pikendatud pÀring - aitab sÀÀsta aega parsimisel. See on see, mida me tegime ja hakkasime kasutama. See on meid ÀÀrmiselt aidanud. Siin on mitte ainult sÀÀst parsimisel. On olemas ka andmete edastamise sÀÀst. Andmete edastamine binaarses formaadis on oluliselt efektiivsem.

Liigume praktikasse. NĂ€eb vĂ€lja nagu tĂŒĂŒpiline rakendus. See vĂ”ib olla Java jne.
Me lÔime statement'i. KÀivitasime kÀsu. LÔime close. Kus on viga? Mis on probleem? Probleeme ei ole. Nii on kirjutatud kÔigis raamatutes. Nii tuleb kirjutada. Kui tahate maksimaalset jÔudlust, kirjutage nii.

Aga praktika nĂ€itas, et see ei tööta. Miks? Sest meil on meetod "close". Ja kui me nii teeme, siis andmebaasi vaatenurgast on see nagu suitsetaja töö andmebaasiga. Me ĂŒtlesime "PARSE EXECUTE DEALLOCATE".
Miks need liigsed statementide loomised ja eksportimine? Need ei ole kellelegi vajalikud. Aga tavaliselt on PreparedStatement'i puhul nii, et kui me need kinni paneme, siis nad sulgevad kÔik andmebaasis. See ei ole see, mida me soovime.

Me tahame, nagu terved inimesed, andmebaasiga töötada. Otsustame ĂŒhe korra valmistada meie statement'i ja siis kĂ€ivitame selle mitu korda. Tegelikult on palju kordi â see on ĂŒks kord kogu rakenduse elu jooksul parsimiseks. Ja kasutame erinevates REST'ides ĂŒhte ja sama statement'i id-d. See on meie eesmĂ€rk.

Kuidas me sellele eesmÀrgile jÔuame?

VĂ€ga lihtne - statements ei pea sulgema. Kirjutame nii: "prepare" "execute".


Kui me midagi sellist kĂ€ivitame, on selge, et kusagil mingi asi ĂŒletĂ€idab. Kui ei ole selge, siis saame mÔÔta. VĂ”tame ja kirjutame benchmark'i, kus on lihtsalt selline meetod. Loome statement'i. KĂ€ivitame mingis versioonis draiverist ja saame, et see kukub ĂŒsna kiiresti kokku mĂ€lukaotusega, mis meil tekib.
Selge, et sellised vead on kergesti parandatavad. Ma ei hakka neist rÀÀkima. Kuid ma ĂŒtlen, et uues versioonis töötab see palju kiiremini. Meetod on mĂ”ttetu, kuid siiski.

Kuidas Ôigesti töötada? Mida me selleks peame tegema?
Tegelikult sulgevad rakendused alati statements'id. KÔikides raamatutes kirjutatakse, et peate need sulgema, vastasel juhul lekib mÀlu.
Ja PostgreSQL ei oska pÀringuid vahemÀlus hoida. Iga seanss peab oma jaoks selle vahemÀlu looma.
Ja aega parsimisele me samuti ei taha kulutada.

Ja nagu tavaliselt, on meil kaks varianti.
Esimene variant on, et vĂ”tame ja ĂŒtleme, et laseme kĂ”ik PgSQL-i mĂ€hkida. Seal on vahemĂ€lu. See kĂ”ik salvestatakse. Tulemus on imeline. Oleme sellist asja vaadanud. Meil on 100500 pĂ€ringut. See ei toimi. Me ei ole nĂ”us â kĂ€sitsi muundama pĂ€ringud protseduurideks. Ei-ei.
Meil on teine variant â vĂ”tta ja ise teha. Avame lĂ€htekoodid, alustame tööd. Töötame ja töötame. Selgub, et seda on mitte nii keeruline teha.

See ilmus augustis 2015. Praegu on juba kaasaegsem versioon. Ja kÔik on suurepÀrane. See töötab nii hÀsti, et me ei muuda rakenduses midagi. Ja me oleme isegi lÔpetanud mÔtlemise PgSQL-i suunas, st see oli piisav, et kÔik kÔrvalkulud vÀhendada praktiliselt nullini.
Seega Server-prepared statements aktiveeritakse 5. tĂ€itmisel, et mitte raisata mĂ€lu andmebaasis iga ĂŒhekordse pĂ€ringu jaoks.

VĂ”ib kĂŒsida â kus arvud? Mida te saate? Ja siin ma arvuteid ei anna, sest iga pĂ€ringu jaoks on need omad.
Meie pĂ€ringud olid sellised, et OLTP-pĂ€ringutes kulutasime umbes 20 millisekundit parsimiseks. Seal oli 0,5 millisekundit tĂ€itmiseks, 20 millisekundit parsimiseks. PĂ€ring â 10 KiB teksti, 170 reavaadet. See on OLTP pĂ€ring. See kĂŒsib 1, 5, 10 rida, mĂ”nikord rohkem.
Aga me ei tahtnud ĂŒldse kulutada 20 millisekundit. Me viisime selle nulli. KĂ”ik on suurepĂ€rane.
Mida te siit kaasa vÔtate? Kui teil on Java, siis vÔtate kaasaegse draiveri versiooni ja olete rÔÔmsad.
Kui teil on mĂ”ni muu keel, siis mĂ”elge â vĂ”ib-olla vajate ka seda? Sest lĂ”ppkeele vaatepunktist, nĂ€iteks kui PL 8 vĂ”i teil on LibPQ, siis ei ole teile ilmselge, et te raiskate aega mitte tĂ€itmiseks, vaid parsimiseks ja see tasub ĂŒle vaadata. Kuidas? KĂ”ik tasuta.

V.a see, et on vigu, mÔned spetsiifikat. Ja neist me just praegu rÀÀgime. Suur osa tuleb tööstuslikust arheoloogiast, sellest, mida oleme leidnud, millele oleme sattunud.

Kui pĂ€ring genereeritakse dĂŒnaamiliselt. Sedasi juhtub. Keegi liidab stringid, tulemuseks saab SQL-pĂ€ring.
Miks see halb on? See on halb, sest iga kord saame lÔpuks erineva stringi.
Ja selle erineva reaga peab hashCode uuesti arvutama. See on tĂ”epoolest CPU ĂŒlesanne â leida pikka pĂ€ringuteksti isegi olemasolevas hash' ĂŒheselt ei ole nii lihtne. SeetĂ”ttu on lihtne jĂ€reldus â Ă€rge genereerige pĂ€ringuid. Hoidke need mingis ĂŒhes muutujas. Ja rÔÔmustage.

JĂ€rgmine probleem. AndmetĂŒĂŒbid on olulised. On olemas ORM-id, mis ĂŒtlevad, et pole vahet, milline NULL, olgu see mingi. Kui on Int, siis ĂŒtleme setInt. Ja kui on NULL, siis olgu see alati VARCHAR. Ja mis vahet seal lĂ”puks on, milline NULL? Andmebaasil on endal kĂ”ik arusaadav. Ja selline pilt ei toimi.
Praktikas ei ole andmebaasilt sugugi kĂ”ik ĂŒhtemoodi. Kui olete esmakordselt öelnud, et see on teil number, ja teisel korral öelnud, et see on VARCHAR, siis ei saa Server-prepared statements'i uuesti kasutada. Sellisel juhul tuleb meie statement jĂ€lle uuesti luua.

Kui tĂ€idate sama pĂ€ringut, siis jĂ€lgige, et andmetĂŒĂŒbid veergudes ei seguneks. Peab jĂ€lgima NULL-i. See on sage viga, mis meil esines pĂ€rast PreparedStatements'i kasutuselevĂ”ttu.

HĂ€sti, lĂŒlitasime sisse. VĂ”ib-olla kasutasime draiverit. Ja jĂ”udlus langes. KĂ”ik muutus halvaks.
Kuidas see on vĂ”imalik? Kas see on bug vĂ”i funktsioon? Kahjuks ei Ă”nnestunud mĂ”ista â kas see on bug vĂ”i funktsioon. Aga on tĂ€iesti lihtne stsenaarium, kuidas seda probleemi paljundada. See ootamatult pĂŒĂŒdis meid kinni. Ja seisneb valimises, mis on praktiliselt ainult ĂŒhest tabelist. Meil oli muidki selliseid pĂ€ringuid. Need sisaldasid reeglina kahte-kolme tabelit, kuid siin on selline paljundamissenaarium. VĂ”tke oma andmebaasist mis tahes versioon ja paljundage.

MĂ”te on selles, et meil on kaks veergu, millest kummaski on indeks. Ăhes veerus on NULL-i vÀÀrtus miljon rida. Teises veerus on vaid 20 rida. Kui tĂ€idame ilma sidumata muutujateta, siis kĂ”ik töötab hĂ€sti.
Kui hakkame tÀitma sidustatud muutujatega, st tÀidame mÀrgi "?" vÔi "$1" meie pÀringule, siis mida me lÔpuks saame?

Esimene tĂ€itmine â nagu alati. Teine â natuke kiiremini. Midagi vahemĂ€llu salvestus. Kolmas-neljanda viies. Siis plahvatab â ja just nii. Ja kĂ”ige halvem on see, et see juhtub kuuendal tĂ€itmisel. Kes teadis, et peab tegema just kuus tĂ€itmist, et aru saada, milline seal reaalselt tĂ€itmisplaan on?

Kes on sĂŒĂŒdi? Mis juhtus? Andmebaas sisaldab optsioonide optimeerimist. Ja see on nagu optimeeritud ĂŒldiseks olukorraks. SeetĂ”ttu, alates mingist hetkest, liigub ta ĂŒldisele plaanile, mis, kahjuks, vĂ”ib osutuda teistsuguseks. Ta vĂ”ib osutuda samaks vĂ”i teistsuguseks. Seal on mingi kĂŒnnisvÀÀrtus, mis toob kaasa sellise kĂ€itumise.
Mida sellega teha? Siin on tĂ”enĂ€oliselt keerulisem midagi oletada. On lihtne lahendus, mida me kasutame. See on +0, OFFSET 0. Kindlasti teate selliseid lahendusi. Lihtsalt vĂ”tame ja lisame pĂ€ringusse â+0â ja kĂ”ik on hĂ€sti. NĂ€itan hiljem.
Ja on veel variant â vaadata hoolikamalt plaane. Arendaja peab mitte ainult pĂ€ringut kirjutama, vaid ka 6 korda ĂŒtlema âexplain analyzeâ. Kui 5, siis ei sobi.
Ja on veel kolmas variant â kirjutada pgsql-hackers-le kiri. Ma kirjutasin, tĂ”si kĂŒll, hetkel ei ole selge â kas see on viga vĂ”i omadus.

Kuni me mĂ”tleme â kas see on viga vĂ”i omadus, lahendame probleemid. VĂ”tame meie pĂ€ringu ja lisame â+0â. KĂ”ik on hĂ€sti. Kaks mĂ€rki ja isegi ei pea mĂ”tlema, kuidas ja mis seal on. VĂ€ga lihtne. Me lihtsalt kĂ€skisime andmebaasil mitte kasutada indeksi selle veeru jaoks. Meil ei ole indeksi veert â+0â ja kĂ”ik, andmebaas ei kasuta indeksit, kĂ”ik on hĂ€sti.

Siin on reegel 6 explain'ist. Hetkel kÀesolevates versioonides tuleb teha 6 korda, kui teil on seotud muutujad. Kui teil ei ole seotud muutujaid, siis teeme nii. Ja meil lÔpuks tÀpselt see pÀring kukub. Asi pole keeruline.
Tundub, kui kaua saab? Siin on viga, seal on viga. Tegelikult on viga igal pool.

Vaatame veel. NĂ€iteks, meil on kaks skeemi. Skeem A tabeliga Đ« ja skeem B sama tabeliga Đ«. PĂ€ring â valida andmed tabelist. Mis meil sellega on? Meil on viga. Meil on kĂ”ik eelnevalt loetletud. Reegel on selline â viga igal pool, meil on kĂ”ik eelnevalt loetletud.

NĂŒĂŒd kĂŒsimus: âMiks?â. Tundub, et on dokumentatsioon, et kui meil on skeem, siis on muutuja âsearch_pathâ, mis ĂŒtleb, kus peab otsima tabelit. Tundub, et muutuja on olemas.
Mis on probleem? Probleem on selles, et serveri valmistatud avaldused ei kahtle, et search_pathi vÔib keegi muuta. See vÀÀrtus jÀÀb andmebaasi jaoks nagu konstantseks. Ja mÔned osad ei pruugi haarata uusi vÀÀrtusi.

Loomulikult sÔltub see versioonist, millega te testite. See sÔltub ka sellest, kui ulatuslikud teie tabelid on. Versioon 9.1 kÀivitab lihtsalt vanad pÀringud. Uuemad versioonid vÔivad aga avastada petu ja öelda, et teil on viga.

Kuidas seda parandada? Lihtne retsept â Ă€rge tehke nii. Ărge muutke search_path'i rakenduse töö kĂ€igus. Kui muudate, siis on parem luua uus ĂŒhendus.
Saame arutada, st avada, arutada ja kirjutada juurde. VĂ”ib-olla suudame veenda andmebaasi arendajaid, et juhul, kui keegi muudab vÀÀrtust, peaks andmebaas sellest kliendile teatama: âVaadake, teie vÀÀrtus on siin uuendatud. Kas peaksite statements'id lĂ€htestama, uuesti looma?â. Praegu kĂ€itub andmebaas salaja ja ei teata mingil moel, et kuskil sees on statements'id muutunud.
Ja ma rĂ”hutan taas â see pole Java puhul tĂŒĂŒpiline. NĂ€eme seda sama ka PL/pgSQL'is sĂ”na-sĂ”nalt. Seal see ka kordub.

Proovime veel andmeid valida. Valime, valime. Meil on tabel, kus on miljon rida. Iga rida ĂŒks kilobait. Umbes gigabait andmeid. Ja meil on Java masina töötav mĂ€lu 128 megabaiti.
Kasutame nagu kÔikides raamatutes soovitatakse voogedastust. St avame resultSet ja loeme sealt andmeid tasapisi. Kas see töötab? Kas see ei kuku mÀlu tÔttu? Kas see loeb natuke? Usume andmebaasi, usume Postgressesse. Ei usu. Kukume OutOfMemory? Kes on kukkunud OutOfMemory? Ja kes suutis pÀrast seda parandada? Keegi suutis parandada.
Kui teil on miljon rida, siis ei saa lihtsalt nii valida. Peab kindlasti olema OFFSET/LIMIT. Kes on sellise variandi poolt? Ja kes on variandi poolt, et tuleb autoCommit'iga mÀngida?
Siin, nagu tavaliselt, osutub kĂ”ige ootamatum variant Ă”igeks. Ja kui te Ă€kitselt lĂŒlitate autoCommit'i vĂ€lja, siis see aitab. Miks nii? Teadusele see ei ole teada.

Aga vaikimisi valivad kĂ”ik kliendid, kes ĂŒhenduvad Postgres andmebaasiga, andmeid tervikuna. PgJDBC selle osas pole erand, valib kĂ”ik read.
On variatsioon FetchSize'il, st saate eraldi statements'i tasandil öelda, et siit palun valige andmeid 10, 50. Kuid see ei tööta, kuni te ei lĂŒlita autoCommit'i vĂ€lja. LĂŒlitasite autoCommit'i vĂ€lja â hakkab tööle.
Koodi lĂ€bimine ja igal pool setFetchSize seadmine on ebamugav. SeetĂ”ttu tegime sellise seadistuse, mis mÀÀrab vaikimisi vÀÀrtuse kogu ĂŒhenduse jaoks.

Nii et me seda ĂŒtlesime. Seadisime parameetri. Ja mis me nĂŒĂŒd saime? Kui valime vĂ€ikese, nĂ€iteks 10 rida, siis on meie kulud ĂŒsna suured. SeetĂ”ttu peaksime selle vÀÀrtuse seadma umbes sajale.

Ideaalis vĂ”iksime muidugi Ă”ppida piirama ka baitides, aga retsept on selline: seab defaultRowFetchSize ĂŒle saja ja oleme rahul.

Liigume andmete sisestamise juurde. Sisestamine on lihtsam, seal on erinevaid variante. NĂ€iteks, INSERT, VALUES. See on hea variant. VĂ”ime rÀÀkida "INSERT SELECT". Praktikas on need ĂŒks ja sama. Erinevust jĂ”udluses ei ole.
Raamatud ĂŒtlevad, et tuleb tĂ€ita Batch statement, raamatud ĂŒtlevad, et vĂ”ib tĂ€ita keerulisemaid kĂ€ske mitme sulgudega. Ja Postgres'is on suurepĂ€rane funktsioon - vĂ”ib teha COPY, st teha seda kiiremini.

Kui mÔÔta, vÔib teha uuesti mitmeid huvitavaid avastusi. Kuidas me tahame, et see töötab? Tahame mitte parsida ja mitte tÀiendavaid kÀske tÀita.

Praktikas ei lase TCP meil nii teha. Kui klient on hÔivatud pÀringu saatmisega, siis andmebaas proovib meile vastuseid saata, aga pÀringuid ei loeta. LÔpuks ootab klient andmebaasi, kuni see pÀringu loeb, samal ajal kui andmebaas ootab klienti, kuni see vastuse loeb.

SeetĂ”ttu peab klient perioodiliselt saatma sĂŒnkroniseerimise paketi. Ăksikud vĂ”rgu suhtlused, aeglane ajakadu.
Ja mida rohkem me neid lisame, seda halvemaks lĂ€heb. Draiver on ĂŒsna pessimistlik ja lisab neid ĂŒsna sageli, umbes iga 200 rea jĂ€rel, sĂ”ltuvalt ridade suurusest jms.

Juhtub, et parandad ĂŒhte rida ja kĂ”ik kiireneb 10 korda. Nii juhtub. Miks? Nagu tavaliselt, mingi konstandi oli seal juba kasutatud. Ja vÀÀrtus â128â tĂ€hendas - mitte kasutada batchingut.

Hea, et see ei sattunud ametlikku versioonisse. Leidsime selle enne, kui hakkasime vÀlja andma vÀljaannet. KÔik vÀÀrtused, mida ma nimetan, pÔhinevad kaasaegsetel versioonidel.

MÔÔgame. Me mÔÔdame lihtsat InsertBatch'i. Me mÔÔdame mitmekordset InsertBatch'i, st sama asi, kuid palju values. Kaval kÀik. Mitte kÔik ei oska nii teha, aga see on lihtne kÀik, palju lihtsam kui COPY.

Saab teha COPY.

Ja saab seda teha struktuurides. Deklareeri kasutaja vaikimisi tĂŒĂŒp, edastades massiivi ja INSERT otse tabelisse.
Kui avate lingi: pgjdbc/ubenchmsrk/InsertBatch.java, siis see kood on GitHubis. Saate vaadata tÀpselt, milliseks pÀringud seal genereeritakse. See ei ole olulised.

Me kĂ€ivitasime selle. Ja esimene asi, mille me mĂ”istsime, oli see, et batchi mittekasutamine on lihtsalt vĂ€listatud. KĂ”ik batchimise variandid on nullid, st tĂ€itmise aeg on praktiliselt null vĂ”rreldes ĂŒhekordse tĂ€itmisega.

Me sisestame andmed. Seal on vÀga lihtne tabel. Kolm veergu. Ja mida me siin nÀeme? Me nÀeme, et kÔik need kolm varianti on umbes vÔrreldavad. Ja COPY on loomulikult parem.

See, kui me tĂŒkikaupa sisestame. Kui me rÀÀkisime, et ĂŒks vÀÀrtus VALUES, kaks vÀÀrtust VALUES, kolm vÀÀrtust VALUES vĂ”i kui me neid 10 komaga nimetasime. See peaks olema nĂŒĂŒd horisontaalsel tasandil. 1, 2, 4, 128. On nĂ€ha, et Batch Insert, mis on siniseks joonistatud, muudab asja kergemaks. St kui sisestate ĂŒkshaaval vĂ”i isegi nelja kaupa, siis see aitab kahel korral paremini, lihtsalt seetĂ”ttu, et me panime VALUESisse veidi rohkem. VĂ€hem EXECUTE operatsioone.
Kopeerimise kasutamine vÀikeste mahtude korral on ÀÀrmiselt vÀhelootustandev. Ma ei joonistanud isegi kahe esimesega. Need lÀhevad taevasse, st need rohelised numbrid jaoks COPY.
COPY-d tuleks kasutada, kui teie andmemaht on vĂ€hemalt ĂŒle saja rea. Ăhenduse avamise kulud on suured. Ja, ausalt öeldes, ma ei ole selle poolega sĂŒvenenud. Batch'i optimeerisin, COPY-d mitte.
Mida me edasi teeme? Me mÔÔdame. Saame aru, et peame kasutama kas struktuure vÔi nutikat batching'u, mis liidab mitu vÀÀrtust.

Mida tuleb tÀnasest ettekandest vÀlja tuua?
- PreparedStatement â see on meie kĂ”ik. See toob vĂ€ga palju kaasa jĂ”udlusele. See toob suure alusikka.
- Ja tuleb teha EXPLAIN ANALYZE 6 korda.
- Ja tuleb lahjendada OFFSET 0 ning trikkidega nagu +0, et korvata veel jÀÀnud protsent meie probleemsetest pÀringutest.
Allikas: habr.com
