Вторичные индексы
В теории баз данных вторичным индексом называется структура данных, позволяющая ускорить выборку из таблицы по неключевым колонкам. Индексируемый кортеж неключевых колонок называется вторичным ключем.
В YTsaurus такие структуры реализованы для сортированных динамических таблиц в виде отдельных таблиц, называемых далее индексными, и специальных объектов, связывающих индексируемую таблицу с индексной. Эти объекты в терминологии YTsaurus и называются вторичными индексами. Они не имеют собственных путей в Кипарисе, являются неизменяемыми и существуют, пока существуют обе индексируемая и индексная таблицы.
Вторичные индексы поддержаны также для реплицированных таблиц.
Использование
По умолчанию индексация происходит по кортежу из одной или нескольких неключевых колонок таблицы, а между строками таблиц имеет место биективное соответствие — такое поведение задается атрибутом вторичного индекса kind=full_sync
.
То, какие выборки индекс способен ускорить, определяется схемой индексной таблицы. Для эффективного выбора диапазона чтения необходимо, чтобы вторичный ключ являлся префиксом ключа индексной таблицы. Быстрый доступ до данных в индексируемой таблице достигается за счет хранения полного её ключа (называемого первичным) в индексной таблице. Поскольку строки сортированных динамических таблиц должны быть уникальными по ключу, то ключ индексной таблицы должен содержать первичный ключ, то есть представлять собой конкатенацию вторичного и первичного ключей (у этого правила есть исключение).
В качестве неключевых колонок индексная таблица может содержать одну или несколько неключевых колонок индексируемой таблицы. Она также может не содержать значимых неключевых значений, но в этом случае придется добавить в схему индексной таблицы фиктивную колонку $empty
типа int64
, в которой значения всегда будут равны null.
Пример создания таблиц и вторичного индекса, связывающего их:
yt create table //path/to/table --attributes '{dynamic=true; schema=[{name=key; type=int64; sort_order=ascending}; {name=value; type=string}]}'
yt create table //path/to/index_table --attributes '{dynamic=true; schema=[{name=value; type=string; sort_order=ascending}; {name=key; type=int64; sort_order=ascending}; {name="$empty"; type=int64}]}'
yt create secondary_index --attributes '{table_path="//path/to/table"; index_table_path="//path/to/index/table"; kind=full_sync}'
Примечание
Обратите внимание, что имена и типы колонок в таблицах полностью совпадают. Наличие в индексной таблице колонок, которых нет в индексируемой, или несовпадение их типов привлечет к ошибке создания вторичного индекса (за исключением фиктивной колонки $empty
— её присутствие в индексируемой таблице не является необходимым).
Вторичный индекс может быть создан, только если обе таблицы отмонтированы. После создания вторичного индекса и монтирования таблиц последующие записи в индексируемую таблицу автоматически отображаются в индексной (о построении индексной таблицы поверх имеющихся данных см. далее). Также становится возможным выполнять эффективные select-запросы с фильтрацией по вторичному ключу, используя ключевое слово WITH INDEX
. Запрос с индексом выполнится эффективно, если предикат позволяет отсечь ненужные строки из индексной таблицы.
yt select-rows "key, value FROM [//path/to/table] WITH INDEX [//path/to/index/table] where value BETWEEN 0 and 10"
При исполнении запроса с таким синтаксисом система сначала прочитает из индексной таблицы строки, подходящие под предикат where value BETWEEN 0 and 10
, а затем выполнит внутреннее соединение (inner join) со строками индексируемой таблицы по общим колонкам. То есть такой запрос эквивалетнен следующему:
yt select-rows "key, value FROM [//path/to/index/table] AS IndexTable JOIN [//path/to/table] ON (IndexTable.key, IndexTable.value) = (key, value) WHERE IndexTable.value BETWEEN 0 and 10"
Актуальный список подключенных к таблице вторичных индексов указан в атрибуте secondary_indices
таблицы. Вторичный индекс, для которого данная таблица является индексной, указан в атрибуте index_to
.
Существует также возможность исключить часть строчек из индекса, создать индекс поверх колонки со списком, а также создать специальный индекс, ограничивающий индексируемые значения в таблице свойством уникальности.
Частичная индексация
Индексная таблица может содержать отображения не всех строк, а только тех, для которых выполняется некий предикат. Предикат задается атрибутом predicate
вторичного индекса. Он должен представлять собой валидное булевое выражение над схемой индексируемой таблицы (предикат может использовать колонки, которых нет в индексной таблице). Атрибут predicate
совместим с любым из видов вторичных индексов.
Это полезно, если запросы, для которых индекс полезен, касаются только некоторой выделяемой части данных. Например, схема таблицы имеет вид [{name=id; sort_order=ascending; type=uint64}; {name=name; type=string}; {name=age; type=int64}; {name=loyalty; type=boolean}; ...]
, а в запросах вы делаете выборку по возрасту только для строк, в которых loyalty
имеет не-null значение. В таком случае вторичный индекс с предикатом not is_null(loyalty)
и индексной таблицей со схемой [{name=age; type=int64; sort_order=ascending}; {name=id; sort_order=ascending; type=uint64}; {name=$empty, type=int64}]
является оптимальным решением — строки с loyalty=#
не попадут в индексную таблицу.
Индекс над списком
Индекс вида kind=unfolding
разворачивает строки по колонке со списком значений примитивного типа. Такой индекс позволяет делать выборки строк, в которых индексируемый список содержит определённые значения. Например, строке {id=0; child_ids=[1, 4, 7]}
в индексной таблице будут соответствовать строки [{child_ids=1; id=0}; {child_ids=4; id=0}; {child_ids=7; id=0}]
. В схемах таблиц должны присутствовать такие одинаково названные колонки, типы которых соотносятся как list<T>
или optional<list<T>>
в индексируемой таблице и T
в индексной, где T
— примитивный тип. В select-запросах ограничение на индексируемое значение можно задавать используя функцию list_contains
. Пример: id FROM [//path/to/table] WITH INDEX [//path/to/index/table] where list_contains(child_ids, 1)
.
Примечание
При указании в предикате нескольких допустимых значений child_ids, например list_contains(child_ids, 1) or list_contains(child_ids, 4)
, в случае наличия в индексируемом списке обоих значений select запрос может вернуть несколько строк соответствующих одному первичному ключу в индексируемой таблице. Для восстановления уникальности результат необходимо дополнительно группировать по первичному ключу таблицы.
Запрос вида list_contains(child_ids, 1) OR list_contains(child_ids, 2) OR ...
не может содержать большого количества условий из-за ограничения на высоту синтаксического дерева во всех выражениях запроса. Для митигации этого ограничения существует возможность записать предикат в форме child_ids IN (1, 2, ...)
, в данном случае система проинтерпретирует обращение к колонке child_ids как обращение к колонке в индексной таблице.
Например, пусть строки в таблице со схемой [{name=id; type=int64; sort_order=ascending}; {name=book_name; type=string}; {name=genres; type_v3={type_name=list; item=string}}; ...]
описывают книги. Для быстрой выборки по жанрам можно использовать unfolding
вторичный индекс над индексной таблицей со схемой [{name=genres; sort_order=ascending; type=string};{name=id; type=int64; sort_order=ascending}; {name=book_name; type=string}]
— присутствие в схеме индексной таблицы колонки book_name
позволит использовать индексную таблицу без присоединения к индексируемой для запросов, в которых нужны только колонки genres
, book_name
и id
, ещё уменьшая количество чтений и ускоряя выполнение запросов.
Уникальный индекс
Индекс вида kind=unique
поддерживает уникальность вторичного ключа в индексируемой таблице. Ключом в схеме индексной таблицы для вторичного индекса такого вида является вторичный ключ, а неключевыми значениями — первичный ключ. При попытке записи, которая привела бы к существованию в индексируемой таблице двух строк с одинаковыми значениями вторичного ключа, пользователю вернётся ошибка UniqueIndexConflict
. Параллельный коммит двух транзакций с записями строк с разными первичными ключами и одинаковыми вторичными приведет к падению одной из транзакций с ошибкой TransactionLockConflict
.
Шардирование вторичного индекса
Для шардирования данных зачастую используются вычисляемые ключевые колонки, которые для индексной и индексируемой таблицы сделаны независимыми друг от друга — они могут иметь одинаковые или разные имена, типы и выражения, благодаря чему можно независимо контролировать шардирование. Однако обратите внимание, что, также как и в случае обычных таблиц, использование вычисляемой колонки в индексной таблице сделает неэффективными запросы с указанием диапазона по индексируемой колонке.
Копирование и перемещение
Примечание
При копировании или перемещении индексной или индексируемой таблицы или обеих сразу вторичный индекс между ними пропадёт.
Ограничения
-
Поскольку select-запрос с использованием индекса выполняет соединение таблиц, чтение должно производиться под транзакцией для получения глобально консистентного среза (по умолчанию select-запросы выполняются с SyncLastCommittedTimestamp, в этом режиме система в общем случае не может гарантировать консистентность между двумя структурами данных).
-
Запрещена индексация по агрегирущим колонкам, а также запрещена запись в индексируемую таблицу с блокировкой
shared write
. -
Запись в индексируемую таблицу map-reduce операцией не отображается в индексной.
-
Изменение схемы таблицы со вторичным индексом также имеет особенности. При добавлении ключевой колонки, её нужно сначала добавить в индексные таблицы, а только потом в индексируемую. При добавлении неключевой колонки — наоборот (или её можно не добавлять в индексные таблицы).
-
Все колонки вторичного ключа, а также колонки, используемые в предикате, должны иметь одинаковую
lock
группу в индексируемой таблице.
Ограничения, накладываемые на реплицированные таблицы
Консистентные чтения из реплицированных таблиц при использовании вторичных индексов возможны только если индексная и индексируемая таблицы имеют пересекающийся набор кластеров с синхронными репликами. Поэтому перед созданием вторичного индекса необходимо убедиться, что индексируемая и все индексные таблицы объединены в одну коллокацию.
Производительность
Недостаток использования вторичных индексов — замедление записи в индексируемую таблицу. Запись в индексируемую таблицу повлечёт также прозрачное для пользователя чтение из неё и запись во все индексные таблицы, а в случае уникальных индексов — также чтение из индексной таблицы. Эти чтения выполняются в конце транзакции, как следствие — латентность записи увеличивается на латентность чтения.