Modelagem de dados e multi-tenancy
"Normalizar é sempre melhor" é meia-verdade — e servir vários clientes no mesmo banco levanta uma pergunta que normalização não responde.
A lição 7 tratou normalização quase como regra fixa: separe em tabelas, evite duplicar dado. Na prática é mais um trade-off do que uma regra — às vezes desnormalizar de propósito é a decisão certa, contanto que você entenda o custo que está aceitando em troca.
Desnormalizar de propósito — você já viu isso na lição 8
itens_pedido.preco_unitario guardando o preço no momento da compra, em vez de sempre consultar produtos.preco, já era uma desnormalização proposital. Outro caso comum: cachear um total calculado pra evitar recalcular em toda leitura.
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
total_gasto_cache DECIMAL NOT NULL DEFAULT 0
-- desnormalizado de propósito: evita somar itens_pedido inteiro
-- toda vez que um dashboard carrega a lista de usuários
);O ganho é leitura instantânea, sem JOIN nem SUM a cada consulta. O custo é real: alguém precisa manter total_gasto_cache sincronizado (um job, um trigger, ou o próprio código da aplicação) — e se essa sincronização falhar silenciosamente uma vez, o valor cacheado fica desatualizado sem ninguém perceber. Normalizar evita esse risco; desnormalizar aceita o risco em troca de velocidade de leitura. Não existe resposta certa universal — existe a pergunta "com que frequência esse dado muda, e o quanto essa leitura precisa ser rápida".
Multi-tenancy — vários clientes, uma aplicação só
Multi-tenancy é quando um único sistema atende vários clientes (tenants) isolados uns dos outros — cada cliente só deveria ver o próprio dado, nunca o de outro. Duas estratégias comuns resolvem isso de formas bem diferentes.
Estratégia 1 — coluna tenant_id compartilhada
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
usuario_id INTEGER REFERENCES usuarios(id),
criado_em TIMESTAMP DEFAULT NOW()
);
SELECT * FROM pedidos WHERE tenant_id = 42 AND usuario_id = 7;
-- toda consulta precisa lembrar de filtrar por tenant_idTodos os clientes compartilham as mesmas tabelas; tenant_id separa o dado de cada um. É operacionalmente simples — um schema só, uma migration só pra manter — mas o isolamento entre clientes depende inteiramente de disciplina: esquecer o WHERE tenant_id = ... numa única consulta vaza dado de um cliente pro outro, porque não existe nenhuma barreira física entre eles.
Estratégia 2 — schema (ou banco) separado por cliente
tenant_empresa_a.pedidos
tenant_empresa_b.pedidos
tenant_empresa_c.pedidos
-- cada cliente tem seu próprio schema (ou banco) com as mesmas tabelasO isolamento aqui é físico, não uma questão de lembrar do filtro certo: um SELECT sem filtro nenhum simplesmente não tem como alcançar dado de outro cliente, porque ele está em outro schema. O custo é operacional — uma migration precisa rodar em cada schema/banco, não em um só, e um relatório que precisa cruzar dado de vários clientes fica bem mais complicado de escrever.
Nenhuma das duas é "a certa" — tenant_id compartilhado escala melhor operacionalmente conforme o número de clientes cresce; schema separado isola melhor, ao custo de multiplicar o trabalho de manutenção. A maioria dos produtos SaaS começa com tenant_id compartilhado, com um índice composto (tenant_id primeiro) garantindo que filtrar por cliente continue rápido mesmo com muitos tenants na mesma tabela.
Exercício
Um sistema multi-tenant usa a estratégia de coluna tenant_id compartilhada. Escreva a consulta que lista os pedidos do tenant 7, e explique numa frase por que esquecer o filtro de tenant_id nessa estratégia é particularmente perigoso.