Партиционирование — разбивка большой таблицы на физически отдельные части (партиции). Запросы читают только нужные партиции, игнорируя остальные.
Когда нужно партиционирование
- Таблица > 50-100 GB и запросы всегда фильтруют по конкретному столбцу (дата, регион)
- Нужно быстро удалять старые данные (
DROP TABLEпартиции vsDELETEмиллионов строк) - Разные партиции хранятся на разных tablespace (архив → медленный диск)
Партиционирование не нужно для таблиц < 10 GB — накладные расходы перевесят выгоду.
RANGE партиционирование (по дате)
-- Партиционированная таблица
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
user_id BIGINT,
amount NUMERIC,
status TEXT
) 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');
-- Партиция по умолчанию (для значений вне всех диапазонов)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
Partition pruning в действии
-- Запрос фильтрует по created_at → читает только нужную партицию
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';
-- Append
-- Seq Scan on orders_2026_03 ← только одна партиция!
-- (orders_2026_01 и orders_2026_02 исключены)
-- Без партиционирования: Seq Scan всей таблицы
LIST партиционирование (по значению)
CREATE TABLE products (
id BIGSERIAL,
name TEXT,
category TEXT NOT NULL,
price NUMERIC
) PARTITION BY LIST (category);
CREATE TABLE products_electronics PARTITION OF products
FOR VALUES IN ('smartphone', 'laptop', 'tablet');
CREATE TABLE products_clothing PARTITION OF products
FOR VALUES IN ('shirts', 'pants', 'shoes');
CREATE TABLE products_food PARTITION OF products
FOR VALUES IN ('fresh', 'frozen', 'drinks');
CREATE TABLE products_other PARTITION OF products DEFAULT;
HASH партиционирование (равномерное распределение)
-- Хорошо когда нет естественного диапазона, но нужно распределить нагрузку
CREATE TABLE users (
id BIGSERIAL,
email TEXT NOT NULL,
name TEXT
) PARTITION BY HASH (id);
-- 4 равные партиции
CREATE TABLE users_p0 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE users_p1 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE users_p2 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE users_p3 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 3);
Индексы на партиционированных таблицах
-- Индекс создаётся на все партиции автоматически
CREATE INDEX ON orders (user_id);
-- ↑ создаст orders_2026_01_user_id_idx, orders_2026_02_user_id_idx и т.д.
CREATE INDEX ON orders (created_at, status);
-- PRIMARY KEY (должен включать ключ партиционирования)
ALTER TABLE orders ADD PRIMARY KEY (id, created_at);
-- ❌ Нельзя: PRIMARY KEY на id без created_at (ключа партиционирования)
Автоматическое создание партиций
PostgreSQL сам не создаёт новые партиции — нужно делать заранее. Автоматизация:
-- Функция для создания партиции на следующий месяц
CREATE OR REPLACE FUNCTION create_monthly_partition(
p_table_name TEXT,
p_year INTEGER,
p_month INTEGER
) RETURNS VOID AS $$
DECLARE
v_partition_name TEXT;
v_from_date DATE;
v_to_date DATE;
BEGIN
v_from_date := MAKE_DATE(p_year, p_month, 1);
v_to_date := v_from_date + INTERVAL '1 month';
v_partition_name := p_table_name || '_' || TO_CHAR(v_from_date, 'YYYY_MM');
EXECUTE FORMAT(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
v_partition_name, p_table_name, v_from_date, v_to_date
);
RAISE NOTICE 'Created partition: %', v_partition_name;
END;
$$ LANGUAGE plpgsql;
-- Создать партиции на следующие 3 месяца
SELECT create_monthly_partition('orders', 2026, 4);
SELECT create_monthly_partition('orders', 2026, 5);
SELECT create_monthly_partition('orders', 2026, 6);
-- Cron job: запускать 1 числа каждого месяца
-- SELECT create_monthly_partition('orders', EXTRACT(YEAR FROM NOW() + INTERVAL '2 months')::INT, ...);
ATTACH/DETACH партиций
-- Создать таблицу отдельно, затем прикрепить как партицию
CREATE TABLE orders_2025 (LIKE orders INCLUDING ALL);
-- Загрузить данные...
COPY orders_2025 FROM '/backup/orders_2025.csv' CSV;
-- Прикрепить к партиционированной таблице (быстро, без блокировки)
ALTER TABLE orders ATTACH PARTITION orders_2025
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
-- Отсоединить партицию (для архивирования)
ALTER TABLE orders DETACH PARTITION orders_2024_01;
-- Таблица orders_2024_01 теперь существует самостоятельно
-- Удалить старую партицию мгновенно (vs DELETE миллионов строк)
DROP TABLE orders_2024_01;
Субпартиционирование
-- Партиции по году, затем по месяцу
CREATE TABLE events (
id BIGSERIAL,
event_date DATE NOT NULL,
event_type TEXT NOT NULL,
data JSONB
) PARTITION BY RANGE (event_date);
CREATE TABLE events_2026 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01')
PARTITION BY LIST (event_type); -- субпартиция по типу
CREATE TABLE events_2026_click PARTITION OF events_2026
FOR VALUES IN ('click', 'view', 'scroll');
CREATE TABLE events_2026_purchase PARTITION OF events_2026
FOR VALUES IN ('add_to_cart', 'checkout', 'purchase');
Мониторинг партиций
-- Размер каждой партиции
SELECT
child.relname AS partition_name,
pg_size_pretty(pg_relation_size(child.oid)) AS size,
pg_size_pretty(pg_total_relation_size(child.oid)) AS total_size
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY pg_relation_size(child.oid) DESC;
-- Количество строк по партициям (приблизительно)
SELECT
child.relname,
child.reltuples::BIGINT AS estimated_rows
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders';
Итог
| Тип | Ключ | Применение |
|---|---|---|
RANGE | Дата/число | Логи, заказы, события по времени |
LIST | Категория | Регион, статус, тип |
HASH | ID/ключ | Равномерное распределение нагрузки |
Главные выгоды партиционирования:
- Partition pruning: запросы читают только нужные партиции
- Быстрое удаление старых данных:
DROP TABLE partitionвместоDELETE - Параллельные запросы по разным партициям
- Разные настройки хранения для горячих/холодных данных