Tarantool CE/EE Documentation portal logo
Помощь

Хранение данных

Кортежи

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

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

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

На внутреннем уровне Tarantool хранит кортежи как массивы MsgPack.

Поля

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

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

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

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

Спейсы

Tarantool хранит кортежи в контейнерах, называемых спейсами (spaces).

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

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

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

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

Типы данных

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

Сравнение Lua и MsgPack

Скалярный / составной

Тип MsgPack

Тип Lua

Пример значения

скалярный

nil

cdata

box.NULL

скалярный

boolean

boolean

true

скалярный

string

string

'A B C'

скалярный

integer

number

12345

скалярный

integer

cdata

12345

скалярный

float64 (double)

number

1.2345

скалярный

float64 (double)

cdata

1.2345

скалярный

binary

cdata

[!!binary 3t7e]

скалярный

ext (for Tarantool decimal)

cdata

1.2

скалярный

ext (for Tarantool datetime)

cdata

'2021-08-20T16:21:25.122999906 Europe/Berlin'

скалярный

ext (for Tarantool interval)

cdata

+1 months, 1 days

скалярный

ext (for Tarantool uuid)

cdata

12a34b5c-de67-8f90-123g-h4567ab8901

составной

map

table (со строковыми ключами)

{'a': 5, 'b': 6}

составной

array

table (с целочисленными ключами)

[1, 2, 3, 4, 5]

составной

array

кортеж (cdata)

[12345, 'A B C']

Подробности о типах полей

nil

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

boolean

Логическое значение (boolean) может быть либо true, либо false.

Пример: true.

integer

Тип integer в Tarantool предназначен для целых чисел в диапазоне от -9223372036854775808 до 18446744073709551615, что составляет около 18 квинтиллионов. Этот тип соответствует типу number в Lua и типу integer в MsgPack.

Пример: -2^63.

unsigned

Тип unsigned в Tarantool предназначен для целых чисел в диапазоне от 0 до 18446744073709551615. Таким образом, он является подмножеством типа integer.

Пример: 123456.

double

Тип поля double существует в основном для соответствия типу данных DOUBLE в Tarantool/SQL. В msgpuck.h (интерфейс Tarantool к MsgPack) типом хранения является MP_DOUBLE, а размер закодированного значения всегда равен 9 байтам. В Lua поля типа double могут содержать только нецелочисленные числовые значения и значения cdata с числами двойной точности с плавающей запятой.

Примеры: 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').

Также можно использовать модуль ffi, чтобы указать тип C, к которому нужно привести число. В этом случае число будет сохранено как cdata.

decimal

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

Пример: значение, возвращаемое функцией из модуля decimal.

datetime

Добавлено в 2.10.0. Тип datetime в Tarantool предназначен для работы с датой и временем с учетом високосных лет и разного количества дней в месяцах. Он хранится как расширение MsgPack (Extension). Операции с этим типом данных используют код из сторонней библиотеки c-dt.

Подробнее см. модуль datetime.

interval

Начиная с: 2.10.0

Тип interval в Tarantool представляет периоды времени. Значения этого типа можно прибавлять к значениям datetime и вычитать из них, а также складывать и вычитать между собой. Операции с этим типом данных используют код из сторонней библиотеки c-dt. Тип хранится как расширение MsgPack (Extension).

Подробнее см. модуль datetime.

строка

Строка — это последовательность байтов переменной длины, обычно представленная алфавитно-цифровыми символами в одинарных кавычках. Как в Lua, так и в MsgPack строки обрабатываются как бинарные данные без попыток определить кодировку строки или выполнить какое-либо преобразование строк — если не задано необязательное правило сортировки. Поэтому обычно сортировка и сравнение строк выполняются побайтово, без применения специальных правил сортировки. Например, числа упорядочиваются по их положению на числовой оси, поэтому 2345 больше 500; в то же время строки упорядочиваются по коду первого байта, затем второго и так далее, поэтому '2345' меньше '500'.

Пример: 'A, B, C'.

bin

Значение bin (двоичное) не поддерживается Lua напрямую, но в Tarantool есть тип varbinary. Подробнее см. в описании модуля varbinary.

Пример: "\65 \66 \67".

uuid

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

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

array

Массив в Lua представляется с помощью {...} (фигурных скобок).

Примеры: списки чисел, представляющих точки геометрических фигур: {10, 11}, {3, 5, 9, 10}.

Таблицы

Таблицы Lua со строковыми ключами хранятся как карты MsgPack; таблицы Lua с целочисленными ключами, начиная с 1, хранятся как массивы MsgPack. Значения nil нельзя использовать в таблицах Lua; в качестве обходного пути можно использовать box.NULL.

Пример: запрос box.space.tester:select() вернет таблицу Lua.

Кортежи

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

Скалярные типы

Значения в скалярном поле могут иметь тип boolean, integer, unsigned, double, number, decimal, string, uuid или varbinary, но не array, map или tuple.

Примеры: true, 1, 'xxx'.

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 использует так называемую бинарную сортировку (binary collation). При этом учитывается только числовое значение каждого байта в строке. Например, кодировка 'A' (то, что раньше называли "ASCII-значением") — 65, кодировка 'B' — 66, а кодировка 'a' — 98. Таким образом, если строка закодирована в ASCII или UTF-8, то 'A' < 'B' < 'a'.

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

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

Дополнительные правила сортировки unicode и unicode_ci используют упорядочивание в соответствии с Default Unicode Collation Element Table (DUCET) и правилами, описанными в 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.T.index.I:select()---- - ['ЕЛЕ']  - ['елейный']  - ['ёлка']  - ['еловый']  - ['елозить']  - ['Ёлочка']  - ['ёлочный']  - ['ЕЛь']...tarantool> box.space.T.index.I:select{'ЁлКа'}---- - ['ёлка']...

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

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

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

Имена специализированных правил сортировки имеют вид unicode_[код_языка]_[уровень], где код_языка — стандартное двух- или трёхбуквенное сокращение названия языка, а уровень — s1 для «основного уровня» (веса уровня 1), s2 для «вторичного» и s3 для «третичного». В Tarantool используются те же коды языков, что и в «списке локалей с поддержкой настройки» на страницах руководства Ubuntu и Fedora. Схемы, поясняющие точные отличия от порядка DUCET, приводятся в Common Language Data Repository.

Значения по умолчанию

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

Задать значение по умолчанию для поля можно в вызове space_object:format(), который определяет формат спейса. Значения по умолчанию применяются независимо от допустимости значения NULL для поля: любой кортеж, в котором это поле пропущено или установлено в nil, получает значение по умолчанию.

Задать значения по умолчанию можно двумя способами: явно или с помощью функции.

Явные значения по умолчанию

Явные значения по умолчанию задаются в параметре default при объявлении поля в вызове space_object:format().

local books = box.schema.space.create('books')books:format({    { name = 'id', type = 'number' },    { name = 'name', type = 'string' },    { name = 'year', type = 'number', default = 2024 },})books:create_index('primary', { parts = { 1 } })

Чтобы использовать значение по умолчанию для поля, пропустите его или присвойте ему nil:

books:insert { 1, 'Thinking in Java' }books:insert { 2, 'How to code in Go', nil }

В качестве значения по умолчанию можно использовать любой объект Lua, который может быть вычислен во время вызова space_object.format(), например:

  • константа: default = 100
  • инициализированная переменная: default = default_size
  • выражение: default = 10 + default_size
  • возвращаемое значение функции: default = count_default()

См. также справочник по space_object:format().

Функции по умолчанию

Значение по умолчанию может быть определено как возвращаемое значение хранимой Lua-функции. Чтобы функция использовалась по умолчанию, она должна быть создана с помощью box.schema.func.create() с указанием тела функции и возвращать одно значение типа поля. Кроме того, о на не должна передавать управление (yield).

box.schema.func.create('current_year', {    language = 'Lua',    body = "function() return require('datetime').now().year end"})

Функции по умолчанию задаются в параметре default_func при объявлении поля в вызове space_object:format(). Чтобы сделать функцию без аргументов используемой по умолчанию для поля, укажите ее имя:

local books = box.schema.space.create('books')books:format({    { name = 'id', type = 'unsigned' },    { name = 'isbn', type = 'string' },    { name = 'title', type = 'string' },    { name = 'year', type = 'unsigned', default_func = 'current_year' }})books:create_index('primary', { parts = { 1 } })

У функции по умолчанию также может быть один аргумент.

box.schema.func.create('randomize', {    language = 'Lua',    body = "function(limit) return math.random(limit.min, limit.max) end"})

Чтобы передать аргумент функции при задании значения по умолчанию, укажите его в параметре default вызова space_object:format():

books:format({    { name = 'id', type = 'unsigned', default_func= 'randomize', default = {min = 0, max = 1000} },    { name = 'isbn', type = 'string' },    { name = 'title', type = 'string' },    { name = 'year', type = 'unsigned', default_func = 'current_year' }})

См. также справочник по space_object.format().

Ограничения

Для более точного контроля над хранимыми данными в Tarantool предусмотрена поддержка ограничений (constraints) — пользовательских ограничений на значения определенных полей или целых кортежей. Вместе с типами данных ограничения позволяют ограничивать диапазоны доступных значений полей как синтаксически, так и семантически.

Например, поле age обычно имеет тип number, поэтому в нем нельзя хранить строки или логические значения. Тем не менее, в нем могут быть значения, не имеющие смысла, например отрицательные числа. Здесь на помощь приходят ограничения.

Типы ограничений

В Tarantool есть два типа ограничений:

  • Ограничения полей (field constraints) проверяют, что значение, присваиваемое полю, удовлетворяет заданному условию. Например, age должно быть неотрицательным.

  • Ограничения кортежей (tuple constraints) проверяют сложные условия, которые могут затрагивать все поля кортежа. Например, кортеж содержит дату в трех полях: year, month и day. Значения day можно проверять на основе значения month (и даже year, если учитывать високосные годы).

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

Функции ограничений

В ограничениях используются хранимые Lua-функции или SQL-выражения, которые должны возвращать true, если ограничение соблюдено. Другие возвращаемые значения (включая nil) и исключения приводят к неудачному завершению проверки и предотвращают вставку или изменение кортежа.

Чтобы создать функцию ограничения, вызовите box.schema.func.create(), указав определение функции в атрибуте body.

Функции ограничений принимают два параметра:

  • Кортеж и имя ограничения для ограничений кортежей.
-- Define a tuple constraint function --box.schema.func.create('check_person', {    language = 'LUA',    is_deterministic = true,    body = 'function(t, c) return (t.age >= 0 and #(t.name) > 3) end'})
  • Значение поля и имя ограничения для ограничений полей.
-- Define a field constraint function --box.schema.func.create('check_age', {    language = 'LUA',    is_deterministic = true,    body = 'function(f, c) return (f >= 0 and f < 150) end'})

Создание ограничений

Чтобы создать ограничение (constraint) в спейсе, укажите имя соответствующей функции в параметре constraint:

  • Ограничения кортежей: при создании или изменении спейса.
-- Create a space with a tuple constraint --customers = box.schema.space.create('customers', {constraint = 'check_person'})
  • Ограничения полей: при настройке формата спейса.
-- Specify format with a field constraint --box.space.customers:format({    {name = 'id', type = 'number'},    {name = 'name', type = 'string'},    {name = 'age',  type = 'number', constraint = 'check_age'},})

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

-- Create one more tuple constraint --box.schema.func.create('another_constraint',    {language = 'LUA', is_deterministic = true, body = 'function(t, c) return true end'})-- Set two constraints with optional names --box.space.customers:alter{    constraint = { check1 = 'check_person', check2 = 'another_constraint'}}

Внешние ключи

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

Поля могут содержать значения, которые существуют только в других полях. Например, заказ в магазине всегда принадлежит покупателю. Следовательно, все значения поля customer спейса orders также должны существовать в поле id спейса customers. В этом случае customers является родительским спейсом для orders (его дочерним спейсом). Когда два спейса связаны внешним ключом, при каждой вставке или изменении кортежа в дочернем спейсе Tarantool проверяет, что соответствующее значение присутствует в родительском спейсе.

Хранение данных

Типы внешних ключей

В Tarantool есть два типа внешних ключей:

  • Полевые внешние ключи проверяют, что значение, присваиваемое полю, присутствует в определенном поле другого спейса. Например, значение customer в кортеже из спейса orders должно совпадать со значением id, хранящимся в спейсе customers.

  • Кортежные внешние ключи проверяют, что несколько полей кортежа имеют совпадение в другом спейсе. Например, если в спейсе orders есть поля customer_id и customer_name, кортежный внешний ключ может проверять, что в спейсе customers содержится кортеж с обоими этими значениями в соответствующих полях.

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

Создание внешних ключей

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

  • Полевые внешние ключи: при настройке формата спейса.
-- Create a space with a field foreign key --box.schema.space.create('orders')box.space.orders:format({    {name = 'id',   type = 'number'},    {name = 'customer_id', foreign_key = {space = 'customers', field = 'id'}},    {name = 'price_total', type = 'number'},})
  • Кортежные внешние ключи: при создании или изменении спейса. Обратите внимание, что для внешних ключей с несколькими полями должен существовать индекс, включающий все эти поля.
-- Create a space with a tuple foreign key --box.schema.space.create("orders", {    foreign_key = {        space = 'customers',        field = {customer_id = 'id', customer_name = 'name'}    }})box.space.orders:format({    {name = "id", type = "number"},    {name = "customer_id" },    {name = "customer_name"},    {name = "price_total", type = "number"},})

Внешним ключам можно задать необязательное имя.

Спейс может иметь несколько кортежных внешних ключей. В этом случае все они должны иметь имена.

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