Escolhendo codecs do ClickHouse pelo perfil de cada coluna
Uma tabela de 100 bilhões de linhas a 4,37 bytes por linha. O codec de cada coluna, o número que ele produziu e o loop para repetir o teste nos seus dados.
- Publicado
- Medido
- , produção,
system.parts_columns - Stack
- ClickHouse 25.x, próprio, bare metal
- Também em
- English
A tabela
Esta é a tabela de auditoria de uma rede de mídia digital out-of-home: uma linha por exibição de anúncio, vinda de cerca de 15.000 telastamanho da frota na data da medição, arredondado em elevadores e shoppings. Cada linha é prova de faturamento, então nada é amostrado nem resumido. Os nomes abaixo são genéricos; o formato é o real.
CREATE TABLE playback_log
(
uid UUID CODEC(ZSTD(1)) TTL started_at + toIntervalMonth(1),
device_id UInt32 CODEC(T64, ZSTD(1)),
order_id UInt32 CODEC(ZSTD(1)),
asset_id UInt32 CODEC(ZSTD(1)),
started_at DateTime CODEC(DoubleDelta, ZSTD(1)),
ended_at DateTime CODEC(DoubleDelta, ZSTD(1)),
imported_at DateTime DEFAULT now('UTC') CODEC(DoubleDelta, ZSTD(1)),
INDEX idx_imported imported_at TYPE minmax GRANULARITY 16
)
ENGINE = ReplacingMergeTree
PARTITION BY toYYYYMM(started_at)
ORDER BY (started_at, order_id, device_id)
TTL started_at + toIntervalYear(1)
SETTINGS index_granularity = 8192;Três coisas para reparar antes de chegar aos codecs:
- A chave de ordenação começa pelo timestamp. Isso decide mais do que qualquer codec. Guarde para a próxima seção.
-
Dois TTLs. A linha vive um ano. A coluna
uidvive um mês, porque ela só existe para deduplicar, e depois de 30 dias não chega mais retransmissão de uma tela que ficou offline. Carregar um UUID em bilhões de linhas por um ano seria desperdício. -
ReplacingMergeTreeé a rede de proteção contra duplicatas, e ela só as colapsa durante os merges. Isso fica para outro artigo.
Quanto cada coluna comprime
No ClickHouse, CODEC(...) tem duas camadas. Primeiro uma codificação que muda a forma dos dados (Delta, DoubleDelta, Gorilla, T64 e alguns mais novos). Depois um compressor de uso geral que roda sobre o resultado (LZ4, ZSTD). A maioria das tabelas que eu vejo só define a segunda camada, ou nenhuma.
Isto é o que a tabela de produção reporta hoje, por coluna, a partir de system.parts_columns:
| Coluna | Tipo | Codec | Taxa | Bytes / linha |
|---|---|---|---|---|
| started_at | DateTime | DoubleDelta, ZSTD(1) | 269,6x | 0,01 |
| ended_at | DateTime | DoubleDelta, ZSTD(1) | 15,6x | 0,26 |
| order_id | UInt32 | ZSTD(1) | 10,5x | 0,38 |
| asset_id | UInt32 | ZSTD(1) | 6,1x | 0,65 |
| imported_at | DateTime | DoubleDelta, ZSTD(1) | 2,9x | 1,40 |
| device_id | UInt32 | T64, ZSTD(1) | 2,4x | 1,67 |
| tabela inteira | 5,5x | 4,37 | ||
Medido em 2026-09-03 sobre 100,78 bilhões de linhas, 410,33 GiB em disco. uid não aparece: o TTL da coluna a descarta depois de 30 dias, então a maioria das partes já não a carrega.
Para ter o mesmo relatório na sua tabela, esta consulta só lê metadados, então é segura num servidor ocupado:
SELECT
column,
type,
formatReadableSize(sum(column_data_uncompressed_bytes)) AS raw,
formatReadableSize(sum(column_data_compressed_bytes)) AS on_disk,
round(sum(column_data_uncompressed_bytes)
/ sum(column_data_compressed_bytes), 1) AS ratio,
round(sum(column_data_compressed_bytes) / sum(rows), 2) AS bytes_per_row
FROM system.parts_columns
WHERE database = currentDatabase()
AND table = 'playback_log'
AND active
GROUP BY column, type
ORDER BY ratio DESC;Mesmo codec, mesmo tipo, 17x de diferença
started_at e ended_at são os dois DateTime com DoubleDelta, ZSTD(1). Um comprime 269,6x375 GiB brutos para 1,39 GiB, cerca de um centésimo de byte por linha, o outro 15,6x. A única diferença é que started_at é a primeira coluna da chave de ordenação.
DoubleDelta guarda a diferença das diferenças entre valores consecutivos. Numa coluna ordenada, o primeiro delta já é pequeno, e o segundo é quase sempre zero. A coluna vira uma longa sequência de zeros, e uma sequência de zeros é a melhor entrada que o ZSTD pode receber.
ended_at é started_at mais alguns segundos de exibição. Os deltas são pequenos, mas a coluna não está ordenada, então o delta de segunda ordem guarda algum ruído: 15,6x. imported_at é quando o lote chegou ao servidor. As telas mandam em lotes e às vezes ficam horas offline, então os intervalos são irregulares: 2,9x, com o mesmo codec.
Ou seja, o maior número desta tabela é, na verdade, um resultado da chave de ordenação com nome de codec. Decida o ORDER BY pelas suas consultas primeiro. Os codecs ficam com o que a ordenação deixa.
T64 no id do aparelho
São cerca de 15.000 aparelhos guardados num UInt32, um tipo que comporta quatro bilhões. O T64 transpõe um bloco de 64 inteiros para que os bits da mesma posição fiquem juntos. Os bits altos são zero em todas as linhas e, depois da transposição, se compactam até quase nada.
Mesmo assim ela terminou como a pior coluna da tabela, 2,4x. Dentro de um grânulo (8.192 linhas ordenadas por tempo) o id do aparelho fica pulando, porque cada exibição vem de uma tela diferente. Ordenar primeiro pelo aparelho consertaria essa coluna e estragaria o timestamp. A chave de ordenação serve às consultas, que filtram por período e pedido, então o id do aparelho fica com seus 1,67 bytes.
Onde o ZSTD puro ganhou
order_id e asset_id não são monotônicos e a faixa deles não é curta o bastante para o T64 fazer diferença. Testei as codificações neles também. Nenhuma superou o ZSTD(1) puro, então fica o ZSTD(1) puro. Descobrir que o codec especializado não ajuda também é resultado, e vale anotar para ninguém testar de novo no ano que vem.
Nível 1 em tudo. Níveis maiores de ZSTD são outro teste, com a sua própria carga de merges, e não vou afirmar números sobre isso aqui.
O que isso significa para o seu disco
Bytes por linha é o número que sobrevive à mudança de uma tabela para outra. Multiplique pelo seu volume e pela sua retenção e você tem o disco de que precisa, antes de rodar um único teste. A largura bruta desta linha de sete colunas é 40 bytes (16 do UUID, 12 dos três UInt32, 12 dos três DateTime); 4,37 é o que sobra depois dos codecs e do TTL da coluna.
Seu volume, sua retenção e um valor de bytes por linha. Comece com os 4,37 medidos e troque pelo seu quando rodar a consulta acima.
Estimativa, não medição: linhas × dias × bytes por linha, em unidades binárias. Merges e o primeiro mês da coluna UUID pedem folga por cima.
Como eu testei
A documentação diz o que cada codec faz. Ela não diz qual ganha nos seus dados. Cheguei a esta configuração carregando milhões de linhas reais em tabelas de teste e medindo, coluna por coluna, cerca de 60 variaçõesbenchmark meu; contagem de memória, aproximada no total. Duas medidas por variação: bytes em disco e o tempo da consulta mais pesada que rodava em produção. Algumas variações ganham no disco e perdem na consulta. Você quer os dois números lado a lado.
O loop é curto. Uma variação é uma tabela, e você só muda o que está testando:
-- mesma estrutura, engine e chave de ordenação da produção
CREATE TABLE codec_test_v12 AS playback_log;
-- muda só o codec em teste
ALTER TABLE codec_test_v12
MODIFY COLUMN device_id UInt32 CODEC(Delta, ZSTD(1));
-- milhões de linhas, para atravessar muitos grânulos e várias partes
INSERT INTO codec_test_v12
SELECT * FROM playback_log
WHERE started_at >= '2026-08-01' AND started_at < '2026-08-08';
-- mede partes já mescladas, não inserts recentes
OPTIMIZE TABLE codec_test_v12 FINAL;
-- depois roda a consulta de parts_columns acima, e a sua consulta mais pesada 3 vezesTrês detalhes que mudaram resultados para mim:
- Milhares de linhas não bastam. Os codecs trabalham por bloco, e a chave de ordenação só mostra o efeito quando as linhas se espalham por muitos grânulos. Use milhões.
-
Rode
OPTIMIZE ... FINALantes de medir. Inserts recentes ficam em partes pequenas; a taxa com que você vai conviver é a de depois dos merges. - Mantenha a consulta pesada no loop. Disco é a metade barata da troca. Um codec que economiza 10% de disco e acrescenta 30% na sua pior consulta é um mau negócio na maioria dos orçamentos.
Hoje a instância inteira roda com três combinações: ZSTD(1) em 257 colunascenso de system.columns, 2026-08-26: 257 / 74 / 35 colunas, DoubleDelta, ZSTD(1) em 74, T64, ZSTD(1) em 35. Sobraram três combinações depois de umas sessenta tabelas de teste.
O que isso não diz
-
Se o seu timestamp não é a chave de ordenação, espere algo como o
ended_at, perto de 15x, e fique feliz com isso. -
Os 5,5x do total são puxados para baixo pelo
device_ide pelouidno primeiro mês dele. Uma tabela estreita sem UUID ficaria melhor, e também perderia a deduplicação. - Codec é assunto de disco e de I/O. O teto das consultas do ClickHouse nessa carga era RAM, não disco. Um bom codec nunca fez uma consulta caber na memória.
- É uma tabela, uma carga, uma data de medição. Copie o método antes de copiar o DDL.