Deep EVM #25: Партиционирование таблиц PostgreSQL — когда таблица превышает 10M строк
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.
Миграция существующей таблицы
Миграция большой таблицы на партиционированную — нетривиальная задача:
- Создайте партиционированную таблицу с новым именем
- Создайте все необходимые партиции
- Копируйте данные batch’ами
- Переключите через 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, и помните: партиционирование решает проблемы масштабирования, но добавляет сложность. Применяйте его, когда таблица реально создаёт проблемы, а не заранее.