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

Visão Geral dos Padrões de Multi-tenancy
Ao projetar uma aplicação multi-tenant, 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, trade-offs e casos de uso distintos. Vamos explorar cada um em detalhe.
Padrão 1: Banco de Dados Compartilhado, Esquema Compartilhado
Este é o padrão de multi-tenancy mais comum, onde todos os inquilinos compartilham o mesmo banco de dados e esquema, com uma coluna tenant_id usada para distinguir os dados do inquilino.
Arquitetura
┌─────────────────────────────────────┐
│ Banco de Dados Único │
│ ┌───────────────────────────────┐ │
│ │ Esquema Compartilhado │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Exemplo de Implementação
Ao implementar padrões multi-tenant, entender os fundamentos de SQL é crucial. Para uma referência abrangente sobre comandos e sintaxe SQL, confira nosso Resumo de Comandos SQL. Veja 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 Nível de Linha (exemplo 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 e comandos específicos do PostgreSQL, incluindo políticas RLS, gerenciamento de esquemas e ajuste de desempenho, consulte nosso Resumo de Comandos PostgreSQL.
Filtragem no Nível da Aplicação
Ao trabalhar com aplicações Go, escolher o ORM correto pode impactar significativamente sua implementação multi-tenant. Os exemplos abaixo usam GORM, mas há várias opções excelentes disponíveis. Para uma comparação detalhada dos ORMs Go, incluindo GORM, Ent, Bun e sqlc, veja nosso guia abrangente de 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 contexto do inquilino
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // De subdomínio, header 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 de banco de dados, infraestrutura mínima
- Operações mais fáceis: Um banco de dados para backup, monitoramento e manutenção
- Mudanças de esquema simples: Migrações aplicadas a todos os inquilinos de uma vez
- Melhor para alto número de inquilinos: Utilização eficiente de recursos
- Análises cross-tenant: 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
- Vizinho barulhento: A carga pesada de um inquilino pode afetar os outros
- Personalização limitada: Todos os inquilinos compartilham o mesmo esquema
- Desafios de conformidade: Mais difícil atender requisitos rigorosos de isolamento de dados
- Complexidade de backup: Não é fácil restaurar dados de inquilinos individuais
Esquema Compartilhado é Melhor para
- Aplicações SaaS com muitos inquilinos pequenos a médios
- Aplicações onde os inquilinos não precisam de esquemas personalizados
- Startups sensíveis a custos
- Quando a contagem de inquilinos é alta (milhares+)
Padrão 2: Banco de Dados Compartilhado, Esquema Separado
Cada inquilino recebe seu próprio esquema dentro do mesmo banco de dados, proporcionando melhor isolamento enquanto compartilha a infraestrutura.
Arquitetura de Esquema Separado
┌─────────────────────────────────────┐
│ Banco de Dados Único │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Tenant1)│ │ (Tenant2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implementação de Esquema Separado
Os esquemas do PostgreSQL são um recurso poderoso para multi-tenancy. Para informações detalhadas sobre gerenciamento de esquemas PostgreSQL, strings de conexão e comandos de administração de banco de dados, consulte nosso Resumo de Comandos PostgreSQL.
-- Criar esquema para o inquilino
CREATE SCHEMA tenant_123;
-- Definir caminho de pesquisa para operações do inquilino
SET search_path TO tenant_123, public;
-- Criar 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-tenant. 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 aprofundada dos ORMs Go, incluindo pooling de conexões, características de desempenho e casos de uso, consulte nosso 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 usar string de conexão PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Vantagens do Esquema Separado
- Melhor isolamento: Separação a nível de esquema reduz o risco de vazamento de dados
- Personalização: Cada inquilino pode ter estruturas de tabelas diferentes
- Custo moderado: Ainda é uma única instância de banco de dados
- Backups por inquilino mais fáceis: Pode fazer backup de esquemas individuais
- Melhor para conformidade: Mais forte 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: Limites na contagem de esquemas (PostgreSQL ~10k esquemas)
- Consultas cross-tenant: Mais complexas, requer referências de esquema dinâmicas
- Limites de recursos: Recursos de banco de dados ainda compartilhados
Esquema Separado é Melhor para
- SaaS de escala média (dezenas a centenas de inquilinos)
- Quando os inquilinos precisam de personalização de esquema
- Aplicações que precisam de melhor isolamento 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, proporcionando isolamento máximo.
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
-- Criar banco de dados para o inquilino
CREATE DATABASE tenant_enterprise_corp;
-- Conectar ao banco de dados do inquilino
\c tenant_enterprise_corp
-- Criar tabelas (tenant_id não é necessário!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gerenciamento de Conexão Dinâmico
// Gerenciador de pool de conexões
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()
// Verificação dupla após adquirir bloqueio de escrita
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Criar 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 do Banco de Dados Separado
- Isolamento máximo: Separação completa de dados
- Melhor segurança: Sem risco de acesso a dados cross-tenant
- Personalização total: Cada inquilino pode ter esquemas completamente diferentes
- Escalabilidade independente: Escalar bancos de dados de inquilinos individualmente
- Conformidade fácil: Atende aos requisitos mais rigorosos de isolamento de dados
- Backups por inquilino: Backup/restauração simples e independente
- Sem vizinhos barulhentos: Cargas de trabalho de inquilinos não se afetam
Desvantagens do Banco de Dados Separado
- Custo mais alto: Múltiplas instâncias de banco de dados requerem 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álises cross-tenant: 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
Banco de Dados Separado é Melhor para
- SaaS Empresarial com clientes de alto valor
- Requisitos de conformidade rigorosos (HIPAA, GDPR, SOC 2)
- Quando os inquilinos precisam de personalização significativa
- Contagem de inquilinos baixa a média (dezenas a baixas centenas)
- 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 Nível de Linha (RLS)
O RLS do PostgreSQL filtra automaticamente consultas por inquilino, fornecendo uma camada de segurança a nível de banco de dados. Este recurso é particularmente poderoso para aplicações multi-tenant. Para mais detalhes sobre RLS do PostgreSQL, políticas de segurança e outros recursos avançados do PostgreSQL, veja nosso Resumo de Comandos PostgreSQL.
-- Habilitar 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);
-- Aplicação define 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ções sobre a escolha do ORM certo para sua aplicação multi-tenant, confira nossa comparação de ORMs Go.
// ❌ RUIM - Filtro de inquilino faltando
db.Where("email = ?", email).First(&user)
// ✅ BOM - Sempre incluir filtro de inquilino
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ MELHOR - Usar escopos ou middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Pooling de Conexões
Use poolers de conexão que suportam contexto de inquilino:
// PgBouncer com pooling de transação
// Ou usar roteamento de conexão no nível da aplicação
4. Logs de Auditoria
Rastreie todo o acesso a dados do 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 Indexação
A indexação adequada é crucial para o desempenho do banco de dados multi-tenant. Compreender estratégias de indexação SQL, incluindo índices compostos e parciais, é essencial. Para uma referência abrangente sobre comandos SQL, incluindo CREATE INDEX e otimização de consultas, veja nosso Resumo de Comandos SQL. Para recursos de indexação específicos do PostgreSQL e ajuste de desempenho, consulte nosso Resumo de Comandos PostgreSQL.
-- Í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 do 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 eficazes de gerenciamento de banco de dados são essenciais para monitorar aplicações multi-tenant. Você precisará rastrear desempenho de 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, confira nossa comparação DBeaver vs Beekeeper. Ambas as ferramentas oferecem excelentes recursos para gerenciar e monitorar bancos de dados PostgreSQL em ambientes multi-tenant.
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 mudanças de esquema. Os exemplos abaixo usam o recurso AutoMigrate do GORM, mas diferentes ORMs têm diferentes estratégias de migração. Para informações detalhadas sobre como vários ORMs Go lidam com migrações e gerenciamento de esquema, veja nossa comparação de ORMs Go.
// Migrações aplicam-se a todos os inquilinos automaticamente
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édio | Baixo-Médio |
| Personalização | Nenhuma | Médio | Alta |
| Complexidade Operacional | Baixo | Médio | Alto |
| Conformidade | Limitado | Bom | Excelente |
| Melhor Contagem de Inquilinos | 1000+ | 10-1000 | 1-100 |
Abordagem Híbrida
Você pode combinar padrões para diferentes níveis de inquilinos:
// Inquilinos pequenos: Esquema compartilhado
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Inquilinos Enterprise: Banco de dados separado
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Melhores Práticas
- Sempre filtre por inquilino: Nunca confie apenas no código da aplicação; use RLS quando possível. Entender os fundamentos de SQL ajuda a garantir a construção adequada de consultas — consulte nosso Resumo de Comandos SQL para melhores práticas de consulta.
- Monitore o uso de recursos do inquilino: Identifique e limite vizinhos barulhentos. Use ferramentas de gerenciamento de banco de dados como as comparadas em nosso guia DBeaver vs Beekeeper para rastrear métricas de desempenho.
- Implemente middleware de contexto do 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 conexões de banco de dados de forma eficiente. Estratégias de pooling de conexão específicas do PostgreSQL são cobertas em nosso Resumo de Comandos PostgreSQL.
- Planeje para migração de inquilinos: Capacidade de mover inquilinos entre padrões
- Implemente exclusão suave: Use deleted_at em vez de exclusões rígidas para dados do inquilino
- Audite tudo: Registre todo o acesso a dados do inquilino para conformidade
- Teste isolamento: Auditorias de segurança regulares para prevenir vazamento de dados cross-tenant
Conclusão
Escolher o padrão de banco de dados multi-tenant certo depende dos seus requisitos específicos de 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 Banco de Dados Separado por Inquilino é necessário para clientes empresariais com necessidades de conformidade rigorosas.
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 evoluem. Priorize sempre a segurança e o isolamento de dados, independentemente do padrão escolhido.
Links Úteis
- Documentação de Segurança de Nível de Linha do PostgreSQL
- Arquitetura de Banco de Dados SaaS Multi-tenant
- Projetando Bancos de Dados Multi-tenant
- Comparando ORMs Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Resumo de Comandos PostgreSQL: Referência Rápida do Desenvolvedor
- DBeaver vs Beekeeper - Ferramentas de Gerenciamento de Banco de Dados SQL
- Resumo de Comandos SQL - comandos SQL mais úteis