Примеры SQL UDF#
После ознакомления с SQL пользовательские функции, в следующих разделах приведено множество примеров корректных SQL UDF. Эти UDF подходят для использования как Inline пользовательские функции или Catalog user-defined functions после изменения имени и примеров вызова.
Примеры объединяют различные поддерживаемые операторы. Для получения дополнительной информации см. документацию по конкретным операторам:
FUNCTION — для общего объявления UDF
SET — для присвоения значений переменным
RETURN — для возврата результатов
Inline и catalog UDF#
Следующий раздел показывает различия в использовании inline и catalog UDF на примере простого SQL UDF. Тот же шаблон применяется ко всем последующим разделам.
Очень простой SQL UDF, который возвращает статическое значение без входных параметров:
FUNCTION answer()
RETURNS BIGINT
RETURN 42
Полный пример этого UDF как inline UDF и его использование в конкатенации строк с приведением типов:
WITH
FUNCTION answer()
RETURNS BIGINT
RETURN 42
SELECT 'The answer is ' || CAST(answer() as varchar);
-- The answer is 42
Если каталог example поддерживает хранение UDF в схеме default, вы
можете использовать следующее:
CREATE FUNCTION example.default.answer()
RETURNS BIGINT
RETURN 42;
После сохранения UDF в каталоге, вы можете вызывать его многократно без повторного определения:
SELECT example.default.answer() + 1; -- 43
SELECT 'The answer is ' || CAST(example.default.answer() as varchar); -- The answer is 42
Альтернативно, вы можете настроить SQL PATH в Config properties на каталог и схему, поддерживающие хранение UDF:
sql.default-function-catalog=example
sql.default-function-schema=default
sql.path=example.default
Теперь вы можете управлять UDF без указания полного пути:
CREATE FUNCTION answer()
RETURNS BIGINT
RETURN 42;
Вызов UDF также работает без полного пути:
SELECT answer() + 5; -- 47
Примеры объявлений#
Результат вызова UDF answer() всегда одинаковый, поэтому вы можете
объявить её как deterministic и добавить некоторую дополнительную информацию:
FUNCTION answer()
LANGUAGE SQL
DETERMINISTIC
RETURNS BIGINT
COMMENT 'Provide the answer to the question about life, the universe, and everything.'
RETURN 42
Комментарий и другая информация о UDF отображаются в выводе SHOW FUNCTIONS.
Простой UDF, который возвращает приветствие для входной строки fullname,
конкатенируя две строки и входное значение:
FUNCTION hello(fullname VARCHAR)
RETURNS VARCHAR
RETURN 'Hello, ' || fullname || '!'
Ниже приведён пример вызова:
SELECT hello('Jane Doe'); -- Hello, Jane Doe!
Первый пример UDF, который использует несколько операторов в блоке BEGIN.
Он вычисляет результат умножения входного целого числа на 99. Тип данных
bigint используется для всех переменных и значений. Значение целого числа
99 приводится к bigint в присвоении значения по умолчанию для переменной x:
FUNCTION times_ninety_nine(a bigint)
RETURNS bigint
BEGIN
DECLARE x bigint DEFAULT CAST(99 AS bigint);
RETURN x * a;
END
Ниже приведён пример вызова:
SELECT times_ninety_nine(CAST(2 as bigint)); -- 198
Условные конструкции#
Первый пример условного управления потоком в SQL UDF с использованием
оператора CASE. Простое входное значение типа bigint сравнивается с
несколькими значениями:
FUNCTION simple_case(a bigint)
RETURNS varchar
BEGIN
CASE a
WHEN 0 THEN RETURN 'zero';
WHEN 1 THEN RETURN 'one';
WHEN 10 THEN RETURN 'ten';
WHEN 20 THEN RETURN 'twenty';
ELSE RETURN 'other';
END CASE;
RETURN NULL;
END
Ниже приведены несколько примеров вызова с результатами и пояснениями:
SELECT simple_case(0); -- zero
SELECT simple_case(1); -- one
SELECT simple_case(-1); -- other (из предложения else)
SELECT simple_case(10); -- ten
SELECT simple_case(11); -- other (из предложения else)
SELECT simple_case(20); -- twenty
SELECT simple_case(100); -- other (из предложения else)
SELECT simple_case(null); -- null .. but really??
Второй пример SQL UDF с оператором CASE, на этот раз с двумя параметрами,
демонстрирующий важность порядка условий:
FUNCTION search_case(a bigint, b bigint)
RETURNS varchar
BEGIN
CASE
WHEN a = 0 THEN RETURN 'zero';
WHEN b = 1 THEN RETURN 'one';
WHEN a = DECIMAL '10.0' THEN RETURN 'ten';
WHEN b = 20.0E0 THEN RETURN 'twenty';
ELSE RETURN 'other';
END CASE;
RETURN NULL;
END
Ниже приведены несколько примеров вызова с результатами и пояснениями:
SELECT search_case(0,0); -- zero
SELECT search_case(1,1); -- one
SELECT search_case(0,1); -- zero (не one, поскольку вторая проверка не достигается)
SELECT search_case(10,1); -- one (не ten, поскольку третья проверка не достигается)
SELECT search_case(10,2); -- ten
SELECT search_case(10,20); -- ten (не twenty)
SELECT search_case(0,20); -- zero (не twenty)
SELECT search_case(3,20); -- twenty
SELECT search_case(3,21); -- other
SELECT simple_case(null,null); -- null .. but really??
Пример Fibonacci#
Этот SQL UDF вычисляет n-е значение в ряду Фибоначчи, где каждое число
является суммой двух предыдущих. Два начальных значения устанавливаются в 1
как значения по умолчанию для a и b. UDF использует условие IF, чтобы
вернуть 1 для всех входных значений 2 или меньше. Затем блок WHILE
начинает вычисление каждого числа в последовательности, начиная с a=1 и
b=1, и выполняется до достижения позиции n. На каждой итерации он задаёт
a и b как два предыдущих значения, чтобы вычислить сумму, и в конце
возвращает её. Обратите внимание, что выполнение UDF занимает всё больше
времени при увеличении значения n, и результат является deterministic:
FUNCTION fib(n bigint)
RETURNS bigint
BEGIN
DECLARE a, b bigint DEFAULT 1;
DECLARE c bigint;
IF n <= 2 THEN
RETURN 1;
END IF;
WHILE n > 2 DO
SET n = n - 1;
SET c = a + b;
SET a = b;
SET b = c;
END WHILE;
RETURN c;
END
Ниже приведены несколько примеров вызова с результатами и пояснениями:
SELECT fib(-1); -- 1
SELECT fib(0); -- 1
SELECT fib(1); -- 1
SELECT fib(2); -- 1
SELECT fib(3); -- 2
SELECT fib(4); -- 3
SELECT fib(5); -- 5
SELECT fib(6); -- 8
SELECT fib(7); -- 13
SELECT fib(8); -- 21
Метки и циклы#
Этот SQL UDF использует метку top для именования блока WHILE, а затем
управляет потоком с помощью условных операторов, ITERATE и LEAVE. Для
значений a=1 и a=2 в первых двух итерациях цикла вызов ITERATE
переносит выполнение к top до того, как b увеличивается. Затем b
увеличивается для значений a=3, a=4, a=5, a=6 и a=7, в результате
чего получается b=5. Затем вызов LEAVE приводит к выходу из блока до того,
как a увеличится до 10, и, следовательно, результат UDF равен 5:
FUNCTION labels()
RETURNS bigint
BEGIN
DECLARE a, b int DEFAULT 0;
top: WHILE a < 10 DO
SET a = a + 1;
IF a < 3 THEN
ITERATE top;
END IF;
SET b = b + 1;
IF a > 6 THEN
LEAVE top;
END IF;
END WHILE;
RETURN b;
END
Этот SQL UDF реализует вычисление n в степени p с помощью повторного
умножения и отслеживания количества выполненных умножений. Обратите внимание,
что этот SQL UDF не возвращает корректное значение 0 для p=0, поскольку
блок top просто пропускается, и возвращается значение n. Такое же
некорректное поведение наблюдается для отрицательных значений p:
FUNCTION power(n int, p int)
RETURNS int
BEGIN
DECLARE r int DEFAULT n;
top: LOOP
IF p <= 1 THEN
LEAVE top;
END IF;
SET r = r * n;
SET p = p - 1;
END LOOP;
RETURN r;
END
Ниже приведены несколько примеров вызова с результатами и пояснениями:
SELECT power(2, 2); -- 4
SELECT power(2, 8); -- 256
SELECT power(3, 3); -- 256
SELECT power(3, 0); -- 3, что неверно
SELECT power(3, -2); -- 3, что неверно
Этот SQL UDF возвращает 7 как результат увеличения b в цикле от
a=3 до a=10:
FUNCTION test_repeat_continue()
RETURNS bigint
BEGIN
DECLARE a int DEFAULT 0;
DECLARE b int DEFAULT 0;
top: REPEAT
SET a = a + 1;
IF a <= 3 THEN
ITERATE top;
END IF;
SET b = b + 1;
UNTIL a >= 10
END REPEAT;
RETURN b;
END
Этот SQL UDF возвращает 2 и демонстрирует, что метки могут повторяться,
и использование метки внутри блока относится к метке этого блока:
FUNCTION test()
RETURNS int
BEGIN
DECLARE r int DEFAULT 0;
abc: LOOP
SET r = r + 1;
LEAVE abc;
END LOOP;
abc: LOOP
SET r = r + 1;
LEAVE abc;
END LOOP;
RETURN r;
END
SQL UDF и встроенные функции#
Эта SQL UDF показывает, что в UDF можно использовать разные типы данных и
встроенные функции, такие как length() и cardinality(). Два вложенных блока
BEGIN также показывают, что имена переменных локальны внутри этих блоков x,
но к глобальной переменной r из блока верхнего уровня можно обращаться из
вложенных блоков:
FUNCTION test()
RETURNS bigint
BEGIN
DECLARE r bigint DEFAULT 0;
BEGIN
DECLARE x varchar DEFAULT 'hello';
SET r = r + length(x);
END;
BEGIN
DECLARE x array(int) DEFAULT array[1, 2, 3];
SET r = r + cardinality(x);
END;
RETURN r;
END
Пример необязательного параметра#
UDF могут вызывать другие UDF и другие функции. Полная сигнатура UDF состоит из имени UDF и параметров и определяет, какая именно UDF будет использоваться. Можно объявить несколько UDF с одинаковым именем, но с разным количеством аргументов или разными типами аргументов. Один из примеров использования — реализация необязательного параметра.
Следующая SQL UDF усекает строку до указанной длины, включая три точки в конце вывода:
FUNCTION dots(input varchar, length integer)
RETURNS varchar
BEGIN
IF length(input) > length THEN
RETURN substring(input, 1, length-3) || '...';
END IF;
RETURN input;
END;
Ниже приведены примеры вызовов и вывод:
SELECT dots('A long string that will be shortened',15);
-- A long strin...
SELECT dots('A short string',15);
-- A short string
Если нужно предоставить UDF с тем же именем, но без параметра длины, можно создать другую UDF, которая вызывает предыдущую UDF:
FUNCTION dots(input varchar)
RETURNS varchar
RETURN dots(input, 15);
Теперь можно использовать обе UDF. Если параметр длины опущен, используется значение по умолчанию из второго объявления.
SELECT dots('A long string that will be shortened',15);
-- A long strin...
SELECT dots('A long string that will be shortened');
-- A long strin...
SELECT dots('A long string that will be shortened',20);
-- A long string tha...
Пример разбора строк дат#
Этот пример SQL UDF разбирает строку даты типа VARCHAR в TIMESTAMP WITH TIME ZONE. Строки дат часто представлены по стандарту ISO 8601, например
2023-12-01, 2023-12-01T23. Строки дат также часто представлены в формате
YYYYmmdd и YYYYmmddHH, например 20230101 и 2023010123. Таблицы Hive
могут использовать этот формат для представления дневных и часовых партиций,
например /day=20230101, /hour=2023010123.
Эта UDF разбирает строки дат по принципу best effort и может использоваться как
замена функциям обработки строк дат, таким как date, date_parse,
from_iso8601_date и from_iso8601_timestamp.
Обратите внимание, что UDF по умолчанию задает значение времени
00:00:00.000, а часовой пояс — как часовой пояс сеанса:
FUNCTION from_date_string(date_string VARCHAR)
RETURNS TIMESTAMP WITH TIME ZONE
BEGIN
IF date_string like '%-%' THEN -- ISO 8601
RETURN from_iso8601_timestamp(date_string);
ELSEIF length(date_string) = 8 THEN -- YYYYmmdd
RETURN date_parse(date_string, '%Y%m%d');
ELSEIF length(date_string) = 10 THEN -- YYYYmmddHH
RETURN date_parse(date_string, '%Y%m%d%H');
END IF;
RETURN NULL;
END
Ниже приведены несколько примеров вызова с результатом и пояснением:
SELECT from_date_string('2023-01-01'); -- 2023-01-01 00:00:00.000 UTC (использует формат ISO 8601)
SELECT from_date_string('2023-01-01T23'); -- 2023-01-01 23:00:00.000 UTC (использует формат ISO 8601)
SELECT from_date_string('2023-01-01T23:23:23'); -- 2023-01-01 23:23:23.000 UTC (использует формат ISO 8601)
SELECT from_date_string('20230101'); -- 2023-01-01 00:00:00.000 UTC (использует формат YYYYmmdd)
SELECT from_date_string('2023010123'); -- 2023-01-01 23:00:00.000 UTC (использует формат YYYYmmddHH)
SELECT from_date_string(NULL); -- NULL (обрабатывает строку NULL)
SELECT from_date_string('abc'); -- NULL (не соответствует ни одному формату)
Человекочитаемые дни#
Trino включает встроенную функцию human_readable_seconds(), которая
форматирует количество секунд в строку:
SELECT human_readable_seconds(134823);
-- 1 day, 13 hours, 27 minutes, 3 seconds
Пример SQL UDF hrd форматирует количество дней в человекочитаемый текст,
который сообщает приблизительное количество лет и месяцев:
FUNCTION hrd(d integer)
RETURNS VARCHAR
BEGIN
DECLARE answer varchar default 'About ';
DECLARE years real;
DECLARE months real;
SET years = truncate(d/365);
IF years > 0 then
SET answer = answer || format('%1.0f', years) || ' year';
END IF;
IF years > 1 THEN
SET answer = answer || 's';
END IF;
SET d = d - cast( years AS integer) * 365 ;
SET months = truncate(d / 30);
IF months > 0 and years > 0 THEN
SET answer = answer || ' and ';
END IF;
IF months > 0 THEN
set answer = answer || format('%1.0f', months) || ' month';
END IF;
IF months > 1 THEN
SET answer = answer || 's';
END IF;
IF years < 1 and months < 1 THEN
SET answer = 'Less than 1 month';
END IF;
RETURN answer;
END;
Следующие примеры показывают вывод для диапазона значений меньше месяца, меньше года и разных больших значений:
SELECT hrd(10); -- Less than 1 month
SELECT hrd(95); -- About 3 months
SELECT hrd(400); -- About 1 year and 1 month
SELECT hrd(369); -- About 1 year
SELECT hrd(800); -- About 2 years and 2 months
SELECT hrd(1100); -- About 3 years
SELECT hrd(5000); -- About 13 years and 8 months
Улучшения SQL UDF могут включать следующие изменения:
Учитывать, что один месяц равен 30.4375 дня.
Учитывать, что один год равен 365.25 дня.
Добавить недели в вывод.
Расширить поддержку на десятилетия, века и тысячелетия.
Усечение длинных строк#
Этот пример SQL UDF strtrunc усекает строки длиннее 60 символов, оставляя
первые 30 и последние 25 символов и вырезая лишние символы в середине:
FUNCTION strtrunc(input VARCHAR)
RETURNS VARCHAR
RETURN
CASE WHEN length(input) > 60
THEN substr(input, 1, 30) || ' ... ' || substr(input, length(input) - 25)
ELSE input
END;
Предыдущее объявление очень компактное и состоит только из одного сложного
оператора с выражением CASE и несколькими вызовами
функций. Поэтому оно может определить всю логику в предложении RETURN.
Следующий оператор показывает ту же возможность внутри самой SQL UDF. Обратите
внимание на дублирующийся RETURN внутри и снаружи оператора CASE и на
обязательный END CASE;. Второй оператор RETURN необходим, потому что SQL
UDF должна заканчиваться оператором RETURN. В результате предложение ELSE
можно опустить:
FUNCTION strtrunc(input VARCHAR)
RETURNS VARCHAR
BEGIN
CASE WHEN length(input) > 60
THEN
RETURN substr(input, 1, 30) || ' ... ' || substr(input, length(input) - 25);
ELSE
RETURN input;
END CASE;
RETURN input;
END;
Следующий пример переходит от CASE к оператору IF и избегает
дублирующегося RETURN:
FUNCTION strtrunc(input VARCHAR)
RETURNS VARCHAR
BEGIN
IF length(input) > 60 THEN
RETURN substr(input, 1, 30) || ' ... ' || substr(input, length(input) - 25);
END IF;
RETURN input;
END;
Все предыдущие примеры создают одинаковый вывод. Ниже приведен пример запроса, который генерирует длинные строки для усечения:
WITH
data AS (
SELECT substring('strtrunc truncates strings longer than 60 characters,
leaving the prefix and suffix visible', 1, s.num) AS value
FROM table(sequence(start=>40, stop=>80, step=>5)) AS s(num)
)
SELECT
data.value
, strtrunc(data.value) AS truncated
FROM data
ORDER BY data.value;
Предыдущий запрос создает следующий вывод со всеми вариантами SQL UDF:
value | truncated
----------------------------------------------------------------------------------+---------------------------------------------------------------
strtrunc truncates strings longer than 6 | strtrunc truncates strings longer than 6
strtrunc truncates strings longer than 60 cha | strtrunc truncates strings longer than 60 cha
strtrunc truncates strings longer than 60 characte | strtrunc truncates strings longer than 60 characte
strtrunc truncates strings longer than 60 characters, l | strtrunc truncates strings longer than 60 characters, l
strtrunc truncates strings longer than 60 characters, leavin | strtrunc truncates strings longer than 60 characters, leavin
strtrunc truncates strings longer than 60 characters, leaving the | strtrunc truncates strings lon ... 60 characters, leaving the
strtrunc truncates strings longer than 60 characters, leaving the pref | strtrunc truncates strings lon ... aracters, leaving the pref
strtrunc truncates strings longer than 60 characters, leaving the prefix an | strtrunc truncates strings lon ... ers, leaving the prefix an
strtrunc truncates strings longer than 60 characters, leaving the prefix and suf | strtrunc truncates strings lon ... leaving the prefix and suf
Возможное улучшение — добавить параметры для общей длины.
Форматирование байтов#
Trino включает встроенную функцию format_number(). Однако она использует
единицы, которые плохо подходят для байтов. Следующая SQL UDF
format_data_size может форматировать большие значения в байтах в
человекочитаемую строку:
FUNCTION format_data_size(input BIGINT)
RETURNS VARCHAR
BEGIN
DECLARE value DOUBLE DEFAULT CAST(input AS DOUBLE);
DECLARE result BIGINT;
DECLARE base INT DEFAULT 1024;
DECLARE unit VARCHAR DEFAULT 'B';
DECLARE format VARCHAR;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'kB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'MB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'GB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'TB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'PB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'EB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'ZB';
END IF;
IF abs(value) >= base THEN
SET value = value / base;
SET unit = 'YB';
END IF;
IF abs(value) < 10 THEN
SET format = '%.2f';
ELSEIF abs(value) < 100 THEN
SET format = '%.1f';
ELSE
SET format = '%.0f';
END IF;
RETURN format(format, value) || unit;
END;
Ниже приведен запрос, который показывает, как она форматирует широкий диапазон значений:
WITH
data AS (
SELECT CAST(pow(10, s.p) AS BIGINT) AS num
FROM table(sequence(start=>1, stop=>18)) AS s(p)
UNION ALL
SELECT -CAST(pow(10, s.p) AS BIGINT) AS num
FROM table(sequence(start=>1, stop=>18)) AS s(p)
)
SELECT
data.num
, format_data_size(data.num) AS formatted
FROM data
ORDER BY data.num;
Предыдущий запрос создает следующий вывод:
num | formatted
----------------------+-----------
-1000000000000000000 | -888PB
-100000000000000000 | -88.8PB
-10000000000000000 | -8.88PB
-1000000000000000 | -909TB
-100000000000000 | -90.9TB
-10000000000000 | -9.09TB
-1000000000000 | -931GB
-100000000000 | -93.1GB
-10000000000 | -9.31GB
-1000000000 | -954MB
-100000000 | -95.4MB
-10000000 | -9.54MB
-1000000 | -977kB
-100000 | -97.7kB
-10000 | -9.77kB
-1000 | -1000B
-100 | -100B
-10 | -10.0B
0 | 0.00B
10 | 10.0B
100 | 100B
1000 | 1000B
10000 | 9.77kB
100000 | 97.7kB
1000000 | 977kB
10000000 | 9.54MB
100000000 | 95.4MB
1000000000 | 954MB
10000000000 | 9.31GB
100000000000 | 93.1GB
1000000000000 | 931GB
10000000000000 | 9.09TB
100000000000000 | 90.9TB
1000000000000000 | 909TB
10000000000000000 | 8.88PB
100000000000000000 | 88.8PB
1000000000000000000 | 888PB
Диаграммы#
В Trino уже есть встроенная функция цвета bar(), но она
использует escape-коды ANSI для вывода цветов и поэтому подходит только для
отображения результатов в терминале. Следующий пример показывает похожую SQL
UDF, которая использует только символы ASCII:
FUNCTION ascii_bar(value DOUBLE)
RETURNS VARCHAR
BEGIN
DECLARE max_width DOUBLE DEFAULT 40.0;
RETURN array_join(
repeat('█',
greatest(0, CAST(floor(max_width * value) AS integer) - 1)), '')
|| ARRAY[' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
[cast((value % (cast(1 as double) / max_width)) * max_width * 8 + 1 as int)];
END;
Ее можно использовать для визуализации значения:
WITH
data AS (
SELECT
cast(s.num as double) / 100.0 AS x,
sin(cast(s.num as double) / 100.0) AS y
FROM table(sequence(start=>0, stop=>314, step=>10)) AS s(num)
)
SELECT
data.x,
round(data.y, 4) AS y,
ascii_bar(data.y) AS chart
FROM data
ORDER BY data.x;
Предыдущий запрос создает следующий вывод:
x | y | chart
-----+--------+-----------------------------------------
0.0 | 0.0 |
0.1 | 0.0998 | ███
0.2 | 0.1987 | ███████
0.3 | 0.2955 | ██████████▉
0.4 | 0.3894 | ██████████████▋
0.5 | 0.4794 | ██████████████████▏
0.6 | 0.5646 | █████████████████████▋
0.7 | 0.6442 | ████████████████████████▊
0.8 | 0.7174 | ███████████████████████████▊
0.9 | 0.7833 | ██████████████████████████████▍
1.0 | 0.8415 | ████████████████████████████████▋
1.1 | 0.8912 | ██████████████████████████████████▋
1.2 | 0.932 | ████████████████████████████████████▎
1.3 | 0.9636 | █████████████████████████████████████▌
1.4 | 0.9854 | ██████████████████████████████████████▍
1.5 | 0.9975 | ██████████████████████████████████████▉
1.6 | 0.9996 | ███████████████████████████████████████
1.7 | 0.9917 | ██████████████████████████████████████▋
1.8 | 0.9738 | ██████████████████████████████████████
1.9 | 0.9463 | ████████████████████████████████████▉
2.0 | 0.9093 | ███████████████████████████████████▍
2.1 | 0.8632 | █████████████████████████████████▌
2.2 | 0.8085 | ███████████████████████████████▍
2.3 | 0.7457 | ████████████████████████████▉
2.4 | 0.6755 | ██████████████████████████
2.5 | 0.5985 | ███████████████████████
2.6 | 0.5155 | ███████████████████▋
2.7 | 0.4274 | ████████████████▏
2.8 | 0.335 | ████████████▍
2.9 | 0.2392 | ████████▋
3.0 | 0.1411 | ████▋
3.1 | 0.0416 | ▋
Также можно рисовать более компактные диаграммы. Ниже приведена SQL UDF, которая рисует вертикальные столбцы:
FUNCTION vertical_bar(value DOUBLE)
RETURNS VARCHAR
RETURN ARRAY[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][cast(value * 8 + 1 as int)];
Ее можно использовать для отображения распределения значений в одном столбце:
WITH
measurements(sensor_id, recorded_at, value) AS (
VALUES
('A', date '2023-01-01', 5.0)
, ('A', date '2023-01-03', 7.0)
, ('A', date '2023-01-04', 15.0)
, ('A', date '2023-01-05', 14.0)
, ('A', date '2023-01-08', 10.0)
, ('A', date '2023-01-09', 1.0)
, ('A', date '2023-01-10', 7.0)
, ('A', date '2023-01-11', 8.0)
, ('B', date '2023-01-03', 2.0)
, ('B', date '2023-01-04', 3.0)
, ('B', date '2023-01-05', 2.5)
, ('B', date '2023-01-07', 2.75)
, ('B', date '2023-01-09', 4.0)
, ('B', date '2023-01-10', 1.5)
, ('B', date '2023-01-11', 1.0)
),
days AS (
SELECT date_add('day', s.num, date '2023-01-01') AS day
-- аргументы табличной функции должны быть константами, но диапазон можно вычислить
-- с помощью: SELECT date_diff('day', max(recorded_at), min(recorded_at)) FROM measurements
FROM table(sequence(start=>0, stop=>10)) AS s(num)
),
sensors(id) AS (VALUES ('A'), ('B')),
normalized AS (
SELECT
sensors.id AS sensor_id,
days.day,
value,
value / max(value) OVER (PARTITION BY sensor_id) AS normalized
FROM days
CROSS JOIN sensors
LEFT JOIN measurements m ON day = recorded_at AND m.sensor_id = sensors.id
)
SELECT
sensor_id,
min(day) AS start,
max(day) AS stop,
count(value) AS num_values,
min(value) AS min_value,
max(value) AS max_value,
avg(value) AS avg_value,
array_join(array_agg(coalesce(vertical_bar(normalized), ' ') ORDER BY day),
'') AS distribution
FROM normalized
WHERE sensor_id IS NOT NULL
GROUP BY sensor_id
ORDER BY sensor_id;
Предыдущий запрос создает следующий вывод:
sensor_id | start | stop | num_values | min_value | max_value | avg_value | distribution
-----------+------------+------------+------------+-----------+-----------+-----------+--------------
A | 2023-01-01 | 2023-01-11 | 8 | 1.00 | 15.00 | 8.38 | ▃ ▄█▇ ▅▁▄▄
B | 2023-01-01 | 2023-01-11 | 7 | 1.00 | 4.00 | 2.39 | ▄▆▅ ▆ █▃▂
Top-N#
В Trino уже есть встроенная агрегатная функция
approx_most_frequent(), которая может вычислять наиболее часто встречающиеся
значения. Она возвращает map, где ключи — это значения, а значения map —
количество вхождений. Map не упорядочены, поэтому при отображении элементы
могут меняться местами при последующих запусках того же запроса, а читателям
все равно нужно сравнивать все частоты, чтобы найти одно наиболее частое
значение. Ниже приведена SQL UDF, которая возвращает упорядоченные результаты
как строку:
FUNCTION format_topn(input map<varchar, bigint>)
RETURNS VARCHAR
NOT DETERMINISTIC
BEGIN
DECLARE freq_separator VARCHAR DEFAULT '=';
DECLARE entry_separator VARCHAR DEFAULT ', ';
RETURN array_join(transform(
reverse(array_sort(transform(
transform(
map_entries(input),
r -> cast(r AS row(key varchar, value bigint))
),
r -> cast(row(r.value, r.key) AS row(value bigint, key varchar)))
)),
r -> r.key || freq_separator || cast(r.value as varchar)),
entry_separator);
END;
Ниже приведен пример запроса для подсчета сгенерированных строк:
WITH
data AS (
SELECT lpad('', 3, chr(65+(s.num / 3))) AS value
FROM table(sequence(start=>1, stop=>10)) AS s(num)
),
aggregated AS (
SELECT
array_agg(data.value ORDER BY data.value) AS all_values,
approx_most_frequent(3, data.value, 1000) AS top3
FROM data
)
SELECT
a.all_values,
a.top3,
format_topn(a.top3) AS top3_formatted
FROM aggregated a;
Предыдущий запрос создает следующий результат:
all_values | top3 | top3_formatted
----------------------------------------------------+-----------------------+---------------------
[AAA, AAA, BBB, BBB, BBB, CCC, CCC, CCC, DDD, DDD] | {AAA=2, CCC=3, BBB=3} | CCC=3, BBB=3, AAA=2