Contexto
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
toStartOfDay:
SELECT acima sobre os dados inseridos em votes, com os resultados enviados para up_down_votes_per_day:
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:
up_down_votes_per_day — devemos ter 1 linha por dia:
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
FINALno 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:
Um exemplo mais complexo
Score e a média de CommentCount. A consulta para calcular isso pode ser semelhante a:
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.
/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:
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:
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 à tabelaNossa consulta final precisa usar o sufixoposts. Aqui, usamosposts_nullpara demonstrar a tabela nula.
Merge nas nossas funções (já que as colunas armazenam estados de agregação parciais):
GROUP BY aqui em vez de FINAL.
Outras aplicações
Filtragem e transformação
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 umNossa visão materializada para essa transformação é mostrada abaixo:INSERT INTO SELECT. A visão materializada nos permite encapsular essa lógica no DDL do ClickHouse e manter nossoINSERTsimples, com a transformação aplicada a todas as novas linhas.
Tabela de referência
comments:
PostId.
Suponha que um usuário queira filtrar por um UserId específico e calcular a média de Score:
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:
Encadeamento / cascata de visões materializadas
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.
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
users.
Lembrando que os schemas das nossas tabelas são:
users já esteja preenchida:
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.daily_badges_by_user.
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
SELECTaciona 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 BYeORDER BYpara eficiência na mesclagem. Ao usarSummingMergeTreeouAggregatingMergeTree, garanta queGROUP BYcorresponda à cláusulaORDER BYna 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
Exemplo de cenário
Explicação
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
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
users.
badges, por exemplo.
users usando os IDs dos usuários nas linhas de badge inseridas:
2936484. Essa consulta também é otimizada pela chave de ordenação Id da tabela.
Visões materializadas e uniões
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:
INSERT INTO:
badges ou comments, uma abordagem ingênua para esse problema seria tentar criar uma visão materializada com a consulta anterior usando UNION:
comments. Por exemplo:
badges não acionam a view, fazendo com que user_activity não receba atualizações:
comments:
badges se refletem na tabela user_activity:
Processamento paralelo vs. sequencial
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:
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:
SELECT a chegada das linhas de cada linha:
uuid das views:
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:
Quando usar processamento paralelo
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.
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
- 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 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).
- 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
JOINouIN, 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.
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.