<? phpukraine СПІВБЕСІДИ
Пошук по платформі
SQL · SENIOR

Коли потрібне партиціонування таблиць і як архівувати старі дані без простою?

Партиціонування потрібне тоді, коли дані природно ріжуться по ключу (найчастіше по часу) і старі шматки треба видаляти чи архівувати цілком: DROP/DETACH PARTITION — це операція над метаданими замість мільйонів DELETE. Прискорення читання — побічний ефект, і лише коли ключ партиціонування є в WHERE.

Таблиця подій виросла до 400 млн рядків, DELETE старих даних не встигає за ніч — що робите?
Чи прискорить партиціонування наші SELECT-и?
Як перевести робочу таблицю на партиції без вікна простою?
Скільки партицій — це забагато?
партиціонування PostgreSQL MySQL архівація maintenance

Партиціонування розбиває одну логічну таблицю на набір фізичних таблиць за значенням ключа: у PostgreSQL це декларативні PARTITION BY RANGE | LIST | HASH (з версії 10), у MySQL — RANGE, RANGE COLUMNS, LIST, HASH, KEY на нативній реалізації InnoDB (з 8.0 інші рушії партиціонування не підтримують). Планувальник під час планування й виконання відкидає партиції, які не можуть містити потрібних рядків — це partition pruning, і саме тут головне непорозуміння: прунінг вимагає, щоб ключ партиціонування був у WHERE чи в умові з’єднання. Запит WHERE user_id = 42 на таблиці, партиціонованій по created_at, читає всі партиції і працює повільніше, ніж на непартиційованій таблиці з нормальним індексом.

Тому справжній мотив партиціонування — не читання, а життєвий цикл даних і обслуговування. Видалення місяця подій через ALTER TABLE ... DETACH PARTITION у PostgreSQL або ALTER TABLE ... DROP PARTITION у MySQL — це операція над метаданими й табличним простором, вона не породжує мільйони версій рядків, не роздуває WAL/undo, не створює лагу реплік і не залишає по собі bloat, який доводиться прибирати pg_repack. Побічно менші стають і VACUUM, ANALYZE, перебудова індексів: вони йдуть по одній партиції, а не по терабайтній таблиці. Плюс холодні партиції можна тримати на дешевшому табличному просторі або взагалі винести з бази.

Плата за це — обмеження й обовʼязкова автоматизація. У PostgreSQL ключ партиціонування має входити в первинний ключ і в кожен UNIQUE, глобальних унікальних індексів немає; зовнішні ключі на партиційовану таблицю зʼявилися лише в PG 12. У MySQL ключ теж мусить входити в усі унікальні ключі, а зовнішніх ключів на партиційованих InnoDB-таблицях немає взагалі. Партиції на майбутнє треба створювати наперед — планувальником, командою Artisan або pg_partman; інакше вставка або впаде, або осяде в DEFAULT-партиції, після чого підключення нової партиції з тим самим діапазоном вимагатиме сканування дефолтної під блокуванням.

Операційна частина архівації виглядає так. Відчепити партицію потрібно неблокуючим способом: DETACH PARTITION ... CONCURRENTLY (PG 14+, поза транзакційним блоком; при перерві залишає стан detach pending, який довершує FINALIZE). Далі відчеплена таблиця — звичайна таблиця: COPY у Parquet/CSV на обʼєктне сховище, перевірка контрольних сум і DROP TABLE. У MySQL аналог — EXCHANGE PARTITION WITH TABLE, щоб вийняти дані в окрему таблицю без копіювання. Обидві операції все одно чекають на метадані-блокування за довгими читаннями, тож нічний скрипт запускають з lock_wait_timeout (MySQL) або lock_timeout (PostgreSQL) і ретраями, а не «наосліп».

Межа застосовності проста: якщо немає ключа, по якому дані і фільтруються, і застарівають, партиціонування додасть складності без вигоди — краще індекси, keyset-пагінація і чистка пачками через pt-archiver. Партиціонування також не є шардингом: воно живе в межах одного сервера і не рятує від нестачі IOPS чи обʼєму, а лише робить обслуговування великої таблиці передбачуваним.

-- PostgreSQL 14+: розбиття по діапазону часу
CREATE TABLE events (
    id          bigserial,
    occurred_at timestamptz NOT NULL,
    payload     jsonb NOT NULL,
    PRIMARY KEY (id, occurred_at)   -- ключ партиціонування має бути в PK
) PARTITION BY RANGE (occurred_at);

CREATE TABLE events_2026_09 PARTITION OF events
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

-- Прунінг спрацює лише за наявності occurred_at у WHERE
EXPLAIN SELECT count(*) FROM events WHERE occurred_at >= '2026-09-01';

-- Архівація місяця: відчіпляємо без довгого блокування (не в транзакції)
ALTER TABLE events DETACH PARTITION events_2026_03 CONCURRENTLY;
-- відчеплена таблиця живе окремо: вивантажити в холодне сховище і прибрати
DROP TABLE events_2026_03;          -- метадані, а не мільйони DELETE

-- Підключення історичної таблиці без повного сканування під блокуванням
ALTER TABLE events_2026_02 ADD CONSTRAINT events_2026_02_rng
    CHECK (occurred_at >= '2026-02-01'
       AND occurred_at <  '2026-03-01') NOT VALID;
ALTER TABLE events_2026_02 VALIDATE CONSTRAINT events_2026_02_rng;
ALTER TABLE events ATTACH PARTITION events_2026_02
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

-- MySQL 8.4: та сама ідея, ключ мусить входити в кожен унікальний ключ
-- ALTER TABLE events DROP PARTITION p2026_03;
-- SET SESSION lock_wait_timeout = 5;  -- щоб не висіти за довгим читанням
Що головний мотив — дешеве видалення й обслуговування (DROP PARTITION замість DELETE, VACUUM та REINDEX по одній партиції, бекап лише активної), а не магічне прискорення запитів.
Що прунінг партицій працює лише коли ключ партиціонування є в WHERE або JOIN; без нього план читає всі партиції, і стає гірше, ніж було.
Обмеження на ключі: у PostgreSQL ключ партиціонування має входити в PRIMARY KEY і будь-який UNIQUE; у MySQL — у кожен унікальний ключ, а зовнішні ключі на партиційованих InnoDB-таблицях не підтримуються.
Операційний план архівації: у PostgreSQL 14+ `DETACH PARTITION ... CONCURRENTLY`, вивантаження відчепленої таблиці й `DROP TABLE`; у MySQL — `ALTER TABLE ... DROP PARTITION` або `EXCHANGE PARTITION`.
Що партиції треба створювати наперед (планувальник, pg_partman), інакше вставка в неіснуючий діапазон падає або втрапляє в DEFAULT-партицію, з якої її потім важко дістати.
Якщо партиціонування не варіант — видалення пачками по keyset-діапазону з паузами, з оглядом на реплікаційний лаг, а не один гігантський DELETE.
Партиціонувати «щоб SELECT був швидший», коли проблему вирішує звичайний індекс: партиціонування не замінює індекс і не зменшує кількість читаних рядків, якщо ключ не у WHERE.
Партиціонувати по `created_at`, а фільтрувати в запитах по `user_id`: прунінгу немає, планувальник обходить сотні партицій, латентність зростає.
Робити партицію на день на п’ять років уперед: тисячі партицій роздувають планування, `pg_class` і памʼять під час prepared statements.
Чистити історію одним `DELETE FROM events WHERE created_at < now() - interval '1 year'`: довга транзакція, розпухання undo/WAL, лаг реплік, а в PostgreSQL місце все одно не повертається без VACUUM FULL чи pg_repack.
Викликати `ATTACH PARTITION` на великій таблиці без заздалегідь провалідованого CHECK — база сканує її цілком під блокуванням.
Забути, що `DROP PARTITION` у MySQL і `DETACH` у PostgreSQL чекають на завершення довгих читань: без `lock_wait_timeout`/`statement_timeout` нічний скрипт стає інцидентом.
ПОРАДА

Сформулюйте критерій одним реченням: партиціонування виправдане, коли є природний ключ життєвого циклу даних і ви хочете видаляти дані шматками, а не рядками. І одразу назвіть ціну — прунінг лише по ключу, обмеження на унікальні ключі, обовʼязкова автоматизація створення партицій.

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Головний виграш — дешеве видалення й обслуговування шматками; прискорення читання отримують лише запити, які фільтрують по ключу партиціонування.