Хранение данных
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).
Скалярный / составной | Тип MsgPack | Тип Lua | Пример значения |
|---|---|---|---|
скалярный | nil | ||
скалярный | boolean |
| |
скалярный | string |
| |
скалярный | integer |
| |
скалярный | integer |
| |
скалярный | float64 (double) |
| |
скалярный | float64 (double) |
| |
скалярный | binary |
| |
скалярный | ext (for Tarantool |
| |
скалярный | ext (for Tarantool |
| |
скалярный | ext (for Tarantool |
| |
скалярный | ext (for Tarantool |
| |
составной | map | table (со строковыми ключами) |
|
составной | array | table (с целочисленными ключами) |
|
составной | array | кортеж (cdata) |
|
В Lua тип nil имеет только одно возможное значение, также называемое
nil. В Tarantool оно отображается как null при использовании
формата YAML по умолчанию.
Значение nil можно сравнивать
со значениями любых типов с помощью операторов == (равно) или ~= (не
равно), но другие операции сравнения не сработают. Значение nil нельзя
использовать в таблицах Lua; в качестве обходного пути можно
использовать box.NULL,
поскольку выражение nil == box.NULL истинно. Пример: nil.
Логическое значение (boolean) может быть либо true, либо false.
Пример: true.
Тип integer в Tarantool предназначен для целых чисел в диапазоне от -9223372036854775808 до 18446744073709551615, что составляет около 18 квинтиллионов. Этот тип соответствует типу number в Lua и типу integer в MsgPack.
Пример: -2^63.
Тип unsigned в Tarantool предназначен для целых чисел в диапазоне от 0 до 18446744073709551615. Таким образом, он является подмножеством типа integer.
Пример: 123456.
Тип поля 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 в 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 в Tarantool хранится как MsgPack ext (Extension). Значения типа decimal не являются числами с плавающей точкой, хотя могут содержать десятичные точки. Они обладают точностью до 38 знаков.
Пример: значение, возвращаемое функцией из модуля decimal.
Добавлено в 2.10.0. Тип
datetime в Tarantool предназначен для работы с датой и временем с
учетом високосных лет и разного количества дней в месяцах. Он хранится
как расширение MsgPack (Extension).
Операции с этим типом данных используют код из сторонней библиотеки c-dt.
Подробнее см. модуль datetime.
Начиная с: 2.10.0
Тип interval в Tarantool представляет периоды времени. Значения этого
типа можно прибавлять к значениям datetime и вычитать из них, а также
складывать и вычитать между собой. Операции с этим типом данных
используют код из сторонней библиотеки c-dt.
Тип хранится как расширение MsgPack (Extension).
Подробнее см. модуль datetime.
Строка — это последовательность байтов переменной длины, обычно
представленная алфавитно-цифровыми символами в одинарных кавычках. Как в
Lua, так и в MsgPack строки обрабатываются как бинарные данные без
попыток определить кодировку строки или выполнить какое-либо
преобразование строк — если не задано необязательное правило сортировки.
Поэтому обычно сортировка и сравнение строк выполняются побайтово, без применения специальных правил
сортировки. Например, числа упорядочиваются по их положению на числовой
оси, поэтому 2345 больше 500; в то же время строки упорядочиваются по
коду первого байта, затем второго и так далее, поэтому '2345' меньше
'500'.
Пример: 'A, B, C'.
Значение bin (двоичное) не поддерживается Lua напрямую, но в Tarantool
есть тип varbinary.
Подробнее см. в описании модуля varbinary.
Пример: "\65 \66 \67".
Тип uuid в Tarantool используется
для универсальных уникальных идентификаторов (Universally Unique Identifiers).
Начиная с версии 2.4.1 Tarantool хранит значения uuid
как MsgPack ext
(Extension).
Пример: 64d22e4d-ac92-4a23-899a-e5934af5479.
Массив в 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'.
Значения в поле этого типа могут иметь тип 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 выполняет проверки целостности при изменении данных в родительских спейсах. При попытке удалить кортеж, на который ссылается внешний ключ, или весь родительский спейс возникнет ошибка.