---
title: "A promessa furada de isolar clientes por schema no PostgreSQL"
author: "Pedro Dalben"
date: "2026-10-23T11:03:11Z"
category: "Arquitetura & CTO"
reading_time: "6 min"
canonical: "https://pedrodalben.com.br/blog/a-promessa-furada-de-isolar-clientes-por-schema-no-postgresql"
---

# A promessa furada de isolar clientes por schema no PostgreSQL

> Separar dados por schemas gera gargalos operacionais graves em migrações e conexões, enquanto RLS resolve o isolamento em um schema único.

**Autor:** Pedro Dalben (CTO no Integrar Plus)  
**Publicado em:** 23 de October de 2026  
**URL Canônica Oficial:** [https://pedrodalben.com.br/blog/a-promessa-furada-de-isolar-clientes-por-schema-no-postgresql](https://pedrodalben.com.br/blog/a-promessa-furada-de-isolar-clientes-por-schema-no-postgresql)  

---


Toda discussão sobre arquitetura multi-tenant em aplicações corporativas costuma descarrilar quando alguém propõe usar um schema do PostgreSQL para cada cliente. A justificativa parece razoável na primeira reunião: schemas oferecem separação visual, facilitam descartar os dados de uma conta que cancelou o contrato e evitam vazamentos acidentais se alguém esquecer uma cláusula de filtro na aplicação. Na prática de produção, essa escolha cobra um pedágio operacional que quase nenhum time coloca na planilha antes do primeiro incidente.

O problema explode quando a base de clientes cresce. Rodar migrações em centenas ou milhares de schemas sequencialmente transforma deploys rotineiros em janelas de manutenção de horas. Se a migração falha no schema número 180 porque uma tabela específica travou esperando um lock exclusivo, o banco de dados fica em um estado inconsistente, com metade da base na versão nova e a outra metade na versão antiga. Para piorar, o catálogo interno do PostgreSQL sofre com o excesso de objetos. Milhares de tabelas duplicadas sobrecarregam o cache de dicionário do banco, drenam memória do servidor e degradam o desempenho de ferramentas de monitoramento e backup.

Existem dois campos consolidados nesse debate, e eu discordo da premissa de ambos.

O primeiro grupo insiste que schema separado é sinônimo de isolamento corporativo. Esse argumento confunde divisão lógica com barreira física. Todos os schemas continuam disputando a mesma CPU, o mesmo subsistema de disco, o mesmo buffer pool e os mesmos limites de conexões do PostgreSQL. Se um cliente executar uma consulta pesada que sequestra recursos do banco, todos os outros clientes sofrerão lentidão da mesma forma. Além disso, usar múltiplos schemas quebra ou complica brutalmente o uso de poolers de conexão como o PgBouncer no modo de transação, porque alternar o caminho de busca com comandos de configuração a cada requisição adiciona sobrecarga desnecessária.

O segundo grupo, cansado dessa complexidade de infraestrutura, foge para o extremo oposto: junta tudo em um schema padrão, adiciona uma coluna com o identificador da conta em todas as tabelas e delega o isolamento inteiramente à aplicação. Em ecossistemas como o Ruby on Rails, isso normalmente significa espalhar escopos globais por meio de bibliotecas ou filtros de modelo. Essa abordagem elimina os problemas de migração e alívio do catálogo, mas cria uma vulnerabilidade silenciosa. Basta um desenvolvedor desatento usar um método sem escopo em uma consulta secundária, em um endpoint de API ou em um job em segundo plano para expor registros confidenciais de um concorrente. Depender exclusivamente da disciplina do time em cada linha de código escrita não é uma estratégia de isolamento aceitável.

A saída que resolve esse impasse sem multiplicar schemas existe há anos no próprio motor do PostgreSQL, mas continua negligenciada: Row-Level Security, ou RLS.

Em vez de fatiar o banco em centenas de namespaces idênticos, você mantém uma estrutura única, adiciona a coluna de identificação do tenant em cada tabela e transfere a responsabilidade da filtragem para o mecanismo de execução de consultas do banco. Mesmo que a sua aplicação execute uma busca direta sem nenhuma restrição na cláusula WHERE, o PostgreSQL intercepta a operação e aplica a regra de visibilidade antes de retornar qualquer registro.

A implementação dessa camada exige disciplina no controle de sessão do banco. No PostgreSQL, a tabela precisa ter o recurso ativado explicitamente e uma política associada:

```sql
ALTER TABLE projetos ENABLE ROW LEVEL SECURITY;
ALTER TABLE projetos FORCE ROW LEVEL SECURITY;

CREATE POLICY isolamento_projetos ON projetos
  FOR ALL
  USING (tenant_id = NULLIF(current_setting("app.current_tenant_id", true), "")::uuid);
```

O comando para forçar a segurança garante que até mesmo o usuário proprietário da tabela respeite a regra durante a execução das consultas. No lado da aplicação, a cada requisição web ou execução de rotina assíncrona, a conexão define o identificador do tenant atual antes de tocar em qualquer dado:

```ruby
class ApplicationController < ActionController::Base
  around_action :definir_tenant_no_banco

  private

  def definir_tenant_no_banco
    tenant = identificar_tenant_atual
    
    ActiveRecord::Base.transaction do
      ActiveRecord::Base.connection.execute(
        ActiveRecord::Base.sanitize_sql(["SET LOCAL app.current_tenant_id = ?", tenant.id])
      )
      yield
    end
  end
end
```

O uso de variáveis locais à transação com a instrução específica é o ponto crítico desse fluxo. Quando a transação termina, a configuração da variável desaparece automaticamente. Isso viabiliza o uso pleno do PgBouncer em modo de transação, pois a conexão devolvida ao pool não carrega o identificador de um cliente para a requisição seguinte de outro usuário.

Essa abordagem não é isenta de exigências técnicas. Manter tudo em uma tabela compartilhada com RLS traz impactos diretos na modelagem de índices e no planejador de consultas.

Primeiro, todo índice composto precisa levar em consideração o identificador do tenant, quase sempre como primeira coluna. Se você criar um índice apenas pelo campo de data de criação ou status, o planejador do banco precisará cruzar o filtro do índice com a política de segurança em nível de linha, resultando em verificações de bitmap ou varreduras sequenciais que degradam a resposta conforme a tabela cresce.

Segundo, você deve lidar com dados mistos ou compartilhados. Tabelas que contêm registros globais do sistema ao lado de dados customizados por cliente exigem políticas com condições adicionais ou políticas distintas para leitura e escrita. Se a lógica da política ficar excessivamente carregada com subconsultas ou junções complexas, o overhead em cada SELECT se torna perceptível. A regra deve ser simples: comparações diretas de igualdade contra variáveis de sessão indexadas.

Terceiro, permissões de superusuário no banco invalidam todo o esforço. Superusuários ignoram as políticas de RLS por padrão no PostgreSQL. O usuário configurado pela aplicação precisa operar com privilégios estritos sobre o schema, sem poderes administrativos globais. Se o seu ambiente de desenvolvimento ou testes utiliza credenciais irrestritas, os testes automatizados podem passar mesmo contendo falhas estruturais de isolamento.

Há situações em que um schema único não atende. Se você vende software para governos ou instituições financeiras com exigências contratuais de chaves de criptografia dedicadas por cliente ou armazenamento fisicamente segregado, nem RLS nem schemas do PostgreSQL serão aceitos em uma auditoria séria. Nesses casos, o único caminho real é isolar instâncias completas de banco ou provisionar servidores dedicados.

Para o restante das aplicações, adotar dezenas de schemas com a promessa de segurança limpa cria uma armadilha operacional difícil de reverter. Deixar o banco em um schema único, com migrações atômicas em milissegundos e políticas de linha aplicadas diretamente pelo motor relacional, elimina o medo de vazamento sem sacrificar a sanidade de quem precisa manter os serviços rodando.
