Как стать автором
Обновить
2339.37
МТС
Про жизнь и развитие в IT

Обходим подводные камни работы с UDA в коде на Lua для ScyllaDB: дружим Java-драйвер и пустые значения

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров372

Привет, Хабр! Мое имя Александр Коваль, я разработчик IoT-сервисов в МТС Web Services. Сейчас ScyllaDB поддерживает ограниченное количество функций, в том числе агрегационных. В стандартном наборе: min, max, count, avg. Но ее функциональность расширяется двумя типами пользовательских функций: скалярными (scalar functions) и агрегационными (aggregate functions). Первые работают со значениями одной строки, а вторые — нескольких. Реализовать такие функции можно на Lua или Rust.

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

Дисклеймер: этот материал написан на основе личного опыта — все решения получены методом проб и ошибок. Конструктивные предложения и советы по их улучшению приветствуется. Код с примерами и ссылки на ресурсы можно найти у меня в репозитории GitHub.

Исходная задача

Итак, нужно среди заданных элементов найти самый часто повторяющейся. В терминах ScyllaDB эта задача звучит так:

«Среди элементов одной колонки, при заданных ограничениях найти тот, который встречается чаще других».

Допустим, нам надо хранить информацию обо всех изменениях одного свойства. У него есть: группа, имя, время изменения и значение. Для этого будем использовать таблицу с именем property и такими колонками: group, name, date и value_string. У всех них тип «текст», кроме date, у нее «время и дата». Group и name определяют основной ключ (primary key). Date — кластерный (cluster key).

CQL-скрипт для создания таблицы выглядит так:

CREATE TABLE IF NOT EXISTS property (
group text,
name text,
date timestamp,
value_string text,
PRIMARY KEY((group,name),date)) WITH CLUSTERING ORDER BY (date DESC);

Отмечу, что для простоты изложения  в скриптах и примерах отсутствует информация о keyspace.

Пусть в таблице будут заданы следующие строки и значения:

group | name | date                            | value_string
------+------+---------------------------------+--------------
    g |    a | 2025-03-11 00:15:17.000000+0000 |       data_5
    g |    a | 2025-03-11 00:15:16.000000+0000 |       data_3
    g |    a | 2025-03-11 00:15:15.000000+0000 |       data_2
    g |    a | 2025-03-11 00:15:14.000000+0000 |       data_2
    g |    a | 2025-03-11 00:15:13.000000+0000 |       data_1

В этом примере видим, что самое часто повторяющееся значение — data_2.

Пишем код на Lua

Предположим, что есть функция most_common_text(text), которая ищет самое частое значение. Она принимает и выдает значение типа текст.

Тогда CQL-запрос будет выглядеть так:

SELECT most_common_text(value_string)
FROM property
WHERE date >= '2025-03-11 00:00:00' AND date < '2025-03-11 23:00:00'

Такая задача, к примеру, решается с использованием словаря. Давайте напишем ее код решения на Lua.

Функция для обработки одного значения:

function accumulate(storage, val)
   if storage == nil then
       storage = {}
   end
   if val == nil then
       return storage
   end
   if storage[val] == nil then
       storage[val] = 1
   else
       storage[val] = storage[val] + 1
   end
   return storage
end

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

function calculate(storage)
   if storage == nil
   then
       return nil
   end
   local value = nil
   local count = 0
   for v, c in pairs(storage) do
       if c > count then
           value = v
           count = c
       end
   end
   return value
end

Функция запуска процесса накопления и вычисления результата:

function most_common(data)
   if data == nil
   then
       return nil
   end
   local storage = {}
   for k, v in pairs(data) do
       storage = accumulate(storage, v)
   end
   return calculate(storage)
end

Пример вызова:

function most_common_test()
   local samples = {'data_1', 'data_2', 'data_2', 'data_3', 'data_5'}
   local result = most_common(samples)
   print(result)
end

Такая функция в итоге выведет нам искомое значение — data_2.

Функция для ScyllaDB

ScyllaDB позволяет использовать для функций различные языки. Ограничения вводятся на уровне самой БД. А указание языка, на котором производится реализация функции, указывается при создании самой функции через ключевое слово LANGUAGE.  Они могут выглядеть следующим образом.

Функция для обработки одного значения:

CREATE OR REPLACE FUNCTION most_common_text_accumulate(storage _type_, val text)
RETURNS NULL ON NULL INPUT
RETURNS _type_
LANGUAGE lua
AS $$
...
$$;

Функция для нахождения результата:

CREATE OR REPLACE FUNCTION most_common_text_calculate(storage _type_)
RETURNS NULL ON NULL INPUT
RETURNS text
LANGUAGE lua AS $$
...
$$;

Функция запуска процесса накопления и вычисления результата:

CREATE OR REPLACE AGGREGATE most_common_text(text)
  SFUNC most_common_text_accumulate
  STYPE _type_
  FINALFUNC most_common_text_calculate
  INITCOND _default_;

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

Предположим, что для типа параметра используется встроенный типа map<text, bigint>. Тогда для начального значения можно задать null. Если так сделать, то при создании функции ScyllaDB выдает следующую ошибку.

CREATE OR REPLACE AGGREGATE most_common_text_map(text)
              ...    SFUNC most_common_text_accumulate_map
              ...    STYPE map<text, bigint>
              ...    FINALFUNC most_common_text_calculate_map
              ...    INITCOND null;
ServerError: <Error from server: code=0000 [Server error] message="marshaling error: read_simple - not enough bytes (expected 4, got 0) Backtrace: 0x415a30e ...libreloc/libc.so.6+0x100352
  --------
  seastar::lambda_task<seastar::execution_stage::flush()::$_5>">

Результаты других попыток создания можно посмотреть у меня в GitHub.

К примеру, если не задавать INITCOND:

CREATE OR REPLACE AGGREGATE most_common_text_map(text)
              ...    SFUNC most_common_text_accumulate_map
              ...    STYPE map<text, bigint>
              ...    FINALFUNC most_common_text_calculate_map;
ServerError: <Error from server: code=0000 [Server error] message="conjunctions are not yet reachable via term_raw_expr::prepare() Backtrace: 0x415a30e .../libreloc/libc.so.6+0x100352
  --------
  seastar::lambda_task<seastar::execution_stage::flush()::$_5>">

ScyllDB использует «@» для пустых значений, но Java-драйвер его не может стандартно обработать. Кажется, что эту проблему невозможно решить без кода.

Решаем проблему с пустыми значениями в ScyllaDB

Методом проб и ошибок я пришел к следующему решению. Я использовал для словаря пользовательский тип, который содержит поле со встроенным типом. И в качестве начального значения применил в нем пустое значение для словарей «{}»:

Тип:

CREATE TYPE IF NOT EXISTS most_common_text_data_map 
( text_data map<bigint, bigint> )

Начальное значение этой функции выглядит так:

{text_data: {}}

Но она все еще не работает. При попытке выполнить select выдается ошибка:

InvalidRequest: Error from server: code=2200 [Invalid query] message="value is not a number"

Нужно внести поправку в новый тип: добавить «frozen» на поле. Более подробно этот момент объяснен в документации. Например, тут и тут.

Итак, обновляем тип:

CREATE TYPE IF NOT EXISTS most_common_text_data 
( text_data frozen<map<bigint, bigint>> );

Работает!

SELECT most_common_text(value_string) FROM property WHERE date >= '2025-03-11 00:00:00' AND date < '2025-03-11 23:00:00';


most_common_text(value_string)
--------------------------------------------
                                    data_2


(1 rows)

Вместо заключения

Пользовательские функции расширяют функциональность ScyllaDB: их можно быстро реализовать и встроить в запросы. Но какие тут могут быть ограничения и на что нужно обратить внимание?

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

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

Теги:
Хабы:
+10
Комментарии0

Полезные ссылки

Пайплайн распознавания номеров транспортных средств: как это устроено

Время на прочтение7 мин
Количество просмотров2.2K
Всего голосов 23: ↑22 и ↓1+25
Комментарии1

Интеграция виджета обратного звонка МТС Exolve в документацию на MkDocs

Время на прочтение8 мин
Количество просмотров404
Всего голосов 5: ↑5 и ↓0+7
Комментарии0

Путь видео в онлайн-кинотеатрах от «стекла до стекла». Middleware — ядро, подписки, сервисы, витрина

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров737
Всего голосов 4: ↑3 и ↓1+4
Комментарии0

Приручая хаос: как структурировать процессы в эксплуатационных командах. Кейс МТС

Время на прочтение6 мин
Количество просмотров695
Всего голосов 3: ↑3 и ↓0+4
Комментарии0

Информация

Сайт
www.mts.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия