DML и выборки: insert/update/upsert, итераторы

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

DML и выборки: insert/update/upsert, итераторы

Сообщение 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 вся работа с данными в режиме box делится на две группы операций. Пять операций изменяют данные (DML): insert, replace, update, upsert, delete. Одна операция читает данные: select. Все они живут в подмодуле box.space и вызываются методом через двоеточие на объекте спейса. Под капотом каждое изменение проходит через транзакционный движок, движок хранения (memtx или vinyl) и пишется в WAL, а чтение обслуживается итератором по индексу. В этом уроке мы разбираем механику каждой операции и шесть типов итераторов выборки: EQ, REQ, GT, GE, LT, LE.

Важно держать в голове базовый инвариант: чтобы вставлять или читать тройки (tuple), у спейса обязан быть хотя бы один индекс. Первый индекс - всегда уникальный первичный ключ. Любое DML-изменение автоматически обновляет ВСЕ индексы спейса, а не только первичный.

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

Кто что принимает на вход. Это ключ к пониманию различий между операциями. Запомните три категории аргументов:
  • insert и replace принимают целую тройку (первичный ключ - часть тройки).
  • delete и update принимают полный ключ ЛЮБОГО уникального индекса (первичного или вторичного); update дополнительно принимает список операций изменения.
  • upsert принимает целую тройку И операции изменения сразу.
  • select принимает любой ключ: первичный или вторичный, уникальный или нет, полный или частичный (префикс).
insert vs replace. Обе кладут тройку. Разница - в проверке существования. insert делает чтение по первичному ключу: если такая тройка уже есть, бросает ошибку ER_TUPLE_FOUND. replace проверки не делает в смысле отказа - если тройка с таким первичным ключом есть, она молча перезаписывается целиком, если нет - создаётся новая. То есть replace это идемпотентная по первичному ключу запись.

update. Сначала находит тройку по уникальному ключу, затем применяет список операций вида {оператор, номер_поля, значение}. Операторы: '=' присвоить, '+' '-' арифметика, '&' '|' '#' '^' битовые, ':' splice (вырезка/вставка в строке), '!' вставить новое поле, '#' удалить поле. Несколько операций применяются по порядку в одной атомарной транзакции. В memtx update всегда делает полную копию тройки (тройки иммутабельны), поэтому стоимость растёт с числом индексов: N индексов - N обновлений индексных структур.

upsert - самая хитрая операция. Семантически это "обнови если есть, вставь если нет". Но реализация принципиально отличается от update:
  • upsert НЕ читает тройку и НЕ делает проверок перед возвратом - ради пропускной способности. Поэтому он не возвращает данные (в отличие от update).
  • Выполнение откладывается: операции записываются, а реально применяются (схлопываются, squash) позже. В vinyl upsert лежит на диске отдельной записью и применяется при чтении ключа или при компакции.
  • upsert никогда не должен падать с ошибкой во время вызова. Если приведёт к нарушению (например ломает тип поля) - изменение тихо отбрасывается на этапе применения, а не на этапе вызова.
  • upsert нельзя использовать на спейсе с уникальным ВТОРИЧНЫМ индексом.
delete. Удаляет одну тройку по полному уникальному ключу и возвращает удалённую тройку.

select и итераторы. select это обёртка над итератором индекса. Вы задаёте ключ (или его префикс) и тип итератора. Итератор определяет, с какой стороны от ключа и в каком направлении идти. Для TREE-индекса доступны все шесть типов; направление и сортировка результата зависят от типа.

Изображение
DML-операции и итераторы выборки по индексу

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

Подготовим спейс и индекс (двойной трек: в 3.x схему обычно объявляют декларативно в YAML, но box.schema API работает и там, и в 1.x/2.x):

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

box.schema.space.create('bands')
box.space.bands:format({
    {name = 'id',   type = 'unsigned'},
    {name = 'band', type = 'string'},
    {name = 'year', type = 'unsigned'},
})
box.space.bands:create_index('primary', {parts = {'id'}})
box.space.bands:create_index('year',    {parts = {'year'}, unique = false})
DML по порядку:

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

box.space.bands:insert{1, 'Roxette', 1986}      -- упадёт, если id=1 уже есть
box.space.bands:replace{1, 'Roxette', 1987}     -- перезапишет целиком
box.space.bands:update({1}, {{'=', 3, 1986}})   -- year := 1986
box.space.bands:update({1}, {{'+', 3, 1}})      -- year += 1
box.space.bands:upsert({2, 'Scorpions', 1965}, {{'=', 3, 1966}}) -- нет -> insert
box.space.bands:upsert({2, 'Scorpions', 1965}, {{'=', 3, 1966}}) -- есть -> update
box.space.bands:delete{2}
Выборки с разными итераторами по неуникальному индексу year:

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

local i = box.index
box.space.bands.index.year:select({1980}, {iterator = i.EQ})  -- ровно 1980
box.space.bands.index.year:select({1980}, {iterator = i.GE})  -- >= 1980, по возр.
box.space.bands.index.year:select({1980}, {iterator = i.GT})  -- >  1980, по возр.
box.space.bands.index.year:select({1980}, {iterator = i.LE})  -- <= 1980, по убыв.
box.space.bands.index.year:select({1980}, {iterator = i.LT})  -- <  1980, по убыв.
box.space.bands.index.year:select({1980}, {iterator = i.REQ}) -- = 1980, в обратном порядке
Правило сортировки: результат идёт по УБЫВАНИЮ ключа, если итератор LT, LE или REQ. Во всех остальных случаях (EQ, GE, GT, ALL) - по ВОЗРАСТАНИЮ.
Эквивалент на SQL (второй трек):

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

INSERT INTO bands VALUES (1, 'Roxette', 1986);
UPDATE bands SET year = year + 1 WHERE id = 1;
DELETE FROM bands WHERE id = 1;
SELECT * FROM bands WHERE year >= 1980 ORDER BY year;
Сводка соответствия итераторов и смысла:

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

Итератор   Смысл                Направление обхода
EQ         равно ключу          возрастание
REQ        равно ключу          убывание
GT         строго больше        возрастание
GE         больше или равно     возрастание
LT         строго меньше        убывание
LE         меньше или равно     убывание
ALL        все тройки           возрастание
Частые заблуждения и грабли
  • "upsert вернёт обновлённую тройку" - нет. upsert ничего не возвращает, потому что выполнение отложено. Нужен результат - читайте select отдельно.
  • Путают аргументы upsert и update. У update первый аргумент - КЛЮЧ, у upsert - целая ТРОЙКА. Однополевой ключ обязательно в фигурных скобках: {2}.
  • Имя итератора без кавычек как переменная: select(key, {iterator = LE}). LE здесь undefined, становится nil, и тихо выполняется EQ. Пишите 'LE' строкой или box.index.LE.
  • HASH-индекс поддерживает только ALL и EQ. GT/GE/LT/LE на нём не работают (в 2.11 GT для HASH объявлен deprecated). Сравнения имеют смысл только для TREE.
  • select() без аргументов и без limit на большом спейсе вытащит всё в память и подвесит инстанс. Для постраничного обхода используйте опции after и fetch_pos, а не offset (offset линейно деградирует).
  • Частичный ключ (префикс) работает только в TREE-индексе. По многочастному индексу можно искать по первым полям.
  • REPLACE дешевле, чем UPDATE для полной перезаписи, но REPLACE сотрёт поля, которые вы не указали; UPDATE правит точечно.
Мини-лаба

Создайте спейс bands со схемой выше и неуникальным индексом year. Вставьте 5 групп с годами 1965, 1986, 1987, 2000, 2010. Затем: (1) одним upsert обновите год группы id=1 на +1, а отсутствующую id=99 вставьте; (2) сделайте три select по индексу year с итераторами GE, LT и REQ для ключа {1987} и объясните словами, почему результат GE отсортирован по возрастанию, а LT - по убыванию, и сколько троек вернул каждый.

Контрольные вопросы
  • Чем insert отличается от replace при попытке записать тройку с уже существующим первичным ключом?
  • Почему upsert не возвращает изменённую тройку и в каком случае его изменение может быть тихо отброшено?
  • Какие типы итераторов дают результат по убыванию ключа, а какие - по возрастанию?
  • Какой ключ принимают delete и update, и чем он отличается от того, что принимает insert?
👍2 ❤️4 🔥3 😄 🤔2
Ответить
← Предыдущая глава
DDL: схема, создание спейсов и индексов, миграции
Следующая глава →
Персистентность: WAL, снапшоты, recovery

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

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

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

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

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