Спейсы и кортежи: форматы, типы данных

Рейтинг: 64% · 16 голосов
Исчерпывающий курс по 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 вся пользовательская модель данных держится на трёх сущностях: спейс (space) -> кортеж (tuple) -> поля (fields) с их системой типов. Если в реляционной СУБД вы думаете "таблица -> строка -> колонка", то здесь аналогия почти прямая, но с важными отличиями: у полей может не быть имён, поле может быть составной структурой (map или array), а сам кортеж физически хранится не как набор колонок, а как один компактный MsgPack-массив.
Спейс - контейнер. Кортеж - запись внутри него (MsgPack-массив). Поле - отдельное значение кортежа, адресуемое по номеру (с 1 в Lua) или по имени, если задан формат.
Эта механика одинакова и в классическом треке (box.cfg + box.schema), и в декларативном Tarantool 3.x: YAML-конфиг описывает топологию и инстансы, но сами спейсы и форматы создаются тем же box.schema API, обычно в скрипте приложения. Поэтому понимание этого уровня переносится между версиями без изменений.

Как это устроено внутри

Спейс - это первичный контейнер хранения. У него есть уникальное имя, числовой id (обычно назначается автоматически как "последний id + 1") и движок: memtx (в памяти, по умолчанию) или vinyl (на диске для больших объёмов). Чтобы со спейсом можно было работать, ему нужен хотя бы первичный индекс. Сам список всех спейсов лежит в системном спейсе box.space._space - это не магия, а обычная таблица метаданных, которую можно прочитать select-ом.

Кортеж хранится как MsgPack-массив - бинарный формат с переменной длиной значений. Это ключевая деталь: маленькое число занимает 1 байт, самое большое целое - 9 байт, строка - длину плюс заголовок. Никакого выравнивания по колонкам нет, поэтому "пропущенные" поля в хвосте просто отсутствуют в массиве и не занимают места. Когда вы получаете кортеж в Lua, это не Lua-таблица, а лёгкая cdata-ссылка на MsgPack в памяти базы - конвертация в таблицу не происходит, пока вы явно не обратитесь к полю. Отсюда скорость доступа.

Поле адресуется номером (в Lua с 1, в C/PHP с 0) или именем. Имя появляется только если задан формат спейса. Формат - это необязательный, но крайне желательный список описаний полей: имя, тип, признак nullable, значение по умолчанию. Формат не меняет физическое хранение (это всё тот же MsgPack-массив), он добавляет слой валидации и именования поверх.

Два набора типов. Разработчик всегда живёт между типами языка (Lua) и типами хранилища (MsgPack). Это источник большинства недоразумений. Lua-число - всегда double-точности с плавающей точкой, а в MsgPack оно может лечь как integer или как float64 - Tarantool решает по значению: если есть десятичная точка или число больше 1e14, оно идёт как float, иначе как integer.

Изображение
Спейс -> кортеж -> поля и система типов

Система типов полей

Tarantool различает скалярные и составные типы. Скалярные: unsigned, integer, number, double, decimal, string, varbinary, boolean, uuid, datetime, interval. Составные: array, map. Плюс два "обёрточных": scalar (любой скаляр, но не array/map/tuple) и any (вообще всё).

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

тип        диапазон / суть                      хранение MsgPack
---------  -----------------------------------  ----------------------
unsigned   0 .. 18446744073709551615            int (positive)
integer    -2^63 .. 2^64-1                       int
number     integer ИЛИ float                     int или float64
double     ровно float64, всегда 9 байт          MP_DOUBLE
decimal    точное, до 38 значащих цифр           ext (extension)
string     байтовая строка переменной длины      str
varbinary  бинарные данные                       bin
boolean    true / false                          bool
uuid       128-битный идентификатор              ext
datetime   момент времени (since 2.10)           ext
interval   период времени (since 2.10)           ext
array      [1,2,3]                               array
map        {a=5, b=6}                             map (строковые ключи)
Ключевые скаляры decimal, uuid, datetime, interval хранятся как MsgPack ext (расширение) и не имеют родного типа в Lua - с ними работают через модули:

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

local decimal  = require('decimal')
local uuid     = require('uuid')
local datetime = require('datetime')

box.cfg{}
local s = box.schema.space.create('events', {if_not_exists = true})
s:format({
    {name = 'id',     type = 'uuid'},
    {name = 'amount', type = 'decimal'},
    {name = 'ts',     type = 'datetime'},
    {name = 'note',   type = 'string', is_nullable = true},
})
s:create_index('pk', {parts = {{field = 'id', type = 'uuid'}}})

s:insert({uuid.new(), decimal.new('19.99'), datetime.new{year=2026}, box.NULL})
Обратите внимание: для индекса по UUID-полю тип в parts тоже указывается явно (type = 'uuid'). Имена полей из формата (id, amount, ts) можно использовать в parts, в get, в update и в обращении tuple.note.

Формат и nullable. По умолчанию is_nullable = false, и положить box.NULL в такое поле нельзя. Поле, объявленное nullable, может отсутствовать в хвосте кортежа - тогда при чтении оно отдаётся как null. Это и есть приём для "узких" кортежей: необязательные поля держат в конце.

Частые заблуждения и грабли
  • integer против unsigned. Если индекс или поле объявлены unsigned, отрицательное число туда не вставить - получите ошибку. unsigned - это подмножество integer, выбирайте integer, если значения могут быть отрицательными.
  • number против double. number в Lua коварен: 1 ляжет как integer, а 1.0 как integer тоже (нет дробной части), но 1.5 как float. Если нужен строго float64 (например, для совместимости с SQL DOUBLE), используйте тип double и ffi.cast('double', x) при вставке и поиске, иначе сравнение по индексу может не найти значение.
  • Кортеж - это не Lua-таблица. Результат select - это cdata-ссылка. Прямое сравнение двух кортежей через == сравнивает ссылки, а не содержимое.
  • nil в таблицах. Lua nil нельзя положить в Lua-таблицу (он обрывает массив). Для пропуска поля используют box.NULL: nil == box.NULL даёт true, но box.NULL безопасно лежит в таблице.
  • Обновление поля после nullable-дыр. Если nullable-поля в хвосте физически отсутствуют, update по номеру поля, который "за дырой", может вернуть ошибку "Field N was not found in the tuple". Заполняйте такие поля явно или работайте по именам.
  • Строки сравниваются побайтово. Без коллации '2345' меньше '500' (сравнение по первому байту), а unicode-порядок включается только явной коллацией в индексе.
  • field_count. Если задать space_opts.field_count = 5, вставка кортежа не ровно из 5 полей запрещена. По умолчанию 0 - длина не фиксирована.
Мини-лаба

Создайте спейс catalog с форматом из четырёх полей: id (unsigned), sku (string), price (decimal), tags (array, nullable). Сделайте первичный индекс по id. Вставьте две записи (вторую - без tags, через box.NULL или просто опустив поле). Затем сделайте select и убедитесь, что у второй записи tags вернулся как null.

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

local decimal = require('decimal')
box.cfg{}
local c = box.schema.space.create('catalog', {if_not_exists = true})
c:format({
    {name='id',    type='unsigned'},
    {name='sku',   type='string'},
    {name='price', type='decimal'},
    {name='tags',  type='array', is_nullable=true},
})
c:create_index('pk', {parts={{field='id', type='unsigned'}}})
c:insert({1, 'A-100', decimal.new('9.90'), {'new','sale'}})
c:insert({2, 'B-200', decimal.new('14.50')})  -- tags опущен
c:select()
-- проверьте: попытка c:insert({-1, ...}) упадёт (unsigned),
-- а c.index.pk:get({2}).tags вернёт nil/null
Контрольные вопросы
  • В каком физическом формате хранится кортеж и почему пропущенные хвостовые поля не занимают места?
  • Чем отличаются типы number и double, и когда обязателен ffi.cast('double', ...)?
  • Почему decimal, uuid и datetime требуют require отдельного модуля, и как это связано с MsgPack ext?
  • Что произойдёт при вставке отрицательного значения в поле типа unsigned и при попытке положить box.NULL в поле с is_nullable=false?
👍3 ❤️3 🔥3 😄 🤔
Ответить
← Предыдущая глава
Интерактив: консоль, admin-консоль, первые команды
Следующая глава →
Типы индексов и их применимость

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

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

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

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

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