🏗️ Раздел 17 · Вопрос #11

В чём разница между шардированием и партиционированием

Оба термина означают разбиение больших объёмов данных на части, но они работают на принципиально разных уровнях архитектуры:


🟢 Junior Level

Оба термина означают разбиение больших объёмов данных на части, но они работают на принципиально разных уровнях архитектуры:

Партиционирование (Внутри ОДНОГО сервера):
┌────────────────────────────────────────────────────────┐
│           Один физический сервер СУБД                  │
│  Таблица "orders"                                      │
│  ├── orders_2023 (файл на диске A)                      │
│  ├── orders_2024 (файл на диске B)                      │
│  └── orders_2025 (файл на диске C)                      │
│  [Общий CPU, общая RAM, один движок ACID-транзакций]   │
└────────────────────────────────────────────────────────┘

Шардирование (Между РАЗНЫМИ серверами):
┌────────────────┐      ┌────────────────┐      ┌────────────────┐
│   Сервер 1     │      │   Сервер 2     │      │   Сервер 3     │
│   (Шард 1)     │      │   (Шард 2)     │      │   (Шард 3)     │
│  Users: A - H  │      │  Users: I - P  │      │  Users: Q - Z  │
│ [Свои CPU/RAM] │      │ [Свои CPU/RAM] │      │ [Свои CPU/RAM] │
└────────────────┘      └────────────────┘      └────────────────┘

Простое правило различия:

  • Партиционирование (Partitioning / Секционирование): Разделение таблицы на отдельные файлы/секции внутри одного инстанса базы данных. Используется для ускорения запросов и мгновенного удаления старых данных.
  • Шардирование (Sharding): Разделение данных между множеством независимых физических серверов (Shared-Nothing Architecture). Используется, когда данные или нагрузка физически не помещаются на один компьютер.

🟡 Middle Level

1. Детальное сравнительное сопоставление

Критерий Партиционирование (Partitioning) Шардирование (Sharding)
Физическая инфраструктура Один сервер СУБД (общие CPU, RAM, дисковый контроллер) Кластер независимых серверов (у каждого свои CPU, RAM, диски)
ACID-транзакции Поддерживаются СУБД из коробки без ограничений Требуют распределенных протоколов (2PC) или Saga
Внешние ключи (Foreign Keys) Работают внутри базы данных Не поддерживаются между шардами на уровне СУБД
Сложность внедрения Низкая: настраивается через DDL-команды SQL (PARTITION BY) Высокая: требует координатора, прокси, роутинга и перебалансировки
Главная цель Оптимизация чтения (Partition Pruning) и обслуживание архивов Горизонтальное масштабирование записи и хранилища

2. Реализация партиционирования в PostgreSQL

Начиная с PostgreSQL 10, поддерживается декларативное партиционирование:

-- 1. Создаем родительскую партиционированную таблицу
CREATE TABLE audit_logs (
    id BIGINT GENERATED ALWAYS AS IDENTITY,
    service_name VARCHAR(64) NOT NULL,
    payload JSONB,
    created_at TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (id, created_at) -- Ключ партиционирования ОБЯЗАН входить в Primary Key!
) PARTITION BY RANGE (created_at);

-- 2. Создаем физические секции (партиции)
CREATE TABLE audit_logs_2026_q1 PARTITION OF audit_logs
    FOR VALUES FROM ('2026-01-01 00:00:00+00') TO ('2026-04-01 00:00:00+00');

CREATE TABLE audit_logs_2026_q2 PARTITION OF audit_logs
    FOR VALUES FROM ('2026-04-01 00:00:00+00') TO ('2026-07-01 00:00:00+00');

3. Механизм Partition Pruning (Отсечение партиций)

Главный выигрыш партиционирования — оптимизатор СУБД при выполнении запроса:

EXPLAIN ANALYZE 
SELECT * FROM audit_logs 
WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';

Планировщик автоматически исключает из сканирования партицию audit_logs_2026_q2 и все остальные. Он обращается только к компактному файлу audit_logs_2026_q1, читая в разы меньше страниц с диска и удерживая лишь нужные индексы в оперативной памяти.


🔴 Senior Level

1. Архитектурная шкала масштабирования БД

Прежде чем прибегать к дорогому и сложному шардированию, инженеры проходят строгую цепочку эволюции:

[1. Индексы и тюнинг SQL] 
       │ (Исчерпали эффективность)
       ▼
[2. Партиционирование таблиц] ──► Ускоряет локальные выборки, облегчает VACUUM
       │ (Один сервер не тянет чтение)
       ▼
[3. Read Replicas (Репликация)] ──► Разгружает чтение (SELECT) через пул реплик
       │ (Один Master не справляется с потоком INSERT/UPDATE или диск переполнен)
       ▼
[4. Шардирование (Sharding)] ──► Разделение записи на независимые Master-узлы

2. Мгновенная очистка данных: DROP TABLE vs DELETE

Почему партиционирование незаменимо для логов, событий и временных рядов (Time-Series data)?

  • Обычная таблица (DELETE): Запрос DELETE FROM logs WHERE created_at < '2025-01-01' для 100 миллионов строк:
    1. Генерирует гигантский объём WAL-логов (запись каждого факта удаления).
    2. В PostgreSQL строки не удаляются физически, а помечаются как Dead Tuples, вызывая табличный Bloat (раздувание) и парализуя базу долгим процессом VACUUM.
    3. Блокирует страницы и пожирает дисковые IOPS часами.
  • Партиционированная таблица (DROP PARTITION): Команда DROP TABLE logs_2024_q4 выполняется за 10 миллисекунд: СУБД просто удаляет соответствующий файл с файловой системы ОС без генерации строк в WAL и без необходимости запускать VACUUM.

3. Композитное секционирование (Hybrid Architecture)

В крупных высоконагруженных enterprise-системах оба подхода объединяются:

  • Шардирование верхнего уровня: База шардируется по tenant_id (организация) или user_id по 16 серверам.
  • Партиционирование внутри каждого шарда: На каждом из 16 серверов таблицы заказов партиционируются по месяцам (created_at).

4 Tricky Questions

1. Почему в PostgreSQL в первичный ключ (PRIMARY KEY) партиционированной таблицы обязаны входить все колонки ключа партиционирования?

Ответ: Каждая партиция в PostgreSQL является независимой таблицей со своим собственным B-Tree индексом первичного ключа. В PostgreSQL нет глобальных индексов (Global Indexes), охватывающих сразу все партиции. Если бы мы создали PRIMARY KEY (id), а партиционировали по created_at, то при вставке строки с id = 100 в партицию за 2026 год база не смогла бы проверить, существует ли уже строка с таким же id = 100 в партиции за 2025 год, без синхронного поиска по индексам всех партиций системы (что убило бы производительность вставки). Поэтому уникальность гарантируется только локально внутри партиции, требуя PRIMARY KEY (id, created_at).

2. В чем опасность создания слишком большого количества партиций (например, 10 000 партиций по дням)?

Ответ:

  1. Накладные расходы планировщика: При генерации плана запроса PostgreSQL обязан открыть и проверить метаданные каждой партиции в системном каталоге (pg_class), что приводит к экспоненциальному росту времени компиляции запроса (Planning Time превышает Execution Time в десятки раз).
  2. Исчерпание памяти и дескрипторов: Каждая открытая партиция удерживает файловые дескрипторы и блокировки в разделяемой памяти (Lock Tables / max_locks_per_transaction), вызывая падение запросов с ошибкой out of shared memory. Правило: В одной таблице не рекомендуется держать более нескольких сотен партиций.

3. В какой момент партиционирование исчерпывает свои возможности и требует перехода к шардированию?

Ответ: Партиционирование перестает помогать, когда бутылочным горлышком становятся физические ресурсы одного сервера:

  1. Насыщение дискового контроллера по записи: Поток INSERT / UPDATE превышает максимальный IOPS и пропускную способность дисковой подсистемы (NVMe/RAID).
  2. Время бэкапа и восстановления (RTO/RPO): Создание резервной копии базы объемом 10–20 ТБ (pg_dump или LVM snapshot) и ее восстановление при аварии начинает занимать сутки, нарушая SLA бизнеса.
  3. Лимиты CPU/RAM: База уперлась в потолок максимальной доступной конфигурации сервера в облаке.

4. В чем разница между Plan-Time и Run-Time Partition Pruning?

Ответ:

  • Plan-Time Pruning: Выполняется на этапе компиляции SQL, когда значения предикатов известны статически (константы: WHERE created_at >= '2026-01-01'). Планировщик сразу отсекает ненужные партиции до начала выполнения.
  • Run-Time Pruning: Выполняется в момент исполнения запроса, если фильтр зависит от параметров prepared statements, подзапросов или параметров функций (например, WHERE created_at >= now() - INTERVAL '1 day'). СУБД динамически отсекает партиции после вычисления значения выражения во время исполнения.

🎯 Шпаргалка для интервью

Главное:

  • Партиционирование — деление таблицы внутри одного сервера СУБД.
  • Шардирование — деление данных между разными серверами кластера.
  • Партиционирование сохраняет полноценные ACID-транзакции, внешние ключи (FK) и управляется через стандартный SQL.
  • Шардирование требует распределенной координации, ломает межшардовые Foreign Keys и ACID.
  • Partition Pruning — ключевой механизм ускорения: база читает только релевантные партиции, игнорируя остальные.
  • DROP TABLE partition_name удаляет гигабайты старых данных за миллисекунды без блокировок и без MVCC dead tuples / bloat.
  • Порядок масштабирования: Индексы $\to$ Партиционирование $\to$ Read Replicas $\to$ Шардирование.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Партиционирование позволяет масштабировать запись на 10 серверов» (Партиционирование всегда ограничено одним сервером).
  • ❌ «Для очистки старых логов в партиционированной таблице нужно запускать DELETE» (Нужно вызывать DROP TABLE partition_name или DETACH PARTITION).
  • ❌ «Можно нарезать таблицу на 50 000 партиций по часам» (Убьет планировщик СУБД и память блокировок).

Связанные темы: