Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Den analytiska databasen ClickHouse hanterar en mÀngd olika strÀngar och förbrukar resurser. För att pÄskynda systemets arbete lÀggs stÀndigt till nya optimeringar. Utvecklaren av ClickHouse, Nikolai Kochetov, berÀttar om strÀngdatatypen, inklusive den nya typen LowCardinality, och förklarar hur man kan snabba upp arbetet med strÀngar.

Spela video

— LĂ„t oss först gĂ„ igenom hur vi kan lagra strĂ€ngar.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Vi har strĂ€ngdatatyper. String Ă€r bra som standard och bör nĂ€stan alltid anvĂ€ndas. Den har en liten overhead — 9 byte per strĂ€ng. Om vi vill att storleken pĂ„ strĂ€ngarna ska vara fast och kĂ€nd pĂ„ förhand, Ă€r det bĂ€ttre att anvĂ€nda FixedString. HĂ€r kan vi ange det antal byte vi behöver, vilket Ă€r praktiskt för data som IP-adresser eller hash-funktioner.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

SjÀlvklart, ibland gÄr nÄgot lÄngsamt. Anta att du gör en förfrÄgan till en tabell. ClickHouse lÀser en ganska stor mÀngd data, lÄt sÀga i hastighet av 100 GB/s, medan strÀngar hanteras vÀldigt lite. Vi har tvÄ tabeller som lagrar nÀstan identiska data. FrÄn den andra tabellen lÀser ClickHouse data snabbare, men antalet strÀngar som lÀses per sekund Àr tre gÄnger lÀgre.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Om vi tittar pĂ„ storleken pĂ„ de komprimerade data, kommer den att visa sig vara nĂ€stan likvĂ€rdig. I sjĂ€lva verket Ă€r de samma data skrivna i tabellerna — den första miljarden siffror — men i den första kolumnen skrivs de som UInt64, och i den andra som String. PĂ„ grund av detta tar den andra förfrĂ„gan lĂ€ngre tid att lĂ€sa data frĂ„n disken och att dekomprimera dem.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

HĂ€r Ă€r ett annat exempel. Anta att det finns ett förutbestĂ€mt antal strĂ€ngar, begrĂ€nsat till en konstant 1000 eller 10 000, och som praktiskt taget aldrig förĂ€ndras. För detta fall passar datatypen Enum, i ClickHouse finns det tvĂ„ — Enum8 och Enum16. Genom att lagra i Enum kan vi snabbt bearbeta förfrĂ„gningarna.

I ClickHouse finns det optimeringar för GROUP BY, IN, DISTINCT och optimeringar för vissa funktioner, till exempel för att jÀmföra med en konstant strÀng. SjÀlvklart omvandlas inte siffror i strÀngen, utan istÀllet omvandlas den konstanta strÀngen till ett vÀrde av Enum. Efter detta jÀmförs allt snabbt.

Men det finns ocksĂ„ nackdelar. Även om vi kĂ€nner till det exakta antalet strĂ€ngar, kan det ibland behöva utökas. Om en ny strĂ€ng kommer in — mĂ„ste vi göra ett ALTER.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

ALTER för Enum i ClickHouse har implementerats pÄ ett optimalt sÀtt. Vi skriver inte om data pÄ disken, men ALTER kan vara trögt pÄ grund av att Enum-strukturer lagras i sjÀlva tabellens schema. DÀrför mÄste vi vÀnta pÄ lÀsförfrÄgningar frÄn tabellen, till exempel.

FrÄgan uppstÄr, kan vi göra det bÀttre? Kanske, ja. Vi kan lagra Enum-strukturen inte i tabellens schema utan i ZooKeeper. Men det kan uppstÄ problem relaterade till synkronisering. Till exempel, en replik fÄr data, en annan gör det inte, och om den har en gammal Enum, kan nÄgot gÄ fel. (I ClickHouse har vi nÀstan fÀrdigstÀllt icke-blockerande ALTER-förfrÄgningar. NÀr vi har slutfört dem helt behöver vi inte vÀnta pÄ lÀsförfrÄgningar.)

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

För att slippa krÄngel med ALTER Enum kan vi anvÀnda externa ordlistor i ClickHouse. Jag pÄminner om att det Àr en key-value datastruktur inuti ClickHouse, som gör att vi kan hÀmta data frÄn externa kÀllor, till exempel frÄn MySQL-tabeller.

I ClickHouse-ordlistan lagrar vi mÄnga olika strÀngar, medan tabellen innehÄller deras identifierare i form av siffror. Om vi behöver en strÀng, anropar vi funktionen dictGet och arbetar med den. Efter det behöver vi inte göra ALTER. För att lÀgga till nÄgot i Enum, sÀtter vi in det i samma MySQL-tabell.

Men hÀr uppstÄr andra problem. För det första, ett besvÀrligt syntax. Om vi vill hÀmta en strÀng mÄste vi anropa dictGet. För det andra, brist pÄ vissa optimeringar. Att jÀmföra med en konstant strÀng för ordlistor gÄr inte lika snabbt.

Det kan ocksÄ bli problem med uppdateringar. Anta att vi begÀrde en strÀng i cache-ordlistan, men den kom inte in i cachen. DÄ mÄste vi vÀnta pÄ att data laddas frÄn den externa kÀllan.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

En allmĂ€n nackdel med bĂ„da metoderna Ă€r att vi lagrar alla nycklar pĂ„ ett stĂ€lle och synkroniserar dem. SĂ„ varför inte lagra ordlistor lokalt? Ingen synkronisering — inga problem. Vi kan lagra ordlistan lokalt pĂ„ en del av disken. Det vill sĂ€ga vi utförde en Insert, registrerade ordlistan. Om vi arbetar med data i minnet kan vi skriva ordlistan antingen i en datablok, en kolonnkli eller i nĂ„gon sorts cache för att snabba upp berĂ€kningarna.

Ordlistkodning av strÀngar

SĂ„ kom vi till skapandet av en ny datatyp i ClickHouse — LowCardinality. Det Ă€r ett format för datalagring: hur de skrivs till disken och hur de lĂ€ses, hur de representeras i minnet och schemat för deras bearbetning.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

PÄ sliden finns det tvÄ kolumner. Till höger lagras rader standardmÀssigt som typ String. Det Àr uppenbart att detta Àr modeller av mobiltelefoner. Till vÀnster finns en likadan kolumn, men av typen LowCardinality. Den bestÄr av en ordbok med mÄnga olika strÀngar (strÀngarna frÄn kolumnen till höger) och en lista med positioner (radnummer).

Med hjĂ€lp av dessa tvĂ„ strukturer kan man Ă„terskapa den ursprungliga kolumnen. Det finns ocksĂ„ en invers index — en hash-tabell som hjĂ€lper till att hitta positionen i ordboken baserat pĂ„ strĂ€ngen. Detta behövs för att snabba upp vissa förfrĂ„gningar. Till exempel, om vi vill jĂ€mföra, söka efter en strĂ€ng i vĂ„r kolumn eller slĂ„ samman dem.

LowCardinality Àr en parametertyp. Den kan vara antingen ett tal, eller nÄgot som lagras som ett tal, eller en strÀng, eller Nullable av dem.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

SĂ€rskilt med LowCardinality Ă€r att den kan sparas för vissa funktioner. PĂ„ sliden ses ett exempel pĂ„ en förfrĂ„gan. I den första raden skapade jag en kolumn av typen LowCardinality frĂ„n String, som jag kallade S. DĂ€refter frĂ„gade jag dess namn — ClickHouse sa att det Ă€r LowCardinality av String. Helt korrekt.

Den tredje raden Àr nÀstan likadan, för vi anropade funktionen length. I ClickHouse returnerar funktionen length datatypen UInt64. Men vÄr kolumn blev LowCardinality av UInt64. Vad Àr meningen med det?

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

I ordboken lĂ„g namnen pĂ„ mobiltelefonerna, och vi tillĂ€mpade funktionen length. Nu har vi en motsvarande ordbok bestĂ„ende enbart av siffror — detta Ă€r lĂ€ngderna pĂ„ strĂ€ngarna. Kolumnen med positioner har inte Ă€ndrats. Resultatet Ă€r att vi har bearbetat mindre data, vilket sparade tid för förfrĂ„gan.

Det kan finnas andra optimeringar, till exempel att lÀgga till en enkel cache. Vid berÀkning av vÀrdet för funktionen kan man komma ihÄg det och skapa ett likadant utan att berÀkna det pÄ nytt.

Det kan ocksĂ„ göras en optimering av GROUP BY, eftersom vĂ„r kolumn med ordboken redan delvis Ă€r aggregerad — man kan snabbare berĂ€kna vĂ€rden för hash-funktioner och ungefĂ€rligt hitta den hink dĂ€r man ska placera den nĂ€sta raden. Dessutom kan man specialisera vissa aggregeringsfunktioner, till exempel uniq, eftersom man bara kan skicka ordboken dit och lĂ€mna positionerna oförĂ€ndrade — sĂ„ kommer allt att fungera snabbare. De första tvĂ„ optimeringarna har vi redan lagt till i ClickHouse.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Vad hÀnder om vi skapar en kolumn med vÄr datatyp och sÀtter in mÄnga dÄliga olika rader? Kommer vÄrt minne att överfullas? Nej, för detta finns det tvÄ specialinstÀllningar i ClickHouse. Den första Àr low_cardinality_max_dictionary_size. Detta Àr den maximala storleken pÄ en ordbok som kan skrivas till disken. InsÀttningen sker pÄ följande sÀtt: nÀr vi sÀtter in data fÄr vi en ström av rader, av dem bildar vi en stor gemensam ordbok. Om ordboken blir större Àn vÀrdet pÄ instÀllningen skriver vi den aktuella ordboken till disken och de ÄterstÄende raderna hamnar nÄgonstans "vid sidan om", nÀra indexen. I slutÀndan kommer vi aldrig att omberÀkna den stora ordboken och fÄ problem med minnet.

Den andra instÀllningen kallas low_cardinality_use_single_dictionary_for_part. FörestÀll dig att i det förra schemat, nÀr vi satte in data, överfullades vÄr ordbok och vi skrev den till disken. FrÄgan uppstÄr, varför skulle vi inte nu bilda en till exakt samma ordbok?

NÀr den överfullnas, skriver vi den Äter till disken och börjar bilda den tredje. Denna instÀllning inaktiverar just den möjligheten som standard.

Faktum Àr att flera ordböcker kan vara anvÀndbara om vi vill sÀtta in en mÀngd rader, men av misstag har satt in "skrÀp". LÄt oss sÀga att vi först satte in dÄliga rader, och sedan satte in bra. DÄ kommer ordboken att delas upp i mÄnga smÄ ordböcker. Vissa av dem kommer att ha "skrÀp", men de senaste kommer att vara med bra rader. Och om vi lÀser, sÀg, bara den senaste granuleringen, sÄ kommer allt fortfarande att fungera snabbt.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Innan vi pratar om fördelarna med LowCardinality, vill jag snabbt nÀmna att vi troligen inte kommer att uppnÄ en minskning av data pÄ disken (Àven om det kan hÀnda) eftersom ClickHouse komprimerar data. Det finns en standardversion - LZ4. Det gÄr ocksÄ att göra komprimering med ZSTD. Men bÄda algoritmerna implementerar redan ordboks komprimering, sÄ vÄr externa ordbok ClickHouse kommer inte vara sÄ mycket till hjÀlp.

För att inte bara prata tomt har jag tagit nĂ„gra data frĂ„n mĂ€tningen – String, LowCardinality(String) och Enum – och sparat dem i olika datatyper. Det blev tre kolumner med en miljard rader. I den första kolumnen, CodePage, finns det totalt 62 vĂ€rden. Och det syns att vi har komprimerat dem bĂ€ttre i LowCardinality(String). String Ă€r nĂ„got sĂ€mre, men det beror troligen pĂ„ att strĂ€ngarna Ă€r korta; vi lagrar deras lĂ€ngd, och de tar mycket plats och komprimeras dĂ„ligt.

Om vi tar PhoneModel, finns det 48 000 – redan fler, och skillnaderna mellan String och LowCardinality(String) Ă€r nĂ€stan obefintliga. För URL har vi ocksĂ„ sparat bara 2 GB – jag tror inte att man ska lita för mycket pĂ„ det.

Bedömning av hastigheten

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex
LÀnk frÄn bilden

Nu ska vi bedöma hastigheten. För att göra det anvĂ€nde jag ett dataset som beskriver taxiresor i New York. Det Ă€r tillgĂ€nglig finns pĂ„ GitHub. Det handlar om lite mer Ă€n en miljard resor. Det visar plats, tid för start och slut, betalningsmetod, antal passagerare och till och med typ av taxi – grön, gul och Uber.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Den första frĂ„gan jag stĂ€llde var ganska enkel – jag frĂ„gade var man oftast bestĂ€ller taxi. För det behövs att vi tar platsen dĂ€r taxin bestĂ€lldes, gör en GROUP BY pĂ„ den och rĂ€knar antalet med funktionen count. HĂ€r Ă€r vad ClickHouse ger.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

För att mĂ€ta hastigheten pĂ„ frĂ„ga skapade jag tre tabeller med samma data, men anvĂ€nde tre olika datatyper för vĂ„r startplats – String, LowCardinality och Enum. LowCardinality och Enum visade sig vara fem gĂ„nger snabbare Ă€n String. Enum Ă€r snabbare eftersom den arbetar med siffror. LowCardinality Ă€r snabbare pĂ„ grund av att det finns en optimering för GROUP BY.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

LĂ„t oss komplicera frĂ„gan ytterligare – vi frĂ„gar var den mest populĂ€ra parken i New York ligger. Återigen kommer vi att mĂ€ta detta pĂ„ grundval av var taxin oftast bestĂ€lls, men filtrerar endast pĂ„ de platser dĂ€r det finns ordet "park". Vi lĂ€gger ocksĂ„ till funktionen like.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

Vi tittar pĂ„ tiden – vi ser att Enum plötsligt började slĂ€pa efter. Det verkar till och med gĂ„ lĂ„ngsammare Ă€n den vanliga datatypen String. Detta hĂ€nder eftersom funktionen like inte Ă€r optimerad för Enum. Vi mĂ„ste omvandla vĂ„ra strĂ€ngar frĂ„n Enum till vanliga strĂ€ngar – vi gör mer arbete. LowCardinality(String) Ă€r inte heller optimerad som standard, men dĂ€r fungerar like pĂ„ ordboken, sĂ„ dĂ€rför Ă€r frĂ„gan snabbare i jĂ€mförelse med String.

NĂ€r man arbetar med Enum finns det ett mer globalt problem. Om vi vill optimera det mĂ„ste vi göra det pĂ„ varje stĂ€lle i koden. LĂ„t oss sĂ€ga att vi har skrivit en ny funktion – vi mĂ„ste absolut tĂ€nka ut en optimering för Enum. Men i LowCardinality Ă€r allt optimerat som standard.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

LÄt oss titta pÄ den senaste frÄgan, som Àr mer konstlad. Vi rÀknar helt enkelt ut hash-funktionen av vÄr plats. Hash-funktionen Àr en ganska lÄngsam förfrÄgan, den tar lÄng tid att berÀkna, dÀrför kommer allt att sakta ner tre gÄnger.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

LowCardinality fungerar fortfarande snabbare, Ă€ven om det inte finns nĂ„gon filtrering hĂ€r. Detta beror pĂ„ att vĂ„ra funktioner arbetar endast med ordboken. HashberĂ€kningsfunktionen har en parameter – den kan behandla mindre data och kan ocksĂ„ returnera LowCardinality.

Optimering av strÀngar i ClickHouse. Presentation frÄn Yandex

VÄr globala plan Àr att uppnÄ hastighet som inte Àr lÀgre Àn för String i alla fall, och att behÄlla hastighetsökningarna. Och kanske kommer vi nÄgon gÄng att ersÀtta String med LowCardinality, du uppdaterar ClickHouse, och allt kommer att fungera lite snabbare.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster