Как реализовать разбивку на страницы для индексов с несколькими клавишами в Tarantool?

#tarantool

#tarantool

Вопрос:

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

В действительности, если кортеж имеет более одного значения в поле массива, он будет выбран #tuple[array_field_idx] несколько раз. Вчера я реализовал «отдельный» выбор (используя ffi для получения адреса указателя кортежа), но, похоже, он не будет работать с разбивкой на страницы.

Есть ли у вас какие-либо идеи, как это можно реализовать в Tarantool?

Ответ №1:

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

Я напишу код, который работает для Tarantool 2.3 (но в будущем он может быть поврежден). Пожалуйста, тщательно протестируйте их и обновите определение FFI в случае обновления версии Tarantool.

Итак, давайте начнем. Во-первых, вы должны знать, что Tarantool BTree хранит данные в специальной структуре memtx_tree_data, которая содержит указатель на ваш кортеж и hint . Это специальное число, которое позволяет ускорить сравнение между кортежами для простого индекса дерева, и это позиция индексируемого элемента в массиве.

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

 local ffi = require('ffi')

ffi.cdef([[

typedef struct index_def;

typedef struct index;

typedef struct memtx_tree;

typedef struct mempool;

typedef uint64_t hint_t;

enum iterator_type {
    /* ITER_EQ must be the first member for request_create  */
    ITER_EQ               =  0, /* key == x ASC order                  */
    ITER_REQ              =  1, /* key == x DESC order                 */
    ITER_ALL              =  2, /* all tuples                          */
    ITER_LT               =  3, /* key <  x                            */
    ITER_LE               =  4, /* key <= x                            */
    ITER_GE               =  5, /* key >= x                            */
    ITER_GT               =  6, /* key >  x                            */
    ITER_BITS_ALL_SET     =  7, /* all bits from x are set in key      */
    ITER_BITS_ANY_SET     =  8, /* at least one x's bit is set         */
    ITER_BITS_ALL_NOT_SET =  9, /* all bits are not set                */
    ITER_OVERLAPS         = 10, /* key overlaps x                      */
    ITER_NEIGHBOR         = 11, /* tuples in distance ascending order from specified point */
    iterator_type_MAX
};


typedef struct iterator {
    /**
     * Iterate to the next tuple.
     * The tuple is returned in @ret (NULL if EOF).
     * Returns 0 on success, -1 on error.
     */
    int (*next)(struct iterator *it, struct tuple **ret);
    /** Destroy the iterator. */
    void (*free)(struct iterator *);
    /** Space cache version at the time of the last index lookup. */
    uint32_t space_cache_version;
    /** ID of the space the iterator is for. */
    uint32_t space_id;
    /** ID of the index the iterator is for. */
    uint32_t index_id;
    /**
     * Pointer to the index the iterator is for.
     * Guaranteed to be valid only if the schema
     * state has not changed since the last lookup.
     */
    struct index *index;
};


struct memtx_tree_key_data {
    /** Sequence of msgpacked search fields. */
    const char *key;
    /** Number of msgpacked search fields. */
    uint32_t part_count;
    /** Comparison hint, see tuple_hint(). */
    hint_t hint;
};

struct memtx_tree_data {
    /* Tuple that this node is represents. */
    struct tuple *tuple;
    /** Comparison hint, see key_hint(). */
    hint_t hint;
};

typedef int16_t bps_tree_pos_t;
typedef uint32_t bps_tree_block_id_t;

typedef uint32_t matras_id_t;

struct matras_view {
    /* root extent of the view */
    void *root;
    /* block count in the view */
    matras_id_t block_count;
    /* all views are linked into doubly linked list */
    struct matras_view *prev_view, *next_view;
};

struct memtx_tree_iterator {
    /* ID of a block, containing element. -1 for an invalid iterator */
    bps_tree_block_id_t block_id;
    /* Position of an element in the block. Could be -1 for last in block*/
    bps_tree_pos_t pos;
    /* Version of matras memory for MVCC */
    struct matras_view view;
};

typedef struct tree_iterator {
    struct iterator base;
    struct memtx_tree_iterator tree_iterator;
    enum iterator_type type;
    struct memtx_tree_key_data key_data;
    struct memtx_tree_data current;
    /** Memory pool the iterator was allocated from. */
    struct mempool *pool;
};

]])


local function get_tree_comparison_hint(box_iterator_state)
    if box_iterator_state == nil then
        return nil
    end

    local casted = ffi.cast("struct tree_iterator*", box_iterator_state)
    --
    -- IMPORTANT: hint is zero-based (as arrays in C)
    -- Lua arrays is one-based.
    --
    return casted.current.hint
end

return {
    get_tree_comparison_hint = get_tree_comparison_hint,
}

  

Затем рассмотрим следующий пример:

 local box_iterator = require('common.box_iterator')

box.cfg{}

local space = box.schema.create_space('dict', {
    format = {
        {name = 'id', type = 'number'},
        {name = 'bundles', type = 'array'}
    },
    if_not_exists = true,
})

space:create_index('pk', {
    unique = true,
    parts = {
        {field = 1, type = 'number'}
    },
    if_not_exists = true,
})

space:create_index('multikey', {
    unique = false,
    parts = {
        {field = 2, type = 'string', path = '[*]'},
        -- Note: I intentionally add primary index parts here
        {field = 1, type = 'number'}
    },
    if_not_exists = true,
})

space:replace({1, {'a', 'b', 'c', 'd'}})
space:replace({2, {'b', 'c'}})
space:replace({3, {'a', 'd'}})
space:replace({4, {'c', 'd'}})

for iter_state, tuple in space.index.multikey:pairs({'a'}, {iterator = 'GE'}) do
    local position = box_iterator.get_tree_comparison_hint(iter_state)   1
    print(
        string.ljust(tostring(tuple), 30),
        position,
        tuple[2][tonumber(position)]
    )
end

os.exit()
  

Вывод:

 # Tuple                         Hint   Indexed element
[1, ['a', 'b', 'c', 'd']]       1ULL    a
[3, ['a', 'd']]                 1ULL    a
[1, ['a', 'b', 'c', 'd']]       2ULL    b
[2, ['b', 'c']]                 1ULL    b
[1, ['a', 'b', 'c', 'd']]       3ULL    c
[2, ['b', 'c']]                 2ULL    c
[4, ['c', 'd']]                 1ULL    c
[1, ['a', 'b', 'c', 'd']]       4ULL    d
[3, ['a', 'd']]                 2ULL    d
[4, ['c', 'd']]                 2ULL    d
  

Как вы видите, порядок строго определен.
Tarantool возвращает мне кортежи в порядке, который определяется с помощью (a) индексированного значения — кортежа [path_to_array][подсказка 1] и первичного ключа.
Второе условие является общим для всех вторичных неуникальных индексов Tarantool.
Tarantool внутренне объединяет первичный ключ с каждым неуникальным индексом.
Все, что вам нужно, это явно указать это в вашей схеме.

Итак, следующий термин cursor . Курсор позволяет продолжить итерацию с того места, где вы ранее остановились. Для уникальных индексов курсор — это поля этих индексов, для неуникального индекса — это поля этих индексов с первичным ключом слияния (подробнее см. Функцию key_def.merge, в настоящее время она не поддерживает индексы с несколькими ключами, но это полезно, если вам нужно понять, как работать с объединением частей индекса). Следующая комбинация ( merge(secondary_index_parts, primary_index_parts) ) всегда является уникальным значением, что позволяет продолжить итерацию со строго определенного места.

Давайте вернемся к моему примеру. Например. Я остановился на строке [1, ['a', 'b', 'c', 'd']] 3ULL c . Мой курсор {'c', 1} .

Ну, теперь я мог бы продолжить с этого момента:

 -- "GE" is changed to "GT" to skip already scanned tuple: [1, ['a', 'b', 'c', 'd']]
for iter_state, tuple in space.index.multikey:pairs({'c', 1}, {iterator = 'GT'}) do
    local position = box_iterator.get_tree_comparison_hint(iter_state)   1
    print(
        string.ljust(tostring(tuple), 30),
        position,
        tuple[2][tonumber(position)]
    )
end
--[[
Result:
[2, ['b', 'c']]                 2ULL    c
[4, ['c', 'd']]                 1ULL    c
[1, ['a', 'b', 'c', 'd']]       4ULL    d
[3, ['a', 'd']]                 2ULL    d
[4, ['c', 'd']]                 2ULL    d
--]]
  

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

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