В 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 принимает любой ключ: первичный или вторичный, уникальный или нет, полный или частичный (префикс).
update. Сначала находит тройку по уникальному ключу, затем применяет список операций вида {оператор, номер_поля, значение}. Операторы: '=' присвоить, '+' '-' арифметика, '&' '|' '#' '^' битовые, ':' splice (вырезка/вставка в строке), '!' вставить новое поле, '#' удалить поле. Несколько операций применяются по порядку в одной атомарной транзакции. В memtx update всегда делает полную копию тройки (тройки иммутабельны), поэтому стоимость растёт с числом индексов: N индексов - N обновлений индексных структур.
upsert - самая хитрая операция. Семантически это "обнови если есть, вставь если нет". Но реализация принципиально отличается от update:
- upsert НЕ читает тройку и НЕ делает проверок перед возвратом - ради пропускной способности. Поэтому он не возвращает данные (в отличие от update).
- Выполнение откладывается: операции записываются, а реально применяются (схлопываются, squash) позже. В vinyl upsert лежит на диске отдельной записью и применяется при чтении ключа или при компакции.
- upsert никогда не должен падать с ошибкой во время вызова. Если приведёт к нарушению (например ломает тип поля) - изменение тихо отбрасывается на этапе применения, а не на этапе вызова.
- upsert нельзя использовать на спейсе с уникальным ВТОРИЧНЫМ индексом.
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})
Код: Выделить всё
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}
Код: Выделить всё
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, в обратном порядке
Эквивалент на SQL (второй трек):Правило сортировки: результат идёт по УБЫВАНИЮ ключа, если итератор LT, LE или REQ. Во всех остальных случаях (EQ, GE, GT, ALL) - по ВОЗРАСТАНИЮ.
Код: Выделить всё
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?