Esta é a Parte 3 de um guia sobre a migração do PostgreSQL para o ClickHouse. Com um exemplo prático, ele demonstra como modelar dados no ClickHouse ao migrar do PostgreSQL.Recomendamos que os usuários que estão migrando do Postgres leiam o guia de modelagem de dados no ClickHouse. Este guia usa o mesmo conjunto de dados do Stack Overflow e explora várias abordagens usando funcionalidades do ClickHouse.
Usuários que vêm de bancos de dados OLTP frequentemente procuram o conceito equivalente no ClickHouse. Ao notar que o ClickHouse oferece suporte à sintaxe
PRIMARY KEY, eles podem ficar tentados a definir o esquema da tabela usando as mesmas chaves do banco de dados OLTP de origem. Isso não é adequado.
Em que as chaves primárias do ClickHouse são diferentes?
- As chaves primárias do Postgres são, por definição, únicas por linha. O uso de estruturas B-tree permite localizar com eficiência linhas individuais por essa chave. Embora o ClickHouse possa ser otimizado para localizar o valor de uma única linha, workloads analíticas normalmente exigem a leitura de poucas colunas, mas de muitas linhas. Na maioria das vezes, os filtros precisam identificar um subconjunto de linhas sobre o qual uma agregação será executada.
- A eficiência de memória e disco é fundamental na escala em que o ClickHouse costuma ser usado. Os dados são gravados nas tabelas do ClickHouse em fragmentos conhecidos como partes, com regras aplicadas para mesclar essas partes em segundo plano. No ClickHouse, cada parte tem seu próprio índice primário. Quando as partes são mescladas, os índices primários da parte resultante também são mesclados. Ao contrário do Postgres, esses índices não são construídos para cada linha. Em vez disso, o índice primário de uma parte tem uma entrada de índice por grupo de linhas — essa técnica é chamada de indexação esparsa.
- A indexação esparsa é possível porque o ClickHouse armazena em disco as linhas de uma parte ordenadas por uma chave especificada. Em vez de localizar diretamente linhas individuais (como um índice baseado em B-Tree), o índice primário esparso permite identificar rapidamente (por meio de uma busca binária nas entradas do índice) grupos de linhas que podem corresponder à consulta. Os grupos localizados de linhas potencialmente correspondentes são então processados em paralelo pelo engine do ClickHouse para encontrar as correspondências. Esse projeto de índice permite que o índice primário seja pequeno (cabendo inteiramente na memória principal) e, ainda assim, acelere significativamente o tempo de execução das consultas, especialmente em consultas de intervalo típicas de casos de uso analíticos.
Todas as colunas de uma tabela serão ordenadas com base no valor da chave de ordenação especificada, independentemente de estarem incluídas na própria chave. Por exemplo, seCreationDatefor usada como chave, a ordem dos valores em todas as outras colunas corresponderá à ordem dos valores na colunaCreationDate. É possível especificar várias chaves de ordenação — isso ordenará os dados com a mesma semântica de uma cláusulaORDER BYem uma consultaSELECT.
Escolhendo uma chave de ordenação
Partições
PARTITION BY. Essa cláusula pode conter uma expressão SQL sobre qualquer coluna, e o resultado dessa expressão define para qual partição uma linha será enviada.
As partes de dados são associadas logicamente a cada partição no disco e podem ser consultadas de forma isolada. No exemplo abaixo, particionamos a tabela posts por ano usando a expressão toYear(CreationDate). À medida que as linhas são inseridas no ClickHouse, essa expressão é avaliada para cada linha e ela é encaminhada para a partição correspondente; se for a primeira linha daquele ano, a partição será criada.
Aplicações das partições
- Gerenciamento de dados - No ClickHouse, você deve considerar o particionamento principalmente como um recurso de gerenciamento de dados, e não como uma técnica de otimização de consultas. Ao separar os dados logicamente com base em uma chave, cada partição pode ser manipulada de forma independente, por exemplo, excluída. Isso permite mover partições e, portanto, subconjuntos de dados, entre camadas de armazenamento com eficiência com base no tempo ou expirar dados/excluí-los do cluster com eficiência. No exemplo abaixo, removemos posts de 2008.
- Otimização de consultas - Embora as partições possam ajudar no desempenho das consultas, isso depende fortemente dos padrões de acesso. Se as consultas atingirem apenas algumas partições (de preferência, uma só), o desempenho pode melhorar. Em geral, isso só é útil se a chave de particionamento não estiver na chave primária e você estiver filtrando por ela. No entanto, consultas que precisam abranger muitas partições podem ter um desempenho pior do que sem particionamento (pois ele pode resultar em mais partes). O benefício de atingir uma única partição será ainda menos perceptível — ou até inexistente — se a chave de particionamento já aparecer no início da chave primária. O particionamento também pode ser usado para otimizar consultas GROUP BY se os valores em cada partição forem únicos. No entanto, em geral, você deve garantir que a chave primária esteja otimizada e só considerar o particionamento como técnica de otimização de consultas em casos excepcionais, nos quais os padrões de acesso se concentram em um subconjunto específico e previsível do dia, por exemplo, particionamento por dia, com a maioria das consultas voltada para o último dia.
Recomendações para partições
Internamente, o ClickHouse cria partes para os dados inseridos. À medida que mais dados são inseridos, o número de partes aumenta. Para evitar um número excessivamente alto de partes, o que degradará o desempenho da consulta (mais arquivos para ler), as partes são mescladas em um processo assíncrono em segundo plano. Se o número de partes exceder um limite pré-configurado, o ClickHouse lançará uma exceção na inserção, como um erro de “partes demais”. Isso não deve acontecer em condições normais de operação e só ocorre se o ClickHouse estiver mal configurado ou sendo usado incorretamente, por exemplo, com muitas inserções pequenas.
Como as partes são criadas de forma isolada para cada partição, aumentar o número de partições faz com que o número de partes também aumente, ou seja, ele será um múltiplo do número de partições. Portanto, chaves de particionamento de alta cardinalidade podem causar esse erro e devem ser evitadas.
Visões materializadas vs projeções
ORDER BY para uma tabela.
Na documentação de modelagem de dados do ClickHouse, exploramos como visões materializadas podem ser usadas no ClickHouse para pré-calcular agregações, transformar linhas e otimizar consultas para diferentes padrões de acesso.
Para este último caso, fornecemos um exemplo em que a visão materializada envia linhas para uma tabela de destino com uma chave de ordenação diferente da tabela original que recebe inserções.
Por exemplo, considere a seguinte consulta:
UserId não é a chave de ordenação.
Anteriormente, resolvemos isso usando uma visão materializada que atuava como lookup para o PostId. O mesmo problema pode ser resolvido
com uma projeção. O comando abaixo adiciona uma
projeção para o ORDER BY user_id.
ALTER, a criação será assíncrona quando o comando MATERIALIZE PROJECTION for executado. Você pode verificar o andamento dessa operação com a consulta a seguir, aguardando até is_done=1.
EXPLAIN, também confirmamos que a projeção foi usada para executar esta consulta:
Quando usar projeções
- É necessária uma reordenação completa dos dados. Embora a expressão na
projeção possa, em teoria, usar um
GROUP BY,as visões materializadas são mais eficazes para manter agregações. O otimizador de consultas também tende mais a aproveitar projeções que usam uma reordenação simples, ou seja,SELECT * ORDER BY x. Você pode selecionar um subconjunto de colunas nessa expressão para reduzir o uso de armazenamento. - Os usuários estão confortáveis com o aumento associado no uso de armazenamento e com a sobrecarga de gravar dados duas vezes. Teste o impacto na velocidade de inserção e avalie a sobrecarga de armazenamento.
Desde a versão 25.5, o ClickHouse oferece suporte à coluna virtual
_part_offset em
projeções. Isso permite uma forma mais eficiente em termos de espaço de armazenar projeções.Para mais detalhes, consulte “Projeções”