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

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

Общие рекомендации по выбору первичного ключа:

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

Все строковые таблицы в YDB отсортированы по возрастанию первичного ключа. Это означает, что запись в строковую таблицу данных с монотонно возрастающим первичным ключом приведет к добавлению новых данных в конец таблицы. Так как YDB разделяет строковые таблицы на партиции по диапазонам ключей, вставки будут обрабатываться одним конкретным сервером, отвечающим за "последнюю" партицию. Сосредоточение нагрузки на одном сервере приведет к медленной загрузке данных и неэффективному использованию распределенной системы.

То же поведение возникает, если монотонно возрастающее значение является единственной компонентой первичного ключа или первой компонентой составного ключа: диапазоны партиций задаются по префиксу ключа, поэтому монотонность «в начале» ключа даёт ту же «горячую точку». Автоинкрементные типы (SERIAL), будучи единственной или первой компонентой PRIMARY KEY, ведут себя так же.

В качестве примера рассмотрим запись лога пользовательских событий в строковую таблицу со схемой ( timestamp, userid, userevent, PRIMARY KEY (timestamp, userid) ).

Значения колонки timestamp монотонно возрастают; как следствие, все новые записи будут добавляться в конец строковой таблицы, и "последняя" партиция, которая отвечает за данный диапазон ключей, будет обслуживать все операции вставки в таблицу. Это приведет к невозможности масштабирования нагрузки на вставку, производительность будет ограничена одним процессом обслуживания этой партиции, и не будет расти с добавлением серверов в кластер.

В YDB поддерживается автоматическое разделение партиции при достижении порогового размера или нагрузки. Но в рассматриваемом случае после разделения новая партиция начнет опять принимать всю нагрузку на вставку, и ситуация повторится.

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

Изменение порядка следования компонент ключа

Запись данных в строковую таблицу со схемой ( timestamp, userid, userevent, PRIMARY KEY (timestamp, userid) ) приводит к неравномерной нагрузке на партиции таблицы из-за монотонно возрастающего первичного ключа. Изменение порядка следования компонент ключа таким образом, чтобы монотонно возрастающая часть не была первой компонентой, может помочь более равномерно распределить нагрузку. Если изменить определение первичного ключа строковой таблицы на PRIMARY KEY (userid, timestamp), то при достаточном количестве пользователей, генерирующих события, запись в БД будет распределена по партициям более равномерно.

Использование хеша от значений ключевых колонок в качестве первичного ключа

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

Например, в рассматриваемую строковую таблицу со схемой ( timestamp, userid, userevent, PRIMARY KEY (userid, timestamp) ) можно включить дополнительное поле, рассчитываемое как хеш-код: userhash = HASH(userid). В результате схема таблицы преобразуется к следующему виду:

( userhash, userid, timestamp, userevent, PRIMARY KEY (userhash, userid, timestamp) )

При правильном выборе функции хеширования строки будут распределены достаточно равномерно по всему пространству ключей, что приведет к более равномерной нагрузке на систему. При этом наличие полей userid, timestamp в составе ключа после поля userhash сохраняет локальность и сортировку данных по времени для конкретного пользователя.

Расчёт значения поля userhash в описанном выше примере должен осуществляться приложением, и явно указываться как при вставке новых записей таблицы, так и при доступе к записям по первичному ключу.

Один из распространённых частных случаев того же принципа — монотонно возрастающие идентификаторы (SERIAL, sequence или последовательный числовой id), а не только временная метка из примера с логом выше.

Монотонно возрастающий идентификатор (SERIAL, sequence, auto increment)

Не рекомендуется использовать монотонно возрастающее значение в качестве единственной или первой компоненты первичного ключа. Типичный пример — автоинкрементный идентификатор (SERIAL), значение из sequence или последовательный числовой id.

Строковые таблицы в YDB партиционируются по диапазонам значений первичного ключа, поэтому новые строки с монотонным ключом последовательно попадают в последнюю партицию и создают «горячую точку»: основная нагрузка на запись концентрируется на ограниченном числе партиций, остальные используются неравномерно.

Та же проблема возникает для составного ключа, если его первой компонентой является монотонно возрастающее значение, например PRIMARY KEY (order_id, customer_id).

YDB поддерживает автоинкрементные типы (SERIAL), однако использование их в качестве единственной или первой компоненты первичного ключа не рекомендуется для высоконагруженных сценариев вставки.

По возможности замените монотонный ключ на случайный идентификатор (например, UUID) или на составной ключ из бизнес-полей без монотонного префикса. Если монотонно возрастающий идентификатор необходимо сохранить в ключе, используйте составной первичный ключ, где первая компонента — хеш от значения идентификатора, а вторая — сам идентификатор. Это позволяет добиться более равномерного распределения записей по партициям и сохранить уникальность (идеальный баланс партиций хеш не гарантирует).

Плохой пример
CREATE TABLE orders (
    order_id Uint64,
    customer_id Uint64,
    amount Double,
    PRIMARY KEY (order_id)
);

INSERT INTO orders (order_id, customer_id, amount) VALUES
    (1001, 10, 999.99),
    (1002, 11, 1499.00),
    (1003, 12, 499.50);

SELECT *
FROM orders
WHERE order_id = 1001;
Хороший пример
CREATE TABLE orders (
    order_hash Uint64,
    order_id Uint64,
    customer_id Uint64,
    amount Double,
    PRIMARY KEY (order_hash, order_id)
);

INSERT INTO orders (order_hash, order_id, customer_id, amount) VALUES
    (Digest::NumericHash(1001), 1001, 10, 999.99),
    (Digest::NumericHash(1002), 1002, 11, 1499.00),
    (Digest::NumericHash(1003), 1003, 12, 499.50);

SELECT *
FROM orders
WHERE order_hash = Digest::NumericHash(1001)
    AND order_id = 1001;

Значение поля order_hash нужно вычислять при записи и при точечном чтении по ключу — так же, как для userhash в примере выше.

Уменьшение количества партиций, затрагиваемых в одном запросе

Предположим, что основной сценарий работы с данными строковой таблицы — прочитать все события по конкретному userid. Тогда при использовании схемы строковой таблицы ( timestamp, userid, userevent, PRIMARY KEY (timestamp, userid) ) каждое чтение будет затрагивать все партиции таблицы. При этом, каждая партиция будет просканирована полностью, так как строки, относящиеся к конкретному userid, расположены в заранее неизвестном порядке. Изменение порядка следования компонент ключа ( timestamp, userid, userevent, PRIMARY KEY (userid, timestamp) ) приведет к тому, что все строки, относящиеся к конкретному userid, будут следовать друг за другом. Такое расположение строк положительно повлияет на скорость чтения информации по конкретному userid, и сократит нагрузку.

Значение NULL в ключевой колонке

В YDB все колонки, включая ключевые, могут содержать значение NULL. Использование NULL в качестве значений в ключевых колонках не рекомендуется. По стандарту языка SQL (ISO/IEC 9075) значение NULL нельзя сравнивать с другими значениями, вследствие чего использование лаконичных конструкции SQL с простыми операторами сравнения может приводить, например, к тому что, строки, содержащие значение NULL, могут быть пропущены при фильтрации.

Ограничение размера строки

Для достижения хорошей производительности не рекомендуется записывать в БД строки размером более 8 МБ и ключевые колонки размером более 2 КБ.

Предыдущая
Следующая