Este guia pressupõe que os usuários já estejam familiarizados com o conceito de visões materializadas incrementais e carregamento de dados com funções de tabela, como s3 e gcs. Também recomendamos que os usuários leiam nosso guia sobre como otimizar o desempenho de inserção a partir do armazenamento de objetos, cujas orientações podem ser aplicadas às inserções ao longo deste guia.
Conjunto de dados de exemplo
pip.
Por exemplo, o subconjunto abrange um único dia — 2024-12-17 — e está disponível publicamente em https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/2024-12-17/. Você pode fazer consultas com:
O conjunto de dados completo do PyPI, com mais de 1 trilhão de linhas, está disponível em nosso ambiente público de demonstração clickpy.clickhouse.com. Para mais detalhes sobre esse conjunto de dados, incluindo como a demonstração aproveita visões materializadas para melhorar o desempenho e como os dados são carregados diariamente, veja aqui.
Cenários de backfill
- Backfill de dados com ingestão de dados existente - Novos dados estão sendo carregados, e os dados históricos precisam passar por backfill. Esses dados históricos já foram identificados.
- Adição de visões materializadas a tabelas existentes - Novas visões materializadas precisam ser adicionadas a uma configuração em que os dados históricos já foram preenchidos e os dados já estão em streaming.
Usando tabelas duplicadas e views
pypi e visão materializada, que calcula o número de downloads por projeto Python:
{101..200}. Embora pudéssemos inserir diretamente em pypi, podemos realizar esse backfill de forma isolada criando tabelas duplicadas.
Se o backfill falhar, nossas tabelas principais não serão afetadas e podemos simplesmente truncar as tabelas duplicadas e repetir o processo.
Para criar novas cópias dessas views, podemos usar a cláusula CREATE TABLE AS com o sufixo _v2:
pypi_v2 e pypi_downloads_v2 e repetir a carga de dados.
Com a carga de dados concluída, podemos mover os dados das nossas tabelas duplicadas para as tabelas principais usando a cláusula ALTER TABLE MOVE PARTITION.
Nomes das partiçõesA chamada
MOVE PARTITION acima usa o nome de partição (). Isso representa a única partição dessa tabela (que não é particionada). Para tabelas particionadas, será necessário invocar várias chamadas MOVE PARTITION — uma para cada partição. O nome das partições atuais pode ser obtido na tabela system.parts, por exemplo, SELECT DISTINCT partition FROM system.parts WHERE (table = 'pypi_v2').pypi e pypi_downloads contêm todos os dados. pypi_downloads_v2 e pypi_v2 podem ser removidas com segurança.
MOVE PARTITION é leve (aproveitando hard links) e atômica, ou seja, ou falha ou é concluída com sucesso, sem nenhum estado intermediário.
Exploramos bastante esse processo em nossos cenários de backfill abaixo.
Observe que esse processo exige que os usuários escolham o tamanho de cada operação de inserção.
Inserções maiores, ou seja, mais linhas, significam que serão necessárias menos operações MOVE PARTITION. No entanto, isso precisa ser equilibrado com o custo de recuperação em caso de falha na inserção, por exemplo, devido a uma interrupção de rede. Você pode complementar esse processo com o agrupamento de arquivos em lotes para reduzir o risco. Isso pode ser feito com consultas de intervalo, por exemplo, WHERE timestamp BETWEEN 2024-12-17 09:00:00 AND 2024-12-17 10:00:00, ou com padrões glob. Por exemplo,
O ClickPipes usa essa abordagem ao carregar dados do armazenamento de objetos, criando automaticamente duplicatas da tabela de destino e de suas visões materializadas, sem exigir que o usuário execute as etapas acima. Além disso, ao usar várias threads de trabalho, cada uma lidando com subconjuntos diferentes (por meio de padrões glob) e com suas próprias tabelas duplicadas, os dados podem ser carregados rapidamente com semântica de exactly-once. Para quem se interessar, mais detalhes podem ser encontrados neste blog.
Cenário 1: Backfill de dados com ingestão de dados existente
- Identifique o checkpoint — um timestamp ou valor de coluna a partir do qual os dados históricos precisam ser restaurados.
- Crie duplicatas da tabela principal e das tabelas de destino das visões materializadas.
- Crie cópias de quaisquer visões materializadas que apontem para as tabelas de destino criadas na etapa (2).
- Insira os dados na tabela principal duplicada criada na etapa (2).
- Mova todas as partições das tabelas duplicadas para suas versões originais. Remova as tabelas duplicadas.
2024-12-17 09:00:00. Usando o processo anterior, criamos tabelas duplicadas e visões materializadas e carregamos o subconjunto usando um filtro pelo timestamp.
Aplicar filtros em colunas de timestamp no Parquet pode ser muito eficiente. O ClickHouse lerá apenas a coluna de timestamp para identificar as faixas completas de dados a serem carregadas, minimizando o tráfego de rede. Índices do Parquet, como min-max, também podem ser aproveitados pelo mecanismo de consultas do ClickHouse.
Use ClickPipes no ClickHouse CloudSe você estiver usando ClickHouse Cloud, deverá usar ClickPipes para restaurar backups históricos se os dados puderem ser isolados em seu próprio bucket (e um filtro não for necessário). Além de reduzir o tempo de carregamento ao paralelizar o processo com vários workers, o ClickPipes automatiza o processo acima e cria tabelas duplicadas tanto para a tabela principal quanto para as visões materializadas.
Cenário 2: Adicionar visões materializadas a tabelas existentes
timestamp ou monotonicamente crescente, que possa ser usada para identificar um ponto no fluxo, é útil nesse caso e evita pausas na ingestão de dados. Nos exemplos abaixo, assumimos ambos os casos e priorizamos abordagens que evitem pausas na ingestão.
Evite POPULATENão recomendamos usar o comando
POPULATE para preencher visões materializadas com dados históricos, exceto em conjuntos de dados pequenos em que a ingestão esteja pausada. Esse comando pode deixar de capturar linhas inseridas na tabela de origem, já que a visão materializada só é criada depois que o POPULATE termina. Além disso, o POPULATE é executado sobre todos os dados e está sujeito a interrupções ou limites de memória em conjuntos de dados grandes.Timestamp ou coluna monotonicamente crescente disponível
- Criar nossa visão materializada com um filtro que considere apenas linhas posteriores a um horário arbitrário em um futuro próximo.
- Executar uma consulta
INSERT INTO SELECTque insira na tabela de destino da nossa visão materializada, lendo da tabela de origem com a consulta de agregação da visão.
SELECT para incluir um filtro que considera apenas linhas com valor superior a um horário arbitrário em um futuro próximo — neste caso, assumimos que 2024-12-17 09:00:00 está alguns minutos no futuro.
INSERT INTO SELECT. Por exemplo, para a visão acima:
No exemplo acima, nossa tabela de destino é uma SummingMergeTree. Nesse caso, podemos simplesmente usar nossa consulta de agregação original. Para casos de uso mais complexos que usam a AggregatingMergeTree, você usará funções
-State para as agregações. Um exemplo disso pode ser encontrado neste guia de integração.pypi_downloads_per_day_v2, inserir nela e anexar as partições resultantes a pypi_downloads_per_day.
Muitas vezes, a consulta da visão materializada pode ser mais complexa (o que não é incomum; caso contrário, os usuários não usariam uma visão!) e consumir recursos. Em casos mais raros, os recursos exigidos pela consulta podem exceder os do servidor. Isso destaca uma das vantagens das visões materializadas do ClickHouse: elas são incrementais e não processam todo o conjunto de dados de uma só vez!
Nesse caso, os usuários têm várias opções:
- Modifique sua consulta para fazer backfill de intervalos, por exemplo,
WHERE timestamp BETWEEN 2024-12-17 08:00:00 AND 2024-12-17 09:00:00,WHERE timestamp BETWEEN 2024-12-17 07:00:00 AND 2024-12-17 08:00:00etc. - Use um Null table engine para preencher a visão materializada. Isso replica o preenchimento incremental típico de uma visão materializada, executando a consulta sobre blocos de dados (com tamanho configurável).
Usando o Null table engine para popular visões materializadas
/dev/null do mundo dos motores de tabela). Embora isso pareça contraditório, as visões materializadas ainda serão executadas sobre os dados inseridos nesse motor de tabela. Isso permite construir visões materializadas sem persistir os dados originais, evitando E/S e o armazenamento associado.
É importante destacar que quaisquer visões materializadas associadas ao motor de tabela ainda são executadas sobre blocos de dados à medida que são inseridos, enviando seus resultados para uma tabela de destino. Esses blocos têm tamanho configurável. Embora blocos maiores possam ser potencialmente mais eficientes (e mais rápidos de processar), eles consomem mais recursos (principalmente memória). O uso desse motor de tabela significa que podemos construir nossa visão materializada de forma incremental, isto é, um bloco por vez, evitando a necessidade de manter toda a agregação em memória.
Considere o exemplo a seguir:
pypi_v2, para receber as linhas que serão usadas para criar nossa visão materializada. Observe como limitamos o schema apenas às colunas de que precisamos. Nossa visão materializada realiza uma agregação sobre as linhas inseridas nessa tabela (um bloco por vez), enviando os resultados para nossa tabela de destino, pypi_downloads_per_day.
Usamos
pypi_downloads_per_day como nossa tabela de destino aqui. Para maior resiliência, os usuários podem criar uma tabela duplicada, pypi_downloads_per_day_v2, e usá-la como tabela de destino da visão, como mostrado nos exemplos anteriores. Após a conclusão da inserção, as partições em pypi_downloads_per_day_v2 poderiam, por sua vez, ser movidas para pypi_downloads_per_day. Isso permitiria a recuperação caso nossa inserção falhe devido a problemas de memória ou interrupções no servidor; ou seja, basta truncar pypi_downloads_per_day_v2, ajustar as configurações e tentar novamente.pypi_v2 os dados relevantes para o backfill a partir de pypi.
639.47 MiB.
Ajuste de desempenho & recursos
- Paralelismo de leitura - O número de threads usadas na leitura. Controlado por
max_threads. No ClickHouse Cloud, isso é determinado pelo tamanho da instância, com padrão igual ao número de vCPUs. Aumentar esse valor pode melhorar o desempenho de leitura, ao custo de maior uso de memória. - Paralelismo de inserção - O número de
threadsde inserção usadas na operação de inserção. Controlado por meio demax_insert_threads. Observação: esse valor é limitado pormax_threads, portanto o paralelismo de inserção efetivo émin(max_insert_threads, max_threads). No ClickHouse Cloud, isso é determinado pelo tamanho da instância (entre 2 e 4) e, no OSS, é definido como 1. Aumentar esse valor pode melhorar o desempenho, ao custo de maior uso de memória. - Tamanho do bloco de inserção - os dados são processados em um loop no qual são extraídos, analisados e agrupados em blocos de inserção na memória com base na chave de particionamento. Esses blocos são ordenados, otimizados, comprimidos e gravados no armazenamento como novas partes de dados. O tamanho do bloco de inserção, controlado pelas configurações
min_insert_block_size_rowsemin_insert_block_size_bytes(não comprimido), afeta o uso de memória e a E/S de disco. Blocos maiores usam mais memória, mas criam menos partes, reduzindo a E/S e os merges em segundo plano. Essas configurações representam limites mínimos (o que for atingido primeiro dispara um flush). - Tamanho do bloco da visão materializada - Além da mecânica descrita acima para a
insertprincipal, antes da inserção em visões materializadas, os blocos também são agrupados para tornar o processamento mais eficiente. O tamanho desses blocos é determinado pelas configuraçõesmin_insert_block_size_bytes_for_materialized_viewsemin_insert_block_size_rows_for_materialized_views. Blocos maiores permitem um processamento mais eficiente, à custa de maior uso de memória. Por padrão, essas configurações assumem, respectivamente, os valores das configurações da tabela de origemmin_insert_block_size_rowsemin_insert_block_size_bytes.
Dica para consultas INSERT SELECT triviais: Para consultas simples
INSERT INTO t1 SELECT * FROM t2, sem transformações complexas, considere habilitar optimize_trivial_insert_select=1. Essa configuração (desabilitada por padrão desde a versão 24.7) ajusta automaticamente o paralelismo do SELECT para corresponder a max_insert_threads, reduzindo o uso de recursos e o número de partes criadas. Isso é particularmente útil para migrações em massa de dados entre tabelas.min_insert_block_size_bytes_for_materialized_views e min_insert_block_size_rows_for_materialized_views para melhorar o desempenho. Se essas configurações forem modificadas, aplique as mesmas boas práticas discutidas para min_insert_block_size_rows e min_insert_block_size_bytes.
Para minimizar o uso de memória, vale experimentar essas configurações. Isso inevitavelmente reduzirá o desempenho. Usando a consulta anterior, apresentamos exemplos abaixo.
Reduzir max_insert_threads para 1 diminui o overhead de memória.
max_threads para 1.
min_insert_block_size_rows como 0 (o que o desativa como fator determinante do tamanho do bloco) e min_insert_block_size_bytes como 10485760 (10MiB).
Sem timestamp ou coluna monotonicamente crescente
- Pause as inserções na tabela principal.
- Crie uma duplicata da tabela de destino principal usando a sintaxe
CREATE AS. - Anexe partições da tabela de destino original à duplicata usando
ALTER TABLE ATTACH. Observação: Essa operação de anexação é diferente da operação de movimentação usada anteriormente. Embora use links físicos, os dados da tabela original são preservados. - Crie novas visões materializadas.
- Reinicie as inserções. Observação: As inserções atualizarão apenas a tabela de destino, e não a duplicata, que fará referência somente aos dados originais.
- Faça o backfill da visão materializada, aplicando o mesmo processo usado acima para dados com timestamps, usando a tabela duplicada como origem.
pypi_downloads_per_day (vamos supor que não podemos usar o timestamp):
pypi_downloads_per_day usando nossa abordagem simples de INSERT INTO SELECT, descrita anteriormente. Isso também pode ser aprimorado com a abordagem da tabela Null documentada acima, com o uso opcional de uma tabela duplicada para maior resiliência.
Embora essa operação exija pausar os inserts, as operações intermediárias normalmente podem ser concluídas rapidamente, minimizando qualquer interrupção na ingestão de dados.