Seus dados chegam em JSON. O ClickHouse oferece várias maneiras de armazená-los, desde colunas totalmente tipadas até uma String bruta. A escolha certa depende de quão previsível é o seu esquema e de você precisar ou não de consultas em nível de campo.
Escopo: Esta página aborda decisões de modelagem de esquema para armazenar dados JSON. Ela não cobre formatos de entrada/saída JSON, funções JSON nem a sintaxe de consultas. Para mais contexto sobre o próprio tipo de coluna JSON, consulte Use JSON where appropriate.
Pressupõe: Familiaridade com a criação de tabelas no ClickHouse, os conceitos básicos de MergeTree e a sintaxe de tipos de coluna.
Decisão rápida
- Se cada campo tiver um tipo conhecido e estável, e o esquema raramente mudar → Colunas tipadas
- Se a maioria dos campos for estável, mas alguma seção for dinâmica ou imprevisível → Híbrido (tipado + JSON)
- Se toda a estrutura for dinâmica, com chaves que aparecem e desaparecem entre registros → Coluna JSON nativa
- Se os campos dinâmicos forem pares chave-valor com um tipo de valor consistente (por exemplo, tags de texto, métricas numéricas)
→
Mapem vez de JSON - Se você só armazena e recupera o blob JSON sem consultas em nível de campo → Armazenamento opaco em String
Detalhes da abordagem
Colunas tipadas
Quando usar: A estrutura do JSON é totalmente conhecida na fase de modelagem. Os campos e tipos não mudam de um registro para outro. Mesmo estruturas aninhadas complexas (arrays de objetos, mapas aninhados) podem ser expressas com os tipos Array, Tuple e Nested.
Desvantagens: Alterações no esquema exigem ALTER TABLE. Campos inesperados são descartados silenciosamente durante a inserção, a menos que o esquema seja atualizado.
Configuração, verificação e cuidados
Configuração
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Verificação
-- Confirme se os tipos das colunas correspondem ao esperado
DESCRIBE TABLE events FORMAT Vertical
-- Insira dados e consulte para validar se o esquema lida com eles
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42}
SELECT service, level, duration_ms FROM events WHERE service = 'api'Atenção a
- Se você inserir dados JSON com
JSONEachRowe o JSON contiver campos que não estão no esquema, o ClickHouse os descartará silenciosamente por padrão. Definainput_format_skip_unknown_fieldscomo0se quiser que isso gere erros.
Híbrido (colunas tipadas + JSON)
Quando usar: Um conjunto principal de campos é estável (timestamps, IDs, códigos de status), mas parte do payload é dinâmica. Pense em atributos definidos pelo usuário, tags, metadados ou campos de extensão que variam entre os registros.
Trade-offs: Desempenho total nas colunas tipadas e flexibilidade na coluna JSON. A coluna JSON ainda traz sobrecarga na inserção e custo de armazenamento para sua parte dinâmica.
Configuração, verificação e pontos de atenção
Configuração
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32,
`attributes` JSON(
max_dynamic_paths = 256,
`http.status_code` UInt16,
`http.method` LowCardinality(String),
SKIP REGEXP 'debug\..*'
)
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Verificação
-- Insira dados de exemplo e inspecione os caminhos inferidos
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42,"attributes":{"http.status_code":200,"http.method":"GET","user.region":"eu-west","custom_tag":"abc"}}
SELECT JSONAllPathsWithTypes(attributes)
FROM events
FORMAT PrettyJSONEachRowAtenção
- Use type hints em caminhos JSON que você já conhece. Essas indicações contornam a coluna discriminadora e armazenam o caminho como uma coluna tipada comum, com o mesmo desempenho e sem sobrecarga.
- Use
SKIPouSKIP REGEXPpara caminhos que você nunca consulta (metadados de depuração, IDs internos de tracing) para economizar armazenamento e reduzir a contagem de subcolunas. - Defina
max_dynamic_pathsde forma proporcional ao número de caminhos distintos que você realmente consulta. O padrão (1024) funciona na maioria dos casos. Reduza esse valor se sua seção dinâmica for pequena. - Não defina
max_dynamic_pathsacima de 10.000. Valores altos aumentam o consumo de recursos e reduzem a eficiência.
Coluna JSON nativa
Quando usar: A estrutura é realmente imprevisível, com chaves que aparecem e desaparecem entre registros. Esquemas gerados por usuários, sistemas de plugins ou ingestão em data lake em que você não controla o esquema upstream.
Trade-offs: Inserções mais lentas do que em colunas tipadas. Leituras do objeto completo mais lentas do que em String. Sobrecarga de armazenamento devido ao gerenciamento de subcolunas. Funciona bem para consultas em nível de campo em caminhos específicos.
Configuração, verificação e armadilhas
Configuração
CREATE TABLE dynamic_events
(
`id` UInt64,
`ts` DateTime DEFAULT now(),
`data` JSON(
max_dynamic_paths = 512,
`event_type` LowCardinality(String),
`version` UInt8
)
)
ENGINE = MergeTree
ORDER BY (data.event_type, ts)Use o formato JSONAsObject ao inserir documentos JSON completos em uma coluna JSON. Ele trata cada linha de entrada como um objeto JSON completo mapeado para a coluna.
Verificação
INSERT INTO dynamic_events (id, data) FORMAT JSONEachRow
{"id": 1, "data": {"event_type": "click", "version": 2, "page": "/home", "button_id": "cta-1"}}
{"id": 2, "data": {"event_type": "purchase", "version": 1, "item_id": "SKU-99", "amount": 49.99, "currency": "USD"}}
-- Verifique quais caminhos o ClickHouse detectou e seus tipos
SELECT JSONAllPathsWithTypes(data) FROM dynamic_events FORMAT PrettyJSONEachRow
-- Consulte um caminho específico
SELECT data.page FROM dynamic_events WHERE data.event_type = 'click'Fique atento a
- Sem type hints, o ClickHouse infere os tipos por caminho com base nos primeiros valores que encontra. Se
scorechegar como"10"(string) em um registro e10(inteiro) em outro, o caminho receberá uma coluna discriminadora e as consultas ficarão mais lentas. Adicione indicações para caminhos com tipos conhecidos. - Quando a contagem de caminhos excede
max_dynamic_paths, os valores excedentes são movidos para uma shared data structure com menor desempenho de consulta. Monitore comJSONDynamicPaths()e mantenha o limite abaixo de 10.000. - Cada caminho dinâmico aceita até
max_dynamic_types(padrão 32) distinct data types. Se um único caminho exceder isso, os tipos extras passarão a usar o armazenamento variant compartilhado. Isso raramente importa, a menos que seus dados tenham tipos muito inconsistentes para o mesmo campo.
Armazenamento opaco em String
Quando usar: Documentos JSON são armazenados e recuperados por inteiro e depois encaminhados para uma aplicação, arquivados ou enviados para sistemas downstream. Sem filtragem por campo nem agregação dentro do ClickHouse.
Trade-offs: inserts mais rápidos e esquema mais simples. Não há consulta em nível de campo sem parsing em tempo de execução (família JSONExtract), o que é lento em escala.
Configuração, verificação e cuidados
Configuração
CREATE TABLE raw_events
(
`id` UInt64,
`received` DateTime DEFAULT now(),
`payload` String
)
ENGINE = MergeTree
ORDER BY (received)Verificação
INSERT INTO raw_events (id, payload) VALUES
(1, '{"type":"click","page":"/home"}'),
(2, '{"type":"purchase","item":"SKU-99","amount":49.99}')
-- Confirme que os dados permanecem intactos na ida e volta
SELECT payload FROM raw_events WHERE id = 1
-- Verifique que ainda é possível extrair campos de forma ad hoc quando necessário
SELECT JSONExtractString(payload, 'type') AS event_type FROM raw_eventsAtenção
- Se os requisitos mudarem e depois você precisar de consultas por campo, será necessário criar uma nova tabela com colunas tipadas ou JSON e fazer backfill dos dados. Se houver qualquer chance de consultar campos individuais, comece com a abordagem híbrida.
- As funções
JSONExtractfazem parsing da string a cada consulta. Isso é aceitável para exploração ad hoc, mas não para dashboards de produção nem workloads com alto QPS. - Considere codecs de compressão (
ZSTD) na coluna String se os payloads JSON forem grandes — a compressão costuma ser boa.
Comparação
| Dimensão | Colunas tipadas | Híbrido | JSON nativo | String |
|---|---|---|---|---|
| Taxa de inserção | Mais rápida | Rápida | Moderada | Mais rápida |
| Consulta em nível de campo | Mais rápidas | Rápidas (tipadas); boas (JSON com indicação) | Boas (com indicação); mais lentas (Dynamic) | Lentas (parsing em tempo de execução) |
| Leitura do objeto completo | Rápida | Moderada | Lenta | Mais rápida |
| Eficiência de armazenamento | Melhor | Boa | Moderada | Boa (comprime bem) |
| Flexibilidade do esquema | Nenhuma (ALTER TABLE) |
Parcial (núcleo rígido, parte flexível) | Total | Total |
| Complexidade | Baixa | Média | Média–alta | Baixa |
Quando Map é mais adequado
Se seus campos dinâmicos forem pares chave-valor homogêneos — ou seja, todos os valores compartilham o mesmo tipo — Map(String, T) é mais simples e mais eficiente do que uma coluna JSON. Exemplos comuns: tags de string (Map(String, String)), métricas numéricas (Map(String, Float64)) ou feature flags (Map(String, Bool)).
CREATE TABLE tagged_events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`tags` Map(String, String) -- e.g., {"env": "prod", "region": "us-east-1", "team": "platform"}
)
ENGINE = MergeTree
ORDER BY (service, timestamp)Map oferece suporte à filtragem no nível da chave (tags['env'] = 'prod'), é mais barato de armazenar do que JSON e evita a sobrecarga de subcoluna do tipo JSON. Observe que as buscas de chave fazem uma varredura linear no map por padrão — isso é adequado para conjuntos pequenos de tags, mas, para maps com mais de 100 chaves, considere a serialização with_buckets. Use JSON quando os valores tiverem tipos mistos ou quando a estrutura tiver aninhamento — use Map quando forem pares chave-valor simples com um tipo de valor uniforme.
- Use JSON quando apropriado — quando usar o tipo de coluna JSON em vez de outras opções
- Referência do tipo de dados JSON — sintaxe completa para type hints, SKIP, max_dynamic_paths e funções de introspecção
- Escolhendo tipos de dados — orientações gerais para escolher tipos
- A New Powerful JSON Data Type for ClickHouse — análise detalhada da arquitetura de armazenamento do tipo JSON
- Referência dos formatos JSON — formatos de entrada/saída para dados JSON (JSONEachRow, JSONAsObject etc.)