Правильно спроектированная схема в Citus может работать до 100 раз быстрее на больших объемах данных, чем одиночный сервер PostgreSQL. Но при условии что правильно проектируете данные, иначе вместо производительности получите прямо противоположный результат.
Правильное проектирование данных в Citus требует понимания распределённой природы системы. Ключевые аспекты:
- Тщательный выбор колонки распределения
- Максимальное использование колокации
- Оптимизация запросов под распределённую среду
- Правильный выбор типа таблиц (распределённые, reference, локальные)
- Учёт ограничений распределённой системы
1. Выбор колонки для распределения (distribution column)
Основной принцип: данные распределяются между узлами на основе значения distribution column (также называемой “колонкой шардирования”).
Правила выбора:
- Должна иметь высокую кардинальность (много уникальных значений)
- Должна равномерно распределять данные и нагрузку
- Часто используется в JOIN и WHERE условиях
Пример:
-- Хороший выбор: user_id с высокой кардинальностью
SELECT create_distributed_table('events', 'user_id');
-- Плохой выбор: status с малым количеством уникальных значений
-- SELECT create_distributed_table('events', 'status'); -- Не рекомендуется
2. Колокация (co-location)
Принцип: Таблицы, которые часто соединяются, должны быть распределены по одинаковой колонке и иметь одинаковое количество шардов.
Правило: Используйте одинаковые типы данных для колонок распределения в связанных таблицах.
Пример:
-- Таблицы users и events колокированы по user_id
SELECT create_distributed_table('users', 'user_id');
SELECT create_distributed_table('events', 'user_id');
-- Теперь JOIN выполняется локально на каждом узле
SELECT u.name, COUNT(e.*)
FROM users u JOIN events e ON u.user_id = e.user_id
GROUP BY u.name;
3. Ограничения ссылочной целостности
Принцип: В распределённой стуре сложно поддерживать FOREIGN KEY между разными таблицами.
Правила:
- FK между колокированными таблицами разрешены
- FK между таблицами с разными колонками распределения запрещены
- Альтернатива: использовать триггеры или логику приложения
Пример:
-- Разрешено (обе таблицы распределены по order_id)
SELECT create_distributed_table('orders', 'order_id');
SELECT create_distributed_table('order_items', 'order_id');
ALTER TABLE order_items ADD CONSTRAINT fk_order
FOREIGN KEY (order_id) REFERENCES orders(order_id);
-- Запрещено (разные колонки распределения)
-- SELECT create_distributed_table('customers', 'customer_id');
-- SELECT create_distributed_table('orders', 'order_id');
-- ALTER TABLE orders ADD CONSTRAINT fk_customer
-- FOREIGN KEY (customer_id) REFERENCES customers(customer_id); -- Ошибка!
4. Выбор типа таблицы
Принцип: Не все таблицы должны быть распределёнными.
Правила:
- Распределённые таблицы - большие таблицы, участвующие в горизонтальном масштабировании
- Reference таблицы - маленькие таблицы, реплицируемые на все узлы
- Локальные таблицы - таблицы, существующие только на координаторе
Пример:
-- Большая таблица - распределённая
SELECT create_distributed_table('events', 'user_id');
-- Маленькая справочная таблица - реплицируемая
SELECT create_reference_table('countries');
-- Таблица только на координаторе (для служебных данных)
CREATE TABLE admin_logs (
id SERIAL PRIMARY KEY,
log_message TEXT
); -- Не распределяется
5. Оптимизация запросов
Принцип: Запросы должны затрагивать минимальное количество шардов.
Правила:
- Фильтруйте по колонке распределения
- Избегайте кросс-шардовых операций (CROSS JOIN, DISTINCT, GROUP BY без колонки распределения)
- Используйте CTE (WITH) для сложных запросов
Пример:
-- Хорошо: фильтр по колонке распределения
SELECT * FROM events WHERE user_id = 123;
-- Плохо: полный сканирование всех шардов
SELECT * FROM events WHERE event_type = 'click';
-- Улучшенный вариант для второго случая
SELECT create_distributed_table('events', 'event_type'); -- Или
SELECT * FROM events WHERE event_type = 'click' AND user_id = 123; -- Если event_type коррелирует с user_id
6. Работа с последовательностями
Принцип: Стандартные последовательности PostgreSQL создают узкое место.
Правила:
- Используйте UUID или другие уникальные идентификаторы
- Или используйте Citus-совместимые последовательности
Пример:
-- Плохо:
CREATE TABLE items (
id SERIAL PRIMARY KEY, -- Стандартный SERIAL
name TEXT
);
-- Лучше:
CREATE TABLE items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT
);
-- Или с совместимой последовательностью
CREATE SEQUENCE items_id_seq;
SELECT create_distributed_table('items', 'id');
7. Размер шардов и количество
Принцип: Слишком мелкие или крупные шарды снижают производительность.
Правила:
- Оптимальный размер шарда: 1-10GB
- Начинайте с 32 шардов для средних нагрузок
- Используйте
citus.shard_count
для настройки
Пример:
-- Установка количества шардов перед созданием таблицы
SET citus.shard_count = 64;
SELECT create_distributed_table('large_table', 'tenant_id');
8. Индексы
Принцип: Индексы создаются на каждом шарде отдельно.
Правила:
- Создавайте индексы на колонках распределения
- Индексы для JOIN колонок должны быть на обоих таблицах
- Избегайте избыточных индексов (занимают место на каждом шарде)
Пример:
-- Хорошая практика
CREATE INDEX idx_events_user_id ON events(user_id); -- Колонка распределения
CREATE INDEX idx_events_event_time ON events(event_time); -- Часто используемая в WHERE
-- Для JOIN
CREATE INDEX idx_orders_customer_id ON orders(customer_id); -- Даже если распределение по order_id
9. Временные данные
Принцип: Временные данные могут не требовать распределения.
Правила:
- Используйте PostgreSQL-партицирование для временных данных
- Рассмотрите локальные таблицы на координаторе
- Для больших объёмов - распределяйте по временному диапазону
Пример:
-- Локальная таблица для временных данных
CREATE TEMPORARY TABLE temp_sessions (
session_id UUID,
user_id INT,
expires_at TIMESTAMP
);
-- Или партицированная распределённая таблица
SELECT create_distributed_table('session_logs', 'created_at');
CREATE TABLE session_logs_2023 PARTITION OF session_logs
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
10. Агрегация данных
Принцип: Агрегатные функции должны по возможности выполняться на шардах.
Правила:
- Используйте
citus.enable_repartition_joins
для сложных агрегаций - Рассмотрите материализованные представления
- Используйте ROLLUP для многоуровневых агрегаций
Пример:
-- Оптимизированная агрегация
SET citus.enable_repartition_joins TO on;
SELECT u.region, COUNT(e.*), AVG(e.value)
FROM users u JOIN events e ON u.user_id = e.user_id
GROUP BY u.region;
-- Материализованное представление для частых агрегаций
CREATE MATERIALIZED VIEW event_stats AS
SELECT user_id, COUNT(*) as event_count
FROM events
GROUP BY user_id;