Optimasi string di ClickHouse. Laporan Yandex

Mesin database analitik ClickHouse memproses banyak jalur yang berbeda, menghabiskan sumber daya. Untuk mempercepat sistem, optimisasi baru terus ditambahkan. Pengembang ClickHouse Nikolay Kochetov berbicara tentang tipe data string, termasuk tipe baru, LowCardinality, dan menjelaskan cara mempercepat kerja dengan string.


- Pertama, mari kita lihat bagaimana Anda dapat menyimpan string.



Kami memiliki tipe data string. String berfungsi dengan baik secara default, harus digunakan hampir selalu. Ini memiliki Overhead kecil - 9 byte per baris. Jika kita ingin ukuran baris diperbaiki dan diketahui sebelumnya, lebih baik menggunakan FixedString. Di dalamnya Anda dapat mengatur jumlah byte yang kami butuhkan, mudah untuk data seperti alamat IP atau fungsi hash.



Tentu saja, terkadang ada sesuatu yang melambat. Misalkan Anda membuat kueri di atas meja. ClickHouse membaca sejumlah besar data, katakanlah, pada kecepatan 100 GB / s, dengan beberapa baris sedang diproses. Kami memiliki dua tabel yang menyimpan data yang hampir sama. ClickHouse membaca data dari tabel kedua dengan kecepatan lebih tinggi, tetapi membaca tiga kali lebih sedikit baris per detik.



Jika kita melihat ukuran data terkompresi, itu akan hampir sama. Faktanya, data yang sama ditulis dalam tabel - miliar angka pertama - hanya di kolom pertama ditulis dalam bentuk UInt64, dan di kolom kedua dalam String. Karena itu, permintaan kedua membaca data dari disk lebih lama dan mendekompresnya.



Ini adalah contoh lain. Misalkan ada satu set garis yang telah ditentukan, terbatas pada konstanta 1000 atau 10.000 dan hampir tidak pernah berubah. Untuk kasus ini, tipe data Enum cocok untuk kita, di ClickHouse ada dua - Enum8 dan Enum16. Karena penyimpanan di Enum, kami dengan cepat memproses permintaan.

ClickHouse memiliki akselerasi untuk GROUP BY, IN, DISTINCT dan optimisasi untuk beberapa fungsi, misalnya, untuk perbandingan dengan string konstan. Tentu saja, angka-angka dalam string tidak dikonversi, tetapi, sebaliknya, string konstan dikonversi ke nilai Enum. Setelah itu, semuanya dengan cepat dibandingkan.

Namun ada juga kekurangannya. Bahkan jika kita tahu rangkaian garis yang tepat, kadang-kadang perlu diisi ulang. Baris baru telah tiba - kita harus melakukan ALTER.



ALTER untuk Enum di ClickHouse diimplementasikan secara optimal. Kami tidak menimpa data pada disk, tetapi ALTER dapat memperlambat karena fakta bahwa struktur Enum disimpan dalam skema tabel itu sendiri. Karena itu, kita harus menunggu permintaan baca dari tabel, misalnya.

Pertanyaannya adalah, bisakah seseorang berbuat lebih baik? Mungkin ya. Anda bisa menyimpan struktur Enum tidak dalam skema tabel, tetapi di ZooKeeper. Namun, masalah sinkronisasi dapat terjadi. Misalnya, satu replika menerima data, yang lain tidak, dan jika memiliki Enum yang lama, maka sesuatu akan pecah. (Di ClickHouse, kami hampir menyelesaikan permintaan ALTER non-pemblokiran. Ketika kami menyelesaikannya sepenuhnya, kami tidak akan harus menunggu permintaan baca.)



Agar tidak mengacaukan dengan ALTER Enum, Anda dapat menggunakan kamus ClickHouse eksternal. Biarkan saya mengingatkan Anda bahwa ini adalah struktur data nilai kunci di dalam ClickHouse, yang dengannya Anda bisa mendapatkan data dari sumber eksternal, misalnya, dari tabel MySQL.

Dalam kamus ClickHouse, kami menyimpan banyak baris yang berbeda, dan dalam tabel pengidentifikasi mereka dalam bentuk angka. Jika kita perlu mendapatkan string, kita memanggil fungsi dictGet dan bekerja dengannya. Setelah itu kita jangan melakukan ALTER. Untuk menambahkan sesuatu ke Enum, kami menyisipkan ini ke dalam tabel MySQL yang sama.

Tetapi ada masalah lain. Pertama, sintaks canggung. Jika kita ingin mendapatkan string, kita harus memanggil dictGet. Kedua, kurangnya beberapa optimasi. Perbandingan dengan string konstan untuk kamus juga tidak cepat dilakukan.

Mungkin masih ada masalah dengan pembaruan. Misalkan kita meminta baris dalam kamus cache, tetapi tidak masuk ke cache. Maka kita harus menunggu hingga data dimuat dari sumber eksternal.



Kelemahan umum kedua metode adalah kami menyimpan semua kunci di satu tempat dan menyinkronkannya. Jadi mengapa tidak menyimpan kamus secara lokal? Tanpa sinkronisasi - tidak ada masalah. Anda dapat menyimpan kamus secara lokal di dalam disk. Yaitu, kami memasukkan, mencatat kamus. Jika kita bekerja dengan data dalam memori, kita dapat menulis kamus ke blok data, atau ke sepotong kolom, atau ke beberapa cache untuk mempercepat perhitungan.

Pengkodean String Kosakata


Jadi kami sampai pada penciptaan tipe data baru di ClickHouse - LowCardinality. Ini adalah format untuk menyimpan data: bagaimana mereka ditulis ke disk dan bagaimana mereka dibaca, bagaimana mereka disajikan dalam memori dan skema pemrosesan mereka.



Ada dua kolom pada slide. Di sebelah kanan, string disimpan secara standar dalam tipe String. Dapat dilihat bahwa ini adalah semacam model ponsel. Di sebelah kiri ada kolom yang persis sama, hanya dalam jenis LowCardinality. Ini terdiri dari kamus dengan banyak baris berbeda (baris dari kolom di sebelah kanan) dan daftar posisi (nomor baris).

Dengan menggunakan dua struktur ini, Anda dapat mengembalikan kolom asli. Ada juga indeks terbalik terbalik - tabel hash yang membantu Anda menemukan posisi dalam kamus per baris. Diperlukan untuk mempercepat beberapa permintaan. Misalnya, jika kita ingin membandingkan, cari baris di kolom kami atau gabungkan keduanya.

LowCardinality adalah tipe data parametrik. Ini bisa berupa angka, atau sesuatu yang disimpan sebagai angka, atau string, atau Dapat dibatalkan dari mereka.



Kekhasan LowCardinality adalah dapat disimpan untuk beberapa fungsi. Contoh permintaan ditampilkan pada slide. Pada baris pertama, saya membuat kolom tipe LowCardinality dari String, menamainya S. Lalu saya bertanya namanya - ClickHouse mengatakan bahwa itu adalah LowCardinality from String. Baiklah.

Baris ketiga hampir sama, hanya kita yang menyebut fungsi panjang. Di ClickHouse, fungsi panjang mengembalikan tipe data UInt64. Tapi kami mendapat LowCardinality dari UInt64. Apa gunanya?



Nama-nama ponsel disimpan dalam kamus, kami menerapkan fungsi panjang. Sekarang kami memiliki kamus serupa, yang hanya terdiri dari angka, ini adalah panjang string. Kolom dengan posisi tidak berubah. Akibatnya, kami memproses lebih sedikit data, disimpan pada waktu permintaan.

Mungkin ada optimasi lain, seperti menambahkan cache sederhana. Saat menghitung nilai suatu fungsi, Anda dapat mengingatnya dan membuatnya sama, jangan menghitung ulang.

Optimalisasi GROUP BY juga dapat dilakukan, karena kolom kami dengan kamus sudah sebagian teragregasi - kami dapat dengan cepat menghitung nilai fungsi hash dan secara kasar menemukan ember tempat meletakkan baris berikutnya. Anda juga dapat mengkhususkan beberapa fungsi agregat, misalnya uniq, karena Anda hanya dapat mengirim kamus ke sana, dan membiarkan posisi tidak tersentuh - dengan cara ini semuanya akan bekerja lebih cepat. Dua optimasi pertama yang telah kami tambahkan ke ClickHouse.



Tetapi bagaimana jika kita membuat kolom dengan tipe data kita dan memasukkan banyak baris berbeda yang buruk ke dalamnya? Apakah ingatan kita penuh? Tidak, ada dua pengaturan khusus untuk ini di ClickHouse. Yang pertama adalah low_cardinality_max_dictionary_size. Ini adalah ukuran maksimum kamus yang dapat ditulis ke disk. Penyisipan terjadi sebagai berikut: ketika kita memasukkan data, aliran garis datang kepada kita, dari mereka kita membentuk kamus umum yang besar. Jika kamus menjadi lebih besar dari nilai pengaturan, kami menulis kamus saat ini ke disk, dan sisa baris di suatu tempat di samping, di sebelah indeks. Akibatnya, kami tidak akan pernah menghitung ulang kamus besar dan tidak mendapatkan masalah memori.

Pengaturan kedua disebut low_cardinality_use_single_dictionary_for_part. Bayangkan dalam skema sebelumnya, ketika kami memasukkan data, kamus kami sudah penuh, dan kami menulisnya ke disk. Muncul pertanyaan, mengapa sekarang tidak membentuk kamus lain yang persis sama?

Ketika meluap, kita akan kembali menulis ke disk dan mulai membentuk yang ketiga. Pengaturan ini hanya menonaktifkan fitur ini secara default.

Faktanya, banyak kamus dapat berguna jika kita ingin memasukkan beberapa baris, tetapi secara tidak sengaja memasukkan "sampah". Katakanlah kita pertama-tama memasukkan garis buruk, dan kemudian kita memasukkan yang baik. Kemudian kamus akan dibagi menjadi banyak kamus kecil. Beberapa dari mereka akan dengan sampah, tetapi yang terakhir akan dengan garis-garis yang baik. Dan jika kita membaca, katakanlah, hanya pelet terakhir, maka semuanya juga akan bekerja dengan cepat.



Sebelum berbicara tentang keuntungan dari LowCardinality, saya akan segera mengatakan bahwa kami tidak mungkin mencapai pengurangan data pada disk (walaupun ini bisa terjadi), karena ClickHouse memampatkan data. Ada opsi default - LZ4. Anda juga dapat melakukan kompresi menggunakan ZSTD. Tetapi kedua algoritma sudah menerapkan kompresi kamus, jadi kamus ClickHouse eksternal kami tidak akan banyak membantu.

Agar tidak berdasar, saya mengambil beberapa data dari metrik - String, LowCardinality (String) dan Enum - dan menyimpannya dalam tipe data yang berbeda. Ternyata tiga kolom, di mana satu miliar baris ditulis. Kolom pertama, CodePage, memiliki total 62 nilai. Dan Anda dapat melihat bahwa di LowCardinality (String), mereka meremasnya dengan lebih baik. Tali sedikit lebih buruk, tetapi ini kemungkinan besar disebabkan oleh fakta bahwa senarnya pendek, kami menyimpan panjangnya, dan mereka mengambil banyak ruang dan tidak kompres dengan baik.

Jika Anda menggunakan PhoneModel, ada lebih dari 48 ribu di antaranya, dan hampir tidak ada perbedaan antara String dan LowCardinality (String). Untuk URL, kami juga menyimpan hanya 2 GB - Saya pikir Anda tidak harus bergantung pada ini.

Estimasi kecepatan kerja



Tautan dari slide

Sekarang mari kita mengevaluasi kecepatan kerja. Untuk mengevaluasinya, saya menggunakan dataset yang menggambarkan naik taksi di New York. Initersediadi GitHub. Ini memiliki sedikit lebih dari satu miliar perjalanan. Ini menunjukkan lokasi, waktu mulai dan akhir perjalanan, metode pembayaran, jumlah penumpang dan bahkan jenis taksi - hijau, kuning dan Uber.



Saya membuat permintaan pertama cukup sederhana - saya bertanya di mana taksi paling sering dipesan. Untuk melakukan ini, Anda harus mengambil lokasi dari tempat Anda memesan, membuat GROUP BY di atasnya dan menghitung fungsi penghitungan. Di sini ClickHouse memberikan sesuatu.



Untuk mengukur kecepatan pemrosesan kueri, saya membuat tiga tabel dengan data yang sama, tetapi menggunakan tiga tipe data yang berbeda untuk lokasi awal kami - String, LowCardinality, dan Enum. LowCardinality dan Enum lima kali lebih cepat dari String. Enum lebih cepat karena berfungsi dengan angka. LowCardinality - karena optimasi GROUP BY diimplementasikan.



Mari kita mempersulit permintaan - tanyakan di mana taman paling populer di New York berada. Sekali lagi, kami akan mengukur ini dengan tempat taksi paling sering dipesan, tetapi pada saat yang sama kami akan menyaring hanya lokasi-lokasi di mana kata "taman" tersedia. Juga tambahkan fungsi seperti.



Kami melihat waktu - kami melihat bahwa Enum tiba-tiba mulai melambat. Dan itu bekerja lebih lambat daripada tipe data String standar. Ini karena fungsi sejenisnya sama sekali tidak dioptimalkan untuk Enum. Kami harus mengubah jalur kami dari Enum ke jalur reguler - kami melakukan lebih banyak pekerjaan. LowCardinality (String) juga tidak dioptimalkan secara default, tetapi seperti berfungsi di kamus, jadi kueri lebih cepat dibandingkan dengan String.

Ada masalah yang lebih global dengan Enum. Jika kita ingin mengoptimalkannya, kita harus melakukannya di setiap tempat kode. Misalkan kita menulis fungsi baru - Anda pasti harus membuat optimasi untuk Enum. Dan dalam LowCardinality, semuanya dioptimalkan secara default.



Mari kita lihat permintaan terakhir, lebih buatan. Kami hanya akan menghitung fungsi hash dari lokasi kami. Fungsi hash adalah permintaan yang agak lambat, butuh waktu lama, jadi semuanya akan melambat tiga kali.



Low Cardinality masih lebih cepat, meskipun tidak ada pemfilteran. Ini disebabkan oleh fakta bahwa fungsi kami hanya berfungsi pada kamus. Fungsi perhitungan hash memiliki satu argumen - ia dapat memproses lebih sedikit data dan juga dapat mengembalikan LowCardinality.



Rencana global kami adalah mencapai kecepatan yang tidak lebih rendah dari String dalam hal apa pun, dan menghemat akselerasi. Dan mungkin suatu hari kita akan mengganti String dengan LowCardinality, Anda akan memperbarui ClickHouse, dan semuanya akan bekerja untuk Anda sedikit lebih cepat.

All Articles