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. Portanto, as consultas podem produzir respostas incorretas, já que linhas de atualização e exclusão podem ser consideradas nas consultas.
Para obter respostas corretas, você precisará complementar as mesclagens em segundo plano com desduplicação em tempo de consulta e remoção de exclusões. Isso pode ser feito usando o operador FINAL.
Considere a tabela Posts acima. Podemos usar o método normal de carregamento desse conjunto de dados, mas especificar as colunas deleted e version, além de valores 0. Para fins de exemplo, carregamos apenas 10000 linhas.
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 de 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.