Типы индексов и их применимость

Рейтинг: 74.1% · 11 голосов
Исчерпывающий курс по Tarantool 3.x: модель данных, движки memtx и vinyl, Lua и файберы, транзакции и MVCC, SQL, конфигурация (box.cfg и декларативная 3.x), репликация и Raft, шардирование vshard, эксплуатация, безопасность. 47 уроков со схемами.
Ответить
Аватара пользователя
denis_tnt
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Типы индексов и их применимость

Сообщение denis_tnt »

Оглавление курса (47)
  1. Что такое Tarantool: in-memory СУБД и сервер приложений
  2. Архитектура изнутри: процесс, потоки, event-loop
  3. Установка и первый запуск: tt CLI, пакеты, Docker
  4. Интерактив: консоль, admin-консоль, первые команды
  5. Спейсы и кортежи: форматы, типы данных
  6. Типы индексов и их применимость (вы здесь)
  7. Движки хранения: memtx vs vinyl
  8. DDL: схема, создание спейсов и индексов, миграции
  9. DML и выборки: insert/update/upsert, итераторы
  10. Персистентность: WAL, снапшоты, recovery
  11. Внутренности memtx: аллокаторы slab/arena, память
  12. Внутренности vinyl: LSM, компакция, тюнинг
  13. Lua и LuaJIT в Tarantool: box, модули, rocks
  14. Файберы: кооперативная многозадачность, каналы
  15. Транзакции: ACID, изоляция, MVCC
  16. Хранимые процедуры, модули, организация приложения
  17. net.box: удалённые вызовы, async
  18. Пулы соединений, балансировка, реконнект
  19. Ошибки и диагностика: box.error, pcall
  20. Типы и сериализация: MsgPack, decimal, datetime, uuid
  21. SQL в Tarantool: возможности и связь с box
  22. SQL: таблицы, JOIN, подзапросы, представления
  23. SQL: подготовленные выражения, транзакции, Lua-интероп
  24. Классическая конфигурация box.cfg (legacy 1.x/2.x)
  25. Декларативная конфигурация 3.x: config.yaml, иерархия
  26. Роли и приложения в 3.x
  27. Централизованная конфигурация: etcd / config storage
  28. tt CLI глубоко: разработка, сборка, запуск
  29. Cartridge (официальный legacy) и миграция на 3.x
  30. Репликация: replicaset, топологии
  31. Механика репликации: WAL-стриминг, vclock
  32. Синхронная репликация и выборы лидера (Raft)
  33. Жизненный цикл узла: bootstrap, join, rejoin
  34. vshard: router/storage, виртуальные бакеты
  35. Решардинг и rebalancing бакетов
  36. Запросы поверх шардов: map-reduce, crud
  37. Мониторинг: метрики, Prometheus, Grafana
  38. Логирование и аудит
  39. Бэкапы и восстановление
  40. Безопасность: аутентификация, RBAC, TLS
  41. Производительность: профилирование, тюнинг
  42. Обновления: схема, rolling upgrade
  43. Деплой в продакшен: Docker, топология (официальные паттерны)
  44. Администрирование через официальный TCM (Tarantool Cluster Manager)
  45. Коннекторы: Python, Go, Java
  46. Ключевые модули (rocks): crud, metrics, queue, expirationd
  47. Capstone: шардированный отказоустойчивый кластер
Зачем вообще нужны индексы

В 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'})
BITSET и RTREE:

Код: Выделить всё

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})
Декларативный трек 3.x (фрагмент YAML-конфигурации, схема задаётся приложением/ролью; саму DDL обычно описывают в Lua-роли при старте, а конфиг задаёт только движок и память):

Код: Выделить всё

# config.yaml
groups:
  group-001:
    replicasets:
      rs-001:
        memtx:
          memory: 1073741824
        instances:
          instance-001: {}
В 3.x создание спейсов/индексов выполняют из кода роли (тот же box.schema), либо декларативно через модуль tarantool/ddl. Сам синтаксис create_index неизменен - меняется только способ запуска инстанса.

Частые заблуждения и грабли
  • "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}? Почему?
  • Какой тип индекса выбрать для поиска объектов внутри прямоугольной области карты и почему именно он?
  • Почему многоключевой и функциональный индексы не могут быть первичным ключом?
👍3 ❤️5 🔥1 😄 🤔3
Ответить
← Предыдущая глава
Спейсы и кортежи: форматы, типы данных
Следующая глава →
Движки хранения: memtx vs vinyl

Все главы курса «Tarantool: in-memory СУБД и сервер приложений с нуля до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Tarantool: СУБД и сервер приложений»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость