Перейти к основному содержимому
DevOpsMar 28, 2026

Deep EVM #25: Партиционирование таблиц PostgreSQL — когда таблица превышает 10M строк

OS
Open Soft Team

Engineering Team

Когда партиционирование становится необходимым

PostgreSQL прекрасно работает с таблицами до нескольких миллионов строк. Но когда таблица транзакций блокчейна растёт до 10, 50, 100 миллионов записей, начинаются проблемы: VACUUM занимает часы, индексы раздуваются, запросы с фильтром по дате замедляются.

Партиционирование — это разделение одной логической таблицы на несколько физических секций. PostgreSQL поддерживает нативное декларативное партиционирование с версии 10, и с каждой версией оно становится лучше.

Три стратегии партиционирования

Range Partitioning (по диапазону)

Самая распространённая стратегия для временных данных:

CREATE TABLE transactions (
    id UUID DEFAULT gen_random_uuid(),
    block_number BIGINT NOT NULL,
    tx_hash TEXT NOT NULL,
    from_address TEXT NOT NULL,
    to_address TEXT,
    value NUMERIC NOT NULL,
    gas_used BIGINT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- Месячные партиции
CREATE TABLE transactions_2024_01
    PARTITION OF transactions
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE transactions_2024_02
    PARTITION OF transactions
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

PostgreSQL автоматически направляет INSERT в правильную партицию и при SELECT сканирует только релевантные партиции (partition pruning).

List Partitioning (по списку)

Для данных с дискретными категориями:

CREATE TABLE events (
    id UUID DEFAULT gen_random_uuid(),
    chain TEXT NOT NULL,
    event_type TEXT NOT NULL,
    data JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY LIST (chain);

CREATE TABLE events_ethereum
    PARTITION OF events FOR VALUES IN ('ethereum');
CREATE TABLE events_polygon
    PARTITION OF events FOR VALUES IN ('polygon');
CREATE TABLE events_arbitrum
    PARTITION OF events FOR VALUES IN ('arbitrum');

Hash Partitioning (по хешу)

Для равномерного распределения без естественного ключа:

CREATE TABLE address_balances (
    address TEXT NOT NULL,
    token TEXT NOT NULL,
    balance NUMERIC NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY HASH (address);

CREATE TABLE address_balances_0
    PARTITION OF address_balances
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE address_balances_1
    PARTITION OF address_balances
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE address_balances_2
    PARTITION OF address_balances
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE address_balances_3
    PARTITION OF address_balances
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Автоматическое создание партиций

Вручную создавать партиции — ненадёжно. Используйте pg_partman:

CREATE EXTENSION pg_partman;

SELECT partman.create_parent(
    p_parent_table := 'public.transactions',
    p_control := 'created_at',
    p_type := 'native',
    p_interval := '1 month',
    p_premake := 3  -- Создавать на 3 месяца вперёд
);

pg_partman автоматически создаёт новые партиции и может удалять старые по retention policy.

Индексы на партиционированных таблицах

Индексы создаются на родительской таблице и автоматически наследуются:

CREATE INDEX idx_transactions_hash
ON transactions (tx_hash);

CREATE INDEX idx_transactions_from
ON transactions (from_address, created_at DESC);

PostgreSQL создаёт отдельный индекс для каждой партиции. Это означает, что каждый индекс меньше и эффективнее.

Уникальные ограничения

Уникальные индексы должны включать ключ партиционирования:

-- Это НЕ сработает:
CREATE UNIQUE INDEX ON transactions (tx_hash);  -- ERROR

-- Это сработает:
CREATE UNIQUE INDEX ON transactions (tx_hash, created_at);

Partition Pruning

Partition pruning — это оптимизация, при которой PostgreSQL исключает ненужные партиции из плана запроса:

-- Сканирует ТОЛЬКО партицию за январь 2024
SELECT * FROM transactions
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';

Проверьте с EXPLAIN:

EXPLAIN SELECT * FROM transactions
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';

-- Append
--   -> Seq Scan on transactions_2024_01  ← Только одна партиция!

Важно: partition pruning работает только с constants и параметризованными запросами. Выражения типа WHERE created_at > NOW() - INTERVAL '30 days' пруниться НЕ будут в старых версиях PostgreSQL.

Миграция существующей таблицы

Миграция большой таблицы на партиционированную — нетривиальная задача:

  1. Создайте партиционированную таблицу с новым именем
  2. Создайте все необходимые партиции
  3. Копируйте данные batch’ами
  4. Переключите через RENAME в транзакции
-- 1. Новая партиционированная таблица
CREATE TABLE transactions_partitioned (
    LIKE transactions INCLUDING ALL
) PARTITION BY RANGE (created_at);

-- 2. Создать партиции...

-- 3. Копирование батчами
INSERT INTO transactions_partitioned
SELECT * FROM transactions
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';
-- ... повторить для каждого месяца

-- 4. Атомарное переключение
BEGIN;
ALTER TABLE transactions RENAME TO transactions_old;
ALTER TABLE transactions_partitioned RENAME TO transactions;
COMMIT;

Мониторинг партиций

Полезные запросы для мониторинга:

-- Размер каждой партиции
SELECT
    schemaname || '.' || tablename AS partition,
    pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS size,
    n_live_tup AS rows
FROM pg_stat_user_tables
WHERE tablename LIKE 'transactions_%'
ORDER BY tablename;

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

  • Таблица меньше 1M строк
  • Запросы не фильтруют по ключу партиционирования
  • Нужны сложные UNIQUE constraints
  • Нужны cross-partition foreign keys

Заключение

Партиционирование PostgreSQL — мощный инструмент для управления большими таблицами. Range partitioning по дате — стандартный выбор для блокчейн-данных. Используйте pg_partman для автоматизации, следите за partition pruning через EXPLAIN, и помните: партиционирование решает проблемы масштабирования, но добавляет сложность. Применяйте его, когда таблица реально создаёт проблемы, а не заранее.