PTEN

DynamoDB filtered export: exporte um tenant sem pagar a tabela inteira

DynamoDB filtered export para S3 num SaaS multi-tenant: só a key condition na partition key reduz o custo. Conta, código TypeScript e limites.

Autor
Will Peixoto
Publicado
05 nov 2026
Leitura
15 min
Idioma
PT

🇺🇸 Also available in English: DynamoDB filtered export: export one tenant without paying for the whole table

Chegou o e-mail do cliente: “precisamos de uma cópia de todos os nossos dados até sexta”. Você tem um SaaS multi-tenant numa tabela única do DynamoDB, 400 GB de dados de todo mundo misturados, e a pergunta que vem na sequência é chata: como eu tiro só esse cliente dali, sem derrubar produção e sem pagar o export para o S3 da tabela inteira?

Até setembro, a resposta era feia. Desde 01/10/2026 existe o DynamoDB filtered export to S3, e ele resolve isso muito bem, com uma condição: o desenho da sua partition key. É ela que decide se o export desse tenant custa centavos ou custa a tabela inteira. Bora ver por quê, com a conta aberta e o código na mão.

O que é o DynamoDB filtered export to S3

O export para S3 do DynamoDB já existia, em duas versões: full (a tabela inteira num ponto no tempo) e incremental (o que mudou numa janela). O problema é que as duas levavam a tabela inteira. Para pegar um cliente só, ou você fazia Query na tabela viva (paginando, consumindo RCU, só o estado de agora), ou exportava tudo e filtrava no Athena, pagando o GB da tabela inteira.

O filtered export adiciona um parâmetro novo na mesma API, ExportTableToPointInTime: o FilterSpecification. Dentro dele você manda até três coisas, usando a mesma sintaxe que você já conhece de Query e Scan:

  • KeyConditionExpression: um único valor de partition key, com igualdade, mais uma condição opcional na sort key.

  • FilterExpression: condição em qualquer atributo (=, <>, OR, contains, IN, begins_with, BETWEEN, attribute_exists, size).

  • ProjectionExpression: quais atributos vão para o arquivo.

Funciona em full e em incremental. E um detalhe que eu gosto muito: o export lê do backup de PITR (point-in-time recovery), e a tabela viva fica fora da história. Ou seja, não consome capacidade de leitura e não encosta na performance de produção. A AWS cita como casos de uso recuperação granular, mover subconjuntos entre contas e analytics com compliance.

E para quem está aqui no Brasil: está disponível em todas as regiões AWS, menos as GovCloud. Inclui sa-east-1. Seu dado não precisa sair de São Paulo para nada disso funcionar.

Fluxo do DynamoDB filtered export: tabela multi-tenant com PITR, export filtrado por partition key do tenant, arquivos no Amazon S3 e consumo via Athena, entrega ao cliente ou import-table
Fig. 01

A regra que decide o custo: key condition reduz, filter não

A doc é direta: como a key condition limita o export a um único valor de partition key, ela reduz o volume de dado que o DynamoDB lê. Já o filter expression é aplicado depois da leitura, então não reduz o dado processado. E a página de pricing fecha a conta: quando o export usa key condition para selecionar um valor de partition key, você paga só pelo dado que casou, com mínimo de 10 MB por export. Sem cobrança extra pelo filtro em si.

Pensa numa transportadora fazendo a mudança de um morador de um prédio. Se você passa o número do apartamento (a partition key), ela sobe, embala aquele apartamento e cobra por ele. Se você só diz “leva as coisas da família Silva” (um filtro num atributo qualquer), ela coloca o prédio inteiro no caminhão, separa as caixas dos Silva na calçada e cobra o caminhão cheio. O resultado no S3 é o mesmo. A fatura, não.

Traduzindo para o seu SaaS:

  • Tabela com PK = TENANT#<id>: o export de um tenant cobra a partição dele.

  • Tabela com PK = ORDER#<id> e um atributo tenantId solto no item: o “export por tenant” vira filter expression e cobra a tabela inteira.

  • tenantId como partition key de um GSI: não salva. O filtered export não suporta índices secundários no lançamento.

Bora abrir a conta (com números ilustrativos)

Combinado antes dos números: o preço por GB sai do exemplo oficial da página de pricing on-demand (29 GB por US$ 2,90, ou seja, US$ 0,10 por GB), que é de us-east-1. Eu não consegui confirmar a tarifa de sa-east-1, e preço na AWS varia por região, então confira na página de pricing antes de levar para o seu orçamento. Os tamanhos de tabela e de tenant são hipotéticos, só para ilustrar.

Cenário: tabela de 400 GB, cliente “acme” com 1,5 GB, cliente “padaria-do-ze” com 2 MB.

CenárioContaCusto do export
Full export da tabela, filtrando depois no Athena400 GB × US$ 0,10US$ 40,00
Filtered export só com FilterExpression em tenantId400 GB × US$ 0,10US$ 40,00
Filtered export com KeyConditionExpression no PK (acme)1,5 GB × US$ 0,10US$ 0,15
Filtered export com key condition (padaria-do-ze, 2 MB)mínimo de 10 MB cobradocerca de US$ 0,001

Agora multiplica. Se o seu suporte faz 50 exports de tenant por mês, o modelo com tenantId como atributo comum custa 50 × US$ 40 = US$ 2.000 no mês, só de export. Com o tenant na partition key, a conta passa a acompanhar o tamanho de cada cliente.

Tem mais uma linha na fatura que você não pode esquecer: o PITR. Ele é pré-requisito e é cobrado pelo tamanho da tabela (dados mais LSIs). No exemplo oficial, US$ 0,20 por GB-mês. Para os nossos 400 GB, seriam US$ 80 por mês se você ainda não liga PITR hoje. E encurtar a retenção (de 35 para 1 dia) não reduz esse custo, segundo a própria página de pricing. Se você já roda PITR por resiliência (e deveria, em produção), essa linha já está paga. S3, PUT requests, Athena e data transfer vêm à parte.

Esse tipo de conta encaixa direto na primeira lei do The Frugal Architect, do Werner Vogels: custo é requisito não funcional. No DynamoDB, isso significa pensar em export por tenant quando você escolhe a chave, porque trocar a chave depois é migração. Falei disso com outro sabor no post sobre custo de resiliência, multi-região ou on-premise.

O desenho da partition key no SaaS multi-tenant

O AWS SaaS Lens do Well-Architected descreve três modelos de isolamento: silo (recurso dedicado por tenant), pool (todo mundo compartilhando) e bridge (uma mistura). No DynamoDB, silo costuma ser tabela por tenant, e pool costuma ser tabela única com o tenant na chave.

Antes do filtered export, silo tinha uma vantagem prática em portabilidade: exportar a tabela do cliente era exportar o cliente. O filtered export encurta muito essa distância para quem está em pool, desde que o tenant seja a partition key.

O desenho que funciona com o filtered export é o clássico de single-table design:

PKSKAtributos
TENANT#acmeUSER#123nome, e-mail, papel
TENANT#acmeORDER#2026-10-01#987status, valor
TENANT#acmeINVOICE#2026-09total, vencimento

Todo dado do tenant mora sob a mesma partition key, então uma key condition PK = TENANT#acme pega tudo de uma vez.

Agora, nem tudo são flores. Tenant gigante numa partição só vira hot partition, e o remédio conhecido é write sharding: espalhar o tenant em TENANT#acme#0 até TENANT#acme#9. Só que a key condition aceita um único valor de partition key por export. Tenant com 10 shards vira 10 exports, cada um com mínimo de 10 MB e com o seu próprio arquivo. Se o sharding resolve a sua hot partition, mantenha. Só garanta que o código de export conheça o mapa de shards do tenant. A mesma lógica vale para N tenants: N exports.

E se o seu modelo atual tem PK = ORDER#id? Aí não tem para onde fugir: o filtered export ainda funciona via filter expression, com o resultado certo e a fatura da tabela inteira. Migrar a chave é decisão grande, que eu só tomaria se exportar por tenant virar rotina.

Na prática: o export de um tenant em TypeScript

Primeiro passo, PITR ligado. Sem ele a chamada falha com PointInTimeRecoveryUnavailableException. Comando da doc oficial:

bash
aws dynamodb update-continuous-backups \
  --table-name SaasTable \
  --point-in-time-recovery-specification PointInTimeRecoveryEnabled=True

Agora o export. Os nomes de campo abaixo são os mesmos do exemplo em SDK JS v3 da doc Requesting a table export. Eu só troquei os valores para o cenário do tenant e adicionei ClientToken e KMS, que estão na API reference:

ts
import {
  DynamoDBClient,
  ExportTableToPointInTimeCommand,
  DescribeExportCommand,
} from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({ region: "sa-east-1" });

export async function exportTenant(tenantId: string, at: Date) {
  const { ExportDescription } = await client.send(
    new ExportTableToPointInTimeCommand({
      TableArn: "arn:aws:dynamodb:sa-east-1:111122223333:table/SaasTable",
      ExportTime: at, // precisa estar dentro da janela de PITR
      S3Bucket: "saas-tenant-exports",
      S3Prefix: `tenants/${tenantId}/`,
      ExportFormat: "DYNAMODB_JSON",
      S3SseAlgorithm: "KMS",
      S3SseKmsKeyId: "arn:aws:kms:sa-east-1:111122223333:key/EXEMPLO",
      // idempotente por 8 horas: retry com o mesmo token não duplica o export
      ClientToken: `export-${tenantId}-${at.getTime()}`,
      FilterSpecification: {
        KeyConditionExpression: "#pk = :pk",
        ExpressionAttributeNames: { "#pk": "PK" },
        ExpressionAttributeValues: { ":pk": { S: `TENANT#${tenantId}` } },
      },
    })
  );

  return ExportDescription?.ExportArn;
}

export async function checkExport(exportArn: string) {
  const { ExportDescription } = await client.send(
    new DescribeExportCommand({ ExportArn: exportArn })
  );
  // BilledSizeBytes mostra o que entrou na fatura deste export
  return {
    status: ExportDescription?.ExportStatus,
    items: ExportDescription?.ItemCount,
    billedBytes: ExportDescription?.BilledSizeBytes,
  };
}

Três cuidados antes de levar isso para produção. O export é assíncrono e não tem SLA de tempo de conclusão, então nada de fluxo que dependa de terminar em X minutos. Orquestre com Step Functions: dispara, espera, consulta o DescribeExport em loop. O ClientToken é o seu Idempotency Key: se a Lambda der retry, o export não duplica. E o BilledSizeBytes é o melhor jeito de provar para o time que a chave está fazendo o trabalho dela.

Para não tomar ValidationException: com key condition, o atributo de chave não pode reaparecer no filter expression, e cada expressão tem limite de 4 KB e 300 operadores.

Se o caso for recuperação de um incidente (alguém rodou um script errado no tenant das 14h às 14h45), o incremental filtrado com NEW_AND_OLD_IMAGES te entrega o antes e o depois de cada item alterado naquela janela, só daquele tenant. A janela do incremental vai de 15 minutos a 24 horas. O blog oficial de lançamento tem esse exemplo completo em CLI.

O que fazer depois que o arquivo chega no S3

No full export, os arquivos caem em <prefixo>/AWSDynamoDB/<ExportId>/, com manifest-summary.json, manifest-files.json e uma pasta data/ com arquivos .json.gz (o incremental usa outro layout, com NewImage e OldImage). No formato DynamoDB JSON do full export, cada linha é um item embrulhado em Item, com os tipos marshalled ({"S": "..."}, {"N": "..."}). A outra opção de formato é Amazon Ion.

Para consultar, o Athena lê os arquivos comprimidos direto. Adaptando a DDL do post oficial de lançamento do export:

sql
CREATE EXTERNAL TABLE IF NOT EXISTS tenant_acme_export (
  Item struct <PK:struct<S:string>,
               SK:struct<S:string>,
               status:struct<S:string>>
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://saas-tenant-exports/tenants/acme/AWSDynamoDB/{EXPORT_ID}/data/';

SELECT Item.SK.S AS sk, Item.status.S AS status
FROM tenant_acme_export
WHERE Item.SK.S LIKE 'ORDER#%';

Daqui saem os três destinos mais comuns:

  • Portabilidade e suporte: converter o JSON marshalled para CSV ou JSON limpo e entregar por URL pré-assinada de expiração curta.

  • Analytics por cliente: Athena só no recorte, sem expor os outros tenants.

  • Restore ou migração: o import-table traz de volta, mas sempre cria uma tabela nova (os GSIs vão em --table-creation-parameters). Para devolver os itens na tabela original, o write-back é código seu, e as escritas que chegaram entre o export e o cutover precisam ser drenadas ou reaplicadas.

E se o que você quer é um fluxo contínuo de mudanças para um data lake, aí a conversa é outra: DynamoDB Streams e as ferramentas de streaming. Comparei essas opções no post de data streaming na AWS com Kinesis, Firehose, Flink e MSK.

LGPD: o que o export ajuda e o que ele cria

Aviso de sempre: sou arquiteto, e nada aqui é aconselhamento jurídico. Quem decide o que sua empresa precisa entregar e como é o jurídico ou o DPO.

Dito isso, a LGPD prevê direitos do titular como acesso e portabilidade dos dados (art. 18), e é comum o cliente B2B pedir a extração dos dados dele por causa de uma solicitação dessas ou porque está saindo da plataforma. O filtered export deixa essa extração barata e sem impacto em produção, desde que a chave ajude.

Do outro lado, ele cria uma cópia nova de dado pessoal num bucket. E o DynamoDB nunca apaga o output do export do seu bucket. Então a parte de engenharia é sua: lifecycle policy expirando o prefixo de export, KMS com policy restrita, acesso ao bucket por role específica, CloudTrail ligado. Isso encosta direto no pilar de Security do Well-Architected (proteção de dados em repouso, least-privilege, rastreabilidade). Sem lifecycle, o export que você fez para um ticket de suporte em 2026 continua lá em 2028.

Limitações para ter no radar

  • PITR obrigatório, e o ponto no tempo tem que estar dentro da janela de retenção (até 35 dias).

  • Mínimo de 10 MB cobrado por export, um valor de partition key por export e nada de índice secundário no lançamento.

  • Quota de até 300 exports concorrentes (ou 100 TB em voo), segundo a página de service quotas. Para exportar centenas de tenants de uma vez, coloque uma fila na frente.

  • Consistência igual à do full export: eventual entre partições e sem noção de transação. Não use como snapshot de invariantes entre partições diferentes.

  • Formato: DynamoDB JSON ou Amazon Ion, comprimido. Para Parquet, é etapa sua depois (Glue, Athena CTAS).

  • Os metadados do export expiram depois de 90 dias e somem do ListExports e do console (os arquivos no S3 ficam). Guarde o ExportArn e o BilledSizeBytes no seu próprio registro se precisar de auditoria.

  • Cross-account funciona, com S3BucketOwner obrigatório para bucket de outra conta. Bom para entregar o recorte numa conta isolada.

Quando usar o quê

Query na tabelaFull export + AthenaFiltered export
Fontetabela viva (RCU)PITRPITR
Impacto em produçãosimnãonão
Ponto no tempoagoraaté 35 diasaté 35 dias
CustoRCU por item lidoGB da tabela inteiraGB da partição (com key condition)
Saídapaginada para o appS3, tudoS3, só o recorte

Query segue boa para o botão “baixar meus dados” com volume pequeno. Full export segue sendo o caminho para o data lake da empresa inteira. O filtered export fica com o caso do meio: um tenant, num ponto no tempo, sem tocar em produção.

O que você leva

  • O filtered export (01/10/2026) adiciona FilterSpecification ao ExportTableToPointInTime e funciona em full e incremental, inclusive em sa-east-1.

  • Só a KeyConditionExpression reduz o volume cobrado. Filter expression em atributo comum entrega o recorte certo e cobra a tabela inteira.

  • Num SaaS em tabela única, TenantId na partition key é o que transforma o export de um cliente em centavos. GSI não ajuda no lançamento.

  • Some à conta o PITR (cobrado pelo tamanho da tabela), o mínimo de 10 MB por export e um export por valor de partition key.

  • Depois do S3: Athena para consultar, lifecycle e KMS para não deixar cópia de tenant esquecida, e import-table sabendo que ele cria tabela nova.

Fontes

Pergunta para você que roda SaaS em DynamoDB: o tenant está na partition key da sua tabela hoje, ou está num atributo solto esperando a primeira fatura de export? Me conta nos comentários. E se o seu time está desenhando multi-tenant agora, manda esse post para ele antes de fechar o modelo de chave. Se curtiu, manda aquele joinha, comenta e compartilha para fortalecer a comunidade, valeu demais!

BUILD. SCALE. REPEAT. =D

Will Peixoto

Will Peixoto é Staff Engineer na Serverless Guru e escreve sobre serverless, arquitetura e agentes de IA na AWS: o que ele escolheria, por quê e quanto custa.

Continue lendo

Post novo direto no seu email

Um tema de serverless, arquitetura ou agentes de IA na AWS por edição: o que eu escolheria, por quê e quanto custa. Zero spam.

Abre a página de inscrição numa nova aba.