Padrões de Banco de Dados Multi-Inquilino com exemplos em Go
Guia completo sobre padrões de banco de dados multi-tenant
Multi-tenancy é um padrão arquitetural fundamental para aplicações SaaS, permitindo que múltiplos clientes (inquilinos) compartilhem a mesma infraestrutura de aplicação, mantendo a isolamento de dados.
Escolher o padrão correto de banco de dados é crucial para escalabilidade, segurança e eficiência operacional.

Visão Geral dos Padrões de Multi-Inquilino
Ao projetar uma aplicação multi-inquilino, você tem três padrões principais de arquitetura de banco de dados para escolher:
- Banco de Dados Compartilhado, Esquema Compartilhado (mais comum)
- Banco de Dados Compartilhado, Esquema Separado
- Banco de Dados Separado por Inquilino
Cada padrão tem características distintas, trade-offs e casos de uso. Vamos explorar cada um em detalhe.
Padrão 1: Banco de Dados Compartilhado, Esquema Compartilhado
Este é o padrão mais comum de multi-inquilino, onde todos os inquilinos compartilham o mesmo banco de dados e esquema, com uma coluna tenant_id usada para distinguir os dados dos inquilinos.
Arquitetura
┌─────────────────────────────────────┐
│ Single Database │
│ ┌───────────────────────────────┐ │
│ │ Shared Schema │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Exemplo de Implementação
Ao implementar padrões de multi-inquilino, compreender fundamentos do SQL é crucial. Para uma referência abrangente sobre comandos e sintaxe SQL, consulte nossa SQL Cheatsheet. Aqui está como configurar o padrão de esquema compartilhado:
-- Tabela de usuários com tenant_id
CREATE TABLE users (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Índice em tenant_id para desempenho
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Segurança de Linha (exemplo do PostgreSQL)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
Para mais recursos específicos do PostgreSQL, incluindo políticas RLS, gerenciamento de esquema e ajuste de desempenho, consulte nossa PostgreSQL Cheatsheet.
Filtragem no Nível da Aplicação
Ao trabalhar com aplicações Go, escolher o ORM certo pode impactar significativamente sua implementação de multi-inquilino. Os exemplos abaixo usam GORM, mas existem várias opções excelentes disponíveis. Para uma comparação detalhada de ORMs Go incluindo GORM, Ent, Bun e sqlc, veja nossa guia abrangente sobre ORMs Go para PostgreSQL.
// Exemplo em Go com GORM
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
var user User
err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
return &user, err
}
// Middleware para definir o contexto do inquilino
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Do subdomínio, cabeçalho ou JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Vantagens do Esquema Compartilhado
- Custo mais baixo: Instância única do banco de dados, infraestrutura mínima
- Operações mais simples: Um único banco de dados para backup, monitoramento e manutenção
- Alterações de esquema simples: Migrações aplicam-se a todos os inquilinos ao mesmo tempo
- Melhor para alto número de inquilinos: Utilização eficiente de recursos
- Análise entre inquilinos: Fácil de agregar dados entre inquilinos
Desvantagens do Esquema Compartilhado
- Isolamento mais fraco: Risco de vazamento de dados se as consultas esquecerem o filtro tenant_id
- Inquilino barulhento: Carga de trabalho pesada de um inquilino pode afetar outros
- Personalização limitada: Todos os inquilinos compartilham o mesmo esquema
- Desafios de conformidade: Mais difícil de atender a requisitos rigorosos de isolamento de dados
- Complexidade de backup: Não é possível restaurar facilmente dados de um único inquilino
Melhor para Esquema Compartilhado
- Aplicações SaaS com muitos inquilinos pequenos a médios
- Aplicações onde os inquilinos não precisam de esquemas personalizados
- Startups sensíveis ao custo
- Quando o número de inquilinos é alto (milhares+)
Padrão 2: Banco de Dados Compartilhado, Esquema Separado
Cada inquilino recebe seu próprio esquema dentro do mesmo banco de dados, fornecendo melhor isolamento enquanto compartilha a infraestrutura.
Arquitetura de Esquema Separado
┌─────────────────────────────────────┐
│ Single Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Tenant1)│ │ (Tenant2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implementação de Esquema Separado
Os esquemas do PostgreSQL são uma funcionalidade poderosa para multi-inquilino. Para informações detalhadas sobre gerenciamento de esquema do PostgreSQL, strings de conexão e comandos de administração de banco de dados, consulte nossa PostgreSQL Cheatsheet.
-- Crie um esquema para o inquilino
CREATE SCHEMA tenant_123;
-- Defina o caminho de pesquisa para operações do inquilino
SET search_path TO tenant_123, public;
-- Crie tabelas no esquema do inquilino
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gerenciamento de Conexão da Aplicação
Gerenciar conexões de banco de dados de forma eficiente é crítico para aplicações multi-inquilino. O código de gerenciamento de conexão abaixo usa GORM, mas você pode querer explorar outras opções de ORM. Para uma comparação detalhada de ORMs Go incluindo pooling de conexão, características de desempenho e casos de uso, consulte nossa guia de comparação de ORMs Go.
// String de conexão com caminho de pesquisa do esquema
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// Ou use a string de conexão do PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Vantagens do Esquema Separado
- Melhor isolamento: Separação no nível do esquema reduz o risco de vazamento de dados
- Personalização: Cada inquilino pode ter estruturas de tabela diferentes
- Custo moderado: Ainda é uma instância única do banco de dados
- Backups por inquilino mais fáceis: É possível fazer backup de esquemas individuais
- Melhor para conformidade: Mais forte do que o padrão de esquema compartilhado
Desvantagens do Esquema Separado
- Complexidade de gerenciamento de esquema: Migrações devem ser executadas por inquilino
- Sobrecarga de conexão: É necessário definir search_path por conexão
- Escalabilidade limitada: O número de esquemas tem limites (PostgreSQL ~10k esquemas)
- Consultas entre inquilinos: Mais complexas, exigem referências dinâmicas de esquema
- Limites de recursos: Recursos do banco de dados ainda são compartilhados
Melhor para Esquema Separado
- SaaS de média escala (dezenas a centenas de inquilinos)
- Quando os inquilinos precisam de personalização de esquema
- Aplicações que precisam de melhor isolamento do que o esquema compartilhado
- Quando os requisitos de conformidade são moderados
Padrão 3: Banco de Dados Separado por Inquilino
Cada inquilino recebe sua própria instância completa de banco de dados, fornecendo o máximo de isolamento.
Arquitetura de Banco de Dados Separado
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Tenant A) │ │ (Tenant B) │ │ (Tenant C) │
└──────────────┘ └──────────────┘ └──────────────┘
Implementação de Banco de Dados Separado
-- Crie um banco de dados para o inquilino
CREATE DATABASE tenant_enterprise_corp;
-- Conecte-se ao banco de dados do inquilino
\c tenant_enterprise_corp
-- Crie tabelas (não é necessário tenant_id!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gerenciamento Dinâmico de Conexão
// Gerenciador de pool de conexão
type TenantDBManager struct {
pools map[uint]*gorm.DB
mu sync.RWMutex
}
func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
m.mu.RLock()
if db, exists := m.pools[tenantID]; exists {
m.mu.RUnlock()
return db, nil
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
// Verifique novamente após adquirir o bloqueio de escrita
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Crie uma nova conexão
db, err := gorm.Open(postgres.Open(fmt.Sprintf(
"host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
tenantID,
)), &gorm.Config{})
if err != nil {
return nil, err
}
m.pools[tenantID] = db
return db, nil
}
Vantagens de Banco de Dados Separado
- Isolamento máximo: Separação completa de dados
- Melhor segurança: Nenhum risco de acesso a dados entre inquilinos
- Personalização total: Cada inquilino pode ter esquemas completamente diferentes
- Escalabilidade independente: Escalabilidade individual dos bancos de dados dos inquilinos
- Conformidade fácil: Atende aos requisitos mais rigorosos de isolamento de dados
- Backups por inquilino: Simples, backup/restauração independente
- Nenhum inquilino barulhento: Cargas de trabalho de inquilinos não afetam uns aos outros
Desvantagens de Banco de Dados Separado
- Custo mais alto: Múltiplas instâncias de banco de dados exigem mais recursos
- Complexidade operacional: Gerenciar muitos bancos de dados (backups, monitoramento, migrações)
- Limites de conexão: Cada instância de banco de dados tem limites de conexão
- Análise entre inquilinos: Requer federação de dados ou ETL
- Complexidade de migração: Deve executar migrações em todos os bancos de dados
- Sobrecarga de recursos: Mais memória, CPU e armazenamento necessários
Melhor para Banco de Dados Separado
- SaaS empresarial com clientes de alto valor
- Requisitos de conformidade rigorosos (HIPAA, GDPR, SOC 2)
- Quando os inquilinos precisam de personalização significativa
- Número de inquilinos baixo a médio (dezenas a centenas baixas)
- Quando os inquilinos têm modelos de dados muito diferentes
Considerações de Segurança
Independentemente do padrão escolhido, a segurança é primordial:
1. Segurança de Linha (RLS)
A RLS do PostgreSQL filtra automaticamente consultas por inquilino, fornecendo uma camada de segurança no nível do banco de dados. Esta funcionalidade é particularmente poderosa para aplicações multi-inquilino. Para mais detalhes sobre RLS do PostgreSQL, políticas de segurança e outras funcionalidades avançadas do PostgreSQL, veja nossa PostgreSQL Cheatsheet.
-- Ative RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Política para isolar por inquilino
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- A aplicação define o contexto do inquilino
SET app.current_tenant = '123';
2. Filtragem no Nível da Aplicação
Sempre filtre por tenant_id no código da aplicação. Os exemplos abaixo usam GORM, mas diferentes ORMs têm suas próprias abordagens para construção de consultas. Para orientação sobre escolher o ORM certo para sua aplicação multi-inquilino, consulte nossa comparação de ORMs Go.
// ❌ RUIM - Filtro de inquilino ausente
db.Where("email = ?", email).First(&user)
// ✅ BOM - Sempre inclua o filtro de inquilino
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ MELHOR - Use escopos ou middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Pooling de Conexão
Use poolers de conexão que suportam contexto de inquilino:
// PgBouncer com pooling de transação
// Ou use roteamento de conexão no nível da aplicação
4. Registros de Auditoria
Rastreie todo o acesso a dados de inquilino:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Otimização de Desempenho
Estratégia de Índices
O índice adequado é crucial para o desempenho do banco de dados multi-inquilino. Compreender estratégias de índice SQL, incluindo índices compostos e parciais, é essencial. Para uma referência abrangente sobre comandos SQL, incluindo CREATE INDEX e otimização de consultas, veja nossa SQL Cheatsheet. Para recursos específicos de índice do PostgreSQL e ajuste de desempenho, consulte nossa PostgreSQL Cheatsheet.
-- Índices compostos para consultas de inquilino
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Índices parciais para consultas comuns específicas de inquilino
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Otimização de Consultas
// Use instruções preparadas para consultas de inquilino
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Operações em lote por inquilino
db.Where("tenant_id = ?", tenantID).Find(&users)
// Use pooling de conexão por inquilino (para padrão de banco de dados separado)
Monitoramento
Ferramentas essenciais de gerenciamento de banco de dados são necessárias para monitorar aplicações multi-inquilino. Você precisará rastrear o desempenho das consultas, uso de recursos e saúde do banco de dados em todos os inquilinos. Para comparar ferramentas de gerenciamento de banco de dados que podem ajudar com isso, consulte nossa comparação entre DBeaver e Beekeeper. Ambas as ferramentas oferecem excelentes recursos para gerenciar e monitorar bancos de dados PostgreSQL em ambientes multi-inquilino.
Monitore métricas por inquilino:
- Desempenho de consultas por inquilino
- Uso de recursos por inquilino
- Contagem de conexões por inquilino
- Tamanho do banco de dados por inquilino
Estratégia de Migração
Padrão de Esquema Compartilhado
Ao implementar migrações de banco de dados, sua escolha de ORM afeta como você lida com alterações de esquema. Os exemplos abaixo usam a funcionalidade AutoMigrate do GORM, mas diferentes ORMs têm diferentes estratégias de migração. Para informações detalhadas sobre como diferentes ORMs Go lidam com migrações e gerenciamento de esquema, veja nossa comparação de ORMs Go.
// Migrações aplicam-se automaticamente a todos os inquilinos
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Padrão de Esquema/Banco de Dados Separado
// Migrações devem ser executadas por inquilino
func MigrateAllTenants(tenantIDs []uint) error {
for _, tenantID := range tenantIDs {
db := GetTenantDB(tenantID)
if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
return fmt.Errorf("tenant %d: %w", tenantID, err)
}
}
return nil
}
Matriz de Decisão
| Fator | Esquema Compartilhado | Esquema Separado | Banco de Dados Separado |
|---|---|---|---|
| Isolamento | Baixo | Médio | Alto |
| Custo | Baixo | Médio | Alto |
| Escalabilidade | Alta | Média | Baixa-Média |
| Personalização | Nenhum | Médio | Alto |
| Complexidade Operacional | Baixa | Média | Alta |
| Conformidade | Limitada | Boa | Excelente |
| Melhor Número de Inquilinos | 1000+ | 10-1000 | 1-100 |
Abordagem Híbrida
Você pode combinar padrões para diferentes níveis de inquilino:
// Inquilinos pequenos: Esquema compartilhado
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Inquilinos empresariais: Banco de dados separado
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Boas Práticas
- Sempre filtre por inquilino: Nunca confie apenas no código da aplicação; use RLS quando possível. Compreender fundamentos do SQL ajuda a garantir a construção correta das consultas—consulte nossa SQL Cheatsheet para melhores práticas de consulta.
- Monitore o uso de recursos dos inquilinos: Identifique e limite inquilinos barulhentos. Use ferramentas de gerenciamento de banco de dados, como as comparadas em nossa guia DBeaver vs Beekeeper, para rastrear métricas de desempenho.
- Implemente middleware de contexto de inquilino: Centralize a extração e validação do inquilino. Sua escolha de ORM afeta como você implementa isso—veja nossa comparação de ORMs Go para diferentes abordagens.
- Use pooling de conexão: Gerencie eficientemente as conexões do banco de dados. Estratégias específicas de pooling de conexão do PostgreSQL são cobertas em nossa PostgreSQL Cheatsheet.
- Planeje a migração de inquilino: Capacidade de mover inquilinos entre padrões
- Implemente exclusão suave: Use deleted_at em vez de exclusões duras para dados de inquilino
- Audite tudo: Registre todos os acessos de dados de inquilino para conformidade
- Teste isolamento: Auditorias de segurança regulares para prevenir vazamento de dados entre inquilinos
Conclusão
Escolher o padrão correto de banco de dados multi-inquilino depende de seus requisitos específicos para isolamento, custo, escalabilidade e complexidade operacional. O padrão Banco de Dados Compartilhado, Esquema Compartilhado funciona bem para a maioria das aplicações SaaS, enquanto o Banco de Dados Separado por Inquilino é necessário para clientes empresariais com requisitos rigorosos de conformidade.
Comece com o padrão mais simples que atenda aos seus requisitos e planeje a migração para um padrão mais isolado conforme suas necessidades evoluam. Sempre priorize segurança e isolamento de dados, independentemente do padrão escolhido.
Links Úteis
- Documentação de Segurança de Linha do PostgreSQL
- Arquitetura de Banco de Dados SaaS Multi-Inquilino
- Projeto de Banco de Dados Multi-Inquilino
- Comparando ORMs Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Uma Referência Rápida para Desenvolvedores
- DBeaver vs Beekeeper - Ferramentas de Gerenciamento de Banco de Dados SQL
- SQL Cheatsheet - comandos SQL mais úteis