Top.Mail.Ru
Модель данных | Tarantool
Tarantool
Узнайте содержание релиза 2.8
Модель данных

Модель данных

Модель данных

В этом разделе описывается то, как в Tarantool организовано хранение данных и какие операции с данными он поддерживает.

Если вы пробовали создать базу данных, как предлагается в упражнениях в «Руководстве для начинающих», то ваша тестовая база данных выглядит следующим образом:

../../../_images/data_model.png

Tarantool обрабатывает данные в виде кортежей.

кортеж

Кортеж — это группа значений данных в памяти Tarantool. По сути, это «запись в базе данных» или «строка». Значения данных в кортеже называются полями.

Когда Tarantool выводит значение кортежа в консоль, по умолчанию используется формат YAML, например: [3, 'Ace of Base', 1993].

В Tarantool кортежи хранятся в виде массивов в формате MsgPack.

поле

Поля — это отдельные значения данных, которые содержатся в кортеже. Они играют ту же роль, что и «столбцы» или «поля записи» в реляционных базах данных, но несколько усовершенствованы:

  • поля могут представлять собой композитные структуры, такие как таблицы типа массива или ассоциативного массива,
  • полям не нужны имена.

В кортеже может быть любое количество полей, и это могут быть поля разных типов.

Номер поля служит его идентификатором. В Lua и некоторых других языках нумерация начинается с 1, в других — с 0 (например, в PHP или C/C++). Таким образом, в некоторых контекстах у первого поля кортежа будет индекс 1 или 0.

Tarantool хранит кортежи в контейнерах, называемых спейсами (space). Спейс в примере выше называется tester.

спейс

В Tarantool спейс — это первичный контейнер, хранящий данные. Он похож на таблицы в реляционных базах данных. Спейсы содержат кортежи — так в Tarantool называются записи в базе данных. Количество кортежей в спейсе не ограничено.

Для хранения данных с помощью Tarantool требуется хотя бы один спейс. У каждого спейса есть следующие атрибуты:

  • уникальное имя, указанное пользователем;
  • уникальный числовой идентификатор, обычно Tarantool назначает его автоматически, но пользователь может его указать сам, если посчитает нужным;
  • движок: memtx (по умолчанию) — движок «in-memory», быстрый, но ограниченный в размере, или vinyl — дисковый движок для огромных наборов данных.

Для работы спейсу нужен первичный индекс. Также он может использовать вторичные индексы.

Всю информацию про индексы можно найти на странице Индексы.

Индекс — это совокупность значений ключей и указателей.

Как и для спейсов, для индексов следует указать имена, а Tarantool определит уникальный числовой идентификатор («ID индекса»).

У индекса всегда есть определенный тип. Тип индекса по умолчанию — TREE. TREE-индексы поддерживаются обоими движками Tarantool, могут индексировать уникальные и неуникальные значения, поддерживают поиск по компонентам ключа, сравнение ключей и упорядочивание результатов. Движок memtx поддерживает и другие типы индексов: HASH, RTREE и BITSET.

Индекс может быть многокомпонентным, то есть можно объявить, что ключ индекса состоит из двух или более полей в кортеже в любом порядке. Например, для обычного TREE-индекса максимальное количество частей равно 255.

Индекс может быть уникальным, то есть можно объявить, что недопустимо дважды задавать одно значение ключа.

Первый индекс, определенный для спейса, называется первичный индекс. Он должен быть уникальным. Все остальные индексы называются вторичными индексами, они могут строиться по неуникальным значениям.

Tarantool представляет собой систему управления базой данных и сервер приложений одновременно. Поэтому разработчику часто приходится работать с двумя системами типов данных: типы языка программирования (например, Lua) и типы формата хранения данных Tarantool (MsgPack).

Скалярный / составной MsgPack-тип   Lua-тип Пример значения
скалярный nil «nil» nil
скалярный boolean «boolean» (логическое значение) true
скалярный string «string» 'A B C'
скалярный integer «number» 12345
скалярный float 64 (double) «number» 1.2345
скалярный float 64 (double) «cdata» 1.2345
скалярный binary «cdata» [!!binary 3t7e]
скалярный ext (для decimal в Tarantool) «cdata» 1.2
скалярный ext (для uuid в Tarantool) «cdata» 12a34b5c-de67-8f90-
123g-h4567ab8901
составной map (ассоциативный массив) «table» (таблица со строковыми ключами) {'a': 5, 'b': 6}
составной array (массив) «table» (таблица с целочисленными ключами) [1, 2, 3, 4, 5]
составной array (массив) tuple («cdata») (кортеж) [12345, 'A B C']

Примечание

Данные в формате MsgPack имеют переменный размер. Так, например, для наименьшего значения number потребуется только один байт, a для наибольшего потребуется девять байтов.

nil. В языке Lua у типа nil есть только одно значение, также называемое nil. Tarantool отображает его как null при использовании формата по умолчанию YAML. Значение nil можно сравнивать со значениями любых типов данных с помощью операторов == (равно) или ~= (не равно), но никакие другие операции сравнения к nil не применимы. Значение nil также нельзя использовать в Lua-таблицах — в качестве обходного пути вместо nil в таком случае можно указать box.NULL, поскольку условие nil == box.NULL является истинным. Пример: nil.

boolean. Логический тип данных boolean принимает значения true или false. Пример: true.

integer. В Tarantool тип полей integer используется для целых чисел от −9 223 372 036 854 775 808 до 18 446 744 073 709 551 615, то есть до примерно 18 квинтиллионов. Такой тип данных соответствует типам number в Lua и integer в MsgPack. Пример: -2^63.

unsigned. Тип unsigned в Tarantool используется для целых чисел от 0 до 18 446 744 073 709 551 615. Он представляет собой подмножество типа integer. Пример: 123456.

double. Поле типа double существует главным образом для соответствия типу DOUBLE data type в Tarantool/SQL . В msgpuck.h (интерфейс Tarantool к MsgPack) тип в хранилище — MP_DOUBLE, а размер закодированного значения всегда составляет 9 байтов. В Lua поля типа double могут содержать только не целые числовые значения и значения cdata с числами с плавающей точкой двойной точности (double). Примеры: 1.234, -44, 1.447e+44.

Чтобы избежать случайного использования неправильного типа значений, используйте явное преобразование типа ffi.cast(), когда вы ищете или изменяете поля типа double. Например, вместо space_object:insert{value} используйте ffi = require('ffi') ... space_object:insert({ffi.cast('double',value)}). Пример:

s = box.schema.space.create('s', {format = {{'d', 'double'}}})
s:create_index('ii')
s:insert({1.1})
ffi = require('ffi')
s:insert({ffi.cast('double', 1)})
s:insert({ffi.cast('double', tonumber('123'))})
s:select(1.1)
s:select({ffi.cast('double', 1)})

Арифметические операции с cdata формата double работают ненадёжно, поэтому для Lua лучше использовать тип number. Это не относится к Tarantool/SQL, так как Tarantool/SQL применяет неявное приведение типов.

number. Поле number в Tarantool может содержать значения как целые, так и с плавающей точкой, хотя в Lua тип number означает число с плавающей точкой двойной точности.

Tarantool по возможности сохраняет числа языка Lua в виде чисел с плавающей запятой, если числовое значение содержит десятичную запятую или если оно очень велико (более 100 триллионов = 1e14). В противном случае Tarantool сохраняет такое значение в виде целого числа. Чтобы даже очень большие величины гарантированно сохранялись как целые числа, используйте функцию tonumber64 или приписывайте в конце суффикс LL (Long Long) или ULL (Unsigned Long Long). Вот примеры записи чисел в обычном представлении, экспоненциальном, с суффиксом ULL и с использованием функции tonumber64: −55, −2.7e+20, 100000000000000ULL, tonumber64('18446744073709551615').

decimal. Тип данных decimal в Tarantool хранится в формате MsgPack ext (Extension). Значения с типом decimal не являются числами с плавающей запятой, хотя могут содержать десятичную запятую. Они представляют собой числа с точностью до 38 знаков. Пример: значение, которое возвращает функция в модуле decimal.

string. Строка (string) представляет собой последовательность байтов переменной длины. Обычно она записывается буквенно-цифровыми символами в одинарных кавычках. Как Lua, так и MsgPack рассматривают строки как двоичные данные, не пытаясь определить кодировку строки или как-то её преобразовать, кроме случаев, когда указаны необязательные правила сравнения символов. Таким образом, обычно сортировка и сравнение строк выполняются побайтово, а дополнительные правила сравнения символов не применяются. Например, числа упорядочены по их положению на числовой прямой, поэтому 2345 больше, чем 500. Строки же упорядочены сначала по кодировке первого байта, затем по кодировке второго байта и так далее, так что '2345' меньше '500'. Пример: 'A, B, C'.

bin. Значения типа bin (двоичные значения) не поддерживаются непосредственно в Lua, но в Tarantool есть тип varbinary, который кодируется в тип binary из MsgPack. Пример вставки varbinary в базу данных см. в рекомендациях по разработке ffi_varbinary_insert (продвинутого уровня). Пример: "\65 \66 \67".

uuid. Тип uuid в Tarantool используется для универсальных уникальных идентификаторов (UUID). Начиная с версии 2.4.1 Tarantool хранит значения uuid в формате MsgPack ext (Extension).

Пример: 64d22e4d-ac92-4a23-899a-e5934af5479.

array. В Lua массив (array) обозначается {...} (фигурными скобками). Примеры: списки чисел, которые обозначают точки геометрической фигуры: {10, 11}, {3, 5, 9, 10}.

table. Lua-таблицы со строковыми ключами хранятся в виде ассоциативных массивов MsgPack map. Lua-таблицы с целочисленными ключами, начиная с 1, хранятся в виде массивов MsgPack array. В Lua-таблицах нельзя использовать nil; вместо этого можно использовать box.NULL. Пример: запрос box.space.tester:select() вернет Lua-таблицу.

tuple. Кортеж (tuple) представляет собой легкую ссылку на массив типа MsgPack array, который хранится в базе данных. Это особый тип (cdata), который создан во избежание конвертации в Lua-таблицу при выборке данных. Некоторые функции могут возвращать таблицы с множеством кортежей. Примеры с кортежами см. в разделе box.tuple.

scalar. Значения в поле типа scalar могут быть следующих типов: boolean, integer, unsigned, double, number, decimal, string, uuid, varbinary. Они не могут иметь тип array, map или tuple. Примеры: true, 1, 'xxx'.

any. Значения в поле типа any могут быть следующих типов: boolean, integer, unsigned, double, number, decimal, string, uuid, varbinary, array, map, tuple. Примеры: true, 1, 'xxx', {box.NULL, 0}.

Примеры запросов вставки с разными типами полей:

tarantool> box.space.K:insert{1,nil,true,'A B C',12345,1.2345}
---
- [1, null, true, 'A B C', 12345, 1.2345]
...
tarantool> box.space.K:insert{2,{['a']=5,['b']=6}}
---
- [2, {'a': 5, 'b': 6}]
...
tarantool> box.space.K:insert{3,{1,2,3,4,5}}
---
- [3, [1, 2, 3, 4, 5]]
...

Индексы ограничивают значения, которые Tarantool может хранить в формате MsgPack. Вот почему, например, есть отдельные типы полей 'unsigned' (число без знака) и 'integer' (целое число), хотя в MsgPack они оба хранятся как целочисленные значения. Индекс типа 'unsigned' содержит только неотрицательные целочисленные значения, а индекс типа 'integer' содержит любые целочисленные значения.

Здесь снова приводятся типы полей, описанные в Описании типов полей, а также типы индексов, где их можно использовать. По умолчанию, тип поля — 'unsigned', тип индекса — TREE. Хотя в качестве типа индексированого поля 'nil' использовать нельзя, индексы могут содержать nil как опцию, которая не используется по умолчанию. Более подробную информацию см. в разделе Описание типов индексированных полей.

Имя типа поля Тип поля
Тип индекса
'boolean' boolean TREE или HASH
'integer' (также может называться ‘int’) integer, может включать в себя значения unsigned (без знака) TREE или HASH
'unsigned' (без знака, также может называться 'uint' или 'num', но 'num' объявлен устаревшим) unsigned TREE, BITSET или HASH
'double' double TREE или HASH
'number' number, может включать в себя значения типа integer или double TREE или HASH
'decimal' decimal TREE или HASH
'string' (строка, также может называться 'str') string TREE, BITSET или HASH
'varbinary' varbinary TREE, HASH или BITSET (с версии 2.7)
'uuid' uuid TREE или HASH
'array' array RTREE
'scalar'

может содержать значения nil, boolean, integer, unsigned, number, decimal, string или varbinary

Когда поле типа scalar содержит значения различных базовых типов, то порядок ключей следующий: nil, затем boolean, затем number, затем string, затем varbinary, затем uuid.

TREE или HASH

Когда Tarantool сравнивает строки, по умолчанию он использует двоичные параметры сортировки (binary collation). При этом он учитывает только числовое значение каждого байта в строке. Например, код символа 'A' (раньше называлась «значение ASCII») — число 65, код 'B' — число 66, а код 'a' – число 98. Поэтому 'A' < 'B' < 'a', если строка закодирована в ASCII или UTF-8.

Двоичная сортировка — лучший выбор для быстрого детерминированного простого обслуживания и поиска с использованием индексов Tarantool.

Но если вы хотите такое упорядочение, как в телефонных справочниках и словарях, то вам нужна одна из дополнительных сортировок Tarantool: unicode или unicode_ci. Они обеспечивают 'a' < 'A' < 'B' и 'a' == 'A' < 'B' соответственно.

Дополнительные виды сортировки unicode и unicode_ci обеспечивают упорядочение в соответствии с Таблицей сортировки символов Юникода по умолчанию (DUCET) и правилами, указанными в Техническом стандарте Юникода №10 – Алгоритм сортировки по Юникоду (Unicode® Technical Standard #10 Unicode Collation Algorithm (UTS #10 UCA)). Единственное отличие между двумя видами сортировки — вес:

  • сортировка unicode принимает во внимание уровни веса L1, L2 и L3 (уровень = „tertiary“, третичный);
  • сортировка unicode_ci принимает во внимание только вес L1 (уровень = „primary“, первичный), поэтому, например, 'a' == 'A' == 'á' == 'Á'.

Для примера возьмем некоторые русские слова:

'ЕЛЕ'
'елейный'
'ёлка'
'еловый'
'елозить'
'Ёлочка'
'ёлочный'
'ЕЛь'
'ель'

…и покажем разницу в упорядочении и выборке по индексу:

  • с сортировкой по unicode:

    tarantool> box.space.T:create_index('I', {parts = {{field = 1, type = 'str', collation='unicode'}}})
    ...
    tarantool> box.space.T.index.I:select()
    ---
    - - ['ЕЛЕ']
      - ['елейный']
      - ['ёлка']
      - ['еловый']
      - ['елозить']
      - ['Ёлочка']
      - ['ёлочный']
      - ['ель']
      - ['ЕЛь']
    ...
    tarantool> box.space.T.index.I:select{'ЁлКа'}
    ---
    - []
    ...
    
  • с сортировкой по unicode_ci:

    tarantool> box.space.T:create_index('I', {parts = {{field = 1, type ='str', collation='unicode_ci'}}})
    ...
    tarantool> box.space.S.index.I:select()
    ---
    - - ['ЕЛЕ']
      - ['елейный']
      - ['ёлка']
      - ['еловый']
      - ['елозить']
      - ['Ёлочка']
      - ['ёлочный']
      - ['ЕЛь']
    ...
    tarantool> box.space.S.index.I:select{'ЁлКа'}
    ---
    - - ['ёлка']
    ...
    

Сортировка включает в себя ещё множество аспектов, кроме показанного в этом примере сопоставления букв верхнего и нижнего регистра, с диакритическими знаками и без них, и с учётом алфавита. Учитываются также вариации одного и того же символа, системы неалфавитного письма и применяются специальные правила для комбинаций символов.

Для английского, русского и большинства других языков используйте «unicode» и «unicode_ci». Если вам нужно, чтобы у кириллических букв „Е“ и „Ё“ веса 1 уровня были одинаковыми, попробуйте киргизскую сортировку.

Специализированные дополнительные виды сортировки: Для других языков Tarantool предлагает специализированные виды сортировки для любого современного языка, на котором говорят более миллиона человек. Кроме того, специализированные дополнительные виды сортировки возможны для особых случаев, когда слова в словаре упорядочиваются не так, как в телефонном справочнике. Чтобы увидеть полный список, выполните команду box.space._collation:select().

Названия специализированных видов сортировки имеют вид unicode_[language code]_[strength]], где language code — это стандартный код языка из 2 или 3 символов, а значение strength может быть s1 для уровня «primary» (вес уровня 1), s2 для уровня «secondary», s3 для уровня «tertiary». Tarantool использует те же коды языков, что указаны в списке специализированных вариантов языковых настроек на страницах руководств по Ubuntu и Fedora. Схемы, в деталях объясняющие отличия от упорядочения по DUCET, можно найти в Общем репозитории языковых данных (Common Language Data Repository).

Последовательность – это генератор упорядоченных значений целых чисел.

Как и для спейсов и индексов, для последовательности вы должны указать имя, а Tarantool создаст уникальный для неё числовой идентификатор («ID последовательности).

Кроме того, можно указать несколько параметров при создании новой последовательности. Параметры определяют, какое значение будет генерироваться при использовании последовательности.

Имя параметра Тип и значение Значение по умолчанию Примеры
start Integer. Значение генерируется, когда последовательность используется впервые 1 start=0
min Integer. Значения, ниже указанного, генерироваться не могут 1 min=-1000
max Integer. Значения, выше указанного, генерироваться не могут 9223372036854775807 max=0
cycle Логическое значение. Если значения не могут быть сгенерированы, начинать ли заново false cycle=true
cache Integer. Количество значений, которые будут храниться в кэше 0 cache=0
step Integer. Что добавить к предыдущему сгенерированному значению, когда генерируется новое значение 1 step=-1
if_not_exists (если отсутствует) Логическое значение. Если выставлено в true (истина) и существует последовательность с таким именем, то игнорировать другие опции и использовать текущие значения false if_not_exists=true

Существующую последовательность можно изменять, удалять, сбрасывать, заставить сгенерировать новое значение или ассоциировать с индексом.

Для первоначального примера сгенерируем последовательность под названием „S“.

tarantool> box.schema.sequence.create('S',{min=5, start=5})
---
- step: 1
  id: 5
  min: 5
  cache: 0
  uid: 1
  max: 9223372036854775807
  cycle: false
  name: S
  start: 5
...

В результате видим, что в новой последовательность есть все значения по умолчанию, за исключением указанных min и start.

Затем получаем следующее значение с помощью функции next().

tarantool> box.sequence.S:next()
---
- 5
...

Результат точно такой же, как и начальное значение. Если мы снова вызовем next(), то получим 6 (потому что предыдущее значение плюс значение шага составит 6) и так далее.

Затем мы создаём новую таблицу и определяем, что ее первичный ключ должен быть получен из последовательности.

tarantool> s=box.schema.space.create('T')
---
...
tarantool> s:create_index('I',{sequence='S'})
---
- parts:
  - type: unsigned
    is_nullable: false
    fieldno: 1
  sequence_id: 1
  id: 0
  space_id: 520
  unique: true
  type: TREE
  sequence_fieldno: 1
  name: I
...
---
...

Затем вставляем кортеж, не указывая значение первичного ключа.

tarantool> box.space.T:insert{nil,'other stuff'}
---
- [6, 'other stuff']
...

В результате имеем новый кортеж со значением 6 в первом поле. Такой способ организации данных, когда система автоматически генерирует значения для первичного ключа, иногда называется «автоинкрементным» (т.е. с автоматическим увеличением) или «по идентификатору».

Для получения подробной информации о синтаксисе и методах реализации см. справочник по box.schema.sequence.

Чтобы обеспечить персистентность данных, Tarantool записывает обновления базы данных в так называемые файлы журнала упреждающей записи (write-ahead log, WAL). При отключении электроэнергии или случайном завершении работы экземпляра Tarantool данные в оперативной памяти теряются. В таком случае Tarantool восстанавливает данные из WAL-файлов, повторно выполняя запросы, записанные в них. Это называется «процесс восстановления». Можно настроить, как часто данные записываются в WAL-файлы, или отключить запись совсем с помощью wal_mode.

Tarantool также сохраняет набор файлов со статическими снимками данных (snapshots). Файл со снимком — это дисковая копия всех данных в базе на какой-то момент. Вместо того, чтобы перечитывать все WAL-файлы с момента создания базы, Tarantool в процессе восстановления может загрузить самый свежий снимок и затем прочитать только те WAL-файлы, которые были созданы начиная с момента сохранения снимка. После создания новых снимков более ранние WAL-файлы могут быть удалены, чтобы освободить место на диске.

Чтобы принудительно создать файл снимка, используйте в Tarantool функцию box.snapshot(). Чтобы включить автоматическое создание файлов снимков, используйте демон создания контрольных точек (checkpoint daemon) Tarantool. Этот демон определяет интервалы для принудительного создания контрольных точек. Он обеспечивает синхронизацию и сохранение на диск образов движков базы данных (как memtx, так и vinyl), а также автоматически удаляет более ранние WAL-файлы.

Файлы со снимками можно создавать, даже если WAL-файлы отсутствуют.

Примечание

Движок memtx регулярно создает контрольные точки с интервалом, указанным в настройках демона создания контрольных точек.

Движок vinyl постоянно сохраняет состояние в контрольной точке в фоновом режиме.

Для получения более подробной информации о методе записи WAL-файлов и процессе восстановления см. раздел Внутренняя реализация.

Tarantool поддерживает следующие основные операции с данными:

  • пять операций по изменению данных (INSERT, UPDATE, UPSERT, DELETE, REPLACE) и
  • одну операцию по выборке данных (SELECT).

Все они реализованы в виде функций во вложенном модуле box.space.

Примеры:

  • INSERT: добавить новый кортеж к спейсу „tester“.

    Первое поле, field[1], будет 999 (тип MsgPack – integer, целое число).

    Второе поле, field[2], будет „Taranto“ (тип MsgPack – string, строка).

    tarantool> box.space.tester:insert{999, 'Taranto'}
    
  • UPDATE: обновить кортеж, изменяя поле field[2].

    Оператор «{999}» со значением, которое используется для поиска поля, соответствующего ключу в первичном индексе, является обязательным, поскольку в запросе update() должен быть оператор, который указывает уникальный ключ, в данном случае – field[1].

    Оператор «{{„=“, 2, „Tarantino“}}» указывает, что назначение нового значения относится к field[2].

    tarantool> box.space.tester:update({999}, {{'=', 2, 'Tarantino'}})
    
  • UPSERT: обновить или вставить кортеж, снова изменяя поле field[2].

    Синтаксис upsert() похож на синтаксис update(). Однако логика выполнения двух запросов отличается. UPSERT означает UPDATE или INSERT, в зависимости от состояния базы данных. Кроме того, выполнение UPSERT откладывается до коммита транзакции, поэтому в отличие от``update()``, upsert() не возвращает данные.

    tarantool> box.space.tester:upsert({999, 'Taranted'}, {{'=', 2, 'Tarantism'}})
    
  • REPLACE: заменить кортеж, добавляя новое поле.

    Это действие также можно выполнить с помощью запроса update(), но обычно запрос update() более сложен.

    tarantool> box.space.tester:replace{999, 'Tarantella', 'Tarantula'}
    
  • SELECT: провести выборку кортежа.

    Оператор «{999}» все еще обязателен, хотя в нем не должен упоминаться первичный ключ.

    tarantool> box.space.tester:select{999}
    
  • DELETE: удалить кортеж.

    В этом примере мы определяем поле, соответствующее ключу в первичном индексе.

    tarantool> box.space.tester:delete{999}
    

Подводя итоги по примерам:

  • Функции insert и replace принимают кортеж (где первичный ключ – это часть кортежа).
  • Функция upsert принимает кортеж (где первичный ключ – это часть кортежа), а также операции по обновлению.
  • Функция delete принимает полный ключ любого уникального индекса (первичный или вторичный).
  • Функция update принимает полный ключ любого уникального индекса (первичный или вторичный), а также операции к выполнению.
  • Функция select принимает любой ключ: первичный/вторичный, уникальный/неуникальный, полный/часть.

Для получения более подробной информации по использованию операций с данными см. справочник по box.space.

Примечание

Помимо Lua можно использовать коннекторы к Perl, PHP, Python или другому языку программирования. Клиент-серверный протокол открыт и задокументирован. См. БНФ с комментариями.

Во вложенных модулях box.space и Вложенный модуль box.index содержится информация о том, как факторы сложности могут повлиять на использование каждой функции.

Фактор сложности Эффект
Размер индекса Количество ключей в индексе равно количеству кортежей в наборе данных. Для TREE-индекса: чем больше ключей, тем больше время поиска, хотя зависимость здесь, конечно же, нелинейная. Для HASH-индекса: чем больше ключей, тем больше нужно оперативной памяти, но количество низкоуровневых шагов остается примерно тем же.
Тип индекса Как правило, поиск по HASH-индексу работает быстрее, чем по TREE-индексу, если в спейсе более одного кортежа.
Количество обращений к индексам

Обычно для выборки значений одного кортежа используется только один индекс. Но при обновлении значений в кортеже требуется N обращений, если в спейсе N индексов.

Примечание по движку базы данных: если обновление не затрагивает поля вторичного индекса, vinyl не производит эти обращения. Таким образом, этот фактор сложности влияет только на memtx, который создает копию всего кортежа при каждом обновлении.

Количество обращений к кортежам Некоторые запросы, например SELECT, могут возвращать несколько кортежей. Обычно этот фактор менее важен, чем другие.
Настройки WAL Важным параметром для записи в WAL является wal_mode. Если запись в WAL отключена или задана запись с задержкой, но этот фактор не так важен. Если же запись в WAL производится при каждом запросе на изменение данных, то при каждом таком запросе приходится ждать, пока отработает обращение к более медленному диску, и данный фактор становится важнее всех остальных.

В Tarantool использование схемы данных опционально.

При создании спейса схему можно не задавать и тогда в кортежах могут лежать произвольные данные. Это правило не распространяется на поля, по которым построены индексы. У таких полей данные должны быть одного типа.

Схему можно задать при создании спейса. Читайте подробнее в описании функции box.schema.space.create(). Если вы создали спейс без схемы, ее можно добавить позже с помощью метода space_object:format().

После указания схемы данные начинают валидироваться по типам. Перед каждой операцией вставки или обновления они проверяются, и в случае несоответствия типов вы получите ошибку.

Мы рекомендуем использовать подход со схемой, потому что он помогает избежать ошибок.

Схему в Tarantool можно задавать двумя разными способами.

Обычно файл с кодом называется init.lua и имеет следующее описание схемы:

box.cfg()

users = box.schema.create_space('users', { if_not_exists = true })
users:format({{ name = 'user_id', type = 'number'}, { name = 'fullname', type = 'string'}})

users:create_index('pk', { parts = { { field = 'user_id', type = 'number'}}})

Этот подход довольно простой. Когда вы запустите tarantool, этот код исполнится и создаст схему. Чтобы запустить файл, используйте следующую команду:

tarantool init.lua

Но это может показаться слишком сложным, если вы не собираетесь глубоко разбираться с языком Lua и его синтаксисом.

Пример возможной сложности: в фрагменте выше есть вызов функций с двоеточием: users:format. Он используется, чтобы передать переменную users в качестве первого аргумента функции format. Это аналог self в объектно-ориентированных языках.

Поэтому вам может быть удобно описать схему через YAML.

Модуль DDL позволяет декларативно описывать схему данных в YAML формате.

Схема будет выглядеть примерно вот так:

spaces:
    users:
      engine: memtx
      is_local: false
      temporary: false
      format:
      - {name: user_id, type: uuid, is_nullable: false}
      - {name: fullname, type: string,  is_nullable: false}
      indexes:
      - name: user_id
        unique: true
        parts: [{path: user_id, type: uuid, is_nullable: false}]
        type: HASH

Этот вариант проще для старта: его проще использовать и не нужно вникать в язык Lua.

Модуль DDL встроен по умолчанию в Cartridge. Cartridge — это кластерное решение для Tarantool. В его веб-интерфейсе есть отдельная вкладка «Schema». Там можно написать схему, проверить ее корректность и применить на всем кластере.

Если вы не используете Cartridge, то чтобы использовать модуль DDL, нужно вставить нижеприведенный код на Lua в файл, с которым вы запускаете Tarantool. Обычно это init.lua.

local yaml = require('yaml')
local ddl = require('ddl')

box.cfg{}

local fh = io.open('ddl.yml', 'r')
local schema = yaml.decode(fh:read('*all'))
fh:close()
local ok, err = ddl.check_schema(schema)
if not ok then
    print(err)
end
local ok, err = ddl.set_schema(schema)
if not ok then
    print(err)
end

Предупреждение

Менять схему в самом DDL после ее применения нельзя. Для миграций есть несколько подходов — они описаны ниже.

Миграцией называется любое изменение схемы данных: добавление/удаление поля, добавление/удаление индекса, изменение формата поля и т. д.

В Tarantool есть только два вида миграции схемы, при которых не нужно мигрировать данные:

  • добавление поля в конец спейса
  • добавление индекса

Добавление поля происходит следующим образом:

local users = box.space.users
local fmt = users:format()

table.insert(fmt, { name = 'age', type = 'number', is_nullable = true })
users:format(fmt)

Обратите внимание: поле обязательно должно иметь параметр is_nullable. Иначе произойдет ошибка.

После создания нового поля, вы, вероятно, захотите заполнить его данными. Для этой задачи удобно использовать модуль tarantool/moonwalker. В README есть описание того, как начать его использовать.

Про добавление индекса рассказывается в описании метода space_object:create_index().

Остальные виды миграций также возможны, однако поддерживать консистентность данных при этом сложнее.

Миграции можно выполнять в двух случаях:

  • при старте Tarantool, пока ни один клиент еще не использует БД
  • в процессе обработки запросов, когда у БД уже есть активные клиенты

Для первого случая достаточно кода миграции, который будет написан и оттестирован. Самая сложная задача — это провести миграцию данных при наличии активных клиентов. Это необходимо иметь в виду при начальном проектировании схемы данных.

Мы выделяем следующие проблемы при наличии активных клиентов:

  • Связанные данные могут атомарно изменяться.
  • Нужно уметь отдавать данные как по новой схеме, так и по старой.
  • Когда данные перемещают в новый спейс, доступ к данным должен учитывать, что данные могут быть как в одном спейсе, так и в другом.
  • Запросы на запись не должны мешать корректной миграции. Типичный подход: всегда писать по новой схеме.

Эти проблемы могут быть или не быть релевантными в зависимости от вашего приложения и требований к его доступности.

В Tarantool есть механизм транзакций. Он полезен при написании миграции, так как позволяет атомарно работать с данными. Но перед использованием механизма транзакций нужно знать об ограничениях.

Подробнее о них описано в разделе про транзакции.

Код миграций исполняется на запущенном инстансе Tarantool. Важно: ни один способ не гарантирует вам транзакционное применение миграций на всем кластере.

Способ 1: написать миграции в коде основного приложения

Это довольно просто: когда вы перезагружаете код, в нужный момент происходит миграция данных и схема базы данных обновляется. Однако такой способ может подойти не всем. У вас может не быть возможности перезапустить Tarantool или обновить код через механизм горячей перезагрузки (hot reload).

Способ 2: tarantool/migrations (только для кластера на Tarantool Cartridge)

Про этот способ подробнее написано в README самого модуля tarantool/migrations.

Примечание

Есть также два способа, которые мы не рекомендуем использовать, но вы можете найти их полезными по тем или иным причинам.

Cпособ 3: утилита tarantoolctl

Утилита tarantoolctl поставляется вместе с Tarantool. Подключитесь к нужному инстансу через команду tarantoolctl connect.

$ tarantoolctl connect admin:password@localhost:3301
  • Если ваша миграция написана в Lua файле, вы можете исполнить его с помощью функции dofile(). Вызовите ее и первым аргументом укажите путь до файла с миграцией. Выглядит это вот так:

    tarantool> dofile('0001-delete-space.lua')
    ---
    ...
    
  • (или) можно скопировать код скрипта миграции, вставить его в консоль и применить.

Способ 4: применение миграции с помощью Ansible

Если вы используете Ansible-роль для развёртывания кластера Tarantool, то вы можете применить eval. Прочитать подробнее про eval можно в документации по Ansible-роли.