Patrones de base de datos con multiinquilino con ejemplos en Go
Guía completa sobre patrones de bases de datos multiinquilino
Multi-tenancy es un patrón arquitectónico fundamental para aplicaciones SaaS, permitiendo que múltiples clientes (inquilinos) compartan la misma infraestructura de aplicación mientras mantienen la aislamiento de datos.
Elegir el patrón de base de datos adecuado es crucial para la escalabilidad, la seguridad y la eficiencia operativa.

Visión general de los patrones de multi-inquilino
Al diseñar una aplicación multi-inquilino, tienes tres patrones principales de arquitectura de base de datos de los que elegir:
- Base de datos compartida, esquema compartido (más común)
- Base de datos compartida, esquema separado
- Base de datos separada por inquilino
Cada patrón tiene características distintas, compromisos y casos de uso. Vamos a explorar cada uno en detalle.
Patrón 1: Base de datos compartida, esquema compartido
Este es el patrón más común de multi-inquilino, donde todos los inquilinos comparten la misma base de datos y esquema, con una columna tenant_id utilizada para distinguir los datos del inquilino.
Arquitectura
┌─────────────────────────────────────┐
│ Single Database │
│ ┌───────────────────────────────┐ │
│ │ Shared Schema │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Ejemplo de implementación
Al implementar patrones multi-inquilino, entender los fundamentos de SQL es crucial. Para una referencia completa sobre comandos y sintaxis SQL, consulta nuestro SQL Cheatsheet. Aquí está cómo configurar el patrón de esquema compartido:
-- Tabla de usuarios con 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 en tenant_id para el rendimiento
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Seguridad de nivel de fila (ejemplo de 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 más características y comandos específicos de PostgreSQL, incluyendo políticas RLS, gestión de esquemas y ajuste de rendimiento, consulta nuestro PostgreSQL Cheatsheet.
Filtrado a nivel de aplicación
Cuando trabajas con aplicaciones Go, elegir el ORM adecuado puede tener un impacto significativo en tu implementación multi-inquilino. Los ejemplos a continuación utilizan GORM, pero hay varias opciones excelentes disponibles. Para una comparación detallada de ORMs Go incluyendo GORM, Ent, Bun y sqlc, consulta nuestra guía completa sobre ORMs Go para PostgreSQL.
// Ejemplo en Go con 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 establecer el contexto del inquilino
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Desde subdominio, encabezado o JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Ventajas del esquema compartido
- Costo más bajo: Instancia de base de datos única, infraestructura mínima
- Operaciones más fáciles: Una base de datos para respaldar, monitorear y mantener
- Cambios de esquema simples: Las migraciones se aplican a todos los inquilinos a la vez
- Mejor para un alto número de inquilinos: Uso eficiente de recursos
- Análisis transversal de inquilinos: Fácil de agregar datos a través de inquilinos
Desventajas del esquema compartido
- Aislamiento más débil: Riesgo de fuga de datos si las consultas olvidan el filtro tenant_id
- Inquilino ruidoso: La carga de trabajo pesada de un inquilino puede afectar a otros
- Personalización limitada: Todos los inquilinos comparten el mismo esquema
- Desafíos de cumplimiento: Más difícil cumplir con requisitos estrictos de aislamiento de datos
- Complejidad de respaldo: No se puede restaurar fácilmente los datos de un inquilino individual
Mejor para el esquema compartido
- Aplicaciones SaaS con muchos inquilinos pequeños a medianos
- Aplicaciones donde los inquilinos no necesitan esquemas personalizados
- Empresas emergentes sensibles al costo
- Cuando el número de inquilinos es alto (miles+)
Patrón 2: Base de datos compartida, esquema separado
Cada inquilino obtiene su propio esquema dentro de la misma base de datos, proporcionando un mejor aislamiento mientras comparten la infraestructura.
Arquitectura de esquema separado
┌─────────────────────────────────────┐
│ Single Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Tenant1)│ │ (Tenant2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implementación de esquema separado
Las esquemas de PostgreSQL son una característica poderosa para multi-inquilino. Para información detallada sobre la gestión de esquemas de PostgreSQL, cadenas de conexión y comandos de administración de bases de datos, consulta nuestro PostgreSQL Cheatsheet.
-- Crear esquema para inquilino
CREATE SCHEMA tenant_123;
-- Establecer ruta de búsqueda para operaciones del inquilino
SET search_path TO tenant_123, public;
-- Crear tablas en el esquema del inquilino
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestión de conexiones de la aplicación
Gestionar eficientemente las conexiones de base de datos es crítico para aplicaciones multi-inquilino. El código de gestión de conexiones a continuación utiliza GORM, pero podrías querer explorar otras opciones de ORM. Para una comparación completa de ORMs Go incluyendo gestión de conexión, características de rendimiento y casos de uso, consulta nuestra guía de comparación de ORMs Go.
// Cadena de conexión con ruta de búsqueda del esquema
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// O usar la cadena de conexión de PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Ventajas del esquema separado
- Mejor aislamiento: El aislamiento a nivel de esquema reduce el riesgo de fuga de datos
- Personalización: Cada inquilino puede tener estructuras de tabla diferentes
- Costo moderado: Todavía es una sola instancia de base de datos
- Respaldos por inquilino más fáciles: Se puede respaldar individualmente los esquemas
- Mejor para cumplimiento: Más fuerte que el patrón de esquema compartido
Desventajas del esquema separado
- Complejidad de gestión de esquemas: Las migraciones deben ejecutarse por inquilino
- Sobrecarga de conexión: Se necesita establecer search_path por conexión
- Escalabilidad limitada: Límite de esquemas (PostgreSQL ~10k esquemas)
- Consultas transversales de inquilino: Más complejas, requieren referencias dinámicas de esquema
- Límites de recursos: Recursos de base de datos compartidos
Mejor para el esquema separado
- SaaS de tamaño medio (docenas a cientos de inquilinos)
- Cuando los inquilinos necesitan personalización del esquema
- Aplicaciones que necesitan un mejor aislamiento que el esquema compartido
- Cuando los requisitos de cumplimiento son moderados
Patrón 3: Base de datos separada por inquilino
Cada inquilino obtiene su propia instancia completa de base de datos, proporcionando el máximo aislamiento.
Arquitectura de base de datos separada
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Tenant A) │ │ (Tenant B) │ │ (Tenant C) │
└──────────────┘ └──────────────┘ └──────────────┘
Implementación de base de datos separada
-- Crear base de datos para inquilino
CREATE DATABASE tenant_enterprise_corp;
-- Conectar a la base de datos del inquilino
\c tenant_enterprise_corp
-- Crear tablas (no se necesita tenant_id!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestión dinámica de conexiones
// Gestor de pool de conexiones
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()
// Revisar de nuevo después de adquirir el bloqueo de escritura
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Crear nueva conexión
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
}
Ventajas de la base de datos separada
- Aislamiento máximo: Separación completa de datos
- Mejor seguridad: Sin riesgo de acceso a datos entre inquilinos
- Personalización completa: Cada inquilino puede tener esquemas completamente diferentes
- Escalado independiente: Escalar las bases de datos de los inquilinos individualmente
- Cumplimiento fácil: Cumple con los requisitos más estrictos de aislamiento de datos
- Respaldos por inquilino: Fácil, respaldo/restauración independiente
- Sin vecinos ruidosos: Las cargas de trabajo de los inquilinos no afectan a otros
Desventajas de la base de datos separada
- Costo más alto: Instancias múltiples de base de datos requieren más recursos
- Complejidad operativa: Gestionar muchas bases de datos (respaldos, monitoreo, migraciones)
- Límites de conexión: Cada instancia de base de datos tiene límites de conexión
- Análisis transversal de inquilino: Requiere federación de datos o ETL
- Complejidad de migración: Debe ejecutarse migraciones en todas las bases de datos
- Sobrecarga de recursos: Más memoria, CPU y almacenamiento necesarios
Mejor para la base de datos separada
- SaaS empresarial con clientes de alto valor
- Requisitos de cumplimiento estrictos (HIPAA, GDPR, SOC 2)
- Cuando los inquilinos necesitan personalización significativa
- Cantidad de inquilinos baja a moderada (docenas a cientos bajos)
- Cuando los inquilinos tienen modelos de datos muy diferentes
Consideraciones de seguridad
Independientemente del patrón elegido, la seguridad es primordial:
1. Seguridad de nivel de fila (RLS)
La RLS de PostgreSQL filtra automáticamente las consultas por inquilino, proporcionando una capa de seguridad a nivel de base de datos. Esta característica es especialmente poderosa para aplicaciones multi-inquilino. Para más detalles sobre la RLS de PostgreSQL, políticas de seguridad y otras características avanzadas de PostgreSQL, consulta nuestro PostgreSQL Cheatsheet.
-- Habilitar RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Política para aislar por inquilino
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Aplicación establece contexto del inquilino
SET app.current_tenant = '123';
2. Filtrado a nivel de aplicación
Siempre filtra por tenant_id en el código de la aplicación. Los ejemplos a continuación utilizan GORM, pero diferentes ORMs tienen sus propios enfoques para construir consultas. Para orientación sobre elegir el ORM adecuado para tu aplicación multi-inquilino, consulta nuestra comparación de ORMs Go.
// ❌ MAL - Filtro de inquilino faltante
db.Where("email = ?", email).First(&user)
// ✅ BIEN - Siempre incluir filtro de inquilino
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ MEJOR - Usar alcances o middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Pools de conexión
Usa pools de conexión que admitan el contexto del inquilino:
// PgBouncer con pooling de transacciones
// O usar enrutamiento de conexión a nivel de aplicación
4. Registro de auditoría
Rastrear todo el acceso a datos del inquilino:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Optimización de rendimiento
Estrategia de índices
Un buen índice es crucial para el rendimiento de bases de datos multi-inquilino. Entender las estrategias de índice de SQL, incluyendo índices compuestos e índices parciales, es esencial. Para una referencia completa sobre comandos SQL incluyendo CREATE INDEX y optimización de consultas, consulta nuestro SQL Cheatsheet. Para características de índice específicas de PostgreSQL y ajuste de rendimiento, consulta nuestro PostgreSQL Cheatsheet.
-- Índices compuestos 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 parciales para consultas comunes específicas de inquilino
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Optimización de consultas
// Usar consultas preparadas para consultas de inquilino
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Operaciones por lote por inquilino
db.Where("tenant_id = ?", tenantID).Find(&users)
// Usar pooling de conexión por inquilino (para patrón de base de datos separada)
Monitoreo
Herramientas esenciales de gestión de bases de datos son necesarias para monitorear aplicaciones multi-inquilino. Necesitarás rastrear el rendimiento de consultas, el uso de recursos y la salud de la base de datos en todos los inquilinos. Para comparar herramientas de gestión de bases de datos que pueden ayudarte con esto, consulta nuestra comparación de DBeaver vs Beekeeper. Ambas herramientas ofrecen excelentes características para gestionar y monitorear bases de datos PostgreSQL en entornos multi-inquilino.
Monitorea métricas por inquilino:
- Rendimiento de consultas por inquilino
- Uso de recursos por inquilino
- Cuentas de conexión por inquilino
- Tamaño de base de datos por inquilino
Estrategia de migración
Patrón de esquema compartido
Cuando implementas migraciones de base de datos, tu elección de ORM afecta cómo manejas los cambios de esquema. Los ejemplos a continuación utilizan la característica AutoMigrate de GORM, pero diferentes ORMs tienen diferentes estrategias de migración. Para información detallada sobre cómo manejan las migraciones y la gestión de esquema diferentes ORMs Go, consulta nuestra comparación de ORMs Go.
// Las migraciones se aplican automáticamente a todos los inquilinos
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Patrón de esquema/base de datos separada
// Las migraciones deben ejecutarse 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 decisión
| Factor | Esquema compartido | Esquema separado | Base de datos separada |
|---|---|---|---|
| Aislamiento | Bajo | Medio | Alto |
| Costo | Bajo | Medio | Alto |
| Escalabilidad | Alta | Medio | Baja-Medio |
| Personalización | Ninguna | Medio | Alta |
| Complejidad operativa | Baja | Medio | Alta |
| Cumplimiento | Limitado | Bueno | Excelente |
| Mejor número de inquilinos | 1000+ | 10-1000 | 1-100 |
Enfoque híbrido
Puedes combinar patrones para diferentes niveles de inquilino:
// Inquilinos pequeños: Esquema compartido
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Inquilinos empresariales: Base de datos separada
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Buenas prácticas
- Siempre filtra por inquilino: Nunca confíes solo en el código de la aplicación; usa RLS cuando sea posible. Entender los fundamentos de SQL ayuda a asegurar la construcción adecuada de consultas—consulta nuestro SQL Cheatsheet para buenas prácticas de consulta.
- Monitorea el uso de recursos por inquilino: Identifica y limita los inquilinos ruidosos. Usa herramientas de gestión de bases de datos como las comparadas en nuestra guía de DBeaver vs Beekeeper para rastrear métricas de rendimiento.
- Implementa middleware de contexto de inquilino: Centraliza la extracción y validación del inquilino. Tu elección de ORM afecta cómo implementas esto—consulta nuestra comparación de ORMs Go para diferentes enfoques.
- Usa pooling de conexión: Gestiona eficientemente las conexiones de base de datos. Las estrategias específicas de pooling de conexión de PostgreSQL se cubren en nuestro PostgreSQL Cheatsheet.
- Planifica para la migración de inquilino: Capacidad para mover inquilinos entre patrones
- Implementa eliminación suave: Usa deleted_at en lugar de eliminaciones duras para datos de inquilino
- Auda todo: Registra todo el acceso a datos de inquilino para cumplimiento
- Prueba el aislamiento: Auditorías de seguridad periódicas para prevenir fuga de datos entre inquilinos
Conclusión
Elegir el patrón adecuado de base de datos multi-inquilino depende de tus requisitos específicos de aislamiento, costo, escalabilidad y complejidad operativa. El patrón de base de datos compartida, esquema compartido funciona bien para la mayoría de las aplicaciones SaaS, mientras que la base de datos separada por inquilino es necesaria para clientes empresariales con requisitos de cumplimiento estrictos.
Comienza con el patrón más simple que cumpla con tus requisitos y planifica para migrar a un patrón más aislado a medida que tus necesidades evolucionen. Siempre prioriza la seguridad y el aislamiento de datos, independientemente del patrón elegido.
Enlaces útiles
- Documentación de seguridad de nivel de fila de PostgreSQL
- Arquitectura de base de datos multi-inquilino SaaS
- Diseño de bases de datos multi-inquilino
- Comparación de ORMs Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Una referencia rápida para desarrolladores
- DBeaver vs Beekeeper - Herramientas de gestión de bases de datos SQL
- SQL Cheatsheet - comandos SQL más útiles