SQLLab
Все статьи

Партиционирование таблиц в PostgreSQL: RANGE, LIST, HASH

Партиционирование PostgreSQL: RANGE по дате, LIST по категории, HASH по ID. Partition pruning, индексы, ATTACH/DETACH, автоматическое создание партиций.

24 марта 2026 г.·5 мин чтения·

Партиционирование — разбивка большой таблицы на физически отдельные части (партиции). Запросы читают только нужные партиции, игнорируя остальные.

Когда нужно партиционирование

  • Таблица > 50-100 GB и запросы всегда фильтруют по конкретному столбцу (дата, регион)
  • Нужно быстро удалять старые данные (DROP TABLE партиции vs DELETE миллионов строк)
  • Разные партиции хранятся на разных 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КатегорияРегион, статус, тип
HASHID/ключРавномерное распределение нагрузки

Главные выгоды партиционирования:

  • Partition pruning: запросы читают только нужные партиции
  • Быстрое удаление старых данных: DROP TABLE partition вместо DELETE
  • Параллельные запросы по разным партициям
  • Разные настройки хранения для горячих/холодных данных

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →