Comparando esquemas normalizados vs. desnormalizados
Uma técnica comum, popularizada por soluções NoSQL, é desnormalizar dados na ausência de suporte a
JOIN, armazenando efetivamente todas as estatísticas ou linhas relacionadas em uma linha pai como colunas e objetos aninhados. Por exemplo, em um esquema de exemplo para um blog, podemos armazenar todos os Comments como um Array de objetos em seus respectivos posts.
Quando usar desnormalização
- Desnormalize tabelas que mudam com pouca frequência ou nas quais um atraso até que os dados estejam disponíveis para consultas analíticas seja aceitável, ou seja, os dados possam ser recarregados integralmente em lote.
- Evite desnormalizar relacionamentos muitos-para-muitos. Isso pode exigir a atualização de muitas linhas se uma única linha de origem mudar.
- Evite desnormalizar relacionamentos de alta cardinalidade. Se cada linha de uma tabela tiver milhares de entradas relacionadas em outra tabela, elas precisarão ser representadas como um
Array— seja de um tipo primitivo ou de tuplas. Em geral, não recomendamos arrays com mais de 1000 tuplas. - Em vez de desnormalizar todas as colunas como objetos aninhados, considere desnormalizar apenas uma estatística usando visões materializadas (veja abaixo).
Evite a desnormalização em dados atualizados com frequência
- Acionar as instruções JOIN corretas quando uma linha da tabela é alterada. O ideal é que isso não faça com que todos os objetos da junção sejam atualizados, mas apenas aqueles que foram afetados. Modificar as junções para filtrar com eficiência as linhas corretas e conseguir fazer isso com alta vazão exige ferramentas externas ou trabalho de engenharia.
- As atualizações de linhas no ClickHouse precisam ser gerenciadas com cuidado, o que introduz complexidade adicional.
Por isso, é mais comum usar um processo de atualização em lote, no qual todos os objetos desnormalizados são recarregados periodicamente.
Casos práticos de desnormalização
Posts que já foi desnormalizada com estatísticas como AnswerCount e CommentCount — os dados de origem são fornecidos dessa forma. Na prática, talvez queiramos normalizar essas informações, já que elas provavelmente mudam com frequência. Muitas dessas colunas também estão disponíveis por meio de outras tabelas; por exemplo, os comentários de um post podem ser obtidos pela coluna PostId na tabela Comments. Para fins deste exemplo, assumimos que os posts são recarregados em um processo em lote.
Também consideramos apenas desnormalizar outras tabelas em Posts, pois esta é nossa tabela principal para análises. Desnormalizar na direção oposta também seria apropriado para algumas consultas, com as mesmas considerações acima.
Para cada um dos exemplos a seguir, assuma que existe uma consulta que exige o uso de ambas as tabelas em uma junção.
Posts and Votes
posts. Há alguns desafios nessa abordagem.
Votos são adicionados aos posts com frequência. Embora isso possa diminuir com o tempo para cada post, a consulta a seguir mostra que temos cerca de 40 mil votos por hora em 30 mil posts.
Score atual representa esse tipo de estatística, ou seja, o total de votos positivos menos os votos negativos. Idealmente, poderíamos simplesmente recuperar essas estatísticas no momento da consulta com um simples lookup (consulte dicionários).
Users e Badges
Users e Badges:
Primeiro, inserimos os dados com o seguinte comando:
Talvez queiramos desnormalizar estatísticas de medalhas para usuários, por exemplo, o número de medalhas. Consideramos esse exemplo ao usar dicionários para esse conjunto de dados no momento da inserção.
Posts e PostLinks
PostLinks relaciona Posts que os usuários consideram relacionados ou duplicados. A consulta a seguir mostra a estrutura e o comando de carregamento:
Exemplo simples de estatística
INSERT INTO SELECT que faz uma junção entre nossa estatística de duplicados e nossos posts.
Usando tipos complexos em relacionamentos um-para-muitos
- Named Tuples - Permitem representar uma estrutura relacionada como um conjunto de colunas.
- Array(Tuple) ou Nested - Um array de tuplas nomeadas, também conhecido como Nested, em que cada elemento representa um objeto. Aplicável a relacionamentos um-para-muitos.
PostLinks em Posts.
Cada post pode conter vários links para outros posts, como mostrado anteriormente no esquema PostLinks. Como um tipo Nested, podemos representar esses posts relacionados e duplicados da seguinte forma:
Observe o uso da configuração flatten_nested=0. Recomendamos desativar o achatamento de dados aninhados.
Podemos realizar essa desnormalização usando um INSERT INTO SELECT com uma consulta com OUTER JOIN:
Observe o tempo aqui. Conseguimos desnormalizar 66 mi de linhas em cerca de 2 min. Como veremos mais adiante, essa é uma operação que podemos agendar.Observe o uso das funções
groupArray para agrupar PostLinks em um array para cada PostId, antes da junção. Esse array é então filtrado em duas sublistas: LinkedPosts e DuplicatePosts, que também excluem quaisquer resultados vazios da junção externa.
Podemos selecionar algumas linhas para ver nossa nova estrutura desnormalizada:
Orquestração e agendamento da desnormalização
Lote
INSERT INTO SELECT. Isso é adequado para transformações periódicas em lote.
Os usuários têm várias opções para orquestrar isso no ClickHouse, desde que um processo periódico de carregamento em lote seja aceitável:
- Refreshable Materialized Views - Views materializadas atualizáveis podem ser usadas para agendar periodicamente uma consulta, enviando os resultados para uma tabela de destino. Ao executar a consulta, a view garante que a tabela de destino seja atualizada atomicamente. Isso oferece uma forma nativa do ClickHouse de agendar esse trabalho.
- Ferramentas externas - Uso de ferramentas como dbt e Airflow para agendar periodicamente a transformação. A integração do ClickHouse para dbt garante que isso seja feito atomicamente, com a criação de uma nova versão da tabela de destino, que depois é trocada atomicamente pela versão que recebe consultas (por meio do comando EXCHANGE).