SQLLab
Все статьи

Multi-tenancy в PostgreSQL: паттерны изоляции данных

Три подхода к мультитенантности в PostgreSQL: отдельные БД, схемы на тенанта, общие таблицы с tenant_id. Сравнение, RLS, производительность.

24 марта 2026 г.·5 мин чтения·

Multi-tenancy — архитектура, где одно приложение обслуживает несколько изолированных клиентов (тенантов). В PostgreSQL есть три основных паттерна изоляции.

Паттерн 1: Отдельная база данных на тенанта

tenant_acme    → database: db_acme
tenant_globex  → database: db_globex
tenant_initech → database: db_initech
# Django: динамически выбирать БД
def get_db_for_tenant(tenant_slug):
    return {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': f'db_{tenant_slug}',
        'HOST': 'localhost',
        ...
    }

# Или через django-tenants / django-db-multitenant

Плюсы:

  • Максимальная изоляция данных
  • Независимые миграции и бэкапы
  • Отдельный pg_dump на клиента

Минусы:

  • Тысячи БД = тысячи подключений к PostgreSQL
  • Сложнее мониторинг и операционное обслуживание
  • Кросс-тенантная аналитика требует UNION ALL по всем БД или ETL

Когда: строгие требования изоляции (медицина, финансы, SLA), небольшое число крупных клиентов (< 100).


Паттерн 2: Отдельная схема на тенанта

-- Создать схему для тенанта
CREATE SCHEMA tenant_acme;

-- Создать таблицы в схеме
CREATE TABLE tenant_acme.users (
    id SERIAL PRIMARY KEY,
    email TEXT,
    ...
);
CREATE TABLE tenant_acme.orders (...);

-- Установить search_path для соединения тенанта
SET search_path = tenant_acme;
-- Теперь SELECT * FROM users → читает из tenant_acme.users
# Django: middleware устанавливает схему при каждом запросе
class TenantMiddleware:
    def process_request(self, request):
        tenant = get_tenant_from_request(request)
        with connection.cursor() as cursor:
            cursor.execute(f"SET search_path = tenant_{tenant.slug}")

Плюсы:

  • Одна БД = один пул соединений
  • Простая изоляция через search_path
  • Каждый тенант может иметь свою схему таблиц (гибкость)

Минусы:

  • Ограничение PostgreSQL: ~10 000 схем на БД (практически)
  • Миграции нужно применять к каждой схеме отдельно
  • Кросс-тенантные запросы через schema.table

Когда: SaaS с сотнями тенантов, каждый тенант может иметь кастомные поля.


Паттерн 3: Общие таблицы с tenant_id (Shared Database)

-- Все тенанты в одних таблицах
CREATE TABLE users (
    id        BIGSERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL REFERENCES tenants(id),
    email     TEXT NOT NULL,
    name      TEXT,
    ...
);

CREATE TABLE orders (
    id        BIGSERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL REFERENCES tenants(id),
    user_id   BIGINT NOT NULL,
    amount    NUMERIC,
    ...
);

-- Составной уникальный индекс: email уникален внутри тенанта
CREATE UNIQUE INDEX ON users (tenant_id, email);

-- Индекс для быстрой фильтрации
CREATE INDEX ON orders (tenant_id, created_at DESC);
CREATE INDEX ON users (tenant_id);

Плюсы:

  • Простейшая операционная модель
  • Кросс-тенантная аналитика тривиальна
  • Один набор миграций

Минусы:

  • Утечка данных при ошибке в WHERE (tenant_id забыли)
  • При миллионах строк нужно партиционирование по tenant_id

Когда: стартапы, B2C приложения с тысячами небольших клиентов.


Row Level Security (RLS) — безопасность на уровне строк

RLS — встроенный механизм PostgreSQL для автоматической фильтрации строк:

-- Включить RLS на таблице
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Политика: пользователь видит только строки своего тенанта
CREATE POLICY tenant_isolation ON users
    USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);

CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);

-- Суперпользователь обходит RLS (для миграций и аналитики)
-- Обычный пользователь приложения подчиняется политике
# В приложении: установить tenant_id перед каждым запросом
def set_tenant(cursor, tenant_id):
    cursor.execute("SET app.current_tenant_id = %s", [tenant_id])

# Django middleware
class RLSMiddleware:
    def __call__(self, request):
        tenant_id = request.tenant.id
        with connection.cursor() as cursor:
            cursor.execute("SET LOCAL app.current_tenant_id = %s", [tenant_id])
        return self.get_response(request)
-- Проверить: пользователь видит только свои данные
SET app.current_tenant_id = 42;
SELECT * FROM users;  -- только tenant_id = 42

SET app.current_tenant_id = 99;
SELECT * FROM users;  -- только tenant_id = 99

-- Сбросить (вернуть доступ ко всему для admin)
RESET app.current_tenant_id;

Производительность: партиционирование по tenant_id

-- Партиционированная таблица (для больших тенантов)
CREATE TABLE orders (
    id        BIGSERIAL,
    tenant_id INTEGER NOT NULL,
    user_id   BIGINT NOT NULL,
    amount    NUMERIC,
    created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY HASH (tenant_id);

-- 8 партиций (хорошо для 100-1000 тенантов)
CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE orders_p1 PARTITION OF orders FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- ... и т.д.

-- Запросы с tenant_id используют partition pruning
EXPLAIN SELECT * FROM orders WHERE tenant_id = 42;
-- → читает только нужную партицию

Сравнение паттернов

Отдельная БДОтдельная схемаShared table
ИзоляцияМаксимальнаяВысокаяСредняя (RLS)
Масштаб< 100 тенантов< 10 000Миллионы
Кросс-тенантный запросСложноЧерез schema.tableТривиально
Операционная сложностьВысокаяСредняяНизкая
Кастомные схемы
Отдельный бэкапМожно

Гибридный подход

Многие SaaS используют гибрид:

Enterprise клиенты → отдельная схема (изоляция, кастомизация)
SMB клиенты        → shared tables с RLS (эффективность)
-- Определить тип тенанта
SELECT isolation_type FROM tenants WHERE id = 42;
-- 'schema' или 'shared'

Практический чеклист

  1. Всегда добавляйте tenant_id в индексы: (tenant_id, ...) — не (...) отдельно
  2. Добавьте tenant_id как первый столбец составных индексов
  3. Используйте RLS как защитный слой даже при shared approach
  4. Тестируйте: проверяйте что запросы не возвращают данные чужих тенантов
  5. Логируйте попытки доступа к данным других тенантов

Multi-tenancy — фундаментальное архитектурное решение. Выбирайте паттерн исходя из числа тенантов, требований изоляции и операционных возможностей команды.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →