DDL: схема, создание спейсов и индексов, миграции

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

DDL: схема, создание спейсов и индексов, миграции

Сообщение 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: шардированный отказоустойчивый кластер
Зачем нужен DDL и что вообще происходит внутри

DDL (Data Definition Language) в Tarantool - это не отдельный язык, а набор операций над схемой: создание спейса, описание format, создание и изменение индексов, удаление объектов. Любое такое изменение в терминологии Tarantool называется миграцией - даже первое создание спейса. Понять урок - значит понять, что DDL это не "файлы со схемой", а транзакционные записи в системные спейсы, которые попадают в WAL и реплицируются, как обычные данные.

Ключевая идея: в Tarantool схема опциональна. Можно создать спейс без format вообще, и тогда в кортежах будут лежать произвольные данные (кроме индексируемых полей - они обязаны иметь одинаковый тип). Схема (format) - это валидатор поверх MsgPack, а не способ хранения. Хранится кортеж всегда как MsgPack-массив; format лишь даёт полям имена и проверяет типы перед каждым insert/update.

Механика: где живёт схема

Вся метаинформация хранится в служебных спейсах с отрицательными/маленькими id, доступных через box.space:
  • _space - список всех спейсов, их id, engine, format
  • _index - описания всех индексов (по какому спейсу, parts, тип, уникальность)
  • _sequence - генераторы значений
  • _func, _user, _priv - функции, пользователи, права
Когда вы вызываете box.schema.create_space или create_index, Tarantool не делает магии: он вставляет строку в _space или _index. Эта вставка - обычная транзакция, она пишется в WAL и реплицируется на реплики. Поэтому DDL автоматически распространяется по кластеру тем же механизмом, что и DML. Отсюда же два важных следствия: DDL подчиняется транзакционной модели (есть ограничения на DDL внутри box.begin/commit), и при восстановлении из снапшота схема "проигрывается" из этих же системных спейсов.
Запомните формулу: DDL = транзакционная запись в _space/_index -> WAL -> репликация. Никаких отдельных файлов схемы у движка нет.
Изображение
DDL-поток: схема, format, индексы и миграции

Два трека: классический box.cfg и декларативный 3.x

Классический трек (1.x/2.x/3.x). Схему описывают императивным Lua-кодом, обычно в init.lua: сначала box.cfg{}, затем create_space, format, create_index. Этот способ работает во всех версиях и остаётся основным языком самих миграций.

Декларативный трек 3.x. В Tarantool 3 конфигурация инстанса задаётся YAML (config.yaml / etcd), но - важная тонкость - сама бизнес-схема спейсов в стандартный YAML-конфиг не входит. Конфиг 3.x управляет топологией, ролями, репликацией. Схему спейсов в 3.x применяют либо тем же Lua (в app-роли через скрипт старта), либо декларативно через официальный модуль ddl, который принимает YAML-описание спейсов и сам раскладывает его в create_space/format/create_index. То есть "декларативность 3.x" для схемы - это модуль ddl, а не box.cfg.

Ключевые команды и короткие примеры

Создание спейса, format и индекса в классическом стиле:

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

box.cfg{}

local users = box.schema.create_space('users', { if_not_exists = true })

users:format({
    { name = 'id',    type = 'unsigned' },
    { name = 'email', type = 'string'   },
    { name = 'age',   type = 'unsigned', is_nullable = true },
})

-- первый индекс = первичный ключ, обязан быть уникальным
users:create_index('primary', {
    parts = { { field = 'id', type = 'unsigned' } },
})

-- вторичный индекс по имени поля (имя доступно благодаря format)
users:create_index('by_email', {
    parts = { 'email' },
    unique = true,
})
Заметьте: чтобы ссылаться на поле по имени в parts, format должен быть задан заранее. Без format пришлось бы указывать номер поля: parts = {{ field = 1, type = 'unsigned' }}.

Декларативное описание тем же модулем ddl (YAML):

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

spaces:
  users:
    engine: memtx
    is_local: false
    temporary: false
    format:
      - { name: id,    type: unsigned, is_nullable: false }
      - { name: email, type: string,   is_nullable: false }
    indexes:
      - name: primary
        unique: true
        type: TREE
        parts: [ { path: id, type: unsigned, is_nullable: false } ]
      - name: by_email
        unique: true
        type: TREE
        parts: [ { path: email, type: string, is_nullable: false } ]
И применение этого YAML:

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

local yaml = require('yaml')
local ddl  = require('ddl')
box.cfg{}

local schema = yaml.decode(io.open('ddl.yml'):read('*all'))
local ok, err = ddl.check_schema(schema)   -- сухая проверка
assert(ok, err)
ok, err = ddl.set_schema(schema)           -- применение
assert(ok, err)
Про типы и индексы. Типы полей в индексе строже, чем в MsgPack: unsigned хранит только неотрицательные целые, integer - любые целые, хотя в MsgPack это один тип. Тип индекса по умолчанию - TREE; он есть и в memtx, и в vinyl, поддерживает уникальные и неуникальные ключи, частичный поиск, диапазоны и упорядоченный обход. memtx дополнительно умеет HASH, RTREE, BITSET. Практический вывод: в 99% случаев нужен TREE. HASH сегодня держат ради совместимости - он требует уникальности, не итерируется по диапазону и почти всегда проигрывает TREE.

Миграции: простые и сложные

Tarantool делит миграции на два класса.

Простые - не трогают существующие данные:
  • Создание нового индекса - можно в любой момент.
  • Добавление поля В КОНЕЦ спейса через расширение format. Новое поле обязано быть is_nullable = true, иначе старые кортежи не пройдут валидацию.

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

local s   = box.space.users
local fmt = s:format()
table.insert(fmt, { name = 'created_at', type = 'unsigned', is_nullable = true })
s:format(fmt)   -- старые кортежи остаются валидными: новое поле = nil
Сложные - требуют перелопатить данные: сменить тип поля, удалить колонку, разнести данные по новым спейсам. Здесь либо мигрируют данные вручную (итерация + транзакции; для больших спейсов удобен модуль moonwalker), либо в Enterprise используют space:upgrade() - неблокирующую трансформацию кортежей фоновой функцией, которая применяется и в фоне, и на лету при чтении, поэтому спейс "обновляется мгновенно". Функция upgrade должна быть зарегистрирована через box.schema.func.create, быть детерминированной, не менять первичный ключ и быть идемпотентной: f(f(t)) = f(t).

Сами миграции - это просто Lua-скрипты, которые запускают на живом инстансе: через hot-reload в коде приложения, либо tt connect ... -f 0001-migration.lua. В Enterprise есть централизованный механизм через etcd (tt и TCM), раскатывающий миграции по всему кластеру с историей.

Частые заблуждения и грабли
  • "Схема обязательна". Нет, format опционален; но индексируемые поля всё равно типизированы.
  • "DDL мгновенный и бесплатный". create_index на большом спейсе строит индекс синхронно и может надолго занять инстанс.
  • "Поле можно добавить куда угодно". Простая миграция - только добавление В КОНЕЦ и только nullable. Вставка в середину ломает позиции полей.
  • "unsigned и integer - одно и то же". Для индекса нет: unsigned отвергает отрицательные значения.
  • "DDL-схему модуля ddl можно потом менять set_schema". Нет: после применения ddl-схему менять запрещено, для изменений - механизм миграций.
  • "box.schema.upgrade() = space:upgrade()". Разные вещи: первое обновляет СИСТЕМНЫЕ спейсы под новую версию Tarantool, второе - формат пользовательского спейса.
  • "Никакого первичного индекса не нужно". До создания первого (первичного, уникального) индекса в спейс нельзя ни вставлять, ни читать.
  • Грабли nullable-индекса: exclude_null = true автоматически ставит is_nullable = true и заставляет индекс пропускать кортежи с null в этом парте.
Мини-лаба

Запустите tarantool, выполните box.cfg{}. Создайте спейс accounts с format из полей id (unsigned) и login (string). Добавьте первичный TREE-индекс по id и вторичный уникальный индекс по login. Вставьте 2-3 кортежа. Затем выполните простую миграцию: добавьте в конец nullable-поле balance (unsigned) и убедитесь, что старые кортежи читаются (balance = nil). В конце посмотрите box.space._index:select{box.space.accounts.id} и найдите там оба своих индекса - это докажет, что схема живёт в системном спейсе.

Контрольные вопросы
  • В какие системные спейсы Tarantool записывает создание спейса и индекса, и почему благодаря этому DDL реплицируется автоматически?
  • Почему добавление поля в конец спейса считается простой миграцией, а смена типа существующего поля - сложной? Какое требование к новому полю обязательно?
  • Чем декларативный трек схемы в 3.x (модуль ddl) отличается от YAML-конфига инстанса box.cfg/config.yaml?
  • В чём разница между box.schema.upgrade() и space:upgrade(), и какие требования предъявляются к функции апгрейда во втором случае?
👍4 ❤️1 🔥1 😄 🤔2
Ответить
← Предыдущая глава
Движки хранения: memtx vs vinyl
Следующая глава →
DML и выборки: insert/update/upsert, итераторы

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

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

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

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

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