Выбор ключей для максимальной производительности колоночных таблиц

В отличие от строковых таблиц YDB, колоночные таблицы партиционируют данные не по первичным ключам, а по специально выделенным ключам — ключам партицирования. При этом внутри каждой партиции данные распределяются в порядке первичного ключа таблицы.

Ключ партиционирования

Ключ партиционирования должен являться непустым подмножеством столбцов первичного ключа. Хэш от ключа партиционирования определяет партицию, в которую попадёт строка. Ключ следует выбирать таким образом, чтобы данные распределялись равномерно по партициям. Обычно этого достигают путём включения в ключ партиционирования высококардинальных столбцов, например, временных меток с высоким разрешением (тип данных Timestamp). Если в качестве ключа партиционирования используется низкокардинальный ключ, данные могут распределиться по партициям неравномерно, что приведёт к перегрузке некоторых партиций. Перегрузка партиций вызывает неоптимальную производительность запросов и/или накладывает ограничения на максимальный поток вставляемых данных.

В настоящий момент колоночные таблицы не поддерживают автоматического репартицирования, поэтому важно указывать правильное число партиций при создании таблицы.

Для базовой оценки числа партиций можно использовать формулу (количество узлов * 4. Это позволит максимально утилизировать ресурсы кластера при выполнении параллельных запросов.

Первичный ключ

Первичный ключ определяет порядок хранения данных внутри партиции, поэтому при выборе первичного ключа необходимо учитывать не только эффективность сценария чтения данных из партиции, но и эффективность вставки данных в партицию. Оптимальный сценарий вставки — запись данных в начало или в конец таблицы с редкими локальными обновлениями уже вставленных данных. Например, эффективен сценарий хранения журналов работы приложений, когда данные хранятся по временным меткам, а новые записи добавляются в конец партиции за счет использования текущего времени в первичном ключе.

Пример

Рассмотрим следующий сценарий: необходимо спроектировать таблицу для хранения и анализа данных из высоконагруженной рекламной сети. Система генерирует события (показы, клики).

Основные сущности:

  • event_timestamp: Время события (точность до микросекунд).
  • user_id: Идентификатор пользователя.
  • campaign_id: Идентификатор рекламной кампании.
  • ad_id: Идентификатор конкретного рекламного объявления.
  • event_type: Тип события ('impression', 'click').
  • event_id: Уникальный идентификатор события.

Типичные аналитические запросы:

  1. Посчитать количество кликов по кампании campaign_id = X за последний час.
  2. Найти все события для пользователя user_id = Y, отсортированные по времени.

Выбор ключа партиционирования

Для обеспечения максимальной производительности необходимо равномерно распределить входящие данные по всем партициям.
Рассмотрим использование различных колонок в качестве ключа партиционирования.

Неудовлетворительные варианты:

  • event_timestamp или event_date. Все поступающие данные за текущий момент времени будут отправляться в одну и ту же партицию, создавая высокую нагрузку на запись в одну партицию на одном узле, в то время как остальные партиции будут простаивать.
  • campaign_id. Рекламные кампании имеют разную популярность. Одна крупная кампания может генерировать 80% всех событий, что приведет к перекосу данных и записи преимущественно в несколько партиций.

Оптимальные варианты:

  • user_id. Идентификаторов пользователей очень много (высокая кардинальность), их активность, скорее всего, распределена случайным образом.
  • композитный ключ, например, (event_timestamp, user_id). Для вычисления партиции данные колонок конкатенируются, и хеш-функция вычисляется уже по итоговой суммарной строке, обеспечивая уникальное значение.

Итог: оптимальным вариантом ключа партиционирования является либо колонка user_id, либо композитный ключ вида (event_timestamp, user_id).
Например, PARTITION BY HASH(event_timestamp, user_id).

Выбор первичного ключа

Выбор первичного ключа важен для следующих задач:

  1. Обеспечение быстрой фильтрации по диапазону (особенно по времени).
  2. Оптимизация записи, делая её максимально приближённой к последовательному добавлению в конец (append-only).
  3. Обеспечение целостности данных.

Вернёмся к требованиям к системе:

  1. Посчитать количество кликов по кампании campaign_id = X за последний час.
  2. Найти все события для пользователя user_id = Y, отсортированные по времени.

Рассмотрим варианты выбора первичного ключа:

  1. Для обеспечения выборок, связанных со временем, и для записи поступающих данных в конец таблиц, оптимально использовать event_timestamp как первую часть ключа.
  2. В один и тот же момент времени могут поступать данные о действиях разных пользователей, поэтому в качестве второй части ключа используем user_id.
  3. Пользователи могут совершать действия в разных кампаниях, поэтому в третью часть ключа добавим поле campaign_id.

Итоговая структура таблицы

CREATE TABLE ad_events (
    -- Ключевые колонки, участвующие в ключах
    user_id Utf8 NOT NULL,
    event_timestamp Timestamp NOT NULL,
    event_id Uint64,
    campaign_id Uint64 NOT NULL,

    -- Остальные колонки
    ad_id Uint64,
    event_type Utf8,
    PRIMARY KEY(event_timestamp, user_id, campaign_id)
)
PARTITION BY HASH(user_id, event_timestamp) -- Равномерно распределяем данные
WITH (
    STORE = COLUMN
);

Такая структура эффективно решает поставленные задачи: входящий поток данных равномерно распределяется по партициям благодаря HASH(user_id, event_timestamp), а аналитические запросы по временным диапазонам работают быстро за счёт сортировки данных по event_timestamp внутри каждой партиции.

Предыдущая