Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Analytisk DB ClickHouse hÄndterer mange forskjellige strenger og bruker ressurser. For Ä akselerere systemet legges det hele tiden til nye optimaliseringer. Utvikler av ClickHouse, Nikolai Kochetov, snakker om datatypen for strenger, inkludert den nye typen, LowCardinality, og forklarer hvordan man kan gjÞre operasjoner med strenger raskere.

Spill video

— La oss fĂžrst se pĂ„ hvordan vi kan lagre strenger.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Vi har datatyper for strenger. String fungerer godt som standard, og det bĂžr nesten alltid brukes. Det har en liten Overhead — 9 byte per streng. Hvis vi Ăžnsker at stĂžrrelsen pĂ„ strenger skal vĂŠre fast og kjent pĂ„ forhĂ„nd, er det bedre Ă„ bruke FixedString. I den kan vi spesifisere Ăžnsket antall byte, og den er praktisk for data som IP-adresser eller hash-funksjoner.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

SelvfÞlgelig hender det noen ganger at noe er tregt. La oss si at du gjÞr en forespÞrsel til en tabell. ClickHouse leser ganske mye data, si med en hastighet pÄ 100 GB/s, mens antallet strenger som behandles er ganske lite. Vi har to tabeller som lagrer nesten identiske data. Fra den andre tabellen leser ClickHouse data raskere, men antallet strenger som leses per sekund er tre ganger lavere.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Hvis vi ser pĂ„ stĂžrrelsen av de komprimerte dataene, vil de vise seg Ă„ vĂŠre nesten like. I virkeligheten er det skrevet de samme dataene i tabellene — den fĂžrste milliarden tall — bare i den fĂžrste kolonnen er de skrevet som UInt64, mens i den andre kolonnen er de skrevet som String. PĂ„ grunn av dette bruker den andre forespĂžrselen lengre tid pĂ„ Ă„ lese data fra disken og dekomprimere dem.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Her er et annet eksempel. Anta at det er et forhĂ„ndsbekjent sett med strenger, begrenset av en konstant pĂ„ 1000 eller 10 000 og som praktisk talt aldri endres. I dette tilfellet passer datatypen Enum for oss, i ClickHouse er det to — Enum8 og Enum16. Ved Ă„ lagre i Enum kan vi raskt behandle forespĂžrslene.

I ClickHouse finnes det akselerasjoner for GROUP BY, IN, DISTINCT og optimaliseringer for visse funksjoner, for eksempel for sammenligning med en konstant streng. SelvfĂžlgelig konverteres ikke tall i strengen, men derimot konverteres den konstante strengen til verdien Enum. Etter dette blir alt raskt sammenlignet.

Men det finnes ogsĂ„ ulemper. Selv om vi vet det eksakte settet av strenger, mĂ„ det noen ganger oppdateres. En ny streng har kommet — vi mĂ„ utfĂžre ALTER.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

ALTER for Enum in ClickHouse is implemented optimally. We do not rewrite data on disk, but ALTER operations can be slow due to the fact that Enum structures are stored in the table’s schema. Therefore, we must wait for read requests from the table, for example.

This raises the question: can it be done better? Probably, yes. You could store the Enum structure not in the table schema, but in ZooKeeper. However, this could create synchronization issues. For instance, one replica may have received the data while another has not, and if it has an old Enum, something could break. (In ClickHouse, we have almost completed non-blocking ALTER queries. Once we finish them, there will be no need to wait for read requests.)

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

To avoid dealing with ALTER Enum, you can use ClickHouse external dictionaries. Recall that this is a key-value data structure within ClickHouse that allows you to retrieve data from external sources, such as MySQL tables.

In the ClickHouse dictionary, we store many different strings, while in the table, we store their identifiers as numbers. When we need to fetch a string, we call the dictGet function and work with it. After that, we don’t need to perform an ALTER operation. To add something to the Enum, we insert it into the same MySQL table.

However, this introduces other problems. First, there’s inconvenient syntax. If we want to get a string, we have to call dictGet. Second, there’s a lack of certain optimizations. Comparing with a constant string for dictionaries is not as fast.

There could also be issues with updates. Suppose we requested a string from the cache dictionary, but it didn’t get into the cache. Then we must wait for the data to load from the external source.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

The overall disadvantage of both methods is that we store all keys in one place and synchronize them. So why not store dictionaries locally? No synchronization means no problems. You can store the dictionary locally in a chunk on disk. That is, we perform an Insert, write the dictionary. If we work with data in memory, we can store the dictionary either in a data block, or in a column chunk, or in some cache to speed up computations.

Dictionary encoding of strings

Thus, we have come to create a new data type in ClickHouse — LowCardinality. This is a data storage format: how it is written to disk, how it is read, how it is represented in memory, and the scheme of its processing.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Lysbildet inneholder to kolonner. Til hÞyre lagres radene standardmessig, som Streng. Det er tydelig at dette er noen mobiltelefonmodeller. Til venstre er det en identisk kolonne, men i LowCardinality-type. Den bestÄr av et ordbok med mange forskjellige strenger (strengene fra kolonnen til hÞyre) og en liste med posisjoner (radnumre).

Med disse to strukturene kan man gjenskape den opprinnelige kolonnen. Det finnes ogsĂ„ en omvendt omvendt indeks — en hash-tabell som hjelper med Ă„ finne posisjonen i ordboken basert pĂ„ strengen. Den er nĂždvendig for Ă„ akselerere enkelte forespĂžrseler. For eksempel, hvis vi Ăžnsker Ă„ sammenligne, sĂžke etter en streng i kolonnen vĂ„r eller slĂ„ dem sammen.

LowCardinality er en parameterisk datatype. Den kan vĂŠre enten et tall, eller noe som lagres som et tall, eller en streng, eller Nullable av dem.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Spesialiteten til LowCardinality er at den kan lagres for enkelte funksjoner. PĂ„ lysbildet vises et eksempel pĂ„ en forespĂžrsel. I den fĂžrste linjen laget jeg en kolonne av LowCardinality av Streng, jeg kalte den S. Deretter spurte jeg om navnet, — ClickHouse sa at det var LowCardinality av Streng. Det stemmer.

Den tredje linjen er nesten lik, bare at vi kalte funksjonen length. I ClickHouse returnerer length-funksjonen datatypen UInt64. Men vi fikk LowCardinality av UInt64. Hva er meningen?

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

I ordboken ble navnene pĂ„ mobiltelefonene lagret, vi brukte funksjonen length. NĂ„ har vi fĂ„tt en tilsvarende ordbok bestĂ„ende bare av tall — dette er lengdene pĂ„ strenger. Kolonnen med posisjoner forble uendret. I resultatet behandlet vi mindre data, sparte tid pĂ„ forespĂžrselen.

Det kan ogsÄ vÊre andre optimaliseringer, for eksempel ved Ä legge til en enkel cache. NÄr man beregner verdien av funksjonen, kan man huske den og lage en lignende uten Ä beregne pÄ nytt.

Det kan ogsĂ„ gjĂžres en optimalisering av GROUP BY, fordi kolonnen vĂ„r med ordboken allerede er delvis aggregert — det kan vĂŠre raskere Ă„ beregne verdien av hash-funksjoner og omtrentlig finne bĂžttet der neste rad kan plasseres. Det er ogsĂ„ mulig Ă„ spesialisere enkelte aggregeringsfunksjoner, for eksempel uniq, ettersom vi kun kan sende ordboken til den, mens posisjonene forblir urĂžrt — slik vil alt fungere raskere. De fĂžrste to optimaliseringene har vi allerede lagt til i ClickHouse.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Hva om vi lager en kolonne med vÄr datatype og setter inn mange dÄrlige, forskjellige strenger? Vil ikke minnet vÄrt bli overfylt? Nei, for dette har ClickHouse to spesielle innstillinger. Den fÞrste er low_cardinality_max_dictionary_size. Dette er den maksimale stÞrrelsen pÄ et ordforrÄd som kan skrives til disk. Innsetting skjer slik: nÄr vi setter inn data, fÄr vi en strÞm av strenger, og av dem danner vi et stort felles ordforrÄd. Hvis ordforrÄdet blir stÞrre enn innstillingsverdien, skriver vi det nÄvÊrende ordforrÄdet til disk, mens de andre strengene blir «liggende» ved siden av indeksene. Resultatet blir at vi aldri mÄ gjenberegne et stort ordforrÄd og fÄr ikke problemer med minnet.

Den andre innstillingen kalles low_cardinality_use_single_dictionary_for_part. Tenk deg at i det forrige scenariet, nÄr vi satte inn data, ble ordforrÄdet vÄrt overfylt, og vi skrev det til disk. SpÞrsmÄlet oppstÄr, hvorfor ikke lage et annet nÞyaktig likt ordforrÄd?

NÄr det blir overfylt, skriver vi det igjen til disk og begynner Ä danne et tredje. Denne innstillingen deaktiverer som standard denne muligheten.

Faktisk kan mange ordforrÄd vÊre nyttige, hvis vi vil sette inn en viss mengde strenger, men tilfeldig har satt inn «sÞppel». Si at vi fÞrst satte inn dÄrlige strenger, og deretter satte inn gode. Da vil ordforrÄdet deles opp i mange smÄ ordforrÄd. Noen av dem vil ha «sÞppel», men de siste vil ha gode strenger. Og hvis vi leser bare den siste granulen, vil alt ogsÄ fungere raskt.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

FĂžr vi snakker om fordelene med LowCardinality, skal jeg straks si at vi neppe vil oppnĂ„ en reduksjon av dataene pĂ„ disken (selv om det kan hende), fordi ClickHouse komprimerer dataene. Det er en standardmulighet — LZ4. Det er ogsĂ„ mulig Ă„ gjĂžre komprimering med ZSTD. Men begge algoritmene implementerer allerede ordkomprimering, sĂ„ vĂ„rt eksterne ordforrĂ„d i ClickHouse vil ikke vĂŠre til stor hjelp.

For the sake of being concrete, I took some data from the metrics — String, LowCardinality(String), and Enum — and saved them in different data types. This resulted in three columns containing one billion rows. In the first column, CodePage, there are only 62 values. It's clear that LowCardinality(String) compresses them better. String is slightly worse, but that's likely due to the short strings; we store their lengths, which takes up a lot of space and compresses poorly.

Taking PhoneModel as an example, there are 48 thousand of them — already more, and the differences between String and LowCardinality(String) are negligible. We also saved just 2 GB for URLs — I think it’s not worth relying on.

Performance Evaluation

Optimalisering av rader i ClickHouse. Forelesning fra Yandex
Link from the slide

Now let’s evaluate the performance. To do this, I used a dataset describing taxi rides in New York. It’s tilgjengelig on GitHub. It has just over a billion rides. It includes the location, start and end time of the ride, payment method, number of passengers, and even the type of taxi — green, yellow, or Uber.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

My first query was quite simple — I asked where taxis are most frequently ordered from. For this, we need to take the location from which the taxi was ordered, GROUP BY it, and count the function count. Here’s what ClickHouse outputs.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

To measure the query processing speed, I created three tables with the same data, but I used three different data types for our starting location — String, LowCardinality, and Enum. LowCardinality and Enum turned out to be five times faster than String. Enum is faster because it works with numbers. LowCardinality is faster due to the GROUP BY optimization.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Let’s complicate the query a bit more — let’s ask where the most popular park in New York is located. Again, we will measure this by where taxis are most frequently ordered, but we will filter only those locations that contain the word “park.” We will also add the like function.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

Looking at the time — we see that Enum suddenly started to lag. In fact, it works even slower than the standard data type String. This happens because the like function is not optimized for Enum at all. We have to convert our strings from Enum to regular strings — we’re doing more work. LowCardinality(String) is also not optimized by default, but there, like operates on the dictionary, so the query is accelerated compared to String.

NĂ„r vi arbeider med Enum, er det et mer overordnet problem. Hvis vi Ăžnsker Ă„ optimalisere det, mĂ„ vi gjĂžre det pĂ„ hver eneste plass i koden. La oss si at vi har skrevet en ny funksjon – vi mĂ„ absolutt finne en optimalisering for Enum. I LowCardinality er alt optimalisert som standard.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

La oss se pÄ den siste forespÞrselen, som er mer kunstig. Vi vil bare beregne hash-funksjonen for plasseringen vÄr. Hash-funksjonen er en ganske treig forespÞrsel som tar tid, sÄ alt vil bli tre ganger langsommere.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

LowCardinality fungerer fortsatt raskere, selv om det ikke er noen filtrering her. Dette skjer fordi funksjonene vĂ„re kun arbeider med ordboken. Hash-beregningsfunksjonen har Ă©n parameter – den kan behandle mindre data og kan ogsĂ„ returnere LowCardinality.

Optimalisering av rader i ClickHouse. Forelesning fra Yandex

VÄr globale plan er Ä oppnÄ en hastighet som ikke er lavere enn String i alle tilfeller, og Ä opprettholde forbedringer. Og kanskje en dag vil vi erstatte String med LowCardinality, du oppdaterer ClickHouse, og alt vil fungere litt raskere.

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster