ClickHouse analitik DBMS resurslarni sarflab, turli qatorlarni qayta ishlaydi. Tizimni tezlashtirish uchun doimiy ravishda yangi optimallashtirishlar qo'shiladi. ClickHouse dasturchisi Nikolay Kochetov string ma'lumotlar turi, jumladan, yangi turdagi LowCardinality haqida gapiradi va strings bilan ishlashni qanday tezlashtirish mumkinligini tushuntiradi.

- Birinchidan, satrlarni qanday saqlash kerakligini aniqlaymiz.

Bizda string ma'lumotlar turlari mavjud. String yaxshi standart va deyarli har doim ishlatilishi kerak. U kichik yukga ega - har bir satr uchun 9 bayt. Agar biz satrlarning o'lchamlari aniq bo'lishini va oldindan ma'lum bo'lishini istasak, u holda FixedString dan foydalangan ma'qul. Unda siz bizga kerak bo'lgan bayt sonini belgilashingiz mumkin, bu IP manzillari yoki xesh funktsiyalari kabi ma'lumotlar uchun qulaydir.

Albatta, ba'zida ishlar sekinlashadi. Aytaylik, siz jadvalni so'rayapsiz. ClickHouse juda katta hajmdagi ma'lumotlarni o'qiydi, masalan, 100 Gb/s tezlikda, bir nechta qatorlarni qayta ishlashda. Bizda deyarli bir xil ma'lumotlarni saqlaydigan ikkita jadval mavjud. ClickHouse ikkinchi jadvaldagi ma'lumotlarni yuqori tezlikda o'qiydi, lekin soniyada uch barobar kamroq qatorlarni o'qiydi.

Agar siqilgan ma'lumotlarning hajmiga qarasak, u deyarli teng. Aslida, jadvallar bir xil ma'lumotlarni o'z ichiga oladi - birinchi milliard raqamlar - faqat birinchi ustunda ular UInt64, ikkinchisida esa - Stringda yoziladi. Shu sababli, ikkinchi so'rov diskdan ma'lumotlarni o'qish va uni ochish uchun ko'proq vaqt talab etadi.

Mana yana bir misol. Faraz qilaylik, oldindan ma'lum bo'lgan qatorlar to'plami bor, u 1000 yoki 10 000 doimiysi bilan cheklangan va deyarli hech qachon o'zgarmaydi. Bu holatda, Enum ma'lumotlar turi biz uchun mos keladi ClickHouse ulardan ikkitasi - Enum8 va Enum16; Enum-da saqlash orqali biz so'rovlarni tezda qayta ishlaymiz.
ClickHouse-da GROUP BY, IN, DISTINCT uchun tezlashtirish va doimiy qatorlarni taqqoslash kabi ba'zi funktsiyalar uchun optimallashtirish mavjud. Albatta, satrdagi raqamlar aylantirilmaydi, aksincha, doimiy satr Enum qiymatiga aylantiriladi. Shundan so'ng, hamma narsa tezda taqqoslanadi.
Lekin kamchiliklari ham bor. Hatto qatorlarning aniq to'plamini bilsak ham, ba'zan uni to'ldirish kerak. Yangi qator keldi - biz ALTER qilishimiz kerak.

ClickHouse-dagi Enum uchun ALTER optimal tarzda amalga oshiriladi. Biz diskdagi ma'lumotlarni qayta yozmaymiz, lekin Enum tuzilmalari jadvalning o'zi sxemasida saqlanganligi sababli ALTER sekinlashishi mumkin. Shuning uchun, masalan, jadvaldan o'qish uchun so'rovlarni kutishimiz kerak.
Savol tug'iladi, biz yaxshiroq qila olamizmi? Ehtimol Ha. Enum strukturasini jadval sxemasida emas, balki ZooKeeper da saqlashingiz mumkin. Biroq, sinxronizatsiya bilan bog'liq muammolar paydo bo'lishi mumkin. Misol uchun, bitta replika ma'lumot oldi, boshqasi yo'q va agar u eski Enumga ega bo'lsa, unda biror narsa buziladi. (ClickHouse’da biz bloklanmagan ALTER so‘rovlarini deyarli yakunladik. Ularni to‘liq bajarganimizda, o‘qish so‘rovlarini kutishning hojati qolmaydi.)

ALTER Enum bilan o'ynamaslik uchun tashqi ClickHouse lug'atlaridan foydalanishingiz mumkin. Eslatib o'taman, bu ClickHouse ichidagi kalit-qiymatli ma'lumotlar strukturasi bo'lib, uning yordamida siz tashqi manbalardan, masalan, MySQL jadvallaridan ma'lumotlarni olishingiz mumkin.
ClickHouse lug'atida biz juda ko'p turli qatorlarni saqlaymiz va jadvalda ularning identifikatorlarini raqamlar shaklida saqlaymiz. Agar biz satrni olishimiz kerak bo'lsa, biz dictGet funksiyasini chaqiramiz va u bilan ishlaymiz. Shundan so'ng biz ALTER qilishimiz shart emas. Enumga biror narsa qo'shish uchun biz uni bir xil MySQL jadvaliga joylashtiramiz.
Ammo bu erda boshqa muammolar paydo bo'ladi. Birinchidan, sintaksis noqulay. Agar biz satr olishni istasak, dictGet ga qo'ng'iroq qilishimiz kerak. Ikkinchidan, ba'zi optimallashtirishlarning yo'qligi. Lug'atlar uchun doimiy qator bilan taqqoslashni tez amalga oshirib bo'lmaydi.
Yangilanish bilan bog'liq muammolar ham bo'lishi mumkin. Aytaylik, biz kesh lug'atidan satr so'radik, lekin u keshga kiritilmadi. Keyin tashqi manbadan ma'lumotlar yuklanishini kutishimiz kerak.

Ikkala usulning umumiy kamchiligi shundaki, biz barcha kalitlarni bir joyda saqlaymiz va ularni sinxronlashtiramiz. Xo'sh, nima uchun lug'atlarni mahalliy darajada saqlamaysiz? Sinxronizatsiya yo'q - muammo yo'q. Siz lug'atni diskdagi bo'lakda mahalliy sifatida saqlashingiz mumkin. Ya'ni, biz Insert qildik, lug'at yozdik. Agar biz xotiradagi ma'lumotlar bilan ishlasak, hisob-kitoblarni tezlashtirish uchun lug'atni ma'lumotlar blokiga yoki ustun bo'lagiga yoki qandaydir keshga yozishimiz mumkin.
Satrlarni lug'at bilan kodlash
Biz ClickHouse-da yangi ma'lumotlar turini yaratishga keldik - LowCardinality. Bu ma'lumotlarni saqlash formati: u diskka qanday yoziladi va qanday o'qiladi, xotirada qanday aks ettiriladi va uni qayta ishlash sxemasi.

Slaydda ikkita ustun mavjud. O'ng tomonda satrlar standart sifatida String turida saqlanadi. Ko'rinib turibdiki, bu mobil telefonlarning ba'zi modellari. Chap tomonda aynan bir xil ustun mavjud, faqat LowCardinality turida. U juda ko'p turli qatorlar (o'ngdagi ustundan satrlar) va pozitsiyalar ro'yxatidan (satr raqamlari) iborat lug'atdan iborat.
Ushbu ikkita tuzilmadan foydalanib, asl ustunni tiklash mumkin. Bundan tashqari, teskari teskari indeks mavjud - lug'atda satr bo'yicha pozitsiyani topishga yordam beradigan xesh-jadval. Bu ba'zi so'rovlarni tezlashtirish uchun kerak. Misol uchun, agar biz solishtirmoqchi bo'lsak, ustunimizdagi satrni qidiring yoki ularni birlashtiring.
LowCardinality - parametrik ma'lumotlar turi. Bu raqam, raqam sifatida saqlanadigan narsa yoki satr yoki ularning null qiymati bo'lishi mumkin.

LowCardinality-ning o'ziga xos tomoni shundaki, u ba'zi funktsiyalar uchun davom etishi mumkin. Slaydda so'rovning namunasi ko'rsatilgan. Birinchi qatorda men String-dan LowCardinality turidagi ustunni yaratdim, uni S deb nomladim. Keyin uning nomini so'radim - ClickHouse bu String-dan LowCardinality ekanligini aytdi. Hammasi to'g'ri.
Uchinchi qator deyarli bir xil, faqat biz uzunlik funksiyasini chaqirdik. ClickHouse-da uzunlik funksiyasi UInt64 ma'lumotlar turini qaytaradi. Ammo endi bizda UInt64-dan LowCardinality mavjud. Buning nima keragi bor?

Lug'atda mobil telefonlarning nomlari bor edi, biz uzunlik funksiyasidan foydalandik. Endi bizda faqat raqamlardan iborat shunga o'xshash lug'at bor - bular qatorlar uzunligi. Joylashuv ustuni o'zgarmadi. Natijada biz kamroq ma'lumotlarni qayta ishladik va so'rov vaqtini tejadik.
Oddiy keshni qo'shish kabi boshqa optimallashtirishlar ham bo'lishi mumkin. Funktsiyaning qiymatini hisoblashda siz uni eslab qolishingiz va uni qayta hisoblamasdan ham xuddi shu narsani yaratishingiz mumkin.
GROUP BY optimallashtirish ham amalga oshirilishi mumkin, chunki bizning lug'atli ustunimiz allaqachon qisman yig'ilgan - biz hash funktsiyalarining qiymatini tezda hisoblashimiz va keyingi qatorni joylashtirish uchun chelakni topishimiz mumkin. Shuningdek, siz ba'zi jamlangan funktsiyalarni ixtisoslashtirishingiz mumkin, masalan, uniq, chunki siz unga faqat lug'at yuborishingiz va pozitsiyalarni tegmasdan qoldirishingiz mumkin - bu bilan hamma narsa tezroq ishlaydi. Biz allaqachon ClickHouse-ga birinchi ikkita optimallashtirishni qo'shdik.

Agar biz ma'lumotlar turimiz bilan ustun yaratsak va unga juda ko'p yomon turli qatorlarni qo'shsak nima bo'ladi? Xotiramiz to'la bo'ladimi? Yo'q, ClickHouse-da buning uchun ikkita maxsus sozlamalar mavjud. Birinchisi past_kardinallik_maksimal_lug'at_o'lchami. Bu diskka yozilishi mumkin bo'lgan lug'atning maksimal hajmi. Qo'shish quyidagicha sodir bo'ladi: biz ma'lumotlarni kiritganimizda, biz katta umumiy lug'at hosil qiladigan satrlar oqimi keladi. Agar lug'at belgilangan qiymatdan kattaroq bo'lsa, biz joriy lug'atni diskka yozamiz, qolgan satrlarni esa indekslar yonida "yon tomonda" joylashtiramiz. Natijada, biz hech qachon katta lug'atni qayta hisoblamaymiz va xotira bilan bog'liq muammolarga duch kelmaymiz.
Ikkinchi sozlama low_cardinality_use_single_dictionary_for_part deb ataladi. Tasavvur qiling-a, oldingi sxemada biz ma'lumotlarni kiritganimizda, lug'atimiz to'ldi va biz uni diskka yozdik. Savol tug'iladi: nega endi boshqa aynan bir xil lug'at yaratmaslik kerak?
U to'lganida, biz uni yana diskka yozamiz va uchinchisini qurishni boshlaymiz. Ushbu sozlama sukut bo'yicha ushbu xususiyatni o'chiradi.
Darhaqiqat, ko'plab lug'atlar, agar biz bir qator qatorlarni kiritishni xohlasak, foydali bo'lishi mumkin, lekin tasodifan kiritilgan "axlat". Aytaylik, avval yomon satrlarni kiritdik, keyin esa yaxshilarini kiritdik. Keyin lug'at ko'plab kichik lug'atlarga bo'linadi. Ulardan ba'zilari "axlat" bo'ladi, ammo oxirgilarida yaxshi chiziqlar bo'ladi. Va agar biz, aytaylik, faqat oxirgi granulani o'qisak, hamma narsa ham tez ishlaydi.

LowCardinality-ning afzalliklari haqida gapirishdan oldin, men darhol aytamanki, biz diskdagi ma'lumotlarning qisqarishiga erisha olmaymiz (garchi bu sodir bo'lishi mumkin), chunki ClickHouse ma'lumotlarni siqadi. Standart variant mavjud - LZ4. ZSTD yordamida siqishni ham qilishingiz mumkin. Ammo ikkala algoritm ham allaqachon lug'atni siqishni amalga oshiradi, shuning uchun bizning tashqi ClickHouse lug'atimiz unchalik yordam bermaydi.
Aniq bo'lishi uchun, men metrikadan ba'zi ma'lumotlarni oldim - String, LowCardinality(String) va Enum - va ularni turli xil ma'lumotlar turlarida saqladim. Natijada bir milliard qatordan iborat uchta ustun paydo bo'ldi. Birinchi ustun, CodePage, jami 62 ta qiymatga ega. Va LowCardinality(String) da biz ularni yaxshiroq siqganimiz aniq. String biroz yomonroq, lekin bu, ehtimol, iplar qisqa, biz ularning uzunligini saqlaymiz, lekin ular juda ko'p joy egallaydi va yomon siqiladi.
Agar biz PhoneModelni olsak, ularning 48 mingtasi bor - allaqachon ko'proq va String va LowCardinality(String) o'rtasida deyarli farq yo'q. URL uchun biz atigi 2 Gbni ham tejadik - men bunga tayanmasligimiz kerak deb o'ylamayman.
Tezlik reytingi

Endi ish tezligini baholaylik. Uni baholash uchun men Nyu-Yorkdagi taksi sayohatlarini tavsiflovchi ma'lumotlar to'plamidan foydalandim. U GitHub-da. Bir milliarddan ortiq sayohatlar mavjud. Unda sayohatning joylashuvi, boshlanish va tugash vaqtlari, to‘lov usuli, yo‘lovchilar soni va hatto taksi turi – yashil, sariq va Uber ko‘rsatilgan.

Men birinchi so'rovni juda oddiy qildim - men ular ko'pincha qayerda taksi buyurtma qilishlarini so'radim. Buni amalga oshirish uchun siz buyurtma bergan joyni olishingiz kerak, unda GROUP BY qiling va hisoblash funksiyasini hisoblang. Bu erda ClickHouse nimadir beradi.

So'rovlarni qayta ishlash tezligini o'lchash uchun men bir xil ma'lumotlarga ega uchta jadval yaratdim, lekin boshlang'ich joylashuvimiz uchun uchta turli xil ma'lumotlar turidan foydalandim - String, LowCardinality va Enum. LowCardinality va Enum Stringga qaraganda besh baravar tezroq edi. Enum tezroq, chunki u raqamlar bilan ishlaydi. LowCardinality - chunki GROUP BY optimallashtirish amalga oshirilgan.

Keling, so'rovni yanada murakkablashtiraylik - Nyu-Yorkdagi eng mashhur park qayerda joylashganligini so'rang. Shunga qaramay, biz buni taksilar eng ko'p buyurtma qilinadigan joylar bilan o'lchaymiz, lekin ayni paytda biz faqat "park" so'zi mavjud bo'lgan joylarni filtrlaymiz. Biz o'xshash funktsiyani ham qo'shamiz.

Biz vaqtga qaraymiz va Enum birdan sekinlasha boshlaganini ko'ramiz. Bundan tashqari, u standart String ma'lumotlar turidan ham sekinroq ishlaydi. Buning sababi, shunga o'xshash funksiya Enum uchun umuman optimallashtirilmagan. Biz Enum satrlarimizni oddiy satrlarga aylantirishimiz kerak - biz ko'proq ish qilamiz. LowCardinality(String) ham sukut bo'yicha optimallashtirilmagan, lekin u erda lug'atda ishlash kabi ishlaydi, shuning uchun so'rov String bilan solishtirganda tezroq bo'ladi.
Enum bilan ishlashda ko'proq global muammo mavjud. Agar biz uni optimallashtirishni istasak, kodning har bir joyida buni qilishimiz kerak. Aytaylik, biz yangi funktsiya yozdik - biz, albatta, Enum uchun optimallashtirishni o'ylab topishimiz kerak. Va LowCardinality-da hamma narsa sukut bo'yicha optimallashtirilgan.

Keling, sun'iyroq bo'lgan oxirgi so'rovni ko'rib chiqaylik. Biz shunchaki xesh funktsiyasini joylashuvimizdan hisoblaymiz. Xash funktsiyasi juda sekin so'rovdir, uni hisoblash uchun ko'p vaqt talab etiladi, shuning uchun hamma narsa uch marta sekinlashadi.

LowCardinality hali ham tezroq, garchi filtrlash yo'q. Buning sababi, bizning funktsiyalarimiz faqat lug'atda ishlaydi. Xeshni hisoblash funksiyasi bitta argumentga ega - u kamroq ma'lumotlarni qayta ishlay oladi va LowCardinality ni ham qaytarishi mumkin.

Bizning global rejamiz barcha holatlarda String-dan past bo'lmagan ishlash tezligiga erishish va tezlikni saqlab qolishdir. Va, ehtimol, bir kun biz Stringni LowCardinality bilan almashtiramiz, siz ClickHouse-ni yangilaysiz va hamma narsa siz uchun biroz tezroq ishlaydi.
Manba: www.habr.com
