ClickHouse

Compressione dati

ClickHouse e' un Columnar Database SQL Open Source con ottime prestazioni sulle attivita' OLAP (On-Line Analytical Processing).

In questa paginetta ne vediamo, dal punto di vista pratico, le funzionalita' di compressione dei dati.
Gia' con la configurazione di default ClickHouse comprime tutti i dati con buoni risparmi ma e' anche possibile utilizzare algoritmi specifici per ottenere risultati ottimali.

Gli argomenti contenuti in questa pagina sono: Introduzione, Compressione, Test compressione, Test prestazioni, Conclusioni, ...
Il documento si riferisce alla versione 19.11 o successive di ClickHouse [NdA perche' da tale versione stable dal 2019-07 sono stati introdotti nuovi Codec particolarmente efficaci].

Introduzione

ClickHouse Datatypes ClickHouse e' il recente database colonnare sviluppato da Yandex adatto alle attivita' OLAP. Con il termine OLAP (On-Line Analytical Processing) si indicano tecniche ed architetture adatte ad analizzare grandi basi dati in tempi molto brevi.
ClickHouse utilizza un approccio differente rispetto alla rappresentazione ISAM e con indici B-Tree tipica dei DB relazionali memorizzando i dati per colonna. Questo consente di utilizzare algoritmi per l'accesso ai dati che possono essere eseguiti in parallelo. Con ClickHouse si utilizzano tutte le CPU sul nodo ospite, ma e' possibile farlo anche in rete con piu' nodi. Le query di ClickHouse in cluster scalano in modo pressoche' lineare come prestazioni.

I datatype disponibili in ClickHouse sono stati scelti per ottenere le massime prestazioni. Piu' i dati sono compatti e migliori sono le prestazioni: per un corretto disegno della base dati va scelto il tipo di dato piu' adatto e di minori dimensioni.

Per i valori numerici i datatype disponibili in ClickHouse sono: UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, Float32, Float64, Decimal e Boolean.
Per le stringhe si usa praticamente un solo datatype: String. ClickHouse non gestisce l'encoding quindi le stringhe sono semplicemente una serie di byte [NdA ma puo' essere specificato il COLLATE nell'ORDER BY]. In qualche caso limitato puo' essere utile anche il FixedString(N).
Per le date: Date, DateTime, DateTime64 [NdA quest'ultimo dalla versione 20.1]. ClickHouse non gestisce la timezone e tutti i tempi fanno riferimento ad Epoch in UTC.
Molto efficiente e' il datatype Enum che pero' richiede definire nella DDL tutti i valori possibili.
I NULL sono gestiti come un datatype aggiuntivo: Nullable(TypeName); tuttavia e' consigliabile evitarli nel disegno. Sono disponibili anche strutture complesse come Array(T) che invece sono molto efficienti.

Poiche' i dati sono raggruppati per colonna e quindi molto omogenei, ClickHouse e' in grado di comprimere efficacemente anche i dati numerici.
La compressione dati e' molto utile anche per minimizzare lo spazio occupato: una base dati ClickHouse e' tipicamente molto piu' piccola rispetto alla quantita' di dati memorizzati. Sugli altri database relazionali lo spazio occupato e' invece molto piu' ampio sia per la memorizzazione degli indici che per la preallocazione degli spazi.

L'organizzazione dei dati per colonna consente di effettuare un'elevata compressione ed ottenere significativi miglioramenti prestazionali: e' l'argomento di questa paginetta.

Compressione dati

La compressione di default automatica (LZ4) ottiene gia' ottimi risultati nella maggioranza delle situazioni. Sono normali compressioni oltre il 50% per i valori numerici ed oltre l'80% per le stringhe. Ma si puo' fare spesso anche molto di piu' [NdA vedremo degli esempi con compressioni superiori al 99%]

ClickHouse mantiene sul data dictionary system tutte le informazioni sulla compressione. La verifica della quantita' di spazio utilizzato/risparmiato si effettua con la seguente query:

SELECT database, table, column, type,
       column_data_compressed_bytes compressed,
       column_data_uncompressed_bytes,
       (column_data_uncompressed_bytes-column_data_compressed_bytes)*100/column_data_uncompressed_bytes ratio
  FROM system.parts_columns
 WHERE active
   AND database<>'system'
   AND column_data_uncompressed_bytes>0
 ORDER BY database, table, column;

Sulla famiglia di Engine MergeTree e' possibile migliorare ulteriormente la compressione utilizzando Codec ed algoritmi specifici scegliendoli tra quelli descritti nella documentazione ufficiale. Un Codec e' una rappresentazione dei dati: per esempio e' possibile rappresentare un dato come differenza rispetto al dato precedente oppure utilizzando un dizionario. In altri casi si utilizza un algoritmo di compressione che utilizza tecniche statistiche per rappresentare con meno bit le sequenze di dati piu' frequenti e puo' essere loseless (con perdita di dati) o meno.

E' possibile applicare in cascata in pipeline piu' Codecs, ovvero piu' codifiche dei dati e quindi un algoritmo di compressione.

La scelta si effettua per singola colonna con la sintassi:

CREATE TABLE codec_example
(
    dt Date CODEC(ZSTD),
    ts DateTime Codec(DoubleDelta, LZ4),
    float_value Float32 CODEC(NONE),
    double_value Float64 CODEC(LZ4HC(9))
    value Float32 CODEC(Delta, ZSTD)
)
...

I codecs specializzati, ovvero che sono adatti a particolari serie di valori sono:

Ai primi codecs generalmente si fa seguire una successiva compressione scegliendo tra:

Qual'e' il migliore? Dipende... proviamo!

Test compressione

In realta' la compressione reale dipende dai dati presenti. Ogni caso e' diverso, la compressione puo' dipendere a seconda della provenienza dei dati dalle frequenze di campionamento, dalla qualita' dei sensori, dalla provenienza dei dati, dai datatype scelti, da eventuali errori, ...
Il modo migliore e' quello di provare con i dati effettivi e cosi' abbiamo fatto. Abbiamo raccolto i dati di interesse (il traffico di rete), abbiamo selezionato un campione significativo (abbiamo scelto 10.000.000 di record, non molti ma sufficienti per il nostro scopo) e li abbiamo inseriti su una tabella di test con differenti combinazioni di codec/compressione. Lo stesso procedimento l'abbiamo eseguito su una tabella di dimensioni inferiori che presentava tipologie di dati diversi (Float). Le tabelle sono state create con piu' colonne con questa DDL:

 machine_id_l String Codec(LZ4),
 machine_id_l1 String Codec(LZ4HC(1)),
 machine_id_l9 String Codec(LZ4HC(9)),
 machine_id_l12 String Codec(LZ4HC(12)),
 machine_id_z1 String Codec(ZSTD(1)),
 machine_id_z22 String Codec(ZSTD(22)),
 machine_id_lcl1 LowCardinality(String) Codec(LZ4HC(1)),
 machine_id_lcz LowCardinality(String) Codec(ZSTD(1)),

Vediamo i primi dati:

Database Table Column Type Compressed Uncompressed Gain % On Default% _ Notes
test_compr traffic_4compress machine_id_lcz LowCardinality(String) 226042 17106509 98.68 77.72%

test_compr traffic_4compress machine_id_z22 String 276601 154822098 99.82 72.73%

test_compr traffic_4compress machine_id_z1 String 287243 154822098 99.81 71.68%

test_compr traffic_4compress machine_id_lcl1 LowCardinality(String) 413673 17106509 97.58 59.22%

test_compr traffic_4compress machine_id_lc LowCardinality(String) 440649 17106509 97.42 56.56%

test_compr traffic_4compress machine_id_l12 String 971983 154822098 99.37 4.18%

test_compr traffic_4compress machine_id_l9 String 972007 154822098 99.37 4.18%

test_compr traffic_4compress machine_id_l1 String 972109 154822098 99.37 4.17%

test_compr traffic_4compress machine_id String 1014372 154822098 99.34 0.00%
Default
test_compr traffic_4compress machine_id_l String 1014372 154822098 99.34 0.00%

test_compr traffic_4compress machine_id_n String 154848573 154822098 -0.02


test_compr traffic_4compress router_id_lc LowCardinality(String) 2839844 8680526 67.28 62.82%

test_compr traffic_4compress router_id String 7637100 160915073 95.25 0.00%
Default
test_compr traffic_4compress router_id_n String 160941548 160915073 -0.02


Il primo datatype che analizziamo sono le String.
Innanzi tutto si deve notare che il risparmio in termine di spazio e' notevolissimo, tutti gli algoritmi ottengono oltre il 95% di compressione.
Considerando solo la percentuale % Gain e' l'algoritmo ZSTD ad ottenere la maggiore compressione con poca differenza tra i livelli passati come parametro. Anche per LZ4 il livello di compressione non produce una variazione significativa di % Gain.
In realta' la specifica LowCardinality() consente un risparmio notevole gia' a monte ed in coppia le due indicazione forniscono il maggior risparmio di spazio.

Anche sulla seconda stringa, piu' lunga, il LowCardinality() fornisce il miglior guadagno finale.

Ora diamo in numeri!
Utilizzando questa DDL specifichiamo diverse modalita' di compressione per gli interi:

 bytes_out_n UInt64 Codec(NONE),
 bytes_out_dn UInt64 Codec(Delta),
 bytes_out_2n UInt64 Codec(DoubleDelta),
 bytes_out_gn UInt64 Codec(Gorilla),
 bytes_out_tn UInt64 Codec(T64),
 bytes_out_dl UInt64 Codec(Delta, LZ4),
 bytes_out_2l UInt64 Codec(DoubleDelta, LZ4),
 bytes_out_gl UInt64 Codec(Gorilla, LZ4),
 bytes_out_tl UInt64 Codec(T64, LZ4),
 bytes_out_dz UInt64 Codec(Delta, ZSTD),
 bytes_out_2z UInt64 Codec(DoubleDelta, ZSTD),
 bytes_out_gz UInt64 Codec(Gorilla, ZSTD),
 bytes_out_tz UInt64 Codec(T64, ZSTD),
Database Table Column Type Compressed Uncompressed Gain % On Default% _ Notes
test_compr traffic_4compress bytes_out_tz UInt64 23236434 69227400 66.43 39.52%

test_compr traffic_4compress bytes_out_tl UInt64 25112027 69227400 63.73 34.64%

test_compr traffic_4compress bytes_out_gz UInt64 27923104 69227400 59.66 27.33%

test_compr traffic_4compress bytes_out_2z UInt64 31337113 69227400 54.73 18.44%

test_compr traffic_4compress bytes_out_gl UInt64 31426615 69227400 54.6 18.21%

test_compr traffic_4compress bytes_out_dz UInt64 31559836 69227400 54.41 17.86%

test_compr traffic_4compress bytes_out_gn UInt64 32537582 69227400 53 15.32%

test_compr traffic_4compress bytes_out_tn UInt64 32982644 69227400 52.36 14.16%

test_compr traffic_4compress bytes_out_2n UInt64 34044794 69227400 50.82 11.39%

test_compr traffic_4compress bytes_out_2l UInt64 34191393 69227400 50.61 11.01%

test_compr traffic_4compress bytes_out UInt64 38422906 69227400 44.5 0.00%
Default
test_compr traffic_4compress bytes_out_dl UInt64 40941024 69227400 40.86 -6.55%

test_compr traffic_4compress bytes_out_n UInt64 69253750 69227400 -0.04


test_compr traffic_4compress bytes_out_dn UInt64 69255858 69227400 -0.04


Ora analizziamo tipi di dati interi che nel nostro caso sono UInt64.
Qui i risparmi ottenuti sono meno significativi perche' i i numeri sono piu' corti delle stringhe.
Anche in questo caso la compressione maggiore si ottiene con ZSTD in coppia al Codec T64, molto vicino e' anche la coppia Codec(T64, LZ4).

Vediamo altri esempi:

Database Table Column Type Compressed Uncompressed Gain % On Default% _ Notes
test_compr traffic_4compress timestamp_dz DateTime 7258100 34613700 79.03 53.36%

test_compr traffic_4compress timestamp_dl DateTime 10348290 34613700 70.1 33.51%

test_compr traffic_4compress timestamp_2z DateTime 15237638 34613700 55.98 2.09%

test_compr traffic_4compress timestamp DateTime 15562730 34613700 55.04 0.00%
Default
test_compr traffic_4compress timestamp_2l DateTime 17065061 34613700 50.7 -9.65%

test_compr traffic_4compress timestamp_tz DateTime 17088075 34613700 50.63 -9.80%

test_compr traffic_4compress timestamp_gz DateTime 17722348 34613700 48.8 -13.88%

test_compr traffic_4compress timestamp_tl DateTime 18604365 34613700 46.25 -19.54%

test_compr traffic_4compress timestamp_gl DateTime 19450979 34613700 43.81 -24.98%

test_compr traffic_4compress timestamp_2n DateTime 23194120 34613700 32.99 -49.04%

test_compr traffic_4compress timestamp_gn DateTime 24184616 34613700 30.13 -55.40%

test_compr traffic_4compress timestamp_tn DateTime 24554428 34613700 29.06 -57.78%

test_compr traffic_4compress timestamp_n DateTime 34626950 34613700 -0.04


test_compr traffic_4compress timestamp_dn DateTime 34628010 34613700 -0.04


Le date sono un importantissime in tutti i DB e sui TSDB in particolare.
I DateTime sono rappresentati come UInt32 in ClickHouse e corrispondono i secondi a partire da Epoch in UTC. Nonostante la dimensione ridotta si ottengono significativi risparmi con il codec Delta e poi comprimendo. La differenza rispetto ad altri metodi e' significativa quindi e' opportuno scegliere tra queste due alternative.

Non abbiamo finito:

Database Table Column Type Compressed Uncompressed Gain % On Default% _ Notes
test_compr tinfo_4compress distribution_gz Float32 958624 8000000 88.02 26.43%

test_compr tinfo_4compress distribution_gl Float32 1061414 8000000 86.73 18.54%

test_compr tinfo_4compress distribution_dl Float32 1166430 8000000 85.42 10.48%

test_compr tinfo_4compress distribution_2z Float32 1247976 8000000 84.4 4.22%

test_compr tinfo_4compress distribution Float32 1303014 8000000 83.71 0.00%
Default
test_compr tinfo_4compress distribution_gn Float32 1417393 8000000 82.28 -8.78%

test_compr tinfo_4compress distribution_2l Float32 1530166 8000000 80.87 -17.43%

test_compr tinfo_4compress distribution_2n Float32 2382970 8000000 70.21 -82.88%

test_compr tinfo_4compress distribution_n Float32 8003100 8000000 -0.04


test_compr tinfo_4compress distribution_dn Float32 8003348 8000000 -0.04












test_compr tinfo_4compress diffusion_dz Float32 286526 8000000 96.42 53.35%

test_compr tinfo_4compress diffusion_gz Float32 344838 8000000 95.69 43.86%

test_compr tinfo_4compress diffusion_gl Float32 471599 8000000 94.11 23.22%

test_compr tinfo_4compress diffusion_dl Float32 491386 8000000 93.86 20.00%

test_compr tinfo_4compress diffusion_2z Float32 505480 8000000 93.68 17.71%

test_compr tinfo_4compress diffusion Float32 614232 8000000 92.32 0.00%
Default
test_compr tinfo_4compress diffusion_2l Float32 663429 8000000 91.71 -8.01%

test_compr tinfo_4compress diffusion_gn Float32 865506 8000000 89.18 -40.91%

test_compr tinfo_4compress diffusion_2n Float32 1340085 8000000 83.25 -118.17%

test_compr tinfo_4compress diffusion_n Float32 8003100 8000000 -0.04


test_compr tinfo_4compress diffusion_dn Float32 8003348 8000000 -0.04


Gli ultimi esempi da analizzare sono relativi a Float32.
Con i Float non e' possibile utilizzare la codifica T64 e quindi non e' stato provato tale Codec. I risultati migliori sono ottenuti con una prima codifica con Gorilla o Delta e quindi la compressione ZSTD o LZ4.
Sicuramente ha inciso la scarsa distribuzione dei valori di input. Infatti ottenere oltre il 90% di compressione su un dato che utilizza 4 byte e' un risultato notevole.

Test Prestazioni

Lo spazio non e' tutto. C'e' anche il tempo! E come e' ben noto lo spazio ed il tempo sono strettamente legati :)

Ma prima e' importante capire come ClickHouse tratta i dati e quando effettua le codifiche.
In memoria i dati sono rappresentati senza codifiche, solo in fase di scrittura su disco vengono compressi. Con l'Engine MergeTree, che e' quello fondamentale, i nuovi dati vengono inseriti i Parts ordinate e solo successivamente viene eseguito il merge con i dati gia' presenti.

Piu' importante e' la fase di lettura perche' su un OLAP la scrittura avviene una sola volta ma le letture sono molte. Da questo punto di vista una tabella o, per essere piu' precisi, la Part della colonna di interesse puo' essere in stato Cold, Warm o Hot.
Quando il dato risiede solo su disco i tempi per accedere sono maggiori e dipendono dalle meccaniche usate (in senso lato: un SSD non ha parti meccaniche): questo e' lo stato Cold. Minore e' la dimensione dei dato minore sara' il tempo per leggerlo da disco.
Nello stato Warm i dati sono gia' in memoria nella buffer cache del sistema, sono ancora compressi ma non e' piu' necessario accedere al disco.
Una part acceduta di recente si trova in stato Hot: e' stata letta da disco, ha attraversato la buffer cache ed e' stata decompressa in memoria. E' nel passaggio da Warm a Hot che avviene la decodifica e si ottengono i dati in chiaro. Quando i dati sono in memoria in chiaro le SELECT per l'analisi utilizzano piu' thread in parallelo con algoritmi vettorizzati.

Dopo questa ampia premessa dovrebbe essere chiaro che non e' semplice misurare le reali differenze prestazionali tra gli algoritmi... ma lo facciamo lo stesso!
ClickHouse compression performances with different Codecs Per misurare la differenza di prestazioni abbiamo utilizzato la stessa INSERT su tabelle con Codecs differenti:

insert into test_compr.traffic_4compress
select byte_in, byte_out
  from sensor_data.traffic
 limit 10000000;

I risultati ottenuti sono riassunti in questa tabella:

Codec Time Delta Note
Delta 0.462 57.92%

NONE 0.482 56.10%

T64 0.692 36.98%

LZ4 0.992 9.65%
Default
T64, LZ4 0.999 9.02%

Gorilla, LZ4 1.098 0.00%

DoubleDelta 1.102 -0.36%

Gorilla 1.184 -7.83%

DoubleDelta, LZ4 1.211 -10.29%

Delta, LZ4 1.307 -19.03%

Gorilla, ZSDT 1.344 -22.40%

DoubleDelta, ZSTD 1.402 -27.69%

T64, ZSDT 1.746 -59.02%

ZSTD 2.101 -91.35%

Delta, ZSTD 2.307 -110.11%

Rispetto alla compressione LZ4 di default, non fare nulla e' piu' veloce! E' normale, ma e' ugualmente veloce il Codec Delta e solo un poco piu' pesante il T64.
Tutti le combinazioni che utilizzando l'algoritmo ZSTD sono piu' pesanti in modo significativo, fino al doppio piu' lenti.

Molte combinazioni di codec risultano avere un peso simile tra loro ed in generale la differenza di tempi non e' drammatica. In pratica la cosa piu' veloce e' non comprimere affatto ma la compressione piu' complessa e' piu' lenta solo di 4 volte.

Conclusioni

Quanto riportato ovviamente e' solo un riassunto e va raccolto con intelligenza poiche' le combinazioni possibili di tutti i codec, provando i diversi livelli di compressione, sono veramente moltissime.
La compressione dipende fortemente dai dati effettivamente presenti. Quindi le indicazioni sull'algoritmo migliore non solo assolute ma da valutare caso per caso.
Un altro punto da sottolineare e' che le modalita' di default sono sicuramente le piu' testate. Nella scelta dell'algoritmo da utilizzare il solo risparmio di spazio non e' l'unico fattore da tenere in considerazione.
Tenendo conto di queste premesse ci sono comunque una serie di indicazioni che e' possibile fornire.

Se la quantita' di dati non e' elevata la compressione di default e' sufficiente. ClickHouse per default utilizza l'algoritmo LZ4 per tutti i dati con buoni risultati sia in lettura che in scrittura.
Per le stringhe con un numero limitato di valori e' assolutamente conveniente utilizzare LowCardinality(String). In pratica questa sola impostazione, associata alla compressione di default, e' sufficiente su basi dati di piccole dimensioni [NdA per piccole dimensioni con ClickHouse si puo' intendere fino ad un Terabyte].

Per colonne che sono utilizzate solo raramente e contengono dati storicizzati vanno utilizzate le combinazioni di algoritmi che consentono il maggior risparmio di spazio. Da questo punto di vista, anche se piu' pesante come algoritmo, generalmente ZSTD e' quello che ottiene la maggiore compressione. Ad esempio Codec(Delta, ZSTD) per le date, ZSTD per gli interi e ZSTD per i Float.

Per colonne di dimensioni significative come dimensioni totali e' consigliabile sperimentare il reale vantaggio della compressione con i dati effettivi: a seconda dei casi possono essere adatte combinazioni differenti di Codec. Quindi utilizzare algoritmi che presentano un buon compromesso tra prestazioni ed efficienza. Ad esempio Codec(T64, LZ4) e Codec(Gorilla, LZ4).
Nel caso di colonne che variano di poco i valori per i timestamp e' molto efficiente il Codec(DoubleDelta) mentre per i float e' efficiente Codec(Gorilla).

Varie ed eventuali

L'articolo New ClickHouse Encodings contiene una trattazione molto completa e precisa dei Codecs e dei test effettuabili.

Il documento Introduzione a ClickHouse contiene una presentazione sulle funzioni di base, il documento Architettura di ClickHouse contiene una presentazione dell'architettura, DBA scripts contiene alcuni dei comandi piu' utili per il DBA.

In ClickHouse Cloud l'algoritmo di compressione di default e' ZSTD anziche' LZ4.


Titolo: Compressione in ClickHouse
Livello: Medio (2/5)
Data: 31 Ottobre 2019 🎃 Halloween
Versione: 1.0.3 - 14 Febbraio 2021 ❤️ San Valentino
© Autore: mail [AT] meo.bogliolo.name