Sistemi analitik i DB ClickHouse përpunon shumë rreshta të ndryshëm, duke konsumuar burime. Për të shpejtuar funksionimin e sistemit, vazhdimisht bëhen optimizime të reja. Zhvilluesi i ClickHouse, Nikolai Kochetov, tregon për tipin e të dhënave me rreshta, përfshirë tipin e ri, LowCardinality, dhe shpjegon se si mund të përshpejtohet puna me rreshta.

â SĂ« pari, le ta kuptojmĂ« si mund tĂ« ruhen rreshtat.

Ne kemi tipe tĂ« dhĂ«nash me rreshta. String Ă«shtĂ« i pĂ«rshtatshĂ«m si opsion default dhe duhet pĂ«rdorur pothuajse gjithmonĂ«. Ka njĂ« Overhead tĂ« vogĂ«l â 9 bytes pĂ«r njĂ« rresht. NĂ«se duam qĂ« madhĂ«sia e rreshtave tĂ« jetĂ« fikse dhe e njohur paraprakisht, Ă«shtĂ« mĂ« mirĂ« tĂ« pĂ«rdorim FixedString. NĂ« tĂ« mund tĂ« caktosh numrin e duhur tĂ« bytes, Ă«shtĂ« i pĂ«rshtatshĂ«m pĂ«r tĂ« dhĂ«nat si adresat IP ose funksionet hash.

Sigurisht, ndonjëherë diçka ngadalëson. Le të themi se bëni një kërkesë në një tabelë. ClickHouse lexon një sasi të madhe të dhënash, le të themi me një shpejtësi prej 100 GB/s, ndërsa rreshtat përpunohen pak. Ne kemi dy tabela që ruajnë pothuajse të dhëna të njëjta. Nga tabela e dytë, ClickHouse lexon të dhëna me një shpejtësi më të madhe, por numri i rreshtave të lexuar për sekondë është tri herë më i vogël.

NĂ«se shikojmĂ« madhĂ«sinĂ« e tĂ« dhĂ«nave tĂ« kompresuara, ajo do tĂ« rezultojĂ« pothuajse e barabartĂ«. NĂ« tĂ« vĂ«rtetĂ«, nĂ« tabela janĂ« tĂ« shkruara tĂ« njĂ«jtat tĂ« dhĂ«na â miliardi i parĂ« i numrave â vetĂ«m se nĂ« kolonĂ«n e parĂ« janĂ« tĂ« shkruara si UInt64, ndĂ«rsa nĂ« tĂ« dytĂ«n â si String. PĂ«r kĂ«tĂ« arsye, kĂ«rkesa e dytĂ« ka nevojĂ« pĂ«r mĂ« shumĂ« kohĂ« pĂ«r tĂ« lexuar tĂ« dhĂ«nat nga disku dhe pĂ«r t'i zbuluar ato.

Ja njĂ« shembull tjetĂ«r. Le tĂ« supozojmĂ« se ka njĂ« grup tĂ« njohur paraprakisht tĂ« vargjeve, i cili Ă«shtĂ« i kufizuar me njĂ« konstantĂ« 1000 ose 10,000 dhe gati asnjĂ«herĂ« nuk ndryshon. PĂ«r kĂ«tĂ« rast na pĂ«rshtatet tipin e tĂ« dhĂ«nave Enum, nĂ« ClickHouse ka dy â Enum8 dhe Enum16. PĂ«r shkak tĂ« ruajtjes nĂ« Enum, ne i pĂ«rpunojmĂ« kĂ«rkesat shpejt.
Në ClickHouse ka përshpejtimet për GROUP BY, IN, DISTINCT dhe optimizime për disa funksione, për shembull për krahasimin me një string konstant. Sigurisht, numrat në string nuk konvertohen, por, përkundrazi, stringu konstant reduktohet në vlerën Enum. Pas kësaj, gjithçka krahasohet shpejt.
Por ka edhe disavantazhe. Edhe nĂ«se e dimĂ« saktĂ«sisht grupin e vargjeve, ndonjĂ«herĂ« ai duhet tĂ« plotĂ«sohet. Kur vjen njĂ« varg i ri â duhet tĂ« bĂ«jmĂ« ALTER.

ALTER për Enum në ClickHouse është implementuar në mënyrë optimale. Ne nuk e riparojmë të dhënat në disk, por ALTER mund të ngadalësohet për shkak se strukturat Enum ruhen në skemën e vetë tabelës. Prandaj, duhet të presim për kërkesat për lexim nga tabela, për shembull.
Ngrihet pyetja, a mund të bëhet më mirë? Mbase, po. Mund të ruhen strukturat Enum jo në skemën e tabelës, por në ZooKeeper. Megjithatë, mund të lindin probleme të lidhura me sinkronizimin. Për shembull, një replikë merr të dhëna, një tjetër jo, dhe nëse ajo ka një Enum të vjetër, diçka mund të prishë. (Në ClickHouse, ne jemi gati me kërkesat ALTER jo bllokuese. Kur t'i përfundojmë ato plotësisht, nuk do të nevojitet të presim për kërkesat për lexim.)

Për të mos u marrë me ALTER Enum, mund të përdorim fjalorët e jashtëm të ClickHouse. Të kujtoj se kjo është një strukturë të dhënash key-value brenda ClickHouse, e cila mundëson marrjen e të dhënave nga burime të jashtme, për shembull nga tabelat MySQL.
NĂ« fjalorin ClickHouse mbajmĂ« shumĂ« rreshta tĂ« ndryshĂ«m, ndĂ«rsa nĂ« tabelĂ« â identifikuesit e tyre si numra. NĂ«se na nevojitet njĂ« rresht, thĂ«rrasim funksionin dictGet dhe punojmĂ« me tĂ«. Pas kĂ«saj nuk duhet tĂ« bĂ«jmĂ« ALTER. PĂ«r tĂ« shtuar diçka nĂ« Enum, e shkruajmĂ« atĂ« nĂ« tĂ« njĂ«jtĂ«n tabelĂ« MySQL.
Por këtu lindin probleme të tjera. Së pari, sintaksa e pakëndshme. Nëse duam të marrim një rresht, duhet të thërrasim dictGet. Së dyti, mungesa e disa optimizimeve. Krahasimi me një rresht konstant për fjalorët nuk bëhet kaq shpejt.
Problemet me përditësimin gjithashtu mund të ndodhin. Supozoni se kërkuam një rresht në fjalorin me këshill, por ai nuk është ngarkuar në cache. Atëherë duhet të presim derisa të ngarkohen të dhënat nga burimi i jashtëm.

NjĂ« mangĂ«si e pĂ«rgjithshme e tĂ« dy metodave Ă«shtĂ« se ne mbajmĂ« tĂ« gjitha çelĂ«sat nĂ« njĂ« vend dhe i sinkronizojmĂ« ato. Pse tĂ« mos i mbajmĂ« fjalorĂ«t nĂ« mĂ«nyrĂ« lokale? Pa sinkronizim â pa probleme. Mund tĂ« ruajmĂ« fjalorin lokalisht nĂ« njĂ« copĂ« nĂ« disk. KĂ«shtu, bĂ«jmĂ« njĂ« Insert, regjistrojmĂ« fjalorin. NĂ«se punojmĂ« me tĂ« dhĂ«nat nĂ« memorie, mund tĂ« regjistrojmĂ« fjalorin ose nĂ« bllokun e tĂ« dhĂ«nave, ose nĂ« njĂ« copĂ« kolone, ose nĂ« ndonjĂ« cache pĂ«r tĂ« pĂ«rshpejtuar llogaritjet.
Kodimi i fjalëve të vargjeve
KĂ«shtu arritĂ«m nĂ« krijimin e njĂ« lloji tĂ« ri tĂ« tĂ« dhĂ«nave nĂ« ClickHouse â LowCardinality. Ky Ă«shtĂ« njĂ« format ruajtjeje tĂ« dhĂ«nash: si shkruhen nĂ« disk dhe si lexohen, si paraqiten nĂ« memorie dhe skema e pĂ«rpunimit tĂ« tyre.

Në slajd ka dy kolona. Nga e djathta, rreshtat ruhen standardisht, në tipin String. Shikohet se janë disa modele telefonash celularë. Nga e majta është një kolonë e ngjashme, vetëm se në tipin LowCardinality. Ajo përbëhet nga një fjalor me shumë rreshta të ndryshëm (rreshtat nga kolona e djathtë) dhe një listë pozita (numra rreshtash).
Me ndihmĂ«n e kĂ«tyre dy strukturave mund tĂ« rikuperohet kolona origjinale. Ka gjithashtu njĂ« indeks tĂ« kundĂ«rt â njĂ« tabelĂ« hash qĂ« ndihmon tĂ« gjejmĂ« pozitat nĂ« fjalor pĂ«rmes rreshtit. Kjo Ă«shtĂ« e nevojshme pĂ«r tĂ« pĂ«rshpejtuar disa pyetje. PĂ«r shembull, nĂ«se duam tĂ« krahasojmĂ«, tĂ« kĂ«rkojmĂ« njĂ« rresht nĂ« kolonĂ«n tonĂ« apo t'i bashkojmĂ« ata.
LowCardinality është një tip parametrik të dhënash. Ai mund të jetë ose numër, ose diçka që ruhet si numër, ose rresht, ose Nullable nga ato.

Karakteristika e LowCardinality Ă«shtĂ« se ai mund tĂ« ruhet pĂ«r disa funksione. NĂ« slajd shihet njĂ« shembull pyetje. NĂ« rreshtin e parĂ« kam krijuar njĂ« kolonĂ« tĂ« tipit LowCardinality nga String, e quajta S. MĂ« pas e pyeta emrin e saj â ClickHouse tha se kjo Ă«shtĂ« LowCardinality nga String. E pĂ«rshtatshme.
Rreshti i tretĂ« Ă«shtĂ« pothuajse i njĂ«jtĂ«, vetĂ«m se ne thirrĂ«m funksionin length. NĂ« ClickHouse funksioni length kthen tipin e tĂ« dhĂ«nave UInt64. Por tani kemi LowCardinality nga UInt64. ĂfarĂ« ka kuptim?

NĂ« fjalor ruheshin emrat e telefonĂ«ve mobilĂ«, ne aplikuam funksionin length. Tani kemi njĂ« fjalor tĂ« ngjashĂ«m, qĂ« pĂ«rbĂ«het vetĂ«m nga numra, â kĂ«to janĂ« gjatĂ«sitĂ« e vargjeve. Kolona me pozita nuk ka ndryshuar. Si rezultat, ne pĂ«rpunuam mĂ« pak tĂ« dhĂ«na, kursyer nĂ« kohĂ«n e pyetjes.
Mund të jenë edhe optimizime të tjera, p.sh., shtimi i një cache të thjeshtë. Kur llogaritet vlera e funksionit, mund ta mbajmë mend atë dhe të formojmë një të ngjashëm, pa e llogaritur përsëri.
Optimizimi i GROUP BY gjithashtu mund tĂ« bĂ«het, sepse kolona jonĂ« me fjalor Ă«shtĂ« tashmĂ« pjesĂ«risht e agreguar â mund tĂ« llogarisim shpejt vlerat e funksioneve tĂ« hash-it dhe tĂ« gjejmĂ« afĂ«rsisht bucket-in ku tĂ« vendosim rreshtin e ardhshĂ«m. Gjithashtu mund tĂ« specializojmĂ« disa funksione agreguese, si uniq, sepse nĂ« tĂ« mund tĂ« dĂ«rgojmĂ« vetĂ«m fjalorin dhe tĂ« lĂ«mĂ« pozitat tĂ« paprekura â kĂ«shtu gjithçka do tĂ« funksionojĂ« mĂ« shpejt. Dy optimizimet e para i kemi shtuar tashmĂ« nĂ« ClickHouse.

ĂfarĂ« ndodh nĂ«se krijojmĂ« njĂ« kolonĂ« me llojin tonĂ« tĂ« dhĂ«nash dhe vendosim shumĂ« rreshta tĂ« kĂ«qinj nĂ« tĂ«? A do tĂ« mbushet memoria jonĂ«? Jo, pĂ«r kĂ«tĂ« ClickHouse ka dy cilĂ«sime speciale. E para Ă«shtĂ« low_cardinality_max_dictionary_size. Ky Ă«shtĂ« madhĂ«sia maksimale e fjalorit qĂ« mund tĂ« shkruhet nĂ« disk. Vendosja ndodh kĂ«shtu: kur vendosim tĂ« dhĂ«na, na vjen njĂ« rrjedhĂ« rreshtash, nga tĂ« cilat formojmĂ« njĂ« fjalor tĂ« madh tĂ« pĂ«rbashkĂ«t. NĂ«se fjalori bĂ«het mĂ« i madh se vlera e cilĂ«simit, ne e regjistruam fjalorin aktual nĂ« disk, ndĂ«rsa rreshtat e tjerĂ« â diku "nĂ« anĂ«", pranĂ« indekseve. Si rezultat, ne kurrĂ« nuk do tĂ« rikonfigurojmĂ« njĂ« fjalor tĂ« madh dhe nuk do tĂ« kemi probleme me memorien.
Konfigurimi i dytë quhet low_cardinality_use_single_dictionary_for_part. Imagjino që në skemën e mëparshme, kur ne po insertonim të dhëna, fjalori ynë u mbush dhe ne e regjistruam atë në disk. Ngrihet pyetja, pse të mos formojmë një tjetër fjalor të tillë?
Kur të mbushet përsëri, ne do ta regjistrojmë në disk dhe do të fillojmë të formojmë të tretin. Ky konfigurim ndalon këtë mundësi nga automatizmi.
Në të vërtetë, shumë fjalorë mund të jenë të dobishëm nëse duam të insertojmë një grup rreshtash, por rastësisht insertonim "mbeturina". Le të themi, së pari insertonim rreshta të këqij dhe pastaj rreshta të mirë. Atëherë fjalori do të ndahet në shumë fjalorë të vegjël. Disa prej tyre do të jenë me "mbeturina", por të fundit do të jenë me rreshta të mirë. Dhe nëse lexohet, le të themi, vetëm granula e fundit, gjithçka do të funksionojë shpejt.

Para parafolloj se benefitet e LowCardinality, dua se shpejti se ne do t'i arrijmĂ« rezultatet e reduktimit tĂ« tĂ« dhĂ«nave nĂ« disk (edhe pse mund ta bĂ«jmĂ«), pasi ClickHouse kompreson tĂ« dhĂ«nat. Ka njĂ« variant parazgjedhje â LZ4. Gjithashtu, mund tĂ« bĂ«het kompresimi me ZSTD. Por tĂ« dy algoritmĂ«t tashmĂ« implementojnĂ« kompresimin me fjalor, kĂ«shtu qĂ« fjalori ynĂ« i jashtĂ«m ClickHouse nuk do tĂ« ndihmojĂ« shumĂ«.
PĂ«r tĂ« mos qenĂ« fjalĂ«-pĂ«r-fjalĂ«, kam marrĂ« disa tĂ« dhĂ«na nga metrika â String, LowCardinality(String) dhe Enum â dhe i kam ruajtur ato nĂ« lloje tĂ« ndryshme tĂ« tĂ« dhĂ«nave. KanĂ« dalĂ« tre kolona, ku janĂ« regjistruar njĂ« miliard rreshta. NĂ« kolonĂ«n e parĂ«, CodePage, ka vetĂ«m 62 vlera. Dhe Ă«shtĂ« e dukshme se nĂ« LowCardinality(String) e kemi kompresuar mĂ« mirĂ«. String Ă«shtĂ« pak mĂ« e dobĂ«t, por kjo Ă«shtĂ« ndoshta pĂ«r shkak se fjalitĂ« janĂ« tĂ« shkurtra, ne ruajmĂ«-ato gjatĂ«si, tĂ« cilat zĂ«nĂ« shumĂ« vend dhe kompresohen keq.
NĂ«se marrim PhoneModel, ata janĂ« 48 mijĂ« â tashmĂ« mĂ« shumĂ«, dhe ndryshimet midis String dhe LowCardinality(String) pothuajse nuk ka. PĂ«r URL gjithashtu kemi kursyer vetĂ«m 2 GB â mendoj se nuk ia vlen tĂ« mbĂ«shtetemi nĂ« kĂ«tĂ«.
Vlerësimi i shpejtësisë së punës

Tani le tĂ« vlerĂ«sojmĂ« shpejtĂ«sinĂ« e punĂ«s. PĂ«r ta vlerĂ«suar, kam pĂ«rdorur njĂ« dataset qĂ« pĂ«rshkruan udhĂ«timet me taksi nĂ« New York. Ai nĂ« GitHub. Aty ka pak mĂ« shumĂ« se njĂ« miliard udhĂ«time. Aty pasqyrohen pozita, koha e fillimit dhe pĂ«rfundimit tĂ« udhĂ«timit, mĂ«nyra e pagesĂ«s, numri i pasagjerĂ«ve dhe madje lloji i taksisĂ« â i gjelbĂ«r, i verdhĂ« dhe Uber.

KĂ«rkesa e parĂ« e bĂ«ra ishte mjaft e thjeshtĂ« â pyeta se ku porositen mĂ« shpesh taksitĂ«. PĂ«r kĂ«tĂ« duhet tĂ« marrim pozitĂ«n nga e cila Ă«shtĂ« porositur, tĂ« bĂ«jmĂ« njĂ« grupim sipas saj dhe tĂ« llogarisim funksionin count. Ja çfarĂ« jep ClickHouse.

PĂ«r tĂ« matur shpejtĂ«sinĂ« e pĂ«rpunimit tĂ« kĂ«rkesĂ«s, krijova tre tabela me tĂ« njĂ«jtat tĂ« dhĂ«na, por pĂ«rdora pĂ«r pozitĂ«n tonĂ« fillestare tre lloje tĂ« dhĂ«nash tĂ« ndryshme â String, LowCardinality dhe Enum. LowCardinality dhe Enum rezultuan pesĂ« herĂ« mĂ« tĂ« shpejta se String. Enum Ă«shtĂ« mĂ« e shpejtĂ« sepse punon me numra. LowCardinality Ă«shtĂ« mĂ« e shpejtĂ« pĂ«r shkak se implementon optimizimin GROUP BY.

Le tĂ« komplikohemi edhe mĂ« tej me kĂ«rkesĂ«n â tĂ« pyesim ku ndodhet parku mĂ« i njohur nĂ« New York. SĂ«rish do ta masim kĂ«tĂ« sipas vendndodhjeve ku porositen mĂ« shpesh taksitĂ«, por do tĂ« filtrojmĂ« vetĂ«m pozitat ku ka fjalĂ«n 'park'. Po ashtu, do tĂ« shtojmĂ« funksionin like.

Po shikimit nĂ« kohĂ«, shohim qĂ« Enum papritur ka filluar tĂ« ngadalĂ«sohet. Madje, funksionon edhe mĂ« ngadalĂ« se tipi standard i tĂ« dhĂ«nave String. Kjo ndodh sepse funksioni like nuk Ă«shtĂ« optimizuar pĂ«r Enum. Na duhet tĂ« konvertojmĂ« stringjet tona nga Enum nĂ« stringje tĂ« zakonshme â ne po bĂ«jmĂ« mĂ« shumĂ« punĂ«. LowCardinality(String) gjithashtu nuk Ă«shtĂ« optimizuar nga fillimi, por atje like punon mbi fjalorin, kĂ«shtu qĂ« kĂ«rkesa pĂ«rshpejtohet krahasuar me String.
Kur punojmĂ« me Enum, ka njĂ« problem mĂ« tĂ« thellĂ«. NĂ«se duam ta optimizojmĂ« atĂ«, duhet ta bĂ«jmĂ« kĂ«tĂ« nĂ« çdo vend tĂ« kodit. Le ta supozojmĂ« se kemi shkruar njĂ« funksion tĂ« ri â duhet patjetĂ«r tĂ« gjejmĂ« njĂ« optimizim pĂ«r Enum. NdĂ«rsa nĂ« LowCardinality, gjithçka Ă«shtĂ« optimizuar nga fillimi.

Të shohim te kërkesa e fundit, një kërkesë më artificiale. Ne thjesht do të llogarisim funksionin hash nga lokacioni ynë. Funksioni hash është një kërkesë mjaft e ngadaltë, bëhet ngadalë, kështu që gjithçka do të ngadalësohet rreth tri herë.

LowCardinality vazhdon tĂ« punojĂ« mĂ« shpejt, ndonĂ«se kĂ«tu nuk ka filtrimit. Kjo ndodh sepse funksionet tona punojnĂ« vetĂ«m mbi fjalorin. Funksioni i llogaritjes sĂ« hash-it ka njĂ« argumentâai mund tĂ« pĂ«rpunojĂ« mĂ« pak tĂ« dhĂ«na dhe gjithashtu mund tĂ« kthejĂ« LowCardinality.

Plani ynë global është të arrijmë një shpejtësi që nuk është më e ulët se ajo e String në çdo rast, dhe të ruajmë përshpejtimet. Dhe ndoshta ndonjëherë do ta zëvendësojmë String me LowCardinality, ju do të përditësoni ClickHouse, dhe gjithçka do të funksionojë pak më shpejt.
Burimi: habr.com
