Правильное проектирование схемы данных для Citus

Официально заявляю, что правильно спроектированная схема в Citus может работать до 100 раз быстрее на больших объемах данных, чем одиночный сервер PostgreSQL. Сам лично это наблюдал. Но проектирование требует особого подхода, иначе вместо производительности получите прямо противоположный результат.

Правильно выбирайте ключ шардирования

Главное правило: Все таблицы, участвующие в JOIN, должны быть шардированы по одинаковому ключу.

-- Хороший пример (одинаковый ключ шардирования)
SELECT create_distributed_table('users', 'tenant_id');
SELECT create_distributed_table('orders', 'tenant_id');  -- JOIN будет эффективным

Плохой выбор ключа:

  • Случайные значения
  • Монотонно возрастающие последовательности (может привести к “горячим” шардам)

Хорошие кандидаты:

  • tenant_id в мультитенантных приложениях
  • user_id в пользовательских системах
  • organization_id в корпоративных решениях

Выбирайте оптимальную структуру таблиц

Распределенные таблицы (Distributed Tables)

-- Большие таблицы, которые нужно шардировать
SELECT create_distributed_table('large_table', 'shard_key');

Справочные таблицы (Reference Tables)

-- Небольшие таблицы, которые реплицируются на все узлы
SELECT create_reference_table('small_lookup_table');

Локальные таблицы (Local Tables)

-- Таблицы, которые остаются только на координаторе
CREATE TABLE local_config (id serial PRIMARY KEY, config jsonb);

Оптимизируйте JOIN опреации

Эффективный JOIN (колокация):

-- Таблицы шардированы по одинаковому ключу
SELECT a.*, b.* 
FROM distributed_table_a a
JOIN distributed_table_b b ON a.shard_key = b.shard_key AND a.id = b.id;

Проблемный JOIN (требует передачи данных между узлами):

-- Таблицы шардированы по разным ключам
SELECT a.*, b.* 
FROM table_a a
JOIN table_b b ON a.other_key = b.other_key;  -- Медленно!

Будьте аккуратны с транзакциями

Ограничения:

  • Транзакции, затрагивающие несколько шардов, работают медленнее
  • Используйте 1 шард на транзакцию, когда это возможно

Хороший паттерн:

BEGIN;
-- Все операции в пределах одного tenant_id
INSERT INTO orders (tenant_id, ...) VALUES (123, ...);
UPDATE inventory SET count = count-1 WHERE tenant_id = 123 AND product_id = 456;
COMMIT;

Не забывайте про особнности индексов

  • Создавайте индексы на каждом шарде отдельно
  • Индексы по ключу шардирования обычно не нужны (данные уже распределены по этому ключу) ```sql – Хороший индекс (для часто фильтруемых полей) CREATE INDEX idx_orders_status ON orders (status);

– Избыточный индекс (ключ шардирования уже используется для распределения) CREATE INDEX idx_orders_tenant_id ON orders (tenant_id); – Обычно не нужен


### Работа с последовательностями (Sequences)

Используйте разные подходы для генерации ID:
```sql
-- Локальные последовательности (на каждом шарде)
CREATE TABLE table_with_local_ids (
  id bigserial,        -- Локальный sequence на каждом шарде
  shard_key int,
  ...
);

-- Глобальные уникальные ID (через uuid)
CREATE TABLE table_with_uuids (
  id uuid DEFAULT gen_random_uuid(),
  shard_key int,
  ...
);

Выбирайте оптимальные типы данных

  • Избегайте очень больших полей в ключах шардирования
  • Используйте эффективные типы данных для ключевых полей ```sql – Хорошо CREATE TABLE events ( event_id bigint, tenant_id int, – Короткий ключ шардирования … );

– Плохо (длинный ключ шардирования) CREATE TABLE events ( event_id bigint, tenant_name text, – Длинный текст как ключ шардирования - неэффективно … );


### И в конце про Геораспределение

Если узлы находятся в разных регионах:
```sql
-- Размещение шардов ближе к пользователям
SELECT master_add_node('worker-us-east', 5432, 'primary', 'us-east');
SELECT master_add_node('worker-eu-west', 5432, 'primary', 'eu-west');

-- Привязка данных к локациям
SELECT create_distributed_table('users', 'region_id');