Пошук уроків, статей та іншого контенту
Дізнаєтеся, як ділити таблиці на партиції за діапазоном, списком або хешем для ефективнішого зберігання й запитів.
Партиціювання — це поділ однієї логічної таблиці на кілька фізичних таблиць, які називаються партиціями.
Для застосунку партиціонована таблиця зазвичай виглядає як звичайна таблиця:
SELECT *
FROM orders
WHERE created_at >= DATE '2026-01-01';СУБД самостійно визначає, з яких партицій потрібно читати дані. Якщо умова запиту відповідає ключу партиціювання, СУБД може пропустити непотрібні партиції. Цей процес називається partition pruning — відсікання партицій.
Партиціювання корисне, коли:
таблиця містить дуже багато рядків;
дані природно поділяються на незалежні групи;
запити часто фільтрують дані за ключем партиціювання;
старі дані потрібно видаляти або архівувати цілими частинами;
окремі частини таблиці потрібно зберігати чи обслуговувати незалежно.
Партиціювання не є автоматичним прискоренням будь-якого запиту. Якщо запит не використовує ключ партиціювання, СУБД може звернутися до всіх партицій.
У батьківської таблиці описується:
структура даних;
метод партиціювання;
ключ партиціювання.
Потім створюються дочірні партиції. Кожна партиція має правило, яке визначає, які рядки до неї належать.
Наприклад, таблицю замовлень можна поділити за роками:
замовлення за 2025 рік;
замовлення за 2026 рік;
замовлення за 2027 рік.
Коли вставляється новий рядок, СУБД перевіряє його значення ключа і направляє рядок у відповідну партицію.
Партиціювання за діапазоном (RANGE) використовують, коли значення можна впорядкувати:
дата або час;
числовий ідентифікатор;
версія;
інший інтервал значень.
Це найпоширеніший варіант для журналів, подій, замовлень і фінансових операцій.
У PostgreSQL нижня межа FROM входить до партиції, а верхня межа TO — не входить.
Наприклад:
FROM ('2026-01-01') TO ('2026-02-01')означає:
2026-01-01 <= created_at < 2026-02-01Тому сусідні партиції можуть мати такі межі:
[2026-01-01, 2026-02-01)
[2026-02-01, 2026-03-01)Між ними немає ні пропуску, ні перекриття.
-- Видаляємо таблицю, якщо приклад запускається повторно
DROP TABLE IF EXISTS orders CASCADE;
-- Батьківська таблиця. Дані безпосередньо в неї не вставляються:
-- рядки розподіляються між дочірніми партиціями.
CREATE TABLE orders (
id bigint NOT NULL,
customer_id bigint NOT NULL,
amount numeric(12, 2) NOT NULL,
created_at date NOT NULL
) PARTITION BY RANGE (created_at);
-- Кожна партиція містить замовлення лише за свій місяць.
CREATE TABLE orders_2026_01
PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02
PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03
PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Партиція DEFAULT приймає рядки, які не потрапили
-- до жодної з попередніх партицій.
CREATE TABLE orders_default
PARTITION OF orders
DEFAULT;
-- Індекс створюється для кожної партиції через батьківську таблицю.
CREATE INDEX orders_created_at_idx
ON orders (created_at);
-- Рядки автоматично потрапляють у відповідні партиції.
INSERT INTO orders (id, customer_id, amount, created_at)
VALUES
(1, 101, 150.00, DATE '2026-01-15'),
(2, 102, 275.50, DATE '2026-02-20'),
(3, 103, 99.99, DATE '2026-05-10');
-- Перші два рядки знаходяться в місячних партиціях,
-- третій — у orders_default.
SELECT tableoid::regclass AS actual_partition, id, amount, created_at
FROM orders
ORDER BY id;
-- Для такого запиту PostgreSQL може прочитати лише orders_2026_02.
EXPLAIN
SELECT *
FROM orders
WHERE created_at >= DATE '2026-02-01'
AND created_at < DATE '2026-03-01';Стовпець tableoid у прикладі допомагає побачити фактичну фізичну партицію, у якій зберігається рядок.
Для великих історичних таблиць зручніше створювати річні партиції:
CREATE TABLE events (
id bigint NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL,
occurred_at timestamptz NOT NULL
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2025
PARTITION OF events
FOR VALUES FROM ('2025-01-01 00:00:00+00')
TO ('2026-01-01 00:00:00+00');
CREATE TABLE events_2026
PARTITION OF events
FOR VALUES FROM ('2026-01-01 00:00:00+00')
TO ('2027-01-01 00:00:00+00');Розмір партицій потрібно підбирати під характер даних. Надто великі партиції зменшують користь від відсікання, а надто дрібні збільшують кількість об'єктів, які потрібно планувати й обслуговувати.
Партиціювання за списком (LIST) використовують для явно визначених категорій.
Типові приклади:
код країни;
регіон;
тип клієнта;
середовище;
статус або інша невелика множина значень.
CREATE TABLE customers (
id bigint NOT NULL,
name text NOT NULL,
region text NOT NULL
) PARTITION BY LIST (region);
CREATE TABLE customers_eu
PARTITION OF customers
FOR VALUES IN ('DE', 'FR', 'PL', 'UA');
CREATE TABLE customers_us
PARTITION OF customers
FOR VALUES IN ('US', 'CA');
CREATE TABLE customers_other
PARTITION OF customers
DEFAULT;
INSERT INTO customers (id, name, region)
VALUES
(1, 'Anna', 'UA'),
(2, 'John', 'US'),
(3, 'Marta', 'BR');
SELECT tableoid::regclass AS actual_partition, id, name, region
FROM customers
ORDER BY id;У партиції customers_eu можуть бути лише значення, перелічені в FOR VALUES IN. Значення BR у прикладі потрапить до партиції DEFAULT.
Партиціювання за списком зручне, якщо набір категорій стабільний і кожна категорія має відносно багато даних. Якщо значення постійно додаються, потрібно регулярно створювати нові партиції або використовувати партицію за замовчуванням.
Партиціювання за хешем (HASH) розподіляє рядки між партиціями на основі хешу значення ключа.
Його використовують, коли:
немає природних діапазонів;
потрібно рівномірно розподілити дані;
ключ має багато різних значень;
важливо зменшити розмір окремих фізичних таблиць.
CREATE TABLE sessions (
id bigint NOT NULL,
user_id bigint NOT NULL,
token text NOT NULL,
created_at timestamptz NOT NULL
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_0
PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1
PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_2
PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_3
PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
INSERT INTO sessions (id, user_id, token, created_at)
VALUES
(1, 10, 'token-a', now()),
(2, 11, 'token-b', now()),
(3, 12, 'token-c', now());
SELECT tableoid::regclass AS actual_partition, id, user_id
FROM sessions
ORDER BY id;У цьому прикладі:
MODULUS 4 означає чотири частини;
REMAINDER визначає номер кожної частини від 0 до 3;
PostgreSQL сам обчислює, до якої партиції належить значення user_id.
Хеш-партиціювання не створює логічних діапазонів. Наприклад, не можна сказати, що одна партиція містить користувачів із певного числового інтервалу.
DEFAULTПартиція DEFAULT приймає рядки, які не відповідають жодній явно визначеній партиції.
Вона корисна, коли:
потрібно не відхиляти нові значення;
партиції створюються із затримкою;
потрібно тимчасово зберігати непередбачені дані.
Водночас DEFAULT може приховати помилку в конфігурації. Наприклад, нові дати можуть почати потрапляти в неї замість запланованої місячної партиції.
Перед створенням нової партиції дані з DEFAULT потрібно перемістити до неї. Нова партиція не може перекриватися з рядками, які вже належать партиції DEFAULT.
Індекс, створений на батьківській партиціонованій таблиці, зазвичай створює відповідні індекси на її партиціях:
CREATE INDEX orders_customer_id_idx
ON orders (customer_id);Це не означає, що існує один фізичний індекс для всіх даних. Фізично індекс буде окремим у кожній партиції.
Зазвичай індекси потрібно створювати для тих самих сценаріїв, що й на звичайній таблиці. Водночас надлишкові індекси на великій кількості партицій збільшують витрати на вставки та обслуговування.
У PostgreSQL глобальна унікальність між усіма партиціями має важливе обмеження: унікальний індекс або первинний ключ на партиціонованій таблиці повинен містити ключ партиціювання.
Наприклад, якщо таблиця партиціонована за created_at, то така конструкція не є достатньою для глобальної унікальності id:
-- Така вимога не підходить для глобальної унікальності
-- в усіх партиціях одночасно.
PRIMARY KEY (id)Замість цього ключ може містити обидва поля:
CREATE TABLE payments (
id bigint NOT NULL,
paid_at date NOT NULL,
amount numeric(12, 2) NOT NULL,
PRIMARY KEY (id, paid_at)
) PARTITION BY RANGE (paid_at);Але це вже означає унікальність комбінації (id, paid_at), а не лише id. Якщо ідентифікатор має бути унікальним у всій таблиці, це потрібно окремо враховувати під час проєктування схеми.
Для регулярного партиціювання за діапазоном заздалегідь створюють майбутні партиції.
Наприклад, перед початком нового місяця:
CREATE TABLE orders_2026_04
PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');Старі дані можна видалити всією партицією:
DROP TABLE orders_2025_12;Або від'єднати партицію від батьківської таблиці:
ALTER TABLE orders
DETACH PARTITION orders_2025_12;Від'єднання зберігає таблицю як окремий об'єкт. Це зручно, якщо дані потрібно спочатку перевірити, експортувати або передати в архів.
Для великих таблиць видалення партиції зазвичай простіше, ніж виконання великого DELETE по рядках. Проте перед видаленням потрібно переконатися, що застосунок більше не потребує цих даних.
Для аналізу плану запиту використовуйте EXPLAIN:
EXPLAIN
SELECT *
FROM orders
WHERE created_at >= DATE '2026-02-01'
AND created_at < DATE '2026-03-01';У плані має бути видно звернення лише до відповідної партиції або до невеликої кількості партицій.
Запити на кшталт цього можуть змусити СУБД перевіряти всі партиції:
SELECT *
FROM orders
WHERE customer_id = 101;Умова фільтрує за customer_id, але таблиця партиціонована за created_at. Партиціювання не дає змоги визначити, у якій саме партиції шукати рядок.
Для перевірки не покладайтеся лише на очікування — аналізуйте фактичний план і час виконання запитів.
RANGE, якщодані мають часову або числову послідовність;
потрібно видаляти старі періоди;
запити фільтрують дані за діапазоном;
для кожного періоду можна заздалегідь створити партицію.
LIST, якщоє невелика або контрольована множина категорій;
кожна категорія має окремі правила зберігання;
запити часто фільтрують за категорією.
HASH, якщозначення потрібно розподілити рівномірно;
природного діапазону немає;
ключ має багато різних значень;
потрібно зменшити розмір фізичних частин таблиці.
Неправильно визначені межі можуть призвести до того, що рядок не матиме відповідної партиції або потраплятиме не в той період.
Для дат використовуйте узгоджені напіввідкриті інтервали:
[початок, кінець)Наприклад:
[2026-01-01, 2026-02-01)
[2026-02-01, 2026-03-01)Кожна партиція створює додаткову роботу для планувальника та операцій обслуговування. Поділ таблиці на партицію для кожного дня або кожного клієнта не завжди є хорошим рішенням.
Для RANGE потрібно заздалегідь створювати наступні періоди. Інакше вставки можуть почати помилятися або накопичуватися в DEFAULT.
Партиціювання допомагає насамперед тоді, коли запит використовує ключ партиціювання. Воно не замінює індекси й не пришвидшує автоматично запити за іншими полями.
Під час перенесення звичайної таблиці на партиції потрібно окремо перевірити первинні ключі, унікальні обмеження та зовнішні ключі. Їхня поведінка може мати обмеження, пов'язані з межами партицій.
DEFAULT без контролюDEFAULT зручна як захисний механізм, але її потрібно регулярно перевіряти. Інакше дані можуть залишатися не в тій партиції, де їх очікує команда.
Партиціювання ділить одну логічну таблицю на кілька фізичних партицій.
RANGE підходить для дат, часу та інших упорядкованих значень.
LIST підходить для явно визначених категорій.
HASH рівномірно розподіляє рядки за хешем ключа.
Запити можуть бути швидшими завдяки відсіканню непотрібних партицій.
Ключ партиціювання потрібно обирати з урахуванням реальних умов фільтрації.
Для часових партицій важливо заздалегідь створювати майбутні періоди.
Партиціювання не замінює індекси й не пришвидшує запити, які не використовують ключ партиціювання.
Перед впровадженням потрібно продумати унікальність, індекси, DEFAULT і життєвий цикл старих партицій.