← Professional Data Engineer: pipelines e decisões de dados
11 / 15 · 135 MIN

Armazenamento, acesso e fidelidade dos resultados

Escolhe chaves, partições e modelos a partir dos acessos; compara eficiência sem perder movimentos elegíveis.

1. Escolher a partir das decisões do consumidor

Começa pelo acesso que o produto de dados precisa de suportar. Uma consulta de histórico de uma carteira, uma atualização transacional de dois registos e uma análise sobre meses de movimentos têm padrões diferentes. Regista granularidade, volume, frequência, latência tolerada, concorrência, consistência e forma de recuperação. Acrescenta o que o consumidor considera um resultado válido. Um serviço pode responder rapidamente e devolver dados que ainda não passaram reconciliação. Separa a decisão sobre armazenamento da decisão sobre quando publicar o resultado para utilização. Num projeto APS fictício, a aplicação operacional atualiza instruções e o reporting calcula posições consolidadas. Não escolhas um único produto só para reduzir o número de nomes no diagrama. Compara candidatos com os acessos concretos. Spanner merece avaliação quando é necessário um modelo relacional transacional distribuído; Bigtable merece avaliação para acessos adequados ao desenho de chaves; BigQuery serve análise e Cloud Storage guarda objetos. Isto não dispensa medir latência, custo e limitações. Para arquivos, inclui leituras e recuperação no custo total. Se um ficheiro passa de acesso raro para reconciliação diária, a classe economicamente adequada pode mudar. A aprovação inicial deve indicar que hipóteses justificam a escolha e quando precisam de ser revistas.

2. Desenhar chaves sem perder os acessos necessários

Uma chave física influencia onde os dados ficam próximos e como são encontrados. Em Bigtable, a ordem lexicográfica permite explorar prefixos e intervalos. Um timestamp crescente no início da row key pode concentrar escritas recentes. Um prefixo com distribuição adequada pode melhorar esse padrão, mas a decisão precisa de considerar a consulta. Não basta espalhar dados se a leitura dominante passar a exigir procurar em todo o conjunto. O ensaio deve incluir contas pequenas e grandes, rajadas de escrita e consultas simultâneas ao histórico recente. Imagina telemetria de várias aplicações de fundos. Uma chave aplicação#tempo pode conservar proximidade por aplicação, mas uma aplicação dominante ainda pode concentrar carga. Regista a distribuição observada, não apenas a contagem de aplicações. Se propuseres vários segmentos por aplicação, documenta quantos intervalos cada leitura terá de combinar e como a equipa diagnostica uma omissão. Aplicar hash à chave inteira remove a ordenação útil do valor original. Essa troca pode exigir um caminho de acesso adicional. No comité técnico, compara desenho, distribuição e consultas com os mesmos dados. Um aumento do número de nós não prova que um hotspot de chave foi resolvido. A decisão deve explicar tanto a escrita como a leitura resultantes.

3. Relacionar a partição com o período de negócio

Data de ocorrência e data de ingestão são campos com significados diferentes. Uma tabela particionada por ingestão organiza fisicamente a chegada dos dados. Um relatório por data de negócio precisa de incluir movimentos tardios que podem estar noutras partições. Antes de limitar a leitura, define o conjunto lógico esperado. Só depois procura uma seleção física que o conserve. Uma margem de alguns dias pode passar num ensaio e continuar incorreta se não houver limite de atraso garantido. Nesse caso, revê o desenho, a captura de correções ou a estratégia de reconciliação. Para BigQuery, usa predicados que permitam determinar as partições relevantes e verifica a consulta concreta. Um filtro que compara a coluna de partição com outra coluna variável pode impedir pruning. Limitar o número de linhas devolvidas não equivale a limitar os dados lidos. O controlo de filtro obrigatório ajuda a evitar consultas sem restrição adequada, mas não verifica se escolheste o mês certo. Regista separadamente o requisito funcional, o predicado físico e a evidência de execução. Compara IDs, contagens e medidas por período. Um total igual pode esconder uma linha de valor zero em falta, duas omissões que se compensam ou atribuições à carteira errada. A otimização deve manter o contrato do resultado.

4. Controlar granularidade e organização interna

Depois de selecionar partições, o clustering pode reduzir os blocos relevantes para certos filtros. A ordem das colunas importa. Se o filtro dominante é account_id, um desenho que começa por outro campo merece comparação com uma alternativa orientada a esse acesso. Não prometas a mesma melhoria para todas as consultas. Recolhe métricas numa amostra representativa e mantém constantes os critérios de correção. O desenho físico deve acompanhar o workload real, incluindo consultas de fecho e de investigação de incidentes. Na modelação, considera relações hierárquicas frequentemente consultadas em conjunto. Uma instrução com um número limitado de componentes pode usar ARRAY de STRUCT. Isso conserva a relação dentro da linha, mas o consumo precisa de respeitar a granularidade. Após UNNEST, o total do pai aparece em várias linhas. Somá-lo por componente multiplica a medida. Um DISTINCT aplicado apenas ao montante também pode fundir instruções legítimas que têm o mesmo valor. Define a chave da medida antes de agregar. Faz um ensaio com zero, um e vários componentes e com dois pais de montante igual. A desnormalização não é uma regra universal: um modelo estrela já adequado pode não beneficiar de maior aninhamento. Mede e documenta a razão da escolha.

5. Governar ficheiros e acessos como um produto

Um lake utilizável precisa de descoberta, significado, autorização e operação. O catálogo ajuda a encontrar ativos, proprietários e relações, mas a presença de uma entrada não prova acesso aos dados nem aprovação de cada alteração. Em modelos federados, os domínios mantêm responsabilidade pelos seus produtos e acordam contratos comuns. Define quem aceita alterações de esquema, qual o significado do período, que consumidor depende do produto e como são comunicadas incompatibilidades. Na documentação consultada, Dataplex Universal Catalog evoluiu para Knowledge Catalog; o guia do exame mantém a terminologia Dataplex. Conserva essa relação nas referências sem inventar uma versão nova do exame. Em BigLake, a delegação permite separar permissões da tabela e do armazenamento subjacente. Inspeciona também caminhos diretos para os objetos, pois uma concessão independente pode contornar o controlo aplicado na tabela. Para o ciclo de vida, distingue elegibilidade, execução e recuperação. Ações lifecycle são assíncronas. Uma política de retenção bloqueada tem consequências irreversíveis: não pode ser removida nem reduzida. O responsável pelo produto deve aceitar essas condições antes do lock. Regista períodos, exceções, custos e evidência de restauro. Não uses a existência de uma regra como prova de que todos os objetos já foram tratados.

6. Laboratório: fidelidade antes do tamanho do scan

O exercício original content/labs/pde-partition-fidelity/run.py compara uma seleção física com o resultado lógico de referência fornecido. Corre python3 content/labs/pde-partition-fidelity/run.py < content/labs/pde-partition-fidelity/case.json na raiz do projeto. Cada linha tem id, eventDate, ingestDate, account, amountCents e declaredBytes. Neste contrato fictício, os montantes são cêntimos de EUR; não há conversão cambial. As datas são dias explícitos no formato YYYY-MM-DD, sem cálculo de fusos horários. IDs têm de ser únicos. O input aceita no máximo 10 000 linhas e rejeita campos inesperados, datas inválidas, duplicados e números fora dos limites. partitionBy escolhe eventDate ou ingestDate. scan define um intervalo físico semiaberto, ou null para ler todas as linhas. query define o intervalo lógico por eventDate e uma conta, ou null para todas as contas. O modelo filtra primeiro as partições e depois aplica o pedido lógico. Em paralelo calcula a referência sobre todas as linhas fornecidas. Mostra IDs, montantes, partições lidas, linhas e bytes declarados. matchesProvidedReference só passa se não faltar nenhuma identidade elegível. O programa não executa SQL, não simula o otimizador BigQuery e não estima faturação. Os bytes são um valor fictício fornecido pelo aluno, útil apenas para comparar os casos deste modelo.

7. Comparar alternativas com contraexemplos

O ficheiro de exemplo tem quatro movimentos. A e B pertencem a 1 de outubro e à conta F1, com 1000 e 2000 cêntimos. A chegou no próprio dia; B chegou a 3 de outubro. C pertence a 2 de outubro e D a 3. Cada linha declara 100 bytes. O pedido lógico procura F1 no intervalo [2026-10-01,2026-10-02). Com partições por ingestDate e scan no mesmo intervalo, só A é lido: 100 bytes, total 1000 e missingIds=[B]. A referência contém A e B, total 3000. O resultado mais pequeno está incorreto para o pedido. Muda partitionBy para eventDate: o mesmo intervalo lê A e B, 200 bytes, sem omissões. Usa scan=null: lê 400 bytes e também conserva o resultado. Agora muda B para zero. O scan estreito volta a produzir o mesmo total da referência, mas continua a falhar por identidade em falta. Por fim, alarga o intervalo físico até 4 de outubro. A comparação passa para estes dados, sem provar um limite universal para atrasos futuros. Escreve as conclusões numa tabela com fidelidade, âmbito e bytes declarados. Separa o que o ensaio demonstra das hipóteses que precisariam de observação do sistema real.

8. Aceitar e operar o desenho escolhido

Uma decisão de armazenamento termina com critérios de operação. Entrega o padrão de consulta esperado, limites de crescimento, responsável pelo esquema, política de retenção, forma de recuperação e métricas que indicam mudança de comportamento. Para tabelas analíticas, mede custo e latência de resultados reconciliados. Para acessos por chave, acompanha distribuição e concentração. Para ficheiros, acompanha volume, classes, acessos e ações de ciclo de vida. Uma redução do valor faturado pode resultar de um consumidor ter deixado de receber dados; compara sempre a métrica financeira com o produto entregue. No caso APS, prepara uma decisão para o comité: modelo anterior, alternativa, requisitos conservados, diferenças conhecidas e plano de retorno. Executa consultas de fecho, investigações ad hoc e leitura de eventos tardios com dados representativos. Se um consumidor continua a usar a granularidade antiga, trata a compatibilidade antes de promover. Define quem pode aceitar uma versão provisória e quem deve ser avisado de correções. O resumo é simples: escolhe o armazenamento pelos acessos, conserva significado ao reorganizar dados e valida as garantias no âmbito certo. O exercício local ensina a construir um contraexemplo; a aceitação real precisa ainda de evidência sobre origem, segurança, execução, capacidade e custos do ambiente concreto.

"""Original local scan model. Declared bytes are not BigQuery billing estimates."""
from datetime import date
import json
import re
import sys


def exact(value, keys):
    if type(value) is not dict or set(value) != set(keys):
        raise ValueError('unexpected or missing fields')


def day(value):
    if type(value) is not str or not re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}', value):
        raise ValueError('date must be YYYY-MM-DD')
    date.fromisoformat(value)
    return value


def bounds(value):
    exact(value, ['from', 'to'])
    start, end = day(value['from']), day(value['to'])
    if start >= end:
        raise ValueError('range must be nonempty and increasing')
    return start, end


def integer(value, low, high):
    if type(value) is not int or not low <= value <= high:
        raise ValueError('integer outside local contract')


def identifier(value):
    if type(value) is not str or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', value):
        raise ValueError('invalid identifier')


def evaluate(document):
    exact(document, ['rows', 'partitionBy', 'scan', 'query'])
    rows = document['rows']
    if type(rows) is not list or len(rows) > 10000:
        raise ValueError('rows must be a list with at most 10000 entries')
    layout = document['partitionBy']
    if layout not in ('eventDate', 'ingestDate'):
        raise ValueError('unknown partition field')
    exact(document['query'], ['from', 'to', 'account'])
    query = document['query']
    start, end = bounds({'from': query['from'], 'to': query['to']})
    if query['account'] is not None:
        identifier(query['account'])
    scan = bounds(document['scan']) if document['scan'] is not None else None
    seen = set()
    for row in rows:
        exact(row, ['id', 'eventDate', 'ingestDate', 'account', 'amountCents', 'declaredBytes'])
        identifier(row['id'])
        identifier(row['account'])
        day(row['eventDate'])
        day(row['ingestDate'])
        integer(row['amountCents'], -10**12, 10**12)
        integer(row['declaredBytes'], 0, 10**9)
        if row['id'] in seen:
            raise ValueError('duplicate row id')
        seen.add(row['id'])
    def eligible(row):
        return start <= row['eventDate'] < end and (query['account'] is None or row['account'] == query['account'])
    scanned = [r for r in rows if scan is None or scan[0] <= r[layout] < scan[1]]
    reference = sorted((r for r in rows if eligible(r)), key=lambda r: r['id'])
    result = sorted((r for r in scanned if eligible(r)), key=lambda r: r['id'])
    found = {r['id'] for r in result}
    missing = [r['id'] for r in reference if r['id'] not in found]
    return {'partitionBy': layout, 'scannedPartitions': sorted({r[layout] for r in scanned}),
            'scannedRowCount': len(scanned), 'scannedDeclaredBytes': sum(r['declaredBytes'] for r in scanned),
            'referenceIds': [r['id'] for r in reference], 'resultIds': [r['id'] for r in result],
            'referenceAmountCents': sum(r['amountCents'] for r in reference),
            'resultAmountCents': sum(r['amountCents'] for r in result),
            'missingIds': missing, 'matchesProvidedReference': not missing,
            'sourceCompletenessProven': False, 'cloudPruningMeasured': False, 'billingEstimate': False}


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError('duplicate JSON field')
        result[key] = value
    return result


if __name__ == '__main__':
    try:
        raw = sys.stdin.read(3_000_001)
        if len(raw) > 3_000_000:
            raise ValueError('input exceeds local limit')
        print(json.dumps(evaluate(json.loads(raw, object_pairs_hook=unique_object)), sort_keys=True))
    except (ValueError, TypeError, RecursionError) as error:
        print(json.dumps({'error': str(error)}), file=sys.stderr)
        sys.exit(2)
NA PRÁTICA

Ler 100 bytes omite B; ler 200 por eventDate conserva A e B. Os bytes são fictícios.

Armadilhas comuns

Confundir ingestão com ocorrência; somar totais do pai após UNNEST; usar metadata como autorização; inferir faturação do modelo local.

Tópicos relacionados: Eventos tardios e replay · Preparação para análise · Custos e recuperação

Leva esta ideia contigo

Uma otimização é candidata a aceitação quando conserva o resultado e tem benefício medido no workload real.

Criar conta

Referência: Professional Data Engineer standard exam guide · Current linked standard guide (document title v4.2); edition date unconfirmed (2026-09-30 inspection)

Google Cloud é uma marca comercial de Google LLC. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Google. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.