Elasticsearch MigrationClickHouse Workshops

Raciocínio dos cenários aplicados

Raciocínio detalhado do ClickHouse por trás dos cinco cenários de múltipla escolha aplicados.

Primeiro, conclua as 20 questões aplicadas de múltipla escolha na avaliação interativa da Parte 4. Os cinco antigos cenários escritos agora aparecem como quatro decisões pontuadas cada. Esta referência mantém os exemplos detalhados para discussão após a avaliação.


Cenário 1 — Projeto do esquema

CREATE TABLE modelo

CREATE TABLE IF NOT EXISTS clickstream_events
(
    `event_id`   UUID                                                  CODEC(ZSTD(1)),
    `event_time` DateTime64(3)                                          CODEC(Delta, ZSTD(1)),
    `event_date` Date          DEFAULT toDate(event_time),
    `user_id`    UUID                                                  CODEC(ZSTD(1)),
    `session_id` UUID                                                  CODEC(ZSTD(1)),
    `product_id` String                                                CODEC(ZSTD(1)),
    `event_type` LowCardinality(String)                                CODEC(ZSTD(1)),
    `value_usd`  Decimal(18, 4)                                        CODEC(ZSTD(1)),
    `attributes` Map(LowCardinality(String), String)                   CODEC(ZSTD(1)),

    -- Skip indexes for non-prefix point-lookups
    INDEX idx_user_id    user_id    TYPE bloom_filter(0.01) GRANULARITY 4,
    INDEX idx_session_id session_id TYPE bloom_filter(0.01) GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY event_date
ORDER BY (event_type, product_id, event_time)
TTL event_date + INTERVAL 90 DAY DELETE
SETTINGS ttl_only_drop_parts = 1, index_granularity = 8192;

Justificativa

  • Escolhas de tipos:

    • event_id → UUID (16 bytes contra 36 se armazenado como String; a coluna raramente é filtrada, apenas armazenada).
    • user_id / session_id → UUID pelo mesmo motivo; cardinalidade de ~50 milhões / ~500 milhões significa que não se deve usar LowCardinality.
    • event_type → LowCardinality(String) — 12 valores distintos é o caso clássico. Também seria possível usar Enum, mas LowCardinality(String) funciona melhor no caso inevitável de alguém adicionar um 13º tipo de evento sem reimplantar o esquema.
    • product_id → String; cerca de 2 milhões de valores distintos supera muito a faixa em que LowCardinality(String) costuma ser recomendado. Testes de desempenho continuam importantes, mas String simples é o padrão defensável.
    • attributes → Map(LowCardinality(String), String) é o padrão seguro para evolução de esquema. Promova chaves muito usadas a colunas MATERIALIZED ao longo do tempo.
    • value_usd → Decimal(18, 4) para evitar arredondamento de ponto flutuante em valores de receita. O comportamento NULL ou zero é adequado; não há necessidade de Nullable.
  • ORDER BY (event_type, product_id, event_time):

    • A Consulta nº 1 (funil por product_id) começa com WHERE event_type IN ('view','add_to_cart','purchase') AND product_id = X — ambas as colunas do prefixo são usadas. A poda de grânulos é enorme.
    • A Consulta nº 3 (receita por minuto) filtra por WHERE event_type = 'purchase' — também usa o prefixo.
    • A Consulta nº 2 (histórico por usuário) não usa o prefixo — para isso serve o índice de salto bloom_filter em user_id.
    • Regra de ordenação por cardinalidade: baixa (12 valores) → média (2 milhões) → alta/intervalo (timestamp). Colunas iniciais de baixa cardinalidade aumentam o poder de salto do índice.
  • PARTITION BY event_date:

    • Uma partição por dia; com 5 bilhões de eventos/dia, a partição permanece grande o bastante para evitar o antipadrão de "milhares de partições minúsculas".
    • Essencialmente, isso se alinha ao limite de TTL, permitindo que ttl_only_drop_parts = 1 remova partições inteiras com baixo custo.
    • Não particione por hora nem por event_type; ambas as opções causam uma explosão no número de partições.
  • TTL event_date + INTERVAL 90 DAY DELETE com ttl_only_drop_parts = 1: remove partições diárias inteiras com 90 dias em uma única operação. Sem varredura linha a linha.

  • Índices de salto: bloom_filter em user_id (condição de join da Consulta nº 2) e session_id (provável necessidade futura). Não use tokenbf_v1 aqui — são UUIDs, não texto. Granularidade = 4 significa que cada Bloom abrange 4 × 8192 = ~32 mil linhas; ajuste medindo a latência das buscas.

  • AggregatingMergeTree para a Consulta nº 3? Sim — defina uma MV:

    CREATE MATERIALIZED VIEW revenue_by_minute_mv
    TO revenue_by_minute AS
    SELECT
        toStartOfMinute(event_time) AS minute,
        product_id,
        sumState(value_usd)         AS revenue,
        countState()                AS purchase_count
    FROM clickstream_events
    WHERE event_type = 'purchase'
    GROUP BY minute, product_id;

    Com 5 bilhões de eventos/dia, consultas de dashboard na tabela bruta serão lentas sob contenção; a pré-agregação do AggMergeTree transfere o custo para a inserção. A Consulta nº 1 (funil) e a Consulta nº 2 (por usuário) não devem receber uma MV — têm cardinalidade alta e variam demais para pré-agregação.

Critérios de decisão

As questões relacionadas recompensam respostas que:

  • Usam MergeTree com um ORDER BY sensato, cujo prefixo corresponde à Consulta nº 1 ou à Consulta nº 3 (ou seja, não (event_id, event_time) nem (event_time, …))
  • Particionam por data (ou semana), não por event_type nem por hora
  • Usam Map (ou JSON) para attributes, não uma coluna por atributo
  • Têm pelo menos um índice de salto bloom_filter em uma coluna de alta cardinalidade usada pela Consulta nº 2
  • Incluem uma cláusula TTL alinhada ao limite da partição

Uma alternativa enganosa está errada quando coloca event_time primeiro no ORDER BY (um instinto comum do ES que prejudica o ClickHouse) ou particiona por algo diferente de uma coluna derivada de data.


Cenário 2 — Plano de migração

Plano-modelo de 6 meses

Fase 1 (semanas 1–2): Descoberta e sandbox. Crie um serviço ClickHouse Cloud da camada Production na mesma região do ES. Provisione um cluster paralelo do OTel Collector (3 nós, atrás de LB) que leia uma derivação do caminho de ingestão existente — inicialmente apenas gravando no CH, SEM receber tráfego de produção diretamente. Objetivo: validar a conexão, estabelecer a linha de base dos custos com 200 mil eventos/s sustentados e comprovar o tratamento de contrapressão. Critério de saída: O CH ingere 1 hora de dados em fluxo duplo com desvio ≤ 1% na verificação de paridade (contagem do ES contra contagem do CH).

Fase 2 (semanas 3–6): Projeto do esquema + 5 primeiros índices de alto valor. Mapeie os requisitos de campos dos 5 dashboards mais consultados do Kibana. Crie instruções CREATE TABLE com Map(LowCardinality(String), String) por padrão para os mais de 60 campos extraídos por grok, mas materialize os 10 campos mais usados (referenciados por ≥3 dashboards) como colunas dedicadas. Defina a distribuição de views materializadas para todas as pré-agregações AggregatingMergeTree necessárias. Critério de saída: As consultas principais de todos os 20 dashboards do Kibana têm um equivalente funcional no CH e retornam resultados no mesmo tempo do ES (ou mais rapidamente).

Fase 3 (semanas 7–12): Execução paralela + migração de dashboards. Altere o OTel Collector para gravação dupla (ES + CH). Migre dashboards em ondas de ~50 pesquisas salvas por semana, priorizando as que fazem muita leitura (varrem mais de 1 TB). Execute o script de validação duas vezes ao dia; investigue qualquer serviço cuja contagem do ES contra CH tenha desvio superior a 5%. Critério de saída: Todas as 300 pesquisas salvas funcionam no CH; latência p95 das consultas ≤ p95 do ES por 7 dias consecutivos.

Fase 4 (semanas 13–18): Desativação do Logstash. Substitua o Logstash pelo OTel Collector. Os mais de 60 padrões grok viram processadores transform do OTel e colunas MATERIALIZED do CH. (Veja a decisão sobre o Logstash abaixo.) Execute Logstash + OTel Collector em paralelo por 2 semanas; remova o Logstash após confirmar a paridade. Critério de saída: Nenhum tráfego passa pelo Logstash por 7 dias consecutivos; regras de alerta migradas para HyperDX Alerts (ou equivalente).

Fase 5 (semanas 19–22): Transição. Interrompa Filebeat → Logstash. Exclua o Logstash. Interrompa a ingestão de dados novos no ES (mantenha o cluster ativo, somente para leitura). Todos os dados novos fluem sem ES. Monitore os dashboards de custos e a latência dos dashboards. Critério de saída: O ES permanece somente para leitura por 14 dias consecutivos, sem ingestão, incidentes ou pedidos de reversão.

Fase 6 (semanas 23–26): Desativação e transferência para conformidade. Snapshot final do ES para o S3 (único, completo, na data da transição). Desative o cluster ES. Documente a localização do bucket S3 para conformidade/auditoria. Mude o modelo mental da equipe de "o ES é a fonte da verdade" para "o ES é o retrovisor de 90 dias". Critério de saída: Cluster ES destruído, snapshot do S3 validado pela equipe de conformidade e runbook atualizado.


Estratégia de esquema para mais de 60 campos extraídos por grok

  • Mês 1: Todos os 60 campos chegam a LogAttributes Map(LowCardinality(String), String). Nenhum tratamento especial.
  • Mês 2: Identifique os 10 principais campos pela contagem de referências nos dashboards. Promova-os com ALTER TABLE … ADD COLUMN field MATERIALIZED LogAttributes['field'].
  • Mês 4: Execute novamente a consulta de referências dos dashboards; promova todos os novos campos muito usados. A essa altura, a cauda longa está no Map e permanece lá.
  • Mês 6: Faça uma auditoria final dos padrões de consulta dos dashboards. O esquema geralmente converge para 15–25 colunas promovidas + o Map para a cauda longa.

Isso evita a armadilha de "projetar o esquema perfeito logo de início", que inviabiliza projetos de migração.


Decisão sobre o Logstash: substituir pelo OTel Collector

Os mais de 60 padrões grok são o principal fator de custo do Logstash, tanto em computação quanto em complexidade operacional. O processador transform (OTTL) do OTel Collector cobre nativamente ~80% dos casos de uso do grok, e os 20% restantes viram colunas MATERIALIZED no esquema de destino (transferindo o custo da análise do caminho do coletor para a camada de armazenamento, o que no ClickHouse significa custo de execução zero — o valor analisado é calculado uma vez na inserção e depois lido sem custo).

O Vector é uma alternativa viável — ele usa VRL (Vector Remap Language), mais familiar aos usuários do Logstash — mas introduz uma terceira ferramenta a operar. Mantenha o OTel Collector para preservar a consistência do ecossistema.

Não mantenha o Logstash. A proposta do laboratório é ter uma ferramenta a menos, não "ES + Logstash + Kibana → CH + Logstash + HyperDX".


Retenção para conformidade (camada fria de 1 ano)

O CH Cloud não tem ILM integrado para snapshots no S3, mas dois padrões funcionam:

  1. Exportação diária para o S3 do cliente. Execute de forma agendada INSERT INTO FUNCTION s3('s3://archive/year/month/day.parquet') SELECT * FROM otel_logs_v2 WHERE event_date = today() - 1. O bucket S3 do cliente mantém 1 ano de Parquet, consultável por Athena, Trino ou pela função de tabela s3() de um serviço CH temporário. O armazenamento S3 IA custa cerca de US$ 23/TB/mês, muito menos que manter 1 ano de dados ativos no CH.
  2. Usar o armazenamento em camadas do ClickHouse Cloud (quando disponível — normalmente na camada Production de determinados provedores). O custo de armazenamento após 90 dias cai ~70%, mas a auditoria de conformidade é mais complexa que na abordagem S3-Parquet, pois os dados continuam dentro do CH.

Recomendamos (1) para conformidade — o auditor quer que "mostre todos os logs de agosto de 2025" seja uma consulta self-service no S3, não um projeto de "vamos iniciar um cluster CH".


Plano de reversão se a transição falhar após 3 semanas

A reversão pressupõe que (a) você já está no modo de gravação dupla e (b) uma parte dos dashboards foi migrada.

  1. Interrompa as gravações somente no CH — reverta o OTel Collector para gravação única no ES; o CH para de crescer.
  2. Retorne os dashboards ao Kibana — as pesquisas salvas originais do Kibana ainda existem. Reative-as.
  3. Mantenha o CH em execução somente para análise forense — os dados ingeridos durante a janela de execução paralela são valiosos para depurar a falha da transição.
  4. Não exclua os artefatos migrados — o novo esquema do CH, as configurações do OTel Collector etc. permanecem. Após corrigir a causa raiz, recomece pela Fase 4 (desativação do Logstash), sem refazer as Fases 1–3.

A propriedade mais importante da migração é: em qualquer ponto das primeiras 5 fases, você pode voltar ao ES com uma única alteração de configuração do OTel Collector.


3 principais riscos + mitigações

  1. Risco: o projeto do esquema é fixado cedo demais. Mitigação: planeje explicitamente uma "revisão de campos mais usados" nos meses 2, 4 e 6. As promoções são instruções ALTER TABLE não destrutivas.
  2. Risco: 5 regras de alerta se comportam de modo diferente no CH. Mitigação: crie as MVs de alerta (conforme o módulo 03, Etapa 8 Opção B) na semana 8 e execute-as em modo sombra por 4 semanas antes de ativar avisos. Compare as contagens de disparos ao histórico de alertas existente no Kibana.
  3. Risco: os padrões grok do cliente incluem construções regex incompatíveis. Mitigação: enumere os 60 padrões na semana 1, identifique os incompatíveis (normalmente lookbehind, condicionais nomeadas etc.) e reescreva-os como OTTL ou colunas materializadas regexpExtract do CH. Não deixe para descobrir isso na semana 14.

Critérios de decisão

As questões relacionadas recompensam planos que:

  • Têm fases claras, cada uma com um critério de saída
  • Substituem o Logstash pelo OTel Collector (ou Vector) — manter o Logstash durante a transição é um erro
  • Usam Map + promoção seletiva para os 60 campos, não "criar todas as 60 colunas de início" nem "coluna JSON para tudo"
  • Indicam um padrão específico de retenção para conformidade (exportação S3 Parquet, armazenamento em camadas ou equivalente) — "resolveremos a conformidade depois" é um erro
  • Identificam um caminho de reversão que não exige reexecutar uma importação CSV

Cenário 3 — Depuração A (desvio da latência p95)

Resposta-modelo

As três causas mais prováveis, em ordem:

Causa 1: As colunas agregadas têm unidades diferentes. O ES geralmente armazenava a latência como número de ponto flutuante em milissegundos (response_time em segundos × 1000 em algumas configurações do ES, ou já em ms). O campo Duration do ClickHouse para traces está em nanossegundos; se a consulta do CH calcular quantile(0.95)(Duration) sem dividir por 1e6, você obterá um número ~1000× maior. Ou, de forma mais sutil, se a consulta do CH ler LogAttributes['run_time'], emitido pelo aplicativo em segundos, enquanto o ES tinha um campo run_time_ms analisado e já convertido, as unidades diferem silenciosamente. Verificação: Execute SELECT min(Duration), max(Duration), avg(Duration) FROM otel_traces e compare aos valores min/max/avg do ES para a mesma janela. Se as ordens de grandeza diferirem, o problema são as unidades.

Causa 2: Algoritmos de quantis diferentes. A agregação percentiles do Elasticsearch usa HDR Histogram ou T-Digest conforme a versão — ambos são algoritmos aproximados. quantile() do ClickHouse também é aproximado (um algoritmo determinístico, porém diferente). Em distribuições de latência com cauda longa, as duas aproximações podem divergir 10–25%, mesmo com dados de entrada idênticos. quantileExact() do ClickHouse é exato, porém lento; quantileTDigest() se aproxima mais do ES. Verificação: Execute a mesma consulta com quantileExact(0.95)(Duration) (lento, mas preciso). Se o valor exato ficar entre as aproximações do ES e do CH, os algoritmos simplesmente escolheram estratégias diferentes de estimativa de quantis.

Causa 3: Janelas de tempo ou conjuntos de amostras diferentes por desvio de relógio ou incompatibilidade de filtros. A consulta do CH pode incluir alguns segundos adicionais de tráfego em uma das extremidades da janela porque now() - INTERVAL 1 HOUR corresponde a um instante diferente daquele da consulta do ES, ou porque a coluna materializada TimestampTime é DateTime (precisão de segundos), enquanto o ES filtrou por @timestamp com precisão de milissegundos. Com uma cauda longa, até um desvio de 5 segundos pode alterar p95 em 20%. Verificação: Fixe a janela de tempo com BETWEEN '2026-05-09 04:00:00' AND '2026-05-09 05:00:00' explícito nos dois lados. Execute novamente. Se os resultados convergirem, o problema era o alinhamento do intervalo de tempo.

Critérios de decisão

As questões relacionadas recompensam um diagnóstico que inclui:

  • Uma causa de conversão de unidade / incompatibilidade de tipo (Duration em ns contra ms)
  • A diferença entre algoritmos de quantis OU o alinhamento da janela de tempo
  • Uma verificação de depuração específica para cada causa, não apenas "investigar"

Cenário 4 — Depuração B (busca lenta por chave de Map)

Diagnóstico

LogAttributes['request_id'] é uma desreferência de Map em tempo de execução — para cada linha varrida, o ClickHouse precisa buscar a chave 'request_id' dentro do Map. Nenhum índice de salto ajuda, e o armazenamento colunar ainda precisa materializar a coluna Map para todas as linhas do intervalo de varredura. Em contrapartida, TraceId é uma coluna String de nível superior com índice de salto bloom_filter no esquema de otel_logs_v2, portanto a pré-verificação por grânulo elimina ~99,9% dos grânulos antes de ler qualquer dado.

Portanto, a lentidão de 200× não ocorre porque Map é lento — ocorre porque a otimização do índice de salto não se aplica às chaves de Map.

Correção rápida: índice de salto com filtro de Bloom em mapValues

ALTER TABLE otel_logs_v2 ADD INDEX idx_request_id
    mapValues(LogAttributes)
    TYPE bloom_filter(0.01)
    GRANULARITY 4;

ALTER TABLE otel_logs_v2 MATERIALIZE INDEX idx_request_id;

SELECT *
FROM otel_logs_v2
WHERE indexHint(has(mapValues(LogAttributes), 'abc123'))
  AND LogAttributes['request_id'] = 'abc123';

Isso cria um Bloom por grânulo para todos os valores do Map. A dica compatível has(mapValues(...)) permite ao índice podar candidatos, enquanto o predicado com chave continua garantindo a correção no nível da linha. Contrapartida: o índice abrange todos os valores do Map, portanto há mais colisões que em um Bloom dedicado a uma coluna promovida. Meça o resultado com EXPLAIN indexes = 1; uma DDL bem-sucedida, por si só, não comprova uma poda útil.

Correção duradoura: promover request_id a coluna materializada

ALTER TABLE otel_logs_v2 ADD COLUMN request_id String MATERIALIZED LogAttributes['request_id'];
ALTER TABLE otel_logs_v2 ADD INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4;
ALTER TABLE otel_logs_v2 MATERIALIZE COLUMN request_id, INDEX idx_request_id;

Agora request_id é uma coluna de nível superior com compactação e filtro de Bloom próprios. Consulta: WHERE request_id = 'abc123' em vez de WHERE LogAttributes['request_id'] = 'abc123'. Contrapartida: cada promoção exige uma reescrita de coluna (uma passagem MATERIALIZE única). Vale a pena para chaves muito usadas; não para as chaves da cauda longa em LogAttributes.

Este é exatamente o padrão de evolução de esquema da Decisão 3 do ADR da Parte 2: usar Map por padrão e promover chaves muito usadas.

Critérios de decisão

As questões relacionadas recompensam um diagnóstico que:

  • Identifica que o problema é a "ausência de um caminho de índice de salto na desreferência de Map", não que "o ClickHouse é lento"
  • Indica o Bloom dos valores do Map + um predicado ou dica de índice compatível como correção rápida a ser medida
  • Indica a promoção a coluna como correção duradoura
  • Reconhece a contrapartida de cada opção (índice de Map = mais colisões; promoção = custo de ALTER)

Cenário 5 — Análise de contrapartidas (projetos de alertas)

Tabela comparativa — resposta-modelo

PropriedadeProjeto A (durante a consulta)Projeto B (MV pré-agregada)
Custo de leitura por avaliaçãoAlto — a cada 60 s, o ClickHouse varre 5 minutos de linhas brutas. Com 1 milhão de eventos/s × 300 s = 300 milhões de linhas varridas por avaliação.Baixo — a MV produz ~1 linha/minuto por serviço. A consulta de 5 minutos lê ~5 linhas. Resposta em microssegundos.
Custo de gravação por linha ingeridaZero — nenhum trabalho por linhaPequeno, porém não nulo — cada inserção aciona a atualização da agregação por bucket da MV. O custo é limitado pelo número de buckets, não de linhas. Empiricamente, menos de 5% de sobrecarga.
Atraso de atualização~60 s (intervalo de consulta) + latência da consulta~60 s (intervalo de consulta); a própria MV é atualizada de forma síncrona a cada inserção
Escala de cardinalidade (100 serviços × 50 endpoints)Mesmo custo de varredura independentemente da cardinalidade (varre a tabela bruta)Contagem de linhas da MV = 5.000 grupos serviço-endpoint × 1/min = 7,2 milhões de linhas/dia. Ainda minúsculo em relação aos eventos brutos; sem preocupação de escala

Recomendação para 1 milhão de eventos/s — O Projeto B vence

O ponto de equilíbrio entre A e B ocorre quando o custo de varredura da tabela bruta supera o custo de atualização da MV por linha. Com 1 milhão de eventos/s:

  • O Projeto A varre 300 milhões de linhas por avaliação. Mesmo com 1 GB/s por CPU e cache frio, são segundos por consulta, a cada 60 segundos, o dia inteiro. Ao longo de uma semana, isso representa milhões de segundos de CPU usados apenas em alertas.
  • O Projeto B amortiza a mesma agregação no caminho de inserção, em que o custo é pago uma vez por linha, em vez de pago novamente a cada minuto. A agregação durante a inserção também se beneficia dos lotes colunares; a atualização de estado por bucket é praticamente gratuita com o throughput SIMD moderno.

A intuição correta: alertas por nova varredura dos dados brutos são o padrão de transformação do Elasticsearch. Alertas por MVs pré-agregadas são o padrão nativo do ClickHouse. O volume transforma este último de conveniência em requisito por volta de 100 mil eventos/s; com 1 milhão de eventos/s, o Projeto A consumiria mais CPU que a própria carga de trabalho que deveria monitorar.

Observação: onde o Projeto A ainda faz sentido

  • Ambientes de baixo volume (menos de 10 mil eventos/s), nos quais o custo da avaliação é irrelevante
  • Alertas ad hoc ou temporários, nos quais você não quer provisionar uma MV
  • Alertas cujo formato de consulta muda semanalmente (o esquema da MV é fixo; consultas ad hoc são flexíveis)

Critérios de decisão

As questões relacionadas recompensam um raciocínio que:

  • Identifica corretamente o custo de leitura do Projeto B como O(buckets) e o do Projeto A como O(eventos brutos varridos)
  • Reconhece a contrapartida simétrica: A não paga nada na gravação e muito na leitura; B paga um pouco na gravação e quase nada na leitura
  • Escolhe o Projeto B com 1 milhão de eventos/s e explica pela amortização (não apenas "MVs são mais rápidas")
  • Recebe pontos extras por observar que este é o padrão de agregação durante a inserção contra durante a consulta, o mesmo demonstrado pelo AggregatingMergeTree logs_summary_1min da Parte 3

Revisão da pontuação aplicada

A avaliação pontuada exige 16 de 20 respostas aplicadas. Se sua pontuação ficou abaixo desse limite, use os critérios de decisão acima para encontrar o cenário que precisa de uma nova revisão. As lacunas mais comuns são:

  • Exercício 1: tratar o ClickHouse como Elasticsearch (armazenar uma coluna por atributo, particionar por event_type, ordenar primeiro por timestamp). Releia schema.sql e a planilha da Parte 2.
  • Exercício 2: esquecer a estratégia de reversão ou a resposta sobre conformidade. A narrativa do laboratório enfatiza repetidamente que a migração é reversível até a desativação — essa é a propriedade a internalizar.
  • Exercício 4: presumir que tipos Map são inerentemente lentos. Não são — o problema se resume às otimizações que se aplicam.

Volte à Parte 4 e refaça o componente aplicado após revisar o cenário em que houve erro.

Nesta página

PT