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.

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

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.

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.

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.

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.

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.)

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.

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.

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.

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?

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.

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.

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

Now letâs evaluate the performance. To do this, I used a dataset describing taxi rides in New York. Itâs 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.

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.

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.

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.

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.

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.

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.

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
