Conceptos básicos de multiinquilino
8 ejemplos para empezar con patrones de multiinquilino: 5 básicos y 3 intermedios.
Prerrequisitos
- PostgreSQL 18.4 con privilegios de creación de roles.
- Un dominio SaaS donde cada objeto de negocio pertenece a un inquilino (organización, cuenta, espacio de trabajo).
Ejemplos básicos
1. Columna de inquilino en cada tabla de negocio
El patrón más simple: tenant_id en todas las filas propiedad del inquilino.
CREATE TABLE tenants (
tenant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
plan text NOT NULL DEFAULT 'starter'
);
CREATE TABLE projects (
project_id bigint GENERATED ALWAYS AS IDENTITY,
tenant_id uuid NOT NULL REFERENCES tenants (tenant_id),
name text NOT NULL,
PRIMARY KEY (tenant_id, project_id)
);- La clave primaria compuesta
(tenant_id, project_id)limita la identidad por inquilino. - Cada consulta debe filtrar
tenant_id; la falta de filtro arriesga fugas entre inquilinos. - Indexar comenzando con
tenant_idpara búsquedas OLTP.
Relacionado: Esquema compartido + tenant_id - patrones de indexación y consulta
2. Restricciones UNIQUE con ámbito
La unicidad se aplica por inquilino, no globalmente.
CREATE TABLE projects (
tenant_id uuid NOT NULL,
project_id bigint GENERATED ALWAYS AS IDENTITY,
slug text NOT NULL,
PRIMARY KEY (tenant_id, project_id),
UNIQUE (tenant_id, slug)
);UNIQUE (tenant_id, slug)permite que dos inquilinos utilicen el slugapi.UNIQUE (slug)global rompe el SaaS multiinquilino.- Las revisiones de aplicaciones y SQL deben señalar los errores únicos globales.
3. Contexto de inquilino de sesión
Establece el inquilino una vez por conexión o transacción para un filtrado coherente.
-- La aplicación lo establece tras la autenticación
SET app.tenant_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
SELECT project_id, name FROM projects
WHERE tenant_id = current_setting('app.tenant_id')::uuid;- GUC personalizado (
app.tenant_id) centraliza el contexto del inquilino. - Emparejar con RLS para que incluso las cláusulas
WHEREolvidadas sean seguras. - Restablecer GUC al devolver la conexión al pool de PgBouncer (
DISCARD ALLoRESET ALL).
Relacionado: Inquilino de seguridad a nivel de fila - plantillas de políticas
4. Consulta de detección de vecino ruidoso
Encuentra inquilinos que consumen recursos desproporcionados.
SELECT
tenant_id,
COUNT(*) AS row_count
FROM audit_events
WHERE logged_at > now() - interval '1 hour'
GROUP BY tenant_id
ORDER BY row_count DESC
LIMIT 10;- Los recuentos de
tenant_idcon picos señalan vecinos ruidosos en un esquema compartido. - Combinar con
pg_stat_statementsagrupados por inquilino cuando se etiquetan en comentarios. - El nivel empresarial puede necesitar aislamiento de esquema por inquilino o base de datos por inquilino.
5. Inquilino en la columna principal del índice
Alinear los índices con las rutas de acceso de WHERE tenant_id = ?.
CREATE INDEX projects_tenant_name_idx ON projects (tenant_id, name);
CREATE INDEX audit_events_tenant_time_idx
ON audit_events (tenant_id, logged_at DESC);- El
tenant_idprincipal permite escaneos de índices por inquilino. - Los datos de series temporales de inquilinos se benefician de
(tenant_id, logged_at DESC). - La falta de
tenant_iden los índices provoca escaneos secuenciales dentro de tablas compartidas grandes.
Ejemplos intermedios
6. Esqueleto de política de seguridad a nivel de fila
Aislamiento de inquilino forzado por base de datos como red de seguridad.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);FORCE ROW LEVEL SECURITYaplica políticas incluso a los propietarios de tablas.WITH CHECKbloquea inserciones en el inquilino incorrecto.- Las políticas complementan, no reemplazan, los filtros de la aplicación.
Relacionado: Inquilino de seguridad a nivel de fila - plantillas de nivel
7. Esquema por inquilino
Aislamiento de espacio de nombres más sólido para el nivel empresarial de mercado medio.
CREATE SCHEMA tenant_acme;
CREATE SCHEMA tenant_globex;
CREATE TABLE tenant_acme.invoices (
invoice_id bigint PRIMARY KEY,
amount numeric(12, 2) NOT NULL
);- Cada esquema de inquilino duplica el DDL de la tabla: riesgo de expansión de migraciones.
search_pathde conexión o nombres calificados dirigen las consultas.- Mejor aislamiento que las tablas compartidas; peor operación que el esquema compartido + RLS.
Relacionado: Esquema por inquilino - compensaciones
8. Base de datos por inquilino para nivel regulado
Aislamiento estricto para clientes con alta carga de cumplimiento.
CREATE DATABASE tenant_acme_db;
CREATE DATABASE tenant_globex_db;
\c tenant_acme_db
CREATE TABLE invoices (
invoice_id bigint PRIMARY KEY,
amount numeric(12, 2) NOT NULL
);- Copia de seguridad, restauración y actualización separadas por base de datos de inquilino.
- Pool de PgBouncer por base de datos o enrutamiento dinámico en el servicio de conexión.
- Mayor coste operativo; la mayor contención de radio de explosión.
Relacionado: Base de datos por inquilino - aislamiento empresarial
Versiones de la pila: Esta página se escribió para PostgreSQL 18.4 (estable 18, mantenimiento 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x y PostGIS 3.5+.