Pular para o conteúdo principal

Contexto

Views materializadas incrementais (views materializadas) permitem transferir o custo da computação do momento da consulta para o momento da inserção, resultando em consultas SELECT mais rápidas. Diferentemente de bancos de dados transacionais como o Postgres, uma visão materializada no ClickHouse é apenas um gatilho que executa uma consulta sobre blocos de dados à medida que eles são inseridos em uma tabela. O resultado dessa consulta é inserido em uma segunda tabela de “destino”. Se mais linhas forem inseridas, os resultados serão novamente enviados para a tabela de destino, onde os resultados intermediários serão atualizados e mesclados. Esse resultado mesclado equivale a executar a consulta sobre todos os dados originais. A principal motivação para usar views materializadas é que os resultados inseridos na tabela de destino representam o resultado de uma agregação, filtragem ou transformação das linhas. Esses resultados geralmente são uma representação menor dos dados originais (um esboço parcial, no caso de agregações). Isso, junto com o fato de a consulta resultante para ler os resultados da tabela de destino ser simples, garante tempos de consulta menores do que se a mesma computação fosse executada sobre os dados originais, transferindo a computação (e, portanto, a latência da consulta) do momento da consulta para o momento da inserção. As visões materializadas no ClickHouse são atualizadas em tempo real à medida que os dados fluem para a tabela em que se baseiam, funcionando mais como índices atualizados continuamente. Isso contrasta com outros bancos de dados, nos quais as views materializadas normalmente são snapshots estáticos de uma consulta que precisam ser atualizados (semelhantes às views materializadas atualizáveis do ClickHouse).

Exemplo

Para fins de exemplo, usaremos o conjunto de dados do Stack Overflow documentado em “Schema Design”. Suponha que queremos obter o número de votos positivos e negativos por dia para uma postagem.
Esta é uma consulta bastante simples no ClickHouse graças à função toStartOfDay:
Essa consulta já é rápida graças ao ClickHouse, mas será que conseguimos melhorar ainda mais? Se quisermos calcular isso no momento de inserção usando uma visão materializada, precisamos de uma tabela para receber os resultados. Essa tabela deve manter apenas 1 linha por dia. Se uma atualização for recebida para um dia já existente, as demais colunas devem ser mescladas na linha daquele dia. Para que essa mesclagem de estados incrementais ocorra, estados parciais devem ser armazenados para as demais colunas. Isso requer um tipo de motor especial no ClickHouse: o SummingMergeTree. Ele substitui todas as linhas com a mesma ordering key por uma única linha contendo os valores somados das colunas numéricas. A tabela a seguir fará o merge de quaisquer linhas com a mesma data, somando todas as colunas numéricas:
Para demonstrar nossa visão materializada, suponha que a tabela de votos esteja vazia e ainda não tenha recebido nenhum dado. Nossa visão materializada executa o SELECT acima sobre os dados inseridos em votes, com os resultados enviados para up_down_votes_per_day:
A cláusula TO é fundamental aqui, indicando para onde os resultados serão enviados, ou seja, up_down_votes_per_day. Podemos repopular a tabela Votes com base na inserção anterior:
Ao final, podemos confirmar o tamanho de up_down_votes_per_day — devemos ter 1 linha por dia:
Reduzimos efetivamente o número de linhas aqui de 238 milhões (em votes) para 5000 ao armazenar o resultado da nossa consulta. O ponto principal, porém, é que, se novos votos forem inseridos na tabela votes, novos valores serão enviados para up_down_votes_per_day no dia correspondente, onde serão mesclados automaticamente de forma assíncrona em segundo plano, mantendo apenas uma linha por dia. Assim, up_down_votes_per_day será sempre pequena e estará sempre atualizada. Como a mesclagem das linhas é assíncrona, pode haver mais de um voto por dia quando um usuário fizer uma consulta. Para garantir que todas as linhas pendentes sejam mescladas no momento da consulta, temos duas opções:
  • Usar o modificador FINAL no nome da tabela. Fizemos isso na consulta de contagem acima.
  • Agregar pela chave de ordenação usada na nossa tabela final, ou seja, CreationDate, e somar as métricas. Em geral, isso é mais eficiente e flexível (a tabela pode ser usada para outras finalidades), mas a primeira opção pode ser mais simples para algumas consultas. Mostramos as duas abaixo:
Isso acelerou nossa consulta de 0.133s para 0.004s — uma melhoria de mais de 25 vezes!
Importante: ORDER BY = GROUP BYNa maioria dos casos, as colunas usadas na cláusula GROUP BY da transformação das visões materializadas devem corresponder às usadas na cláusula ORDER BY da tabela de destino ao utilizar os motores de tabela SummingMergeTree ou AggregatingMergeTree. Esses motores dependem das colunas de ORDER BY para mesclar linhas com valores idênticos durante as operações de merge em segundo plano. Um desalinhamento entre as colunas de GROUP BY e ORDER BY pode levar a consultas menos eficientes, merges subótimos ou até mesmo discrepâncias nos dados.

Um exemplo mais complexo

O exemplo acima usa visões materializadas para calcular e manter duas somas por dia. As somas representam a forma mais simples de agregação para manter estados parciais, pois basta adicionar novos valores aos já existentes quando eles chegam. No entanto, as visões materializadas do ClickHouse podem ser usadas para qualquer tipo de agregação. Suponha que queiramos calcular algumas estatísticas dos posts para cada dia: o percentil 99,9 de Score e a média de CommentCount. A consulta para calcular isso pode ser semelhante a:
Como antes, podemos criar uma visão materializada que executa a consulta acima à medida que novos posts são inseridos na nossa tabela posts. Para fins de exemplo e para evitar carregar os dados de posts a partir do S3, criaremos uma tabela duplicada posts_null com o mesmo schema de posts. No entanto, essa tabela não armazenará nenhum dado e será usada apenas pela visão materializada quando linhas forem inseridas. Para impedir o armazenamento de dados, podemos usar o motor de tabela Null.
O engine de tabela Null é uma otimização poderosa — pense nele como /dev/null. Nossa visão materializada calculará e armazenará nossas estatísticas resumidas quando a tabela posts_null receber linhas no momento da inserção — ela funciona apenas como um gatilho. No entanto, os dados brutos não serão armazenados. Embora, no nosso caso, provavelmente ainda queiramos armazenar os posts originais, essa abordagem pode ser usada para calcular agregações e evitar a sobrecarga de armazenamento dos dados brutos. A visão materializada então fica assim:
Observe como acrescentamos o sufixo State ao final das nossas funções de agregação. Isso garante que o estado de agregação da função seja retornado em vez do resultado final. Ele contém informações adicionais que permitem mesclar esse estado parcial com outros estados. Por exemplo, no caso de uma média, isso inclui uma contagem e a soma da coluna.
Estados de agregação parciais são necessários para calcular resultados corretos. Por exemplo, ao calcular uma média, simplesmente calcular a média das médias de subintervalos produz resultados incorretos.
Agora criamos a tabela de destino para esta view post_stats_per_day, que armazena esses estados de agregação parciais:
Embora antes o SummingMergeTree fosse suficiente para armazenar contagens, precisamos de um tipo de motor mais avançado para outras funções: o AggregatingMergeTree. Para garantir que o ClickHouse saiba que estados de agregação serão armazenados, definimos Score_quantiles e AvgCommentCount com o tipo AggregateFunction, especificando a função usada para gerar os estados parciais e o tipo das colunas de origem. Assim como no SummingMergeTree, linhas com o mesmo valor de chave de ORDER BY serão mescladas (Day no exemplo acima). Para popular nossa post_stats_per_day por meio da visão materializada, podemos simplesmente inserir todas as linhas de posts em posts_null:
Em produção, você provavelmente anexaria a visão materializada à tabela posts. Aqui, usamos posts_null para demonstrar a tabela nula.
Nossa consulta final precisa usar o sufixo Merge nas nossas funções (já que as colunas armazenam estados de agregação parciais):
Note que usamos GROUP BY aqui em vez de FINAL.

Outras aplicações

O conteúdo acima se concentra principalmente no uso de visões materializadas para atualizar incrementalmente agregações parciais de dados, transferindo assim o processamento do momento da consulta para o momento da inserção. Além desse caso de uso comum, as visões materializadas têm várias outras aplicações.

Filtragem e transformação

Em algumas situações, talvez queiramos inserir apenas um subconjunto das linhas e colunas na inserção. Nesse caso, nossa tabela posts_null poderia receber inserts, com uma consulta SELECT filtrando as linhas antes da inserção na tabela posts. Por exemplo, suponha que quiséssemos transformar uma coluna Tags da nossa tabela posts. Ela contém uma lista de nomes de tags delimitada por pipe. Ao convertê-la em um array, podemos agregar com mais facilidade por tag individual.
Poderíamos fazer essa transformação ao executar um INSERT INTO SELECT. A visão materializada nos permite encapsular essa lógica no DDL do ClickHouse e manter nosso INSERT simples, com a transformação aplicada a todas as novas linhas.
Nossa visão materializada para essa transformação é mostrada abaixo:

Tabela de referência

Você deve considerar os padrões de acesso ao escolher uma chave de ordenação do ClickHouse. Colunas usadas com frequência em cláusulas de filtro e agregação devem ser utilizadas. Isso pode ser restritivo em cenários em que os usuários têm padrões de acesso mais diversos, que não podem ser encapsulados em um único conjunto de colunas. Por exemplo, considere a seguinte tabela comments:
A chave de ordenação aqui otimiza a tabela para consultas que filtram por PostId. Suponha que um usuário queira filtrar por um UserId específico e calcular a média de Score:
Embora seja rápido (os dados são pequenos para o ClickHouse), podemos ver, pelo número de linhas processadas — 90,38 milhões —, que isso exige uma varredura completa da tabela. Para datasets maiores, podemos usar uma visão materializada para consultar os valores de PostId da nossa chave de ordenação ao filtrar pela coluna UserId. Esses valores podem então ser usados para fazer uma consulta eficiente. Neste exemplo, nossa visão materializada pode ser bem simples, selecionando apenas PostId e UserId de comments no insert. Esses resultados, por sua vez, são enviados para uma tabela comments_posts_users, ordenada por UserId. Abaixo, criamos uma versão Null da tabela Comments e a usamos para preencher nossa visão e a tabela comments_posts_users:
Agora podemos usar essa view em uma subconsulta para acelerar a consulta anterior:

Encadeamento / cascata de visões materializadas

As visões materializadas podem ser encadeadas (ou em cascata), permitindo criar fluxos de trabalho complexos. Para mais informações, consulte o guia “Visões materializadas em cascata”.

Visões materializadas e JOINs

Views materializadas atualizáveisO texto a seguir se aplica apenas a views materializadas incrementais. Views materializadas atualizáveis executam periodicamente sua consulta sobre todo o conjunto de dados de destino e oferecem suporte completo a JOINs. Considere usá-las para JOINs complexos se for aceitável ter resultados menos atualizados.
As views materializadas incrementais no ClickHouse oferecem suporte completo a operações JOIN, mas com uma restrição crucial: a visão materializada só é acionada por inserts na tabela de origem (a tabela mais à esquerda na consulta). As tabelas do lado direito em JOINs não disparam atualizações, mesmo que seus dados mudem. Esse comportamento é especialmente importante ao criar views materializadas incrementais, nas quais os dados são agregados ou transformados no momento do insert. Quando uma view materializada incremental é definida com um JOIN, a tabela mais à esquerda na consulta SELECT atua como origem. Quando novas linhas são inseridas nessa tabela, o ClickHouse executa a consulta da visão materializada apenas com essas linhas recém-inseridas. As tabelas do lado direito no JOIN são lidas integralmente durante essa execução, mas mudanças somente nelas não acionam a view. Esse comportamento faz com que JOINs em views materializadas funcionem de forma semelhante a uma junção de snapshot com dados de dimensão estáticos. Isso funciona bem para enriquecer dados com tabelas de referência ou de dimensão. No entanto, atualizações nas tabelas do lado direito (por exemplo, metadados de usuário) não atualizam retroativamente a visão materializada. Para ver os dados atualizados, novos inserts precisam chegar à tabela de origem.

Exemplo

Vamos ver um exemplo concreto usando o dataset do Stack Overflow. Usaremos uma visão materializada para calcular badges diários por usuário, incluindo o nome de exibição do usuário da tabela users. Lembrando que os schemas das nossas tabelas são:
Vamos assumir que nossa tabela users já esteja preenchida:
A visão materializada e a tabela de destino associada a ela são definidas da seguinte forma:
Alinhamento entre agrupamento e ordenaçãoA cláusula GROUP BY na visão materializada deve incluir DisplayName, UserId e Day para corresponder ao ORDER BY na tabela de destino SummingMergeTree. Isso garante que as linhas sejam agregadas e combinadas corretamente. Omitir qualquer um desses campos pode levar a resultados incorretos ou a merges ineficientes.
Se agora popularmos os badges, a visão será disparada, preenchendo nossa tabela daily_badges_by_user.
Suponha que queiramos ver os badges obtidos por um usuário específico. Podemos escrever a seguinte consulta:
Agora, se este usuário ganhar um novo badge e uma linha for inserida, nossa view será atualizada:
Observe a latência do insert aqui. A linha de usuário inserida é submetida a uma junção com toda a tabela users, o que afeta significativamente o desempenho do insert. Propomos abordagens para resolver isso abaixo em “Usando a tabela de origem em filtros e junções”.
Por outro lado, se inserirmos um badge para um novo usuário, seguido da linha do usuário, nossa visão materializada deixará de capturar as métricas dos usuários.
A view, neste caso, só é executada quando o badge é inserido antes de a linha do usuário existir. Se inserirmos outro badge para o usuário, uma linha será inserida, como esperado:
Observe, no entanto, que esse resultado está incorreto.

Boas práticas para JOINs em visões materializadas

  • Use a tabela mais à esquerda como gatilho. Apenas a tabela no lado esquerdo da instrução SELECT aciona a view materializada. Alterações nas tabelas do lado direito não disparam atualizações.
  • Insira antes os dados das tabelas associadas. Garanta que os dados nas tabelas associadas existam antes de inserir linhas na tabela de origem. O JOIN é avaliado no momento da inserção, portanto, dados ausentes resultarão em linhas sem correspondência ou valores nulos.
  • Limite as colunas extraídas dos JOINs. Selecione apenas as colunas necessárias das tabelas associadas para minimizar o uso de memória e reduzir a latência no momento da inserção (veja abaixo).
  • Avalie o desempenho no momento da inserção. JOINs aumentam o custo das inserções, especialmente com tabelas grandes no lado direito. Faça benchmark das taxas de inserção usando dados representativos de produção.
  • Prefira Dictionaries para lookups simples. Use Dictionaries para lookups de chave-valor (por exemplo, de ID de usuário para nome) e evite operações de JOIN custosas.
  • Alinhe GROUP BY e ORDER BY para eficiência na mesclagem. Ao usar SummingMergeTree ou AggregatingMergeTree, garanta que GROUP BY corresponda à cláusula ORDER BY na tabela de destino para permitir a mesclagem eficiente de linhas.
  • Use aliases de coluna explícitos. Quando as tabelas tiverem nomes de coluna sobrepostos, use aliases para evitar ambiguidades e garantir resultados corretos na tabela de destino.
  • Considere o volume e a frequência de inserção. JOINs funcionam bem em workloads moderados de inserção. Para ingestão com alta vazão, considere usar tabelas de staging, pré-junções ou outras abordagens, como Dictionaries e visões materializadas atualizáveis.

Como usar a tabela de origem em filtros e junções

Ao trabalhar com visões materializadas no ClickHouse, é importante entender como a tabela de origem é tratada durante a execução da consulta da visão materializada. Em particular, a tabela de origem na consulta da visão materializada é substituída pelo bloco de dados inserido. Esse comportamento pode levar a resultados inesperados se não for bem compreendido.

Exemplo de cenário

Considere a seguinte configuração:

Explicação

No exemplo acima, temos duas visões materializadas, mvw1 e mvw2, que executam operações semelhantes, mas com uma pequena diferença na forma como referenciam a tabela de origem t0. Em mvw1, a tabela t0 é referenciada diretamente em uma subconsulta (SELECT * FROM t0) no lado direito do JOIN. Quando dados são inseridos em t0, a consulta da visão materializada é executada com o bloco de dados inserido substituindo t0. Isso significa que a operação de JOIN é executada apenas sobre as linhas recém-inseridas, e não sobre a tabela inteira. No segundo caso, ao fazer join com vt0, a visão lê todos os dados de t0. Isso garante que a operação de JOIN considere todas as linhas de t0, e não apenas o bloco recém-inserido. A principal diferença está em como o ClickHouse trata a tabela de origem na consulta da visão materializada. Quando uma visão materializada é acionada por um insert, a tabela de origem (t0, neste caso) é substituída pelo bloco de dados inserido. Esse comportamento pode ser aproveitado para otimizar consultas, mas também exige atenção para evitar resultados inesperados.

Casos de uso e ressalvas

Na prática, você pode usar esse comportamento para otimizar visões materializadas que precisam processar apenas um subconjunto dos dados da tabela de origem. Por exemplo, você pode usar uma subconsulta para filtrar a tabela de origem antes de fazer um JOIN com outras tabelas. Isso pode ajudar a reduzir a quantidade de dados processados pela visão materializada e melhorar o desempenho.
Neste exemplo, o conjunto criado a partir da subconsulta IN (SELECT id FROM t0) contém apenas as linhas recém-inseridas, o que pode ajudar a filtrar t1 com base nele.

Exemplo com Stack Overflow

Considere nosso exemplo anterior de visão materializada para calcular badges diários por usuário, incluindo o nome de exibição do usuário da tabela users.
Esta view afetou significativamente a latência de inserção na tabela badges, por exemplo.
Usando a abordagem acima, podemos otimizar esta view. Vamos adicionar um filtro à tabela users usando os IDs dos usuários nas linhas de badge inseridas:
Isso não só acelera a inserção inicial de badges:
Mas isso também significa que futuras inserções na tabela badge são eficientes:
Na operação acima, apenas uma linha é recuperada da tabela users para o ID de usuário 2936484. Essa consulta também é otimizada pela chave de ordenação Id da tabela.

Visões materializadas e uniões

Consultas UNION ALL são comumente usadas para combinar dados de várias tabelas de origem em um único conjunto de resultados. Embora UNION ALL não tenha suporte direto em visões materializadas incrementais, você pode obter o mesmo resultado criando uma visão materializada separada para cada ramificação SELECT e gravando os resultados em uma tabela de destino compartilhada. Para este exemplo, usaremos o conjunto de dados do Stack Overflow. Considere as tabelas badges e comments abaixo, que representam as medalhas obtidas por um usuário e os comentários que ele faz em posts:
Eles podem ser populados com os seguintes comandos INSERT INTO:
Suponha que queiramos criar uma visão unificada da atividade dos usuários, mostrando a última atividade de cada usuário ao combinar essas duas tabelas:
Suponha que haja uma tabela de destino para receber os resultados desta consulta. Observe o uso do motor de tabela AggregatingMergeTree e de AggregateFunction para garantir que os resultados sejam mesclados corretamente:
Como queremos que esta tabela seja atualizada à medida que novas linhas sejam inseridas em badges ou comments, uma abordagem ingênua para esse problema seria tentar criar uma visão materializada com a consulta anterior usando UNION:
Embora isso seja sintaticamente válido, produzirá resultados indesejados - a view só será acionada por inserções na tabela comments. Por exemplo:
As inserções na tabela badges não acionam a view, fazendo com que user_activity não receba atualizações:
Para resolver isso, basta criar uma visão materializada para cada instrução SELECT:
Inserções em qualquer uma das tabelas agora produzem os resultados corretos. Por exemplo, se inserirmos na tabela comments:
Da mesma forma, as inserções na tabela badges se refletem na tabela user_activity:

Processamento paralelo vs. sequencial

Como mostrado no exemplo anterior, uma tabela pode atuar como origem para várias visões materializadas. A ordem em que elas são executadas depende da configuração parallel_view_processing. Por padrão, essa configuração é igual a 0 (false), o que significa que as visões materializadas são executadas sequencialmente, na ordem de uuid. Por exemplo, considere a tabela source a seguir e 3 visões materializadas, cada uma enviando linhas para uma tabela target:
Observe que cada uma das views faz uma pausa de 1 segundo antes de inserir suas linhas na tabela target, incluindo também seu nome e o horário da inserção. Inserir uma linha na tabela source leva ~3 segundos, com cada view sendo executada em sequência:
Podemos confirmar com um SELECT a chegada das linhas de cada linha:
Isso corresponde ao uuid das views:
Por outro lado, considere o que acontece se inserirmos uma linha com parallel_view_processing=1 ativado. Com essa opção ativada, as views são executadas em paralelo, sem garantia da ordem em que as linhas chegam à tabela de destino:
Embora a ordem de chegada das linhas de cada view seja a mesma, isso não é garantido — como mostra a semelhança no tempo de inserção de cada linha. Observe também a melhora no desempenho de inserção.

Quando usar processamento paralelo

Habilitar parallel_view_processing=1 pode melhorar significativamente a taxa de inserção, como mostrado acima, especialmente quando várias visões materializadas estão associadas a uma única tabela. No entanto, é importante entender os trade-offs:
  • Maior pressão de inserção: Todas as visões materializadas são executadas simultaneamente, aumentando o uso de CPU e memória. Se cada visão realizar processamento pesado ou junções, isso pode sobrecarregar o sistema.
  • Necessidade de uma ordem de execução estrita: Em fluxos de trabalho raros em que a ordem de execução das visões importa (por exemplo, dependências encadeadas), a execução paralela pode levar a um estado inconsistente ou a condições de corrida. Embora seja possível contornar isso no projeto, essas configurações são frágeis e podem falhar em versões futuras.
Padrões históricos e estabilidadeA execução sequencial foi o padrão por muito tempo, em parte devido à complexidade do tratamento de erros. Historicamente, uma falha em uma visão materializada podia impedir a execução das demais. Versões mais recentes melhoraram isso ao isolar falhas por bloco, mas a execução sequencial ainda oferece uma semântica de falha mais clara.
Em geral, habilite parallel_view_processing=1 quando:
  • Você tiver várias visões materializadas independentes
  • Quiser maximizar o desempenho de inserção
  • Estiver ciente da capacidade do sistema de lidar com a execução concorrente das visões
Deixe-o desabilitado quando:
  • As visões materializadas tiverem dependências entre si
  • Você precisar de uma execução previsível e ordenada
  • Estiver depurando ou auditando o comportamento de inserção e quiser uma reprodução determinística

Visões materializadas e expressões de tabela comuns (CTE)

As expressões de tabela comuns (CTEs) não recursivas são suportadas em visões materializadas.
As Expressões de Tabela Comuns não são materializadasO ClickHouse não materializa CTEs; em vez disso, substitui a definição da CTE diretamente na consulta, o que pode levar a múltiplas avaliações da mesma expressão (se a CTE for usada mais de uma vez).
Considere o exemplo a seguir, que calcula a atividade diária de cada tipo de post.
Embora a CTE não seja estritamente necessária aqui, para fins de exemplo, a view funcionará como esperado:
No ClickHouse, as CTEs são expandidas inline, o que significa que elas são efetivamente copiadas e coladas na consulta durante a otimização e não são materializadas. Isso significa:
  • Se a sua CTE fizer referência a uma tabela diferente da tabela de origem (ou seja, aquela à qual a visão materializada está associada) e for usada em uma cláusula JOIN ou IN, ela se comportará como uma subconsulta ou um join, e não como um gatilho.
  • A visão materializada ainda será acionada apenas por inserções na tabela de origem principal, mas a CTE será reexecutada a cada inserção, o que pode causar sobrecarga desnecessária, especialmente se a tabela referenciada for grande.
Por exemplo,
Nesse caso, a CTE users é reavaliada a cada insert em posts, e a visão materializada não será atualizada quando novos usuários forem inseridos — apenas quando houver insert em posts. Em geral, use CTEs para lógica que opera na mesma tabela de origem à qual a visão materializada está associada ou garanta que as tabelas referenciadas sejam pequenas e pouco propensas a causar gargalos de desempenho. Como alternativa, considere as mesmas otimizações usadas para junções com visões materializadas.
Última modificação em 29 de junho de 2026