ClickHouse ինֆորմացիոն համակարգը ներգրավում է բազմաթիվ տողեր, սպառելով ռեսուրսներ: Համակարգի աշխատանքը արագացնելու համար մշտապես ավելացվում են նոր օպտիմիզացիաներ: ClickHouse-ի մշակող Նիկոլայ Կոչետով խոսում է տողային տվյալների տեսակների մասին, ներառյալ նոր LowCardinality տիպի մասին և բացատրում, թե ինչպես կարելի է արագացնել աշխատելը տողերի հետ:

– Նախ եկեք պարզենք, թե ինչպես կարելի է պահել տողեր:

Մենք ունենք տողային տվյալների տիպեր: String-ը լավ է ձեռնտու, պետք է օգտագործել հիմնականում միշտ: Այն ունի փոքր Overhead՝ 9 բայթ մեկ տողի համար: Եթե ուզում ենք, որ տողերի չափը լինի հաստատուն և հայտնի նախօրոք, ավելի լավ է օգտագործել FixedString: Այդ տիպով կարող ենք նշել անհրաժեշտ բայթերի քանակը, այն հարմար է IP հասցեների կամ հեշ ֆունկցիաների համար:

Իհարկե, երբեմն ինչ-որ բան դանդաղում է: Բաց թողնենք, որ դուք հարցում եք անում փոխանակության եղանակով: ClickHouse-ը կարդում է բավական մեծ քանակությամբ տվյալներ, ասենք, 100 ԳԲ/վայրկյան արագությամբ, սակայն տողերն արտահանվում են քիչ: Մենք ունենք երկու աղյուսակ, որոնք պահում են գրեթե նույն տվյալները: Երկրորդ աղյուսակից ClickHouse-ը տվյալները կարդում է ավելի մեծ արագությամբ, սակայն տողերը մեկ վայրկյանի ընթացքում կարդացվում են երեք անգամ քիչ:

Եթե նայենք սեղմված տվյալների չափսերին, դրանք գրեթե հավասար կլինեն: Իրականում աղյուսակներում գրանցված են оди նույն տվյալները՝ առաջին միլիարդ ինքնաթիռներ՝ միայն առաջին սյունակում դրանք գրանցված են UInt64 ձևաչափով, իսկ երկրորդում՝ String. Դրա պատճառով երկրորդ հարցումը ավելի երկար է կարդում տվյալները սարքից և իրենից դուրս է բերում:

Այլ օրինակ. فرضեք, որ կա заранее известное множество строк, оно ограничено константой 1000 или 10 000 и практически никогда не меняется. Для этого случая нам подходит тип данных Enum, в ClickHouse их два — Enum8 и Enum16. За счет хранения в Enum мы быстро обрабатываем запросы.
ClickHouse-ում կա արագացումներ GROUP BY, IN, DISTINCT համար և օպտիմիզացիաներ որոշ ֆունկցիաների համար, օրինակ՝ համեմատել մշտական տողերի հետ: Of course, numbers in strings are not converted, but on the contrary, the constant string is translated into an Enum value. After this, everything is quickly compared.
Բայց կան նաև բացասական ենթադրություններ: Ն nawet եթե գիտենք վճռել տողերի կոնկրետ խումբ, manchmal оно должно пополняться. Пришел новый ряд — мы должны сделать ALTER.

ClickHouse-ում Enum-ի համար ALTER-ը իրականացված է օպտիմալ: Մենք չենք վերագրանցում տվյալները սարքում, սակայն ALTER-ը կարող է ուշանալ այն պատճառով, որ Enum-ի կառուցվածքները պահվում են աղյուսակի շեմում: Այսպես, մենք պետք է սպասենք հարցումների ընթերցումը աղյուսակից, օրինակ:
Հարց է առաջանում, հնարավոր է արդյոք ավելի լավ անել: Ա seguramente, այո. Enum-ի կառուցվածքը կարող է պահպանվել ոչ թե աղյուսակի սխեմայում, այլ ZooKeeper-ում: Սակայն կարող են առաջանալ սինխրոնացման հետ կապված խնդիրներ: Օրինակ, մեկ օրինակ ստացել է տվյալները, մյուսը` ոչ, եւ եթե նրա Enum-ը հին է, ապա ինչ-որ բան կխափանվի: (ClickHouse-ում մենք գրեթե ավարտել ենք ոչ արգելափակող ALTER հարցումները: Երբ դրանք ամբողջությամբ ավարտենք, չենք ստիպված լինի սպասել ընթերցման հարցումներին):

ALTER Enum-ով չզբաղվելու համար կարող եք օգտագործել ClickHouse-ի արտաքին բառարանները: Հիշեցնում եմ, որ սա key-value տվյալների կառուցվածք է ClickHouse-ում, որի օգնությամբ կարելի է ձեռք բերել տվյալներ արտաքին աղբյուրներից, օրինակ, MySQL-ի աղյուսակներից:
ClickHouse-ի բառարանում մենք պահում ենք մի շարք տարբեր տողեր, իսկ աղյուսակում՝ դրանց համարը թվերով: Եթե պետք է ստանալ տող, մենք կանչում ենք dictGet ֆունկցիան եւ աշխատում դրա հետ: Դրանից հետո մենք չպետք է անել ALTER: Եթե ինչ-որ բան ավելացնելու համար Enum-ին, մենք ավելացնում ենք այն նույն MySQL աղյուսակում:
Բայց այստեղ ծագում են այլ խնդիրներ: Նախ, անհարմար սինտեքսիս: Եթե ցանկանում ենք ստանալ տող, պետք է կանչենք dictGet: Երկրորդ, որոշ օպտիմիզացիաների բացակայություն: Բառարանների համար համեմատությունը մշտական տողի հետ այնքան արագ հնարավոր չէ անել:
Եւ կարող են լինել թարմացման հետ կապված խնդիրներ: Մասնավորապես, մենք արդյոք հարցրել ենք տողը cache բառարանում, բայց դա cache-ից դուրս չի մտել: Այդ դեպքում պետք է սպասենք, մինչեւ տվյալները կներբեռնվեն արտաքին աղբյուրից:

Գլխավոր թերությունը երկու մեթոդների համար էլ այն է, որ մենք պահում ենք բոլոր բանալիները մեկ տեղում եւ սինխրոնացնում ենք դրանք: Ուստի, ինչու ոչ պահել բառարանները տեղում: Չի եղել սինխրոնացում՝ չկան խնդիրներ: Բառարանը կարելի է պահել տեղում դեպի սկավառակ: Այսինքն, մենք կատարեցինք Insert, գրանցեցինք բառարը: Եթե մենք աշխատում ենք հիշողության մեջ, կարող ենք բառարանը գրանցել կամ տվյալների բլոկում, կամ մի քանի կոլոնների կտորների մեջ, կամ որեւէ cache-ում, որպեսզի արագացնենք հաշվարկները:
Տողերի բառարանական կոդավորում
Այսպես մենք հասնում ենք ClickHouse-ում նոր տվյալների տեսակ ստեղծելուն՝ LowCardinality: Սա տվյալների պահելու ֆորմատ է. ինչպես դրանք գրվում են սկավառակում եւ ինչպես կարդացվում են, ինչպես են դրանք ներկայացված հիշողությունում եւ դրանց մշակման սխեման:

Սլայդում երկու սյունակ կա: Աջ կողմում տողերը պահպանվում են սովորական ձևով, String տեսակի մեջ: Շնորհիվ, որ դրանք որոշակի մոբիլ հեռախոսների մոդելներ են: Աջ կողմում նույն սյունակն է, բայց LowCardinality տեսակի մեջ: Դա բաղկացած է բառարանից բազմաբնույթ տողերով (աջ կողմի սյունակից տողեր) եւ դիրքերի ցուցակից (տողերի համար):
Այս երկու կառուցվածքների միջոցով կարելի է վերադարձնել սկզբնական սյունակը: Ակնհայտ է, որ հակառակ.inverse ինդեքսը կա՝ hash-ի հեռախոսիկ, որը օգնում է տողի հարաբերության վրա գտնել դիրքը բառարանում: Այն անհրաժեշտ է որոշ հարցումների արագացմամբ: Օրինակ, եթե մենք ցանկանում ենք համեմատել, որոնեք տողը մեր սյունակում, կամ միացնել դրանք իրար:
LowCardinality — պարամետրական տվյալների կառավարող տեսակ։ Այն կարող է լինել կամ թիվ, կամ ինչ-որ բան, որը պահպանվում է որպես թիվ, կամ տող, կամ Nullable դրանցից։

LowCardinality-ի առանձնահատկությունը այն է, որ այն կարող է պահպանվել որոշ ֆունկցիաների համար։ Սրանից տեսնում ենք խնդրանքի օրինակ։ Առաջին տողում ես ստեղծել եմ LowCardinality տեսակով S տողի սյուն։ Հետո հարցրել եմ նրա անունը՝ ClickHouse-ն ասել է, որ դա LowCardinality է String- ի տեսակ։ Ամեն բան ճիշտ է։
Երրորդ տողը գրեթե նույնն է, միայն մենք կանչեցինք length ֆունկցիան։ ClickHouse-ում length ֆունկցիան վերադարձնում է UInt64 տվյալների տեսակ։ Բայց մենք ստացանք LowCardinality UInt64-ից։ Ի՞նչ խորաթափանցություն կա։

Լեզվաբառարանում պահպանվել են բջջային հեռախոսների անունները, մենք կիրառել ենք length ֆունկցիան։ Այժմ ունենք նմանատիպ բառարան, որը բաղկացած է միայն թվերից՝ տողերի երկարություններ։ Պոզիտիվների սյունը չի փոխվել։ Պարզվում է, մենք մշակել ենք ավելի ցածր տվյալներ, խնայել ենք հարցման ժամանակը։
Անձին էն էլ այլ օպտիմիզացիաներ կարող են լինել, օրինակ պարզ կեշի ավելացումը։ Ֆունկցիայի արժեքը հաշվարկելիս կարելի է հիշել այն և ստեղծել թվային բացակա՝ նորից հաշվարկել առանց։
Այլ կերպ կարող է հետագծվել GROUP BY ։ Քանի որ մեր բառարանի սյունը արդեն մասնակի համադրելով է՝ կարելի է ավելի արագ հաշվարկել hash ֆունկցիաների արժեքները և մոտավոր որոշել bucket, որտեղ դնել հաջորդ տողը։ Կա նաև նման հատուկ ագրեգատային ֆունկցիաների մասնագիտացում, օրինակ uniq, քանի որ դրա մեջ կարելի է միայն բառարան ուղարկել, իսկ պոզիտիվները թողնել անփոփոխ՝ այնպես որ բոլորը ավելի արագ աշխատի։ Առաջին երկու օպտիմիզացիաները արդեն ավելացրել ենք ClickHouse-ում։

Ի՞նչ կլինի, եթե մենք ստեղծենք մեր տվյալների տիպով սյուն և դրան ավելացնենք բազմաթիվ վատ տարբեր տողեր։ Կկրկնվի՞ մեր հիշողությունը։ Ոչ, դրա համար ClickHouse-ում կան երկու հատուկ կարգավորում։ Առաջինը՝ low_cardinality_max_dictionary_size։ Սա է առավելագույն բառարանի չափը, որը կարող է գրանցվել սկավառակում։ Ներմուծումը տեղի է ունենում այսպես։ Երբ մենք ներմուծում ենք տվյալներ, մեզ մոտ գալիս է տողերի հոսք, դրանցից կազմում ենք մեծ ընդհանուր բառարան։ Եթե բառարանը դառնում է ավելի մեծ, քան կարգավորումը, մենք պահպանում ենք ներկայիս բառարանը սկավառակում, իսկ մնացած տողերը՝ ինչ-որ «կողքին», ցուցակների կողքին։ Պարզվում է, մենք երբեք չենք վերաբոքում մեծ բառարան և չենք ունենա հիշողության խնդիր։
Երկրորդ կարգավորումն անվանումվում է low_cardinality_use_single_dictionary_for_part։ Կարծեցեք, որ անցյալ նախագծում, երբ մենք ներմուծեցինք տվյալներ, մեր բառարանը переполнվել, և մենք գրանցեցինք այն սկավառակում։ Հարց է առաջանում, բայց ինչու հիմա չպետք է կազմենք ևս մեկ նման բառարան։
Երբ այն переполнится, մենք կրկին գրանցենք այն սկավառակում և կսկսենք կազմել երրորդը։ Այս կարգավորումը հենց անջատում է նման հնարավորություն նախնական։
Իմաստով շատ բառարաններ կարող են օգտակար լինել, եթե ցանկանում ենք ներառել որոշ տողեր, բայց պատահաբար ներառել "թափոն"։ Ասենք, սկզբում ներառեցինք վատ տողեր, ապա լավ տողեր։ Այդ դեպքում բառարանը կբաժանվի բազմաթիվ փոքր բառարանների։ Աշխարհագրության մի մասը կբուն լինի "թափոն"-ով, բայց վերջինները լավ տողեր կունենան։ Եվ եթե կարդանք, ասենք, միայն վերջին հատիկը, այն դեռ կաշխատի արագ։

Առաջ прежде чем говорить о преимуществах LowCardinality, сразу скажу, что мы вряд ли добьемся уменьшения данных на диске (хотя это может произойти), поскольку ClickHouse сжимает данные։ Есть вариант по умолчанию — LZ4։ Также можно сделать сжатие с помощью ZSTD։ Но оба алгоритма уже реализуют словарное сжатие, поэтому наш внешний словарь ClickHouse не очень поможет։
Որպեսզի չլինեմ դատարկ խոսակցություն, ես վերցրեցի որոշ տվյալներ մետրիկայից — String, LowCardinality(String) և Enum — և պահպանեցի դրանք տարբեր տվյալների տեսակներով։ Գտանք երեք սյունակ, որտեղ է գրանցված մեկ միլիարդ տող։ Առաջին սյունակում, CodePage, ընդամենը 62 արժեքներ են։ Եվ երևում է, որ LowCardinality(String)-ում այն ավելի լավ սեղմեց։ String-ը մի քիչ ավելի վատն է, բայց դա հավանաբար այն պատճառով է, որ տողերը կարճ են, մենք պահում ենք դրանց երկարությունները, իսկ դրանք մեծ տարածք են զբաղեցնում, վատ սեղմվում։
Եթե վերցնենք PhoneModel-ը, դրանք 48 հազար — արդեն ավելի շատ, իսկ String-ի և LowCardinality(String)-ի միջև տարբերությունը գրեթե չի նկատվում։ URL-ի համար մենք նույնպես ընդամենը 2 ԳԲ խնայեցինք — կարծում եմ, դրա վրա ապավինել չի պետք։
Աշխատանքի արագության գնահատում

Հիմա գնահատենք աշխատանքի արագությունը։ Ստանալու համար ես օգտագործել եմ տաքսի ուղևորությունների նկարագրության տվյալների հավաքածու, որը գտնվում է GitHub-ում։ Այն ունի ավելի քան մեկ միլիարդ արխիվ։ Там отражены локация, время старта и окончания поездки, способ оплаты, число пассажиров и даже вид такси — зеленый, желтый и Uber։

Առաջին հարցումը ես կատարեցի բավական պարզ — հարցրեցի, որտեղ ավելի հաճախ են պատվիրում տաքսի։ Դա անելու համար нужно взять локацию, откуда заказывали, сделать по ней GROUP BY и посчитать функцию count։ Вот ClickHouse что-то выдает։

Հարցման արագությունը չափելու համար ես ստեղծել եմ երեք աղյուսակ նույն տվյալներով, բայց օգտագործել եմ մեր մեկնարկային լոկացիայի համար երեք տարբեր տվյալների տեսակ — String, LowCardinality և Enum։ LowCardinality և Enum են գտնվել հինգ անգամ արագ hơn, քան String-ը։ Enum-ը ավելի արագ է, քանի որ աշխատում է թվերի հետ։ LowCardinality-ը ավելիը, քան String-ը։

Դարձյալ դեռ ավելի բարդացնենք հարցումը — հարցնենք, որտեղ գտնվում է ամենահայտնի պարկը Նյու Յորքում։ Որպեսզի չափենք, որտեղ ավելի հաճախ են պատվիրում տաքսի, բայց նաև ֆիլտրելու էինք այն լոկացիաները, որտեղ կա "պարկ" բառը։ Այսպիսով ևս մի Լուսանկար վիրտուալ կամ հարմարեցնենք функции like։

Ժամանակին նայելով տեսնում ենք, որ Enum-ը հանկարծ սկսել է դանդաղել: Դա տեղի է ունենում, որովհետև այն աշխատում է նույնիսկ ավելի դանդաղ, քան ստանդարտ String տվյալը: Սա տեղի է ունենում, որովհետև like գործառույթը ամբողջությամբ ոչ արդյունավետ է Enum-ի համար: Մենք ստիպված ենք Ստանդարտ Enum-ի մեր ստրինգները փոխակերպելու սովորական ստրինգների մեջ՝ ավելի շատ աշխատանք կատարելով: LowCardinality(String) նույնպես ըստ դեֆոլտի ոչ արդյունավետ է, բայց այնտեղ like-ը աշխատում է բառարանի վրա, ուստի հարցումը արագանում է համեմատությամբ String-ի:
Enum-ով աշխատելիս կա ավելի գլոբալ խնդիր: Եթե ցանկանում ենք այն օպտիմիզացնել, պետք է դա անել յուրաքանչյուր կոդի մասում: Գնահատենք, որ նոր ֆունկցիա ենք գրել՝ պարտադիր պետք է մտածել Enum-ի օպտիմիզացիայի մասին: Իսկ LowCardinality-ը ըստ դեֆոլտի օպտիմիզացված է:

Դիտենք վերջին հարցումը, ավելի արհեստական: Մենք պարզապես կհաշվենք մեր դիրքի hash գործառույթը: Hash գործառույթը բավական դանդաղ հարցում է, պատրաստվում է երկար, ուստի ամեն բան դանդաղեցվում է մոտ երեք անգամ:

LowCardinality-ը դեռևս արագ աշխատում է, թեև այստեղ ֆիլտրացիա չկա: Սա տեղի է ունենում, որովհետև մեր գործառույթները աշխատում են միայն բառարանի վրա: Hash հաշվարկելու ֆունկցիայի մեկ հիմնահարց կա՝ այն կարող է մշակել ավելի քիչ տվյալներ և նույնպես կարող է վերադարձնել LowCardinality:

Մեր գլոբալ պլանը՝ ապահովել, որ աշխատելու արագությունը լինի ոչ ավելի դանդաղ, քան String-ի ցանկացած պարագայում, և պահպանել արագացումները: Եվ, հնարավոր է, մի օր մենք փոխենք String-ը LowCardinality-ի վրա, դուք կհափիչեք ClickHouse-ը, և ամեն բան ձեզ համար աշխատելու է մի փոքր ավելի արագ:
Ընտանիք: habr.com
