Baza analitike ClickHouse përpunon shumë lloje të ndryshme rreshtash, duke konsumuar burime. Për të përshpejtuar funksionimin e sistemit, vazhdimisht shtohen optimizime të reja. Zhvilluesi i ClickHouse, Nikolai Kochetov, flet për llojin e dhënave string, përfshirë një tip të ri, LowCardinality, dhe shpjegon se si mund të përshpejtojmë punën me rreshtat.

â SĂ« pari, le tĂ« kuptojmĂ« si mund tĂ« ruajmĂ« rreshta.

Ne kemi lloje tĂ« dhĂ«nash string. String Ă«shtĂ« i pĂ«rshtatshĂ«m si parazgjedhje dhe duhet pĂ«rdorur pothuajse gjithmonĂ«. Ai ka njĂ« Overhead tĂ« vogĂ«l â 9 byte pĂ«r çdo rresht. NĂ«se duam qĂ« madhĂ«sia e rreshtave tĂ« jetĂ« e ngurtĂ« dhe e njohur paraprakisht, atĂ«herĂ« Ă«shtĂ« mĂ« mirĂ« tĂ« pĂ«rdorim FixedString. NĂ« tĂ« mund tĂ« caktojmĂ« numrin e duhur tĂ« byte-ve, qĂ« Ă«shtĂ« i pĂ«rshtatshĂ«m pĂ«r tĂ« dhĂ«na si adresat IP ose funksionet hash.

Sigurisht, ndonjëherë diçka mund të ngadalësojë. Supozoni se po bëni një kërkesë në një tabelë. ClickHouse lexon një sasi të madhe të të dhënave, le të themi, me shpejtësi 100 GB/s, megjithatë, numri i rreshtave të përpunuar është i vogël. Ne kemi dy tabela që ruajnë pothuajse të njëjtat të dhëna. Nga tabela e dytë, ClickHouse lexon të dhënat me një shpejtësi më të madhe, por numri i rreshtave të lexuar në sekondë është tre herë më i ulët.

NĂ«se shikojmĂ« madhĂ«sinĂ« e tĂ« dhĂ«nave tĂ« kompresuara, ajo do tĂ« jetĂ« pothuajse e barabartĂ«. NĂ« tĂ« vĂ«rtetĂ«, tĂ« dhĂ«nat nĂ« tabela janĂ« tĂ« njĂ«jtat â miliardi i parĂ« i numrave â vetĂ«m qĂ« nĂ« kolonĂ«n e parĂ« ato janĂ« regjistruar si UInt64, ndĂ«rsa nĂ« tĂ« dytĂ«n si String. PĂ«r kĂ«tĂ« arsye, kĂ«rkesa e dytĂ« lexon mĂ« ngadalĂ« tĂ« dhĂ«nat nga disku dhe i çkompreson ato.

Ja njĂ« shembull tjetĂ«r. Supozoni se ekziston njĂ« grup i njohur paraprakisht rreshtash, i kufizuar nĂ« njĂ« konstantĂ« 1000 ose 10,000 dhe qĂ« nuk ndryshon kurrĂ«. PĂ«r kĂ«tĂ« rast, lloji i tĂ« dhĂ«nave Enum na pĂ«rshtatet, nĂ« ClickHouse ka dy â Enum8 dhe Enum16. Duke ruajtur nĂ« Enum, ne pĂ«rpunojmĂ« kĂ«rkesat me shpejtĂ«si.
Në ClickHouse ka optimizime 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, përkundrazi, stringu konstant shndërrohet në një vlerë Enum. Pas kësaj, gjithçka krahasohet shpejt.
Por ka dhe disavantazhe. Edhe nëse e dimë saktësisht grupin e rreshtave, ndonjëherë ai duhet të zgjerohet. Nëse mb arrives një rresht i ri, ne duhet të bëjmë ALTER.

ALTER për Enum në ClickHouse është realizuar në mënyrë optimale. Ne nuk e shkruajmë të dhënat në disk, por ALTER mund të ngadalësohen për shkak të faktit se strukturat Enum ruhen në skemën e tabelës vetë. Prandaj, duhet të presim për kërkesat për lexim nga tabela, për shembull.
ShqetĂ«simi Ă«shtĂ«, a mund tĂ« bĂ«jmĂ« mĂ« mirĂ«? Ndoshta, po. Mund tĂ« ruajmĂ« strukturĂ«n Enum jo nĂ« skemĂ«n e tabelĂ«s, por nĂ« ZooKeeper. MegjithatĂ«, mund tĂ« lindin probleme qĂ« lidhen me sinkronizimin. PĂ«r shembull, njĂ« replikĂ« mĂ« merr tĂ« dhĂ«nat, ndĂ«rsa njĂ« tjetĂ«r jo, dhe nĂ«se ajo ka njĂ« Enum tĂ« vjetĂ«r, diçka do tĂ« dĂ«shtojĂ«. (NĂ« ClickHouse, pothuajse kemi pĂ«rfunduar kĂ«rkesat ALTER me pa bllokim. Kur tâi pĂ«rfundojmĂ« plotĂ«sisht, nuk do tĂ« duhet 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. Dua tâju kujtoj se kjo Ă«shtĂ« njĂ« strukturĂ« tĂ« dhĂ«nash key-value brenda ClickHouse, me ndihmĂ«n e sĂ« cilĂ«s mund tĂ« marrim tĂ« dhĂ«na nga burime tĂ« jashtme, pĂ«r shembull nga tabelat MySQL.
Në fjalorin ClickHouse ne ruajmë shumë rreshta të ndryshëm, ndërsa në tabelë ruhen identifikuesit e tyre në formën e numrave. Nëse na duhet të marrim një rresht, ne 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, ne e vendosim atë në të njëjtën tabelë MySQL.
Por këtu lindin probleme të tjera. Së pari, sintaksë e pakëndshme. Nëse duam të marim një rresht, duhet të thërrasim dictGet. Së dyti, mungesa e optimizimeve të caktuara. Krahasimi me një rresht konstant për fjalorët nuk është po aq i shpejtë.
Përveç kësaj, mund të ketë probleme me përditësimin. Dëgjojmë se kërkuam një rresht në fjalorin e memorizuar dhe nuk është regjistruar në memorie. Në këtë rast, ne duhet të presim derisa të ngarkohen të dhënat nga burimi i jashtëm.

DobĂ«sia e pĂ«rgjithshme e tĂ« dy metodave Ă«shtĂ« se ne ruajmĂ« tĂ« gjithĂ« çelĂ«sat nĂ« njĂ« vend dhe i sinkronizojmĂ«. Pra, pse tĂ« mos i ruajmĂ« fjalorĂ«t lokal? Pa sinkronizim â pa probleme. Mund tĂ« ruajmĂ« fjalorin lokal nĂ« njĂ« bllok nĂ« disk. KĂ«shtu, bĂ«jmĂ« Insert, shĂ«nojmĂ« fjalorin. NĂ«se punojmĂ« me tĂ« dhĂ«nat nĂ« memorie, mund tĂ« regjistrojmĂ« fjalorin ose nĂ« njĂ« bllok tĂ« dhĂ«nash, ose nĂ« njĂ« copĂ« tĂ« kolonnĂ«s, ose nĂ« ndonjĂ« memorie cache pĂ«r tĂ« pĂ«rshpejtuar llogaritjet.
Kodimi i fjalorëve të rreshtave
KĂ«shtu arritĂ«m nĂ« krijimin e njĂ« tipi 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 pĂ«rfaqĂ«sohen nĂ« memorie dhe skema e pĂ«rpunimit tĂ« tyre.

Në slajd ka dy kolonat. Në të djathtë, rreshtat ruhen standardisht, në llojin String. Duket se janë disa modele telefonash celularë. Në të majtë ka një kolonë të ngjashme, por në llojin LowCardinality. Ajo përbëhet nga një fjalor me shumë rreshta të ndryshëm (rreshtat nga kolona e djathtë) dhe një listë pozita (numrat e rreshtave).
Me kĂ«to dy struktura, mund tĂ« rikonstruktojmĂ« kolonĂ«n origjinale. Ka gjithashtu njĂ« indeks tĂ« kundĂ«rt â njĂ« tabelĂ« hash, qĂ« ndihmon tĂ« gjejmĂ« pozitat nĂ« fjalor nĂ«pĂ«rmjet rreshtit. Ajo Ă«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Ă« ose t'i bashkojmĂ« bashkĂ«.
LowCardinality është një tip parametrik të dhënash. Ai mund të jetë ose një numër, ose diçka që ruhet si numër, ose një rresht, ose Nullable prej tyre.

Karakteristika e LowCardinality Ă«shtĂ« se ai mund tĂ« ruhet pĂ«r disa funksione. NĂ« slajd Ă«shtĂ« njĂ« shembull pyetjeje. NĂ« rreshtin e parĂ« krijova njĂ« kolonĂ« me tipin LowCardinality nga String, e quajta S. MĂ« pas e pyeta emrin e saj â ClickHouse tha se Ă«shtĂ« LowCardinality nga String. E gjitha Ă«shtĂ« e saktĂ«.
Rreshti i tretĂ« Ă«shtĂ« pothuajse i njĂ«jtĂ«, vetĂ«m se kemi thirrur funksionin length. NĂ« ClickHouse funksioni length kthen njĂ« tip tĂ« dhĂ«nash UInt64. Por tani kemi LowCardinality nga UInt64. ĂfarĂ« kuptimi ka kjo?

NĂ« fjalor ruheshin emrat e telefonave celularĂ«, ne aplikuam funksionin length. Tani kemi njĂ« fjalor tĂ« ngjashĂ«m, qĂ« pĂ«rbĂ«het vetĂ«m nga numra â janĂ« gjatĂ«sitĂ« e rreshtave. Kolona me pozitat nuk u ndryshua. NĂ« fund, pĂ«rpunuam mĂ« pak tĂ« dhĂ«na, kursyem kohĂ« kĂ«rkese.
Mund të ketë edhe optimizime të tjera, për shembull shtimin e një cacher të thjeshtë. Kur llogarisim vlerën e funksionit, mund ta mbajmë mend atë dhe të formojmë një të tillë, pa e llogaritur përsëri.
Gjithashtu mund tĂ« bĂ«het optimizimi GROUP BY, sepse kolona jonĂ« me fjalor Ă«shtĂ« pjesĂ«risht e agreguar â mund tĂ« llogaritim mĂ« shpejt vlerat e funksioneve hash dhe pĂ«rafĂ«rsisht tĂ« gjejmĂ« bucket-in ku tĂ« vendosim rreshtin e ardhshĂ«m. PĂ«r mĂ« tepĂ«r, mund tĂ« specializojmĂ« disa funksione agregate, pĂ«r shembull uniq, sepse nĂ« tĂ« mund tĂ« dĂ«rgohet vetĂ«m fjalori, ndĂ«rsa pozitat tĂ« mbeten tĂ« paprekura â kĂ«shtu qĂ« gjithçka do tĂ« funksionojĂ« mĂ« shpejt. Dy optimizimet e para i kemi shtuar tashmĂ« nĂ« ClickHouse.

ĂfarĂ« do tĂ« ndodhte nĂ«se krijojmĂ« njĂ« kolonĂ« me llojin tonĂ« tĂ« tĂ« dhĂ«nave dhe vendosim shumĂ« vargje tĂ« kĂ«qija? A do tĂ« mbushet memorja jonĂ«? Jo, pĂ«r kĂ«tĂ« ClickHouse ka dy cilĂ«sime tĂ« veçanta. E para Ă«shtĂ« low_cardinality_max_dictionary_size. Kjo Ă«shtĂ« masa maksimale e fjalorit qĂ« mund tĂ« regjistrohet nĂ« disk. Vendosja ndodh siç vijon: kur ne vendosim tĂ« dhĂ«na, na vjen njĂ« rrjedhĂ« vargjesh, nga tĂ« cilat ne formojmĂ« njĂ« fjalor tĂ« madh tĂ« pĂ«rbashkĂ«t. NĂ«se fjalori bĂ«het mĂ« i madh se vlera e cilĂ«simit, ne regjistrojmĂ« fjalorin aktual nĂ« disk, ndĂ«rsa vargjet e tjera i vendosim diku 'nĂ« anĂ«', pranĂ« indekseve. Si rezultat, ne kurrĂ« nuk e llogarisim njĂ« fjalor tĂ« madh dhe nuk kemi probleme me memorjen.
Cilësimi i dytë quhet low_cardinality_use_single_dictionary_for_part. Imagjinoni që në skemën e kaluar, kur ne vendosim të dhëna, fjalori ynë u mbush dhe ne e regjistruam atë në disk. Shfaqet pyetja, pse tani të mos formojmë një fjalor tjetër të tillë?
Kur ai mbushet, ne përsëri do ta regjistrojmë në disk dhe do të fillojmë të formojmë të tretin. Ky cilësim përjashton pikërisht një mundësi të tillë si parazgjedhje.
Në të vërtetë, shumë fjalorë mund të jenë të dobishëm, nëse ne dëshirojmë të vendosim një sasi të caktuar vargjesh, por rastësisht kemi vendosur 'të papërshtatshme'. Le të themi, fillimisht vendosëm vargje të këqija dhe më pas vendosëm të mira. Atëherë fjalori do të ndahen në shumë fjalorë të vegjël. Një pjesë e tyre do të ketë 'të papërshtatshme', por të fundit do të jenë me vargje të mira. Dhe nëse ne e lexojmë, le të themi, vetëm granulat e fundit, gjithçka do të funksionojë shpejt.

Para se tĂ« flasim pĂ«r avantazhet e LowCardinality, do tĂ« thosha se vĂ«shtirĂ« se do tĂ« arrijmĂ« tĂ« ulim tĂ« dhĂ«nat nĂ« disk (ndonĂ«se kjo mund tĂ« ndodhĂ«), sepse ClickHouse e kompreson tĂ« dhĂ«nat. Ka njĂ« variant parazgjedhje â LZ4. Gjithashtu, mund tĂ« bĂ«jmĂ« kompresim me ndihmĂ«n e ZSTD. Por tĂ« dy algoritmĂ«t tashmĂ« implementojnĂ« kompresimin e fjalorit, prandaj fjalori ynĂ« i jashtĂ«m ClickHouse nuk do tĂ« ndihmojĂ« shumĂ«.
PĂ«r tĂ« mos qenĂ« i paqartĂ«, 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. KĂ«shtu kemi krijuar tre kolona, ku janĂ« regjistruar njĂ« miliard rreshtash. NĂ« kolonĂ«n e parĂ«, CodePage, ka vetĂ«m 62 vlera. Dhe Ă«shtĂ« e qartĂ« se nĂ« LowCardinality(String) ato janĂ« kompresuar mĂ« mirĂ«. String Ă«shtĂ« pak mĂ« keq, por kjo ndoshta Ă«shtĂ« pĂ«r shkak se vargjet janĂ« tĂ« shkurtra, ne ruajmĂ« gjatĂ«si tĂ« tyre, dhe ato zĂ«nĂ« shumĂ« hapĂ«sirĂ«, kompresohen keq.
NĂ«se marim PhoneModel, ka 48 mijĂ« â tashmĂ« mĂ« shumĂ«, dhe dallimet midis String dhe LowCardinality(String) janĂ« pothuajse tĂ« padukshme. PĂ«r URL ne gjithashtu kursyem 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 ta vlerĂ«sojmĂ« shpejtĂ«sinĂ« e punĂ«s. PĂ«r ta vlerĂ«suar, pĂ«rdora njĂ« dataset me pĂ«rshkrimin e udhĂ«timeve me taksi nĂ« Nju Jork. Ai ndodhet nĂ« GitHub. Pjesa ka pak mĂ« shumĂ« se njĂ« miliard udhĂ«timesh. Atje shfaqet lokacioni, koha e fillimit dhe pĂ«rfundimit tĂ« udhĂ«timit, mĂ«nyra e pagesĂ«s, numri i pasagjerĂ«ve dhe madje edhe lloji i taksisĂ« â e gjelbĂ«r, e verdhĂ« dhe Uber.

KĂ«rkesa e parĂ« e bĂ«ra ishte mjaft e thjeshtĂ« â pyeta se ku shpesh porositen taksit. PĂ«r kĂ«tĂ« nevojitet tĂ« marrim lokacionin nga ku u bĂ« porosia, tĂ« bĂ«jmĂ« GROUP BY dhe tĂ« llogarisim funksionin count. Ja çfarĂ« nxjerr ClickHouse.

PĂ«r tĂ« matur shpejtĂ«sinĂ« e pĂ«rpunimit tĂ« kĂ«rkesĂ«s, krijova tri tabela me tĂ« dhĂ«na tĂ« njĂ«jta, por pĂ«rdora pĂ«r lokacionin tonĂ« fillestar tri lloje tĂ« ndryshme tĂ« tĂ« dhĂ«nave â String, LowCardinality dhe Enum. LowCardinality dhe Enum rezultuan pesĂ« herĂ« mĂ« tĂ« shpejtĂ« se String. Enum Ă«shtĂ« mĂ« i shpejtĂ« sepse punon me numra. LowCardinality â pĂ«r shkak se Ă«shtĂ« realizuar optimizimi GROUP BY.

Tani le tĂ« complicojmĂ« kĂ«rkesĂ«n â tĂ« pyesim se ku ndodhet parku mĂ« i njohur nĂ« Nju Jork. PĂ«rsĂ«ri do ta masim kĂ«tĂ« sipas asaj se ku shpesh porositen taksit, por kĂ«tĂ« herĂ« do filtrojmĂ« vetĂ«m ato lokacione ku ka fjalĂ«n "park". gjithashtu do tĂ« shtojmĂ« funksionin like.

ShikojmĂ« kohĂ«n â shohim se Enum papritur filloi tĂ« ngadalĂ«sohet. Dhe po funksionon madje mĂ« ngadalĂ« se tipi standard i tĂ« dhĂ«nave String. Kjo ndodh sepse funksioni like nuk Ă«shtĂ« aspak i optimizuar pĂ«r Enum. Na duhet tĂ« konvertojmĂ« vargjet tona nga Enum nĂ« vargje normale â ne po bĂ«jmĂ« mĂ« shumĂ« punĂ«. LowCardinality(String) gjithashtu nuk Ă«shtĂ« i optimizuar pĂ«r default, por atje like punon mbi fjalorin, prandaj kĂ«rkesa shpejtohet nĂ« krahasim me String.
Kur punojmĂ« me Enum ka njĂ« problem mĂ« tĂ« madh. NĂ«se duam ta optimizojmĂ«, duhet ta bĂ«jmĂ« kĂ«tĂ« nĂ« çdo vend tĂ« kodit. Le tĂ« supozojmĂ« se kemi shkruar njĂ« funksion tĂ« ri â patjetĂ«r duhet tĂ« mendojmĂ« pĂ«r optimizimin pĂ«r Enum. NdĂ«rsa nĂ« LowCardinality gjithçka Ă«shtĂ« optimizuar me default.

Të shohim kërkesën e fundit, më artificiale. Thjesht do të llogarisim funksionin hash nga vendndodhja jonë. Funksioni hash është një kërkesë mjaft e ngadalshme, llogaritet me kohë, prandaj gjithçka do të ngadalsohet rreth tri herë.

LowCardinality ende funksionon mĂ« shpejt, edhe pse kĂ«tu nuk ka filtrimin. Kjo ndodh sepse funksionet tona punojnĂ« vetĂ«m mbi fjalorin. Funksioni pĂ«r llogaritjen e 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ë shpejtësinë e funksionimit jo më të ulët se 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
