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

Как нужно работать с шардированной базой данных?

2.2 Middle🔥 211 комментариев
#Базы данных и SQL#Архитектура систем

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

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

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

Как нужно работать с шардированной базой данных?

Работа с шардированной БД — это совсем другой уровень сложности. Нужно понимать специфику и избежать распространённых ошибок.

Основной принцип шардирования

Шардирование — горизонтальное разбиение данных на несколько независимых БД по ключу шарда.

Одна большая БД (1 млн пользователей):
  users (id 1-1,000,000)
        ↓
        Шардирование по user_id
        ↓
Три независимых БД:
  Shard 1: users (id 1-333,333)
  Shard 2: users (id 333,334-666,666)
  Shard 3: users (id 666,667-1,000,000)

Как выбрать ключ шарда (Shard Key)

Ключ шарда — идентификатор, по которому определяется, в какой шард поместить запись.

Хорошие кандидаты:

  • user_id — максимально частая фильтрация
  • tenant_id — в SaaS приложениях
  • customer_id — для e-commerce
  • organization_id — в B2B системах

Плохие кандидаты:

  • country — очень неравномерное распределение
  • email — может измениться
  • subscription_plan — часто меняется

Правильный выбор:

  1. Уникален — каждая запись имеет один ключ шарда
  2. Стабилен — не меняется со временем
  3. Распределяется равномерно — нет "горячих" шардов
  4. Используется в большинстве запросов — по нему фильтруем

Определение шарда (Shard Resolution)

Формула распределения:

shard_id = hash(shard_key) % number_of_shards

Пример:

def get_shard(user_id, num_shards=3):
    return hash(user_id) % num_shards

user_id = 42
shard_id = get_shard(42, 3)
# hash(42) = 8765
# 8765 % 3 = 2
# Данные пользователя 42 в Shard 2

Важно: Никогда не меняй функцию хеша или количество шардов без перебалансировки!

Когда можешь запрашивать без знания шарда

1. Если знаешь shard_key в запросе

# Хорошо: знаем user_id
user_id = 42
shard_id = get_shard(user_id)
query = f"SELECT * FROM users WHERE id = {user_id}"
result = execute_on_shard(shard_id, query)

Время: ~5ms (одна БД)

2. Если нужны все шарды (VERY EXPENSIVE!)

# Плохо: нужно запросить все 3 шарда
query = "SELECT * FROM users WHERE status = 'active'"

results = []
for shard_id in range(num_shards):
    result = execute_on_shard(shard_id, query)
    results.extend(result)

# Объединить результаты
final_result = aggregate_results(results)

Время: 3 × 100ms = ~300ms (потому что 3 параллельные БД)

Проблемы:

  • Нужно запросить ВСЕ шарды
  • Сложнее обработать ошибки (одна шард может упасть)
  • Нужно объединить результаты в памяти
  • Может вернуться 1 млн строк

Чего НЕЛЬЗЯ делать в шардированной БД

1. JOIN между шардами

-- НЕЛЬЗЯ: orders в Shard 1, products в Shard 2
SELECT o.*, p.name
FROM orders o
JOIN products p ON o.product_id = p.id;

-- Почему: data of orders и products в разных БД!

Решение:

# Загрузить в приложение
orders = get_orders(shard_1, user_id)
product_ids = [o['product_id'] for o in orders]
products = get_products(shard_2, product_ids)  # Может быть другой sharding key!

# JOIN в памяти приложения
result = join_in_memory(orders, products)

2. Фильтрация без shard_key

-- ОЧЕНЬ ПЛОХО: фильтр по email
SELECT * FROM users WHERE email = 'ivan@example.com';

-- Почему: нужно запросить ВСЕ 3 шарда, потому что не знаешь какой user_id

Решение:

# Вспомогательная таблица (lookup table)
# В шарде 0 (или отдельной БД) хранить маппинг:
email -> user_id

email = 'ivan@example.com'
user_id = lookup_table.get(email)  # Получить user_id
shard_id = get_shard(user_id)
user = execute_on_shard(shard_id, f"SELECT * FROM users WHERE id = {user_id}")

3. Распределённые транзакции

-- НЕЛЬЗЯ: транзакция охватывает 2 шарда
BEGIN TRANSACTION;
  UPDATE accounts SET balance = balance - 100 WHERE shard_key = 1;
  UPDATE accounts SET balance = balance + 100 WHERE shard_key = 2;
COMMIT;

-- Если упадёт между UPDATE-ами, данные несогласованные!

Решение: Используй компенсирующие транзакции или Saga паттерн.

Архитектура работы с шардами

Вариант 1: Прямое подключение (Simple)

def get_shard_connection(shard_id):
    shards = {
        0: "db1.example.com:5432",
        1: "db2.example.com:5432",
        2: "db3.example.com:5432",
    }
    return connect(shards[shard_id])

def get_user(user_id):
    shard_id = hash(user_id) % 3
    conn = get_shard_connection(shard_id)
    return conn.execute(f"SELECT * FROM users WHERE id = {user_id}")

Плюсы: Просто Минусы: Клиент знает логику шардирования

Вариант 2: Middleware (Proxy)

Приложение -> Shard Proxy -> Shard Resolver -> Shard 1, 2, 3
                 (один вход)

Примеры: Vitess (для MySQL), Citus (для PostgreSQL).

# Приложение не знает о шардировании
result = proxy.execute(
    "SELECT * FROM users WHERE user_id = 42 AND ..."
)
# Proxy сам определит shard_id и маршрутизирует запрос

Плюсы: Приложение не знает о шардах, проще разработка Минусы: Дополнительный слой, задержка

Типичные операции с шардами

1. Создание записи (CREATE)

def create_order(user_id, product_id, amount):
    shard_id = get_shard(user_id)
    
    query = """
    INSERT INTO orders (id, user_id, product_id, amount)
    VALUES (uuid_generate_v4(), %s, %s, %s)
    """
    execute_on_shard(shard_id, query, [user_id, product_id, amount])

Правило: Всегда включай shard_key в запрос!

2. Чтение записи (READ)

def get_user_orders(user_id):
    shard_id = get_shard(user_id)
    
    query = "SELECT * FROM orders WHERE user_id = %s"
    return execute_on_shard(shard_id, query, [user_id])

Оптимизация: Добавить индекс на user_id внутри каждого шарда.

3. Обновление (UPDATE)

def update_order_status(user_id, order_id, new_status):
    shard_id = get_shard(user_id)
    
    query = """
    UPDATE orders 
    SET status = %s 
    WHERE user_id = %s AND id = %s
    """
    execute_on_shard(shard_id, query, [new_status, user_id, order_id])

Правило: WHERE включает shard_key!

4. Скан по диапазону (RANGE)

def get_all_orders_by_date(start_date, end_date):
    # БЕЗ user_id — нужны ВСЕ шарды!
    results = []
    
    for shard_id in range(num_shards):
        query = """
        SELECT * FROM orders 
        WHERE created_at >= %s AND created_at <= %s
        """
        result = execute_on_shard(shard_id, query, [start_date, end_date])
        results.extend(result)
    
    return results

Проблема: O(N × M) где N = количество шардов, M = записей в каждом.

Перебалансировка (Resharding)

Проблема: Со временем нужно добавить новые шарды.

Использование растёт, 3 шарда уже не хватает.
Нужно добавить 4-й шард.

Что произойдёт:
user_id = 42: hash(42) % 3 = 2 -> Shard 2
user_id = 42: hash(42) % 4 = 2 -> Shard 2 (OK, совпадает!)

user_id = 45: hash(45) % 3 = 0 -> Shard 0
user_id = 45: hash(45) % 4 = 1 -> Shard 1 (ПРОБЛЕМА! Перемещён!)

Решение: Использовать consistent hashing:

from hashring import HashRing

ring = HashRing(nodes=["shard0", "shard1", "shard2", "shard3"])
shard = ring.get_node(user_id)

# При добавлении нового шарда: минимум данных перемещается

Когда использовать шардирование

Используй шардирование если:

  • Данные > 500 GB
  • TPS (транзакций в сек) > 10,000
  • Одна БД физически не может справиться
  • Геораспределённые данные

НЕ используй шардирование если:

  • Данные < 100 GB
  • Нужны частые JOIN-ы между таблицами
  • Много операций без shard_key
  • Новая система (слишком рано усложнять)

Совет System Analyst

  1. Выбери правильный shard key — это 80% успеха
  2. Каждый запрос ДОЛЖЕН включать shard_key в WHERE
  3. JOIN между шардами = смерть приложения — избегай
  4. Используй lookup tables для альтернативных фильтров
  5. Компенсирующие транзакции вместо распределённых
  6. Мониторь распределение данных — избегай горячих шардов
  7. Документируй стратегию шардирования — команда должна знать

Шардирование — это точка невозврата. После её пересечения разработка становится сложнее в 10 раз. Используй только когда действительно нужно.