Boas PráticasJun 15, 202610 min de leitura

Projetando um Schema de Banco de Dados Multi-Tenant: Padrões e Trade-offs

Multi-tenancy é uma das decisões arquiteturais mais consequentes no design de banco de dados SaaS. Aprenda os três padrões principais, quando usar cada um e como implementar o isolamento de tenants corretamente.

Quando você constrói uma aplicação SaaS, uma das primeiras decisões arquiteturais que tomará é como lidar com multi-tenancy no nível do banco de dados. Essa escolha afeta o isolamento de dados, a performance de consultas, a complexidade operacional, a postura de conformidade e o quão difícil será migrar tenants individuais no futuro. Vale a pena dedicar tempo significativo a isso antes de escrever qualquer código da aplicação.

Existem três abordagens fundamentais para o design de banco de dados multi-tenant, cada uma com um conjunto distinto de trade-offs. A escolha certa depende da sua contagem de tenants, requisitos de conformidade, tamanho do time e trajetória de crescimento. O ER Flow facilita o design e a visualização de qualquer padrão que você escolher antes de se comprometer com uma implementação.

Abordagem 1: Tabelas Compartilhadas com `tenant_id`

Esta é a abordagem mais comum para produtos SaaS de alto crescimento. Cada tabela com escopo de tenant recebe uma coluna tenant_id — uma chave estrangeira para uma tabela central de tenants. Todos os tenants compartilham as mesmas tabelas, e o isolamento de tenants é imposto no nível da aplicação e da consulta. O schema se parece com: tenants(id, name, slug, plan_id, created_at), e então cada tabela de entidade adiciona tenant_id NOT NULL REFERENCES tenants(id). Uma tabela users se torna users(id, tenant_id, email, password_hash, created_at). Uma tabela orders se torna orders(id, tenant_id, user_id, status, total_cents, created_at).

Vantagens: Simples de implementar, fácil de adicionar novos tenants (basta inserir uma linha em tenants), baixa sobrecarga operacional e uso eficiente dos recursos do banco de dados. Uma migration de schema se aplica a todos os tenants simultaneamente. O escalonamento horizontal é direto — você pode fazer sharding por tenant_id quando uma única instância de banco de dados atingir seus limites.

Desvantagens: Bugs no nível da aplicação podem acidentalmente expor dados entre tenants. Um único filtro WHERE tenant_id = ? faltando em uma consulta é um potencial incidente de segurança. Consultas pesadas de um tenant grande podem degradar a performance para outros tenants compartilhando a mesma instância de banco de dados. Clientes corporativos com requisitos rígidos de conformidade podem recusar esse modelo completamente.

Abordagem 2: Schema por Tenant

No PostgreSQL, um "schema" é um namespace dentro de um banco de dados. Cada tenant recebe seu próprio schema: tenant_acme.users, tenant_beta.users. A aplicação define o search_path para o schema correto no momento da conexão. Essa abordagem fornece isolamento significativamente mais forte do que tabelas compartilhadas — um bug no código da aplicação precisaria tanto ignorar a configuração do search_path quanto de alguma forma acessar o schema de outro tenant.

Vantagens: Isolamento forte sem o risco de vazamento de dados entre tenants por bugs de consulta. O schema de cada tenant pode ter estrutura ligeiramente diferente se necessário (útil durante rollouts graduais). Mover um único tenant para infraestrutura dedicada é direto — copie o schema para a nova instância e atualize o roteamento de conexão.

Desvantagens: PostgreSQL suporta muitos schemas por banco de dados, mas a performance pode degradar com milhares deles. As migrations devem ser aplicadas a cada schema de tenant individualmente — suas ferramentas de migration precisam iterar por todos os schemas de tenants, o que é mais lento e mais difícil de fazer rollback com segurança. Operações de backup e restore se tornam mais complexas. Esse padrão também é específico do PostgreSQL na sua implementação; schemas MySQL são equivalentes a bancos de dados, tornando o schema-por-tenant efetivamente o mesmo que banco-por-tenant nesse motor.

Abordagem 3: Banco de Dados por Tenant

A abordagem mais isolada: cada tenant recebe uma instância de banco de dados completamente separada. A aplicação tem uma camada de roteamento que mapeia tenant_id para a string de conexão correta. Este é o modelo usado por produtos SaaS corporativos com requisitos rígidos de conformidade — HIPAA, SOC 2 Type II, FedRAMP. Os dados de cada tenant estão fisicamente separados de todos os outros.

Vantagens: Isolamento perfeito — um incidente de banco de dados afetando um tenant tem impacto zero nos outros. Você pode executar diferentes versões de banco de dados, diferentes configurações ou diferentes agendas de backup por tenant. Torna trivialmente fácil oferecer aos tenants uma opção de instância dedicada a um preço premium. Certificações de conformidade como HIPAA se tornam diretas de alcançar e demonstrar.

Desvantagens: Enorme sobrecarga operacional. As migrations devem ser aplicadas a cada banco de dados de tenant, geralmente através de um sistema de orquestração que reprocessa falhas e acompanha o progresso. O pool de conexões se torna complexo — um servidor com 1.000 tenants precisa de gerenciamento sofisticado de conexões. O custo escala linearmente: 1.000 tenants significa pelo menos 1.000 instâncias de banco de dados. Esse padrão só é viável com forte automação e um time grande o suficiente para gerenciá-lo.

Row-Level Security no PostgreSQL

O Row-Level Security (RLS) do PostgreSQL oferece isolamento de tabelas compartilhadas imposto no nível do banco de dados em vez da aplicação. Você define políticas de segurança em cada tabela: ALTER TABLE orders ENABLE ROW LEVEL SECURITY, depois CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.current_tenant_id')::uuid). Sua aplicação define a variável de sessão antes de cada consulta: SET LOCAL app.current_tenant_id = 'tenant-uuid-aqui'.

O PostgreSQL então automaticamente acrescenta a condição da política a cada consulta nessa tabela — mesmo que o código da aplicação esqueça de filtrar por tenant_id. Isso é o melhor dos dois mundos: a simplicidade de tabelas compartilhadas com isolamento imposto pelo banco de dados. A desvantagem é que o RLS adiciona uma pequena sobrecarga a cada consulta e requer testes cuidadosos para garantir que as políticas funcionem corretamente para todos os padrões de consulta incluindo joins.

Indexação para Schemas Multi-Tenant

Cada coluna indexada em um schema multi-tenant deve incluir tenant_id como a primeira coluna no índice. Em vez de CREATE INDEX idx_orders_status ON orders(status), use CREATE INDEX idx_orders_status ON orders(tenant_id, status). Isso é crítico para a performance de consultas: o banco de dados usa o índice para encontrar todos os pedidos de um tenant específico com um determinado status, em vez de varrer todos os pedidos de todos os tenants.

Para referências polimórficas (comumente usadas em logs de auditoria e feeds de atividade), o índice composto deve ser (tenant_id, auditable_type, auditable_id). O tenant_id sempre vem primeiro em índices compostos para tabelas multi-tenant — é o filtro de maior seletividade que elimina o máximo de linhas no passo de varredura mais cedo possível. No ER Flow, você pode adicionar definições de índice diretamente a cada tabela no diagrama, tornando sua estratégia de indexação visível e revisável junto com a estrutura do schema.

Cascade Deletes e Limpeza de Tenant

Quando um tenant cancela e você precisa excluir todos os seus dados, cascade deletes são sua ferramenta mais confiável. Defina ON DELETE CASCADE em cada chave estrangeira que referencia tenants(id). Isso significa que um único DELETE FROM tenants WHERE id = ? cascateará por todo o seu schema — excluindo todos os usuários, pedidos, faturas e outros registros com escopo de tenant na ordem correta de dependência.

Teste seu comportamento de cascade completamente em um ambiente de staging antes de depender dele em produção. Para tenants muito grandes, um cascade delete pode se tornar uma transação de longa duração que bloqueia tabelas e impacta outros tenants. A abordagem mais segura é fazer soft delete do tenant primeiro (tenants.deleted_at = NOW()), depois excluir em lote os registros filhos em segundo plano ao longo do tempo, e finalmente fazer hard delete da linha do tenant quando todos os filhos tiverem sido limpos. Documente esse workflow de exclusão no seu diagrama ER usando o recurso de notas do ER Flow, para que o procedimento operacional esteja anexado ao próprio design do schema.

Qualquer que seja a abordagem que você escolha, modele-a explicitamente no ER Flow antes de implementá-la. O diagrama visual torna o padrão de isolamento de tenant concreto — você pode ver de relance se cada tabela tem um tenant_id, se as chaves estrangeiras estão cascateando corretamente a partir da tabela tenants e se os índices estão estruturados adequadamente para padrões de consulta multi-tenant. Decisões de design que parecem abstratas em uma discussão técnica se tornam imediatamente verificáveis em um canvas compartilhado.