В Tarantool данные живут в спейсах (spaces), а каждая запись - это кортеж (tuple), упорядоченный набор полей в MsgPack. Сам по себе спейс - это просто куча кортежей в памяти. Чтобы быстро найти кортеж по значению поля, нужен индекс: отдельная структура данных, которая хранит ключи и указатели на кортежи. Без хотя бы одного индекса в спейс нельзя ни вставлять, ни читать - первый созданный индекс становится обязательным первичным ключом.
Ключевое, что надо понять с самого начала: индексы в Tarantool не копируют данные. Кортеж лежит в памяти ровно в одном экземпляре, а индекс хранит только ключ и указатель (ссылку) на этот кортеж. Поэтому пять индексов на спейс - это пять структур указателей, а не пять копий данных. Память тратится на сами структуры (узлы дерева, бакеты хэша), но не на дублирование тел кортежей.
Первичный против вторичных
Первый индекс спейса - первичный ключ (primary key). Он всегда уникален и не допускает NULL в своих частях. Именно по нему кортеж физически идентифицируется внутри движка. Все остальные индексы - вторичные (secondary). Внутри вторичного индекса в качестве указателя на кортеж в memtx хранится прямой указатель на тело кортежа, а вот в движке vinyl вторичный индекс ссылается на запись через значение первичного ключа - поэтому в vinyl выборка по вторичному индексу делает дополнительный поиск по первичному.
Четыре типа индексов и как они устроены внутриПравило проектирования: делайте первичный ключ из первых полей кортежа. Сравнение кортежей и формирование ключа тогда идут быстрее за счёт особенностей раскладки данных. И помните: изменить первичный ключ существующего кортежа нельзя - это удаление и вставка заново.
TREE - универсальный индекс, реализован как B*-дерево (BPS-tree в memtx, LSM-дерево в vinyl). Хранит ключи в отсортированном порядке, поэтому умеет всё: уникальный и неуникальный поиск, диапазоны (GT/GE/LT/LE), упорядоченную выдачу, поиск по префиксу составного ключа, пагинацию через опцию after. Доступен в обоих движках - memtx и vinyl. В 95% случаев это ваш выбор.
HASH - хэш-таблица, только memtx. Даёт O(1) на точечный поиск по полному ключу и чуть компактнее TREE по памяти. Но: обязан быть уникальным, не умеет диапазоны, не умеет частичный поиск по префиксу, не даёт упорядоченной итерации (только ALL и EQ). На практике почти всегда проигрывает TREE и оставлен в основном ради обратной совместимости.
BITSET - индекс по битовым маскам, только memtx, не уникальный, не может быть составным и не может быть первичным. Хранит значения как наборы битов и умеет искать по итераторам BITS_ALL_SET (все указанные биты взведены), BITS_ANY_SET (хотя бы один) и BITS_ALL_NOT_SET. Идеален, когда поле - это вектор флагов/атрибутов и нужно фильтровать по комбинации признаков.
RTREE - пространственный индекс (R-дерево), только memtx, до 20 измерений. Не уникален и не может быть первичным. Индексируемое поле - это массив (array) из 2 или 4 чисел (точка x,y или прямоугольник x1,y1,x2,y2). Поддерживает геометрические итераторы OVERLAPS и NEIGHBOR (поиск ближайших), а также distance-функции euclid и manhattan.

Первичный и вторичные индексы по типам
Составные и многоключевые индексы
Составной (multi-part) индекс собирает ключ из нескольких полей в указанном порядке - до 255 частей в TREE. Главная механика - лексикографическое сравнение слева направо. Поэтому индекс по {country, city} обслуживает запрос только по country или по {country, city}, но НЕ по одному city. Это прямой аналог leftmost-prefix правила из реляционных СУБД.
Многоключевой (multikey) индекс позволяет одному кортежу породить НЕСКОЛЬКО ключей. Достигается через JSON-путь с плейсхолдером [*]: например, поле tags - массив, и путь tags[*] заведёт по одному ключу на каждый элемент массива. Один кортеж окажется в индексе многократно, поэтому multikey не может быть первичным ключом.
Функциональный (functional) индекс строит ключ не из полей напрямую, а через детерминированную persistent-функцию, которая принимает кортеж и возвращает ключ. Если функция помечена is_multikey = true, она может вернуть несколько ключей - и тогда это функциональный multikey. Работает только в memtx.
Команды и короткие примеры
Классический box-трек (1.x/2.x/3.x в консоли):
Код: Выделить всё
box.schema.space.create('users')
box.space.users:format({
{name='id', type='unsigned'},
{name='age', type='unsigned'},
{name='city', type='string'},
{name='tags', type='array'},
})
-- первичный TREE
box.space.users:create_index('primary', {parts={'id'}})
-- вторичный составной TREE
box.space.users:create_index('city_age',
{unique=false, parts={'city','age'}})
-- многоключевой по элементам массива tags
box.space.users:create_index('by_tag',
{unique=false, parts={{field='tags', type='string', path='[*]'}}})
-- частичный поиск по префиксу составного ключа
box.space.users.index.city_age:select({'Moscow'})
Код: Выделить всё
box.space.users:create_index('flags',
{unique=false, type='BITSET', parts={{field=2, type='unsigned'}}})
box.space.users.index.flags:select(0x02, {iterator='BITS_ANY_SET'})
box.schema.space.create('geo')
box.space.geo:create_index('primary', {parts={'id'}})
box.space.geo:create_index('pos',
{unique=false, type='RTREE', parts={{field=2, type='array'}}})
box.space.geo.index.pos:select({55, 37}, {iterator='neighbor', limit=5})
Код: Выделить всё
# config.yaml
groups:
group-001:
replicasets:
rs-001:
memtx:
memory: 1073741824
instances:
instance-001: {}
Частые заблуждения и грабли
- "HASH быстрее TREE" - без замеров это миф; TREE универсальнее и обычно не медленнее на реальных нагрузках.
- Частичный поиск по префиксу работает ТОЛЬКО в TREE. На HASH/BITSET/RTREE его нет - ожидание префиксного select приведёт к ошибке или пустоте.
- Порядок частей в составном индексе критичен: {a,b} не помогает запросам только по b. Меняйте порядок под паттерн запросов.
- RTREE-итератор без кавычек: select(rect, {iterator = LE}) - LE здесь неопределённая переменная (= nil), итератор молча становится EQ. Пишите 'le' строкой.
- NEIGHBOR без limit вытащит весь спейс в порядке удаления - убьёт latency.
- Функция функционального индекса обязана быть persistent и детерминированной; вызов math.random или os.time внутри неё сломает консистентность индекса.
- Multikey и functional - только memtx; в vinyl их нет. И ни тот, ни другой не может быть первичным ключом.
- Индекс не дублирует кортеж, но узлы дерева/бакеты хэша всё равно едят память - десяток вторичных индексов на широкий спейс заметно раздувает потребление RAM.
Поднимите инстанс (tt start или интерактивную консоль), создайте спейс articles с полями id (unsigned), status (unsigned, битовая маска флагов), tags (array of string). Создайте: первичный TREE по id, BITSET по status, multikey TREE по tags[*]. Вставьте 3-4 кортежа с разными наборами тегов и флагов. Проверьте: выборку по одному тегу через by_tag:select(...), и выборку статей с взведённым битом 0x01 через flags:select(0x01, {iterator='BITS_ANY_SET'}). Убедитесь, что один кортеж с двумя тегами находится по обоим тегам.
Контрольные вопросы
- Почему вторичный поиск в vinyl дороже, чем в memtx, при прочих равных?
- У вас составной индекс {region, created_at}. Какие из запросов он ускорит: по region; по created_at; по {region, created_at}? Почему?
- Какой тип индекса выбрать для поиска объектов внутри прямоугольной области карты и почему именно он?
- Почему многоключевой и функциональный индексы не могут быть первичным ключом?