🏗️ Розділ 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 партицій за годинами» (Уб’є планувальник СУБД і пам’ять блокувань).

Пов’язані теми: