Partitioning в SQL: разбиение больших таблиц
Зачем партиционировать таблицу
Когда таблица вырастает до миллионов, десятков миллионов, сотен миллионов строк — обычные запросы начинают тормозить, даже с индексами. Чтобы достать данные за вчерашний день из таблицы с 5 миллиардами записей, БД приходится сканировать огромный индекс или часть таблицы.
Партиционирование решает это на уровне архитектуры. Таблица физически разбивается на несколько меньших кусков (партиций). Запрос, который работает только с частью данных (например, за последний месяц), читает только нужные партиции, остальные игнорирует.
На практике это выглядит так: таблица orders на 1 миллиард строк бьётся по месяцам на 24 партиции по 40 миллионов. Запрос WHERE created_at >= '2026-04-01' читает только 1 партицию из 24 — в 24 раза меньше данных.
Когда партиционирование оправдано
Не каждую большую таблицу нужно партиционировать. Критерии:
Таблица больше 50-100 миллионов строк. Для меньших размеров выигрыш обычно не перекрывает сложность. Хорошие индексы работают и на 10-50 миллионах.
Запросы часто фильтруют по одной колонке — обычно это дата. Если запросы равномерно обращаются ко всем частям таблицы, партиционирование не поможет.
Старые данные можно удалять или архивировать. Партиционирование сильно упрощает: DROP PARTITION за секунду, вместо DELETE миллионов строк часами.
Нужен bulk-insert новых данных. Загрузка в пустую партицию быстрее, чем insert в огромную таблицу с множеством индексов.
Типы партиционирования
Три основных схемы. Самая частая — первая.
Range partitioning. Данные разбиваются по диапазонам значений колонки. Обычно по датам: партиция за январь 2026, за февраль и так далее. Идеально для time-series данных.
List partitioning. Данные разбиваются по списку значений. Например, партиция для региона «ЕС», отдельная для «Россия», отдельная для «Азия». Редко используется.
Hash partitioning. Строки распределяются по партициям на основе хэша от колонки. Не даёт преимуществ по фильтрации (запрос всё равно читает все партиции), но хорош для параллелизации и распределения нагрузки.
Синтаксис PostgreSQL
С версии 10 в Postgres появилась declarative partitioning. Создание партиционированной таблицы:
-- Родительская таблица с указанием partition key
CREATE TABLE orders (
order_id BIGSERIAL,
user_id BIGINT,
amount NUMERIC(10, 2),
created_at TIMESTAMPTZ NOT NULL,
status VARCHAR(20)
) PARTITION BY RANGE (created_at);
-- Создаём партиции вручную
CREATE TABLE orders_2026_q1 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE orders_2026_q2 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
CREATE TABLE orders_2026_q3 PARTITION OF orders
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');После этого все INSERT в orders автоматически попадают в нужную партицию по значению created_at. SELECT c WHERE created_at >= '2026-04-01' читает только orders_2026_q2 и позже.
Партиции можно создавать заранее или по мере поступления данных. Если данные приходят в партицию, которой ещё нет — ошибка. Поэтому обычно партиции создают скриптами за несколько месяцев вперёд.
Автоматическое создание партиций
Вручную создавать новую партицию каждый месяц — скучно и опасно (забыли → данные за новый месяц не проходят). Есть решения.
Самое простое — расширение pg_partman для PostgreSQL. Оно автоматически создаёт партиции по расписанию и удаляет старые:
-- Один раз настроить
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'created_at',
p_type => 'native',
p_interval => 'monthly',
p_premake => 3 -- создаёт на 3 месяца вперёд
);Расширение раз в сутки создаёт новые партиции и опционально удаляет старые, настроенные на archive.
Альтернатива — свой скрипт на Python или PL/pgSQL, который раз в месяц создаёт новую партицию.
Запросы на партиционированной таблице
Для запросов ничего не меняется — вы обращаетесь к основной таблице orders, а Postgres сам выбирает нужные партиции. Это называется partition pruning:
-- Postgres читает только orders_2026_q2
SELECT * FROM orders
WHERE created_at >= '2026-04-15' AND created_at < '2026-04-20';В EXPLAIN это видно как «Append» с участием конкретных партиций. Если фильтр не подходит под partition key (например, WHERE user_id = 42), читаются все партиции — partition pruning не срабатывает.
Поэтому критически важно, чтобы запросы использовали колонку, по которой партиционировано. Иначе вся польза теряется.
Глубоко разобраться, как проектировать схему данных под большой объём — это часть senior-скиллов. В тренажёре Карьерник есть задачи на оптимизацию и проектирование, которые помогают понять, где партиционирование оправдано.
Архивирование и удаление данных
Одно из главных преимуществ — простота удаления старых данных.
Без партиций: DELETE FROM orders WHERE created_at < '2025-01-01'; на миллионе строк может занять часы и заблокировать таблицу.
С партициями: DROP TABLE orders_2024_q1; — за секунду, без блокировок.
Перед удалением можно сделать архив:
-- Отсоединить партицию (остаётся отдельной таблицей)
ALTER TABLE orders DETACH PARTITION orders_2024_q1;
-- Теперь orders_2024_q1 — независимая таблица, можно её бэкапить или переместить
-- Потом удалить
DROP TABLE orders_2024_q1;ClickHouse partitioning
В ClickHouse партиционирование работает по-другому и встроено глубоко в engine.
CREATE TABLE events (
event_time DateTime,
user_id UInt64,
event_name String,
value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (user_id, event_time);PARTITION BY toYYYYMM(event_time) создаёт по партиции на месяц. ClickHouse автоматически создаёт и удаляет партиции, ничего руками делать не надо.
Кроме партиционирования, в ClickHouse есть ORDER BY — это primary index, который внутри каждой партиции сортирует данные. Комбинация даёт очень быстрые запросы: сначала partition pruning, потом индекс внутри.
Hash partitioning для распределения нагрузки
Если у вас нет естественного критерия для range partitioning, но есть проблема с нагрузкой, — hash partitioning:
CREATE TABLE users (
user_id BIGINT,
name TEXT,
email TEXT
) PARTITION BY HASH (user_id);
CREATE TABLE users_0 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE users_1 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE users_2 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE users_3 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 3);Пользователи распределяются по 4 партициям равномерно. Это не ускоряет поиск одного пользователя, но параллельные запросы по разным user_id нагружают разные партиции.
Ошибки и ловушки
Неправильно выбранный partition key. Если партиционировали по city, а запросы идут по created_at — profit отсутствует. Выбирайте ту колонку, по которой чаще всего фильтруете.
Слишком много партиций. 1000 партиций — overhead на query planning и memory. Оптимум — 10-100 активных партиций.
Слишком мало. 2 партиции на таблицу в 1 миллиард строк — каждая по 500M, почти не помогает. Нужно как минимум 10-20.
Забыть создать следующую партицию. INSERT приходит — а партиции нет. Ошибка. Используйте pg_partman или cron.
Индексы на всей таблице. В старых Postgres индексы на партиционированной таблице работали криво. Сейчас (12+) это ОК, но проверяйте, что индексы ожидаемо ускоряют.
Партиционирование vs sharding
Не путайте. Партиционирование — разбиение таблицы на куски внутри одной БД. Все партиции на одном сервере.
Sharding — разбиение между серверами. Разные куски данных живут на разных физических машинах. Это нужно, когда одна БД не справляется по CPU/memory/disk.
Sharding сложнее: нужна маршрутизация запросов, distributed transactions, и так далее. Начинают с партиционирования, переходят к sharding только когда одной БД действительно не хватает.
Читайте также
FAQ
Постгрес партиционирование с 10 версии стабильное?
Да. Declarative partitioning в 10-й версии был ограниченным, но с 12-13 работает полноценно. Новые проекты на Postgres 14+ используют партиционирование без проблем.
Сколько партиций максимум?
Практический лимит — 1000-5000 активных партиций в одной таблице. Больше — проблемы с планировщиком. Если нужно больше, подумайте о sharding или о более крупных партициях.
Можно ли добавить партиционирование к существующей таблице?
Напрямую — нет. Нужно создать новую партиционированную таблицу, перенести данные, переключить имена. Для таблицы в миллиарды строк это проект на несколько дней.
ClickHouse лучше Postgres для больших таблиц?
Для аналитики на миллиарды строк — да, в 10-100 раз быстрее. Для OLTP — Postgres. Часто используют оба: Postgres для транзакций, ClickHouse для аналитики.