Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Шардирование в 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:
- Сложность ACID транзакций — шардирование ломает транзакции между шардами
- Фокус на надёжности — лучше иметь один мощный сервер + replicas, чем много шардов
- Оставить гибкость — разработчики выбирают свой инструмент для шардирования
Решения для шардирования на уровне приложения
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% задач.
Рекомендация
Для большинства приложений я бы рекомендовал:
- PostgreSQL + Read Replicas (масштабирование чтения)
- Redis для кеша (самое эффективное)
- Денормализация данных где нужно (таблицы с урезанными данными)
- Citus или application-level sharding только когда действительно нужно
Шардирование добавляет сложность в проект на порядок. Лучше сначала оптимизировать с помощью более простых инструментов.