← Назад к вопросам

Есть ли шардирование в PostgreSQL?

2.3 Middle🔥 71 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Шардирование в PostgreSQL

Это хороший вопрос, потому что PostgreSQL не имеет встроенного автоматического шардирования, в отличие от MongoDB или некоторых других БД. Расскажу о состоянии дел.

Нативное шардирование в PostgreSQL: его нет

После более чем 30 лет развития PostgreSQL не имеет встроенного горизонтального шардирования (horizontal partitioning).

Есть только вертикальное партиционирование (table partitioning):

  • Range partitioning (по диапазонам дат)
  • List partitioning (по конкретным значениям)
  • Hash partitioning (по хешу ключа)
-- Пример: партиционирование по дате
CREATE TABLE orders (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL,
    amount DECIMAL NOT NULL
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Это только помогает с производительностью на одном сервере
-- Все партиции — в одной БД!

Партиционирование имеет преимущества:

  • Быстрее индексирование и поиск (меньше данных в индексе)
  • Проще архивирование (удалить старую партицию)
  • Параллельный поиск по партициям

НО это не решает проблему масштабирования: всё всё равно на одном сервере.

Почему нет встроенного шардирования?

Это архитектурное решение разработчиков PostgreSQL:

  1. Сложность ACID транзакций — шардирование ломает транзакции между шардами
  2. Фокус на надёжности — лучше иметь один мощный сервер + replicas, чем много шардов
  3. Оставить гибкость — разработчики выбирают свой инструмент для шардирования

Решения для шардирования на уровне приложения

1. Собственное приложение (Application-Level Sharding)

Делим данные в коде по условию.

class ShardRouter:
    def __init__(self, shard_count: int = 4):
        self.shard_count = shard_count
        self.connections = {
            i: create_connection(f"db{i}") 
            for i in range(shard_count)
        }
    
    def get_shard_id(self, user_id: UUID) -> int:
        """Детерминировано вычисляем, на каком шарде данные"""
        return int(user_id.hex, 16) % self.shard_count
    
    async def get_user(self, user_id: UUID) -> User:
        shard_id = self.get_shard_id(user_id)
        connection = self.connections[shard_id]
        
        result = await connection.execute(
            "SELECT * FROM users WHERE id = %s",
            (user_id,)
        )
        return result.fetchone()
    
    async def save_user(self, user: User):
        shard_id = self.get_shard_id(user.id)
        connection = self.connections[shard_id]
        
        await connection.execute(
            "INSERT INTO users (id, email) VALUES (%s, %s)",
            (user.id, user.email)
        )

Проблемы:

  • Нужно выбирать поле шардирования (обычно user_id)
  • Менять количество шардов потом сложно (нужна миграция)
  • JOIN между шардами становится невозможным

2. Проксирующий слой (Database Proxy)

Инструменты, которые автоматизируют маршрутизацию запросов.

Application → Citus / Pgx / Proxy → PostgreSQL Shards

Citus: расширение PostgreSQL для шардирования

Есть сторонний инструмент Citus, который добавляет шардирование в PostgreSQL.

-- Citus: координатор + workers
CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    amount DECIMAL,
    created_at TIMESTAMPTZ
);

-- Шардируем по customer_id
SELECT create_distributed_table('orders', 'customer_id');

-- Теперь PostgreSQL автоматически распределяет данные
INSERT INTO orders (order_id, customer_id, amount) 
VALUES (1, 123, 99.99);

-- Запрос может коснуться одного шарда
SELECT * FROM orders WHERE customer_id = 123;

-- Запрос без customer_id затронет все шарды
SELECT * FROM orders WHERE amount > 100;

Преимущества Citus:

  • Автоматическое распределение данных
  • Параллельные запросы
  • Относительно прозрачно для приложения

Недостатки:

  • Требует специальной лицензии для больших систем
  • JOIN между разными шардами сложный
  • Не все PostgreSQL функции работают

Альтернативы для масштабирования

1. Read Replicas (рекомендую для большинства случаев)

# Master для writes
master_db = create_connection("master.db.local")

# Read-only replicas для reads
replicas = [
    create_connection("replica1.db.local"),
    create_connection("replica2.db.local"),
    create_connection("replica3.db.local")
]

class ReplicaRouter:
    def __init__(self, master, replicas):
        self.master = master
        self.replicas = replicas
        self.replica_index = 0
    
    async def write(self, query, params):
        return await self.master.execute(query, params)
    
    async def read(self, query, params):
        replica = self.replicas[self.replica_index % len(self.replicas)]
        self.replica_index += 1
        return await replica.fetchone(query, params)

# Использование
router = ReplicaRouter(master_db, replicas)
await router.write("INSERT INTO users VALUES (%s)", (user,))
user = await router.read("SELECT * FROM users WHERE id = %s", (user_id,))

2. Кеширование (Redis/Memcached)

Основной инструмент для масштабирования чтения в большинстве систем.

from redis.asyncio import Redis

class CachedUserRepository:
    def __init__(self, db, redis: Redis):
        self.db = db
        self.redis = redis
    
    async def get_user(self, user_id: UUID) -> User | None:
        # Сначала в кеше
        cached = await self.redis.get(f"user:{user_id}")
        if cached:
            return User.model_validate_json(cached)
        
        # Потом в БД
        user = await self.db.execute(
            "SELECT * FROM users WHERE id = %s",
            (user_id,)
        )
        
        if user:
            await self.redis.setex(
                f"user:{user_id}",
                3600,  # 1 час TTL
                user.model_dump_json()
            )
        
        return user

3. Денормализация и асинхронные события

Вместо множества JOIN'ов используем асинхронное обновление денормализованных данных.

# Вместо:
# SELECT orders.*, users.name, users.email FROM orders JOIN users ON ...

# Используем денормализацию:
CREATE TABLE orders_denormalized (
    id UUID PRIMARY KEY,
    user_id UUID,
    user_name VARCHAR,  -- Копия из users
    user_email VARCHAR,  -- Копия из users
    amount DECIMAL
);

# При обновлении user.name отправляем событие
class UserUpdatedEvent:
    def __init__(self, user_id: UUID, new_name: str):
        self.user_id = user_id
        self.new_name = new_name

# Асинхронный consumer обновляет денормализацию
async def handle_user_updated(event: UserUpdatedEvent):
    await db.execute(
        "UPDATE orders_denormalized SET user_name = %s WHERE user_id = %s",
        (event.new_name, event.user_id)
    )

Когда реально нужно шардирование?

Признаки, что пора шардировать:

  • Таблица > 10GB и растёт экспоненциально
  • Запросы на одном сервере замедляются
  • CPU/Memory на 80%+ постоянно
  • Даже с индексами и кешем запросы медленные

До этого: read replicas + кеш + денормализация решают 95% задач.

Рекомендация

Для большинства приложений я бы рекомендовал:

  1. PostgreSQL + Read Replicas (масштабирование чтения)
  2. Redis для кеша (самое эффективное)
  3. Денормализация данных где нужно (таблицы с урезанными данными)
  4. Citus или application-level sharding только когда действительно нужно

Шардирование добавляет сложность в проект на порядок. Лучше сначала оптимизировать с помощью более простых инструментов.