Upserts automáticos de linhas inseridas
ALTER ou DELETE ineficientes, oferecendo a possibilidade de inserir várias cópias da mesma linha e indicar uma delas como a versão mais recente. Um processo em segundo plano, por sua vez, remove de forma assíncrona as versões mais antigas da mesma linha, simulando com eficiência uma operação de atualização por meio de inserções imutáveis.
Isso depende da capacidade do motor de tabela de identificar linhas duplicadas. Isso é feito usando a cláusula ORDER BY para determinar a unicidade; ou seja, se duas linhas têm os mesmos valores nas colunas especificadas em ORDER BY, elas são consideradas duplicadas. Uma coluna version, especificada na definição da tabela, permite manter a versão mais recente de uma linha quando duas linhas são identificadas como duplicadas, ou seja, a linha com o maior valor de versão é preservada.
Ilustramos esse processo no exemplo abaixo. Aqui, as linhas são identificadas de forma única pela coluna A (o ORDER BY da tabela). Pressupomos que essas linhas tenham sido inseridas em dois batches, resultando na formação de duas partes de dados em disco. Mais tarde, durante um processo assíncrono em segundo plano, essas partes são mescladas.
O ReplacingMergeTree também permite especificar uma coluna deleted. Ela pode conter 0 ou 1, sendo que o valor 1 indica que a linha (e suas duplicatas) foi excluída, e 0 é usado caso contrário. Observação: linhas excluídas não são removidas durante a mesclagem.
Durante esse processo, ocorre o seguinte na mesclagem de partes:
- A linha identificada pelo valor 1 na coluna A tem tanto uma linha de atualização com versão 2 quanto uma linha de exclusão com versão 3 (e valor 1 na coluna deleted). A linha mais recente, marcada como excluída, é, portanto, mantida.
- A linha identificada pelo valor 2 na coluna A tem duas linhas de atualização. A linha mais recente é mantida com o valor 6 na coluna price.
- A linha identificada pelo valor 3 na coluna A tem uma linha com versão 1 e uma linha de exclusão com versão 2. Essa linha de exclusão é mantida.
Observe que linhas excluídas nunca são removidas. Elas podem ser removidas à força com um
OPTIMIZE table FINAL CLEANUP. Isso exige a configuração experimental allow_experimental_replacing_merge_with_cleanup=1. Isso só deve ser executado nas seguintes condições:
- Você deve ter certeza de que nenhuma linha com versões antigas (daquelas que estão sendo excluídas com o cleanup) será inserida depois que a operação for executada. Se isso acontecer, elas serão mantidas incorretamente, já que as linhas excluídas não estarão mais presentes.
- Certifique-se de que todas as réplicas estejam sincronizadas antes de executar o cleanup. Isso pode ser feito com o comando:
O tratamento de exclusões com o ReplacingMergeTree só é recomendado para tabelas com um número baixo a moderado de exclusões (menos de 10%), a menos que seja possível agendar períodos de limpeza nas condições acima.
Dica: Você também pode executar OPTIMIZE FINAL CLEANUP em partições específicas que não estejam mais sujeitas a alterações.
Escolhendo uma chave primária/de desduplicação
ORDER BY identificam exclusivamente uma linha ao longo das alterações. Portanto, ao migrar de um banco de dados transacional como o Postgres, a chave primária original do Postgres deve ser incluída na cláusula ORDER BY do ClickHouse.
Os usuários do ClickHouse já estarão familiarizados com a escolha das colunas na cláusula ORDER BY de suas tabelas para otimizar o desempenho das consultas. Em geral, essas colunas devem ser selecionadas com base nas suas consultas mais frequentes e listadas em ordem crescente de cardinalidade. É importante destacar que o ReplacingMergeTree impõe uma restrição adicional: essas colunas devem ser imutáveis, ou seja, ao replicar do Postgres, só adicione colunas a essa cláusula se elas não mudarem nos dados subjacentes do Postgres. Embora outras colunas possam mudar, estas precisam permanecer consistentes para garantir a identificação exclusiva da linha.
Para cargas de trabalho analíticas, a chave primária do Postgres geralmente tem pouca utilidade, pois você raramente fará buscas pontuais por linha. Como recomendamos que as colunas sejam ordenadas em ordem crescente de cardinalidade, além do fato de que correspondências em colunas listadas antes no ORDER BY normalmente serão mais rápidas, a chave primária do Postgres deve ser acrescentada ao final do ORDER BY (a menos que tenha valor analítico). Caso várias colunas formem uma chave primária no Postgres, elas devem ser acrescentadas ao ORDER BY, respeitando a cardinalidade e a probabilidade de serem úteis nas consultas. Você também pode querer gerar uma chave primária exclusiva usando uma concatenação de valores por meio de uma coluna MATERIALIZED.
Considere a tabela posts do conjunto de dados Stack Overflow.
ORDER BY (PostTypeId, toDate(CreationDate), CreationDate, Id). A coluna Id, única para cada post, garante que as linhas possam ser deduplicadas. As colunas Version e Deleted são adicionadas ao schema, conforme necessário.
Consultando ReplacingMergeTree
ORDER BY como identificador único e mantém apenas a versão mais alta, ou remove todas as duplicatas se a versão mais recente indicar uma exclusão. No entanto, isso oferece apenas correção eventual — não garante que as linhas serão desduplicadas, e você não deve confiar nisso.
Use FINAL para ler dados desduplicadosComo a desduplicação só acontece durante as mesclagens em segundo plano, um
SELECT simples ainda pode retornar linhas duplicadas ou excluídas. Para ler resultados corretos em tempo de consulta, use o modificador FINAL, que conclui a desduplicação e a remoção de exclusões enquanto a consulta é executada.INSERT INTO SELECT:
INSERT INTO SELECT.
FINAL à tabela retorna o resultado correto.
Desempenho do FINAL
FINAL de fato impõe uma pequena sobrecarga de desempenho às consultas.
Isso fica mais perceptível quando as consultas não filtram pelas colunas da chave primária,
o que faz com que mais dados sejam lidos e aumenta a sobrecarga de desduplicação. Se você
filtrar pelas colunas de chave usando uma condição WHERE, a quantidade de dados carregados e enviados para
desduplicação será reduzida.
Se a condição WHERE não usar uma coluna de chave, o ClickHouse atualmente não utiliza a otimização PREWHERE ao usar FINAL. Essa otimização busca reduzir o número de linhas lidas em colunas não filtradas. Exemplos de como emular esse PREWHERE e, assim, potencialmente melhorar o desempenho podem ser encontrados aqui.
Aproveitando partições com ReplacingMergeTree
do_not_merge_across_partitions_select_final=1 para melhorar o desempenho de consultas com FINAL. Essa configuração faz com que as partições sejam mescladas e processadas de forma independente ao usar FINAL.
Considere a seguinte tabela posts, na qual não usamos particionamento:
FINAL tenha trabalho a fazer, atualizamos 1 milhão de linhas, incrementando AnswerCount por meio da inserção de linhas duplicadas.
FINAL:
do_not_merge_across_partitions_select_final=1.
Considerações sobre o comportamento de mesclagem
Lógica de seleção de mesclagem
Comportamento da mesclagem em partes grandes
max_bytes_to_merge_at_max_space_in_pool, ela deixa de ser selecionada para novas mesclagens, mesmo que min_age_to_force_merge_seconds esteja definido. Como resultado, não se pode mais contar com as mesclagens automáticas para remover duplicatas que possam se acumular com a inserção contínua de dados.
Para resolver isso, você pode executar OPTIMIZE FINAL para mesclar manualmente as partes e remover duplicatas. Diferentemente das mesclagens automáticas, OPTIMIZE FINAL ignora o limite de max_bytes_to_merge_at_max_space_in_pool, mesclando partes com base apenas nos recursos disponíveis, especialmente espaço em disco, até que reste uma única parte em cada partição. No entanto, essa abordagem pode consumir muita memória em tabelas grandes e pode exigir execuções repetidas à medida que novos dados são adicionados.
Para uma solução mais sustentável que mantenha o desempenho, recomenda-se particionar a tabela. Isso pode ajudar a evitar que as partes de dados atinjam o tamanho máximo de mesclagem e reduzir a necessidade de otimizações manuais contínuas.
Particionamento e mesclagem entre partições
Ajustando mesclagens para melhorar o desempenho das consultas
min_age_to_force_merge_seconds e min_age_to_force_merge_on_partition_only são definidos como 0 e false, respectivamente, o que desativa esses recursos. Nessa configuração, o ClickHouse aplicará o comportamento padrão de mesclagem, sem forçar mesclagens com base na idade da partição.
Se um valor for especificado para min_age_to_force_merge_seconds, o ClickHouse ignorará as heurísticas normais de mesclagem para partes mais antigas do que o período especificado. Embora isso geralmente só seja eficaz quando o objetivo é minimizar o número total de partes, pode melhorar o desempenho das consultas no ReplacingMergeTree ao reduzir o número de partes que precisam ser mescladas no momento da consulta.
Esse comportamento pode ser ajustado ainda mais definindo min_age_to_force_merge_on_partition_only=true, o que exige que todas as partes da partição sejam mais antigas do que min_age_to_force_merge_seconds para que ocorra uma mesclagem agressiva. Essa configuração permite que partições mais antigas sejam gradualmente mescladas até se tornarem uma única parte, o que consolida os dados e mantém o desempenho das consultas.
Configurações recomendadas
min_age_to_force_merge_seconds com um valor baixo — significativamente menor que o período da partição. Isso minimiza o número de partes e evita mesclagens desnecessárias no momento da consulta com o operador FINAL.
Por exemplo, considere uma partição mensal que já tenha sido mesclada em uma única parte. Se um insert pequeno e isolado criar uma nova parte nessa partição, o desempenho da consulta pode ser prejudicado, porque o ClickHouse precisará ler várias partes até que a mesclagem seja concluída. Definir min_age_to_force_merge_seconds pode garantir que essas partes sejam mescladas de forma agressiva, evitando a degradação do desempenho da consulta.