Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Il database analitico ClickHouse elabora molte diverse stringhe, consumando risorse. Per accelerare le prestazioni del sistema, vengono costantemente aggiunte nuove ottimizzazioni. Lo sviluppatore di ClickHouse, Nikolai Kočetov, parla del tipo di dato stringa, incluso un nuovo tipo, LowCardinality, e spiega come migliorare l'efficienza nella gestione delle stringhe.

Riproduci video

— Prima di tutto, capiremo come possiamo memorizzare le stringhe.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Abbiamo i tipi di dati stringa. String è una scelta predefinita molto adatta e dovrebbe essere utilizzata quasi sempre. Ha un piccolo Overhead — 9 byte per ogni stringa. Se vogliamo che la dimensione delle stringhe sia fissa e nota in anticipo, è meglio utilizzare FixedString. In questo modo possiamo specificare il numero desiderato di byte, rendendolo comodo per dati come indirizzi IP o funzioni hash.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Certo, a volte ci sono dei rallentamenti. Supponiamo che tu stia effettuando una richiesta su una tabella. ClickHouse legge una quantità piuttosto elevata di dati, diciamo a una velocità di 100 GB/s, mentre il numero di stringhe elaborate è ridotto. Abbiamo due tabelle che memorizzano dati quasi identici. ClickHouse legge i dati dalla seconda tabella a una velocità maggiore, ma il numero di stringhe lette al secondo è tre volte inferiore.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Se guardiamo la dimensione dei dati compressi, essa risulta praticamente equivalente. In realtà, nelle tabelle sono registrati gli stessi dati: il primo miliardo di numeri — solo che nella prima colonna sono scritti come UInt64, mentre nella seconda come String. A causa di questo, la seconda query impiega più tempo a leggere i dati dal disco e a decomprimerli.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Ecco un altro esempio. Supponiamo che ci sia un insieme di stringhe noto in anticipo, limitato a una costante di 1000 o 10.000 e che cambi praticamente mai. In questo caso, il tipo di dato Enum è adatto; in ClickHouse ce ne sono due: Enum8 ed Enum16. Grazie allo storage in Enum, siamo in grado di elaborare rapidamente le query.

In ClickHouse ci sono ottimizzazioni per GROUP BY, IN, DISTINCT e per alcune funzioni, ad esempio per il confronto con una stringa costante. Certamente, i numeri nelle stringhe non vengono convertiti; al contrario, la stringa costante viene tradotta nel valore Enum. Dopo di che, tutto viene confrontato rapidamente.

Ma ci sono anche degli svantaggi. Anche se conosciamo esattamente l'insieme di stringhe, a volte deve essere ampliato. Se arriva una nuova stringa, dobbiamo eseguire un ALTER.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

ALTER per Enum in ClickHouse è implementato in modo ottimale. Non riscriviamo i dati su disco, ma gli ALTER potrebbero rallentare a causa del fatto che le strutture Enum sono memorizzate nello schema della tabella stessa. Pertanto, dobbiamo attendere le query di lettura dalla tabella, per esempio.

Sorge il quesito: si può fare meglio? Probabilmente sì. Si potrebbe mantenere la struttura Enum non nello schema della tabella, ma in ZooKeeper. Tuttavia, potrebbero sorgere problemi di sincronizzazione. Ad esempio, una replica ha ricevuto i dati, l'altra no, e se ha un Enum obsoleto, qualcosa potrebbe rompersi. (In ClickHouse abbiamo quasi completato le query ALTER non bloccanti. Una volta terminate completamente, non sarà necessario attendere le query di lettura.)

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Per evitare di occuparsi di ALTER Enum, si possono utilizzare dizionari esterni ClickHouse. Ricordo che si tratta di una struttura di dati key-value all'interno di ClickHouse, tramite la quale è possibile ottenere dati da fonti esterne, ad esempio da tabelle MySQL.

Nel dizionario ClickHouse memorizziamo molte diverse stringhe, mentre nella tabella sono memorizzati i loro identificatori come numeri. Se abbiamo bisogno di ottenere una stringa, chiamiamo la funzione dictGet e lavoriamo con essa. Dopo di ciò, non dovremmo effettuare ALTER. Per aggiungere qualcosa a Enum, lo inseriamo nella stessa tabella MySQL.

Tuttavia, sorgono altri problemi. Prima di tutto, la sintassi scomoda. Se vogliamo ottenere una stringa, dobbiamo chiamare dictGet. In secondo luogo, manca alcune ottimizzazioni. Non è altrettanto veloce effettuare il confronto con una stringa costante per i dizionari.

Possono esserci anche problemi con l'aggiornamento. Supponiamo di aver richiesto una stringa nella cache-dizionario e che non sia finita nella cache. Dobbiamo quindi aspettare che i dati vengano caricati da una fonte esterna.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Un difetto comune di entrambi i metodi è che memorizziamo tutte le chiavi in un unico posto e le sincronizziamo. Perché non memorizzare i dizionari localmente? Niente sincronizzazione, niente problemi. Possiamo memorizzare il dizionario localmente in un pezzo su disco. Quindi abbiamo effettuato un Insert, registrando il dizionario. Se lavoriamo con dati in memoria, possiamo registrare il dizionario o in un blocco di dati, o in un pezzo di colonna, o in qualche cache, per accelerare i calcoli.

Codifica lessicale delle stringhe

Così siamo giunti alla creazione di un nuovo tipo di dato in ClickHouse: LowCardinality. Questo è un formato di archiviazione dei dati: come vengono scritti su disco e come vengono letti, come sono rappresentati in memoria e lo schema della loro elaborazione.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Nella slide ci sono due colonne. A destra, le stringhe sono memorizzate in modo standard, nel tipo String. Si può vedere che si tratta di modelli di telefoni cellulari. A sinistra c'è esattamente la stessa colonna, ma nel tipo LowCardinality. Essa consiste in un dizionario con molte stringhe diverse (stringhe dalla colonna a destra) e un elenco di posizioni (numeri di riga).

Con queste due strutture è possibile ricostruire la colonna originale. Esiste anche un indice inverso, una tabella hash, che aiuta a trovare la posizione nel dizionario in base alla stringa. È necessaria per accelerare alcune query. Ad esempio, se vogliamo confrontare o cercare una stringa nella nostra colonna o unirle tra loro.

LowCardinality è un tipo di dato parametrico. Può essere un numero, oppure qualcosa memorizzato come numero, oppure una stringa, oppure Nullable su di essi.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

La caratteristica di LowCardinality è che può essere mantenuta per alcune funzioni. Nella diapositiva è visibile un esempio di query. Nella prima riga ho creato una colonna di tipo LowCardinality da String, chiamandola S. Poi ho chiesto il suo nome: ClickHouse ha risposto che si tratta di LowCardinality di String. Tutto corretto.

La terza riga è quasi identica, solo che abbiamo chiamato la funzione length. In ClickHouse, la funzione length restituisce il tipo di dato UInt64. Ma ora è diventato LowCardinality di UInt64. Qual è il significato?

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Nel dizionario erano memorizzati i nomi dei telefoni cellulari, abbiamo applicato la funzione length. Ora abbiamo un dizionario equivalente, composto solo da numeri: sono le lunghezze delle stringhe. La colonna con le posizioni non è cambiata. In definitiva, abbiamo elaborato meno dati, risparmiando tempo nella query.

Possono esserci anche altre ottimizzazioni, ad esempio l'aggiunta di una semplice cache. Quando si calcola il valore di una funzione, è possibile memorizzarne uno e crearne uno identico, senza doverlo ricalcolare.

Può anche essere ottimizzata la GROUP BY, poiché la nostra colonna con il dizionario è già parzialmente aggregata: è possibile calcolare più rapidamente il valore delle funzioni hash e trovare approssimativamente il bucket in cui collocare la nuova riga. Inoltre, è possibile specializzare alcune funzioni aggregate, come uniq, poiché in essa può essere inviato solo il dizionario, lasciando intatte le posizioni: in questo modo tutto funzionerà più velocemente. Le prime due ottimizzazioni sono già state aggiunte in ClickHouse.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

E se creassimo una colonna con il nostro tipo di dati e inserissimo al suo interno molte righe diverse e problematiche? La nostra memoria si riempirà? No, per questo ClickHouse ha due impostazioni speciali. La prima è low_cardinality_max_dictionary_size. Questo è il massimo dimensione del dizionario che può essere scritto su disco. L'inserimento avviene nel seguente modo: quando inseriamo dati, riceviamo un flusso di righe, da cui formiamo un grande dizionario comune. Se il dizionario diventa più grande del valore della configurazione, scriviamo l'attuale dizionario su disco, mentre le altre righe le mettiamo da qualche parte "a lato", vicino agli indici. Di conseguenza, non ricomputeremo mai un grande dizionario e non avremo problemi di memoria.

La seconda impostazione si chiama low_cardinality_use_single_dictionary_for_part. Immagina che, nello schema precedente, quando abbiamo inserito i dati, il nostro dizionario fosse diventato pieno e lo abbiamo scritto su disco. Si pone quindi la domanda: perché non generare un altro dizionario identico?

Quando si riempirà, lo scriveremo di nuovo su disco e cominceremo a generare il terzo. Questa impostazione disabilita proprio questa possibilità per impostazione predefinita.

In realtà, avere molti dizionari può essere utile se vogliamo inserire un certo insieme di righe, ma abbiamo accidentalmente inserito "spazzatura". Diciamo, prima abbiamo inserito righe sbagliate e poi quelle corrette. In questo caso, il dizionario si dividerà in tanti piccoli dizionari. Alcuni di essi conterranno "spazzatura", ma gli ultimi avranno righe corrette. E se leggiamo, per esempio, solo l'ultimo granulo, tutto funzionerà comunque rapidamente.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Prima di parlare dei vantaggi di LowCardinality, voglio dire subito che è improbabile ottenere una riduzione dei dati su disco (anche se potrebbe accadere), perché ClickHouse comprime i dati. C'è un'opzione predefinita: LZ4. È possibile anche utilizzare la compressione tramite ZSTD. Ma entrambi gli algoritmi implementano già la compressione per dizionario, quindi il nostro dizionario esterno ClickHouse non ci sarà di grande aiuto.

Per non essere poco credibile, ho preso alcuni dati dalla metrica — String, LowCardinality(String) ed Enum — e li ho salvati in diversi tipi di dati. Sono risultati tre colonne, con un miliardo di righe registrate. Nella prima colonna, CodePage, ci sono solo 62 valori. E si vede che in LowCardinality(String) li abbiamo compressi meglio. String è un po' peggiore, ma probabilmente è dovuto al fatto che le stringhe sono corte; teniamo traccia delle loro lunghezze e occupano molto spazio, quindi si comprimono male.

Se prendiamo PhoneModel, sono 48.000 — già di più, e le differenze tra String e LowCardinality(String) sono quasi nulle. Anche per l'URL abbiamo risparmiato solo 2 GB — penso che non valga la pena contare su questo.

Valutazione della velocità operativa

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex
Link dalla diapositiva

Ora valutiamo la velocità operativa. Per valutarla, ho usato un dataset con la descrizione dei viaggi in taxi a New York. Esso è disponibile su GitHub. Ci sono poco più di un miliardo di corse. Sono rappresentati la posizione, l'orario di inizio e di fine corsa, il metodo di pagamento, il numero di passeggeri e persino il tipo di taxi — verde, giallo e Uber.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Ho iniziato con una richiesta piuttosto semplice — ho chiesto dove vengono ordinati più frequentemente i taxi. Per fare questo, bisogna prendere la posizione da cui sono stati ordinati, fare un GROUP BY su di essa e contare il risultato. ClickHouse restituisce qualcosa.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Per misurare la velocità di elaborazione della richiesta, ho creato tre tabelle con gli stessi dati, ma ho usato tre diversi tipi di dati per la nostra posizione di partenza — String, LowCardinality ed Enum. LowCardinality ed Enum si sono rivelati cinque volte più veloci rispetto a String. Enum è più veloce perché lavora con numeri. LowCardinality — perché è implementata un'ottimizzazione per il GROUP BY.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Complichiamo ulteriormente la richiesta — chiediamo dove si trova il parco più popolare a New York. Anche in questo caso, misureremo dove vengono ordinati più frequentemente i taxi, ma filtreremo solo le posizioni che contengono la parola «parco». Aggiungeremo anche la funzione like.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Guardiamo il tempo e vediamo che Enum ha improvvisamente iniziato a rallentare. Inoltre, funziona ancora più lentamente rispetto al tipo di dati standard String. Questo accade perché la funzione like non è affatto ottimizzata per Enum. Dobbiamo convertire le nostre stringhe da Enum in normali stringhe, il che comporta un lavoro aggiuntivo. LowCardinality(String) non è ottimizzato per impostazione predefinita, ma in quel caso like funziona su un dizionario, quindi la query si accelera rispetto a String.

Quando si lavora con Enum, c'è un problema più globale. Se vogliamo ottimizzarlo, dobbiamo farlo in ogni punto del codice. Supponiamo di aver scritto una nuova funzione: è necessario pensare a un'ottimizzazione per Enum. Mentre in LowCardinality è tutto ottimizzato per impostazione predefinita.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Analizziamo l'ultima query, più artificiale. Contiamo semplicemente la funzione hash della nostra posizione. La funzione hash è una query piuttosto lenta, richiede tempo per essere calcolata, quindi tutto rallenterà di circa tre volte.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

LowCardinality continua a funzionare più velocemente, anche se qui non c'è filtraggio. Questo avviene perché le nostre funzioni operano solo sul dizionario. La funzione di calcolo dell'hash ha un argomento — può elaborare meno dati e può anche restituire LowCardinality.

Ottimizzazione delle righe in ClickHouse. Relazione di Yandex

Il nostro obiettivo globale è raggiungere una velocità di funzionamento pari o superiore a quella di String in tutti i casi, mantenendo i miglioramenti. E, forse, un giorno sostituiremo String con LowCardinality, aggiornerete ClickHouse e tutto funzionerà un po' più velocemente.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster