Lambda-выражения и функции высшего порядка над массивами
В прошлых уроках мы видели один способ работать с массивами — UNNEST: развернуть массив в строки, обработать реляционно, при необходимости собрать обратно. Способ рабочий, но тяжёлый: разворот в строки и сборка назад — это лишний exchange, лишняя материализация. Часто массив нужно преобразовать «на месте», не покидая строку: применить функцию к каждому элементу, отфильтровать элементы, свернуть массив в одно значение.
Для этого в Trino есть функции высшего порядка — функции, которые принимают в качестве аргумента другую функцию. А функция-аргумент записывается lambda-выражением. Этот урок — про синтаксис lambda и про три ключевые функции высшего порядка: transform, filter, reduce.
Lambda-выражение: функция как значение
Lambda-выражение — это анонимная функция, записанная прямо в SQL и передаваемая как аргумент. Синтаксис лаконичен:
параметр -> выражение
(параметр1, параметр2) -> выражение
Слева от -> — имена параметров, справа — выражение, которое функция вычисляет. x -> x * 2 — функция «удвоить аргумент». (x, y) -> x + y — функция «сложить два аргумента». Lambda не вызывается сама по себе; она передаётся в функцию высшего порядка, которая решает, с какими значениями и сколько раз её применить.
Это знакомая идея из любого современного языка — map, filter, reduce над коллекциями. SQL долго обходился без неё, и работа с массивами сводилась к UNNEST. Функции высшего порядка вернули в SQL функциональный стиль обработки коллекций — и сделали его частью декларативного запроса.
transform: применить функцию к каждому элементу
transform(array, x -> expr) применяет lambda к каждому элементу массива и возвращает новый массив той же длины с преобразованными элементами. Это map в терминах функционального программирования.
SELECT transform(ARRAY[1, 2, 3, 4], x -> x * x) AS squares,
transform(ARRAY['anna','BEN'], s -> upper(s)) AS uppered;
squares | uppered
--------------+--------------
[1, 4, 9, 16]| [ANNA, BEN]
Lambda здесь — однопараметрическая: параметр x (или s) по очереди принимает значение каждого элемента. Длина результата всегда равна длине входа — transform ничего не выбрасывает, только преобразует.
Практический смысл: пересчитать цены в массиве позиций заказа с учётом скидки, нормализовать строки, привести типы элементов — всё это transform, без разворота в строки. У transform есть и двухаргументная форма transform(array1, array2, (x, y) -> expr) — она проходит два массива параллельно, попарно.
filter: оставить элементы по предикату
filter(array, x -> predicate) применяет lambda-предикат к каждому элементу и возвращает новый массив только из тех элементов, для которых предикат истинен. Длина результата меньше либо равна длине входа.
SELECT filter(ARRAY[1, -3, 5, -2, 8], x -> x > 0) AS positives,
filter(ARRAY['', 'data', '', 'lake'], s -> s <> '') AS non_empty;
positives | non_empty
-------------+--------------
[1, 5, 8] | [data, lake]
Lambda для filter обязана возвращать BOOLEAN — это предикат. filter — инструмент очистки массива: убрать отрицательные значения, пустые строки, NULL-элементы, оставить элементы, удовлетворяющие условию. В связке с cardinality он же отвечает на вопрос «сколько элементов массива удовлетворяют условию»: cardinality(filter(arr, x -> ...)).
reduce: свернуть массив в одно значение
reduce — самая мощная и наименее очевидная. Она сворачивает массив в одно скалярное значение, последовательно накапливая результат. Это fold функционального программирования. Сигнатура:
reduce(array, начальное_состояние, (состояние, элемент) -> новое_состояние, состояние -> результат)
Четыре аргумента. Первый — массив. Второй — начальное состояние аккумулятора. Третий — lambda-комбинатор: берёт текущее состояние и очередной элемент, возвращает новое состояние; она применяется к каждому элементу по очереди. Четвёртый — финальная lambda: преобразует итоговое состояние в результат (часто — тождественная s -> s).
SELECT reduce(ARRAY[1, 2, 3, 4], 0, (s, x) -> s + x, s -> s) AS arr_sum,
reduce(ARRAY[3, 7, 2, 9], 0, (s, x) -> greatest(s, x), s -> s) AS arr_max;
arr_sum | arr_max
---------+---------
10 | 9
Проследим arr_sum. Начальное состояние 0. Элемент 1: 0 + 1 = 1. Элемент 2: 1 + 2 = 3. Элемент 3: 3 + 3 = 6. Элемент 4: 6 + 4 = 10. Финальная lambda s -> s отдаёт 10. reduce способна выразить любую агрегацию по массиву: сумму, максимум, конкатенацию, подсчёт по условию, и более сложные свёртки, где состояние — не число, а структура.
Композиция и почему это эффективнее UNNEST
Сила функций высшего порядка — в композиции. Они вкладываются друг в друга, образуя конвейер обработки массива внутри одного выражения:
-- средний квадрат положительных элементов массива
SELECT reduce(
transform(filter(arr, x -> x > 0), x -> x * x),
CAST(ROW(0, 0) AS ROW(total INTEGER, cnt INTEGER)),
(s, x) -> CAST(ROW(s.total + x, s.cnt + 1) AS ROW(total INTEGER, cnt INTEGER)),
s -> IF(s.cnt = 0, 0e0, CAST(s.total AS DOUBLE) / s.cnt)
) AS avg_sq_positive
FROM (VALUES ARRAY[2, -1, 3, -5, 4]) AS t(arr);
avg_sq_positive
-----------------
9.666666666666666
Здесь filter оставил положительные, transform возвёл в квадрат, reduce со структурой-состоянием посчитал сумму и количество разом и в финале вернул среднее. Целая аналитическая операция над массивом — одним выражением, без единого UNNEST.
Почему это «до железа» лучше UNNEST. UNNEST физически разворачивает массив в строки: одна строка с массивом из 100 элементов становится 100 строками. Это материализация — новые строки в Page’ах, и если потом нужно собрать результат обратно в массив (array_agg с GROUP BY), добавляется ещё стадия группировки с exchange. Функции высшего порядка обрабатывают массив на месте, внутри значения, не покидая строку и не плодя строк: меньше материализации, нет лишнего exchange. Когда задача — преобразовать массив, а не присоединить его элементы к другим таблицам, функции высшего порядка и быстрее, и читаются как единое выражение.
Ориентир выбора. Если элементы массива нужно соединить с другими таблицами или сгруппировать вместе со строками из разных массивов — нужен UNNEST: задача реляционная. Если массив нужно преобразовать, отфильтровать или свернуть «в себе», не выходя за пределы строки, — берите transform / filter / reduce: меньше материализации, нет лишних стадий, и выражение остаётся компактным.
Другие функции высшего порядка
Кроме трёх ключевых Trino несёт ещё ряд функций высшего порядка над массивами и MAP — назовём, чтобы знать об их существовании:
| Функция | Что делает |
|---|---|
any_match(array, x -> pred) | истина, если хотя бы один элемент удовлетворяет предикату |
all_match(array, x -> pred) | истина, если все элементы удовлетворяют предикату |
none_match(array, x -> pred) | истина, если ни один не удовлетворяет |
array_sort(array, (a, b) -> ...) | сортировка массива с пользовательским компаратором |
transform_values(map, (k, v) -> ...) | применить lambda к значениям MAP |
map_filter(map, (k, v) -> pred) | отфильтровать пары MAP по предикату |
any_match / all_match / none_match отвечают на вопросы существования и универсальности по массиву без явного разворота — короткая и читаемая замена связке cardinality(filter(...)). transform_values и map_filter переносят ту же идею функций высшего порядка с массивов на MAP.
Попробуй сам
На песочнице курса (Trino 481):
-
Выполните
SELECT transform(ARRAY[1,2,3,4,5], x -> x * 10);иSELECT filter(ARRAY[1,2,3,4,5], x -> x % 2 = 0);. Объясните разницу в длине результата относительно входа дляtransformи дляfilterи почему она именно такая. -
Разберите
reduceпошагово. ВыполнитеSELECT reduce(ARRAY[5,1,8,3], 0, (s,x) -> greatest(s,x), s -> s);. Распишите на бумаге, какое значение принимает состояниеsпосле каждого из четырёх элементов, и какой результат вернёт финальная lambda. -
Рассуждение в двух абзацах. Дана таблица заказов с массивом-полем
item_prices ARRAY(DECIMAL(10,2)). Задача А: посчитать сумму цен внутри каждого заказа, оставив одну строку на заказ. Задача Б: получить плоскую таблицу «заказ, цена позиции» по строке на позицию. Для какой задачи возьмётеreduce, для какой —UNNEST, и почему — свяжите ответ с тем, плодит ли операция строки и нужна ли сборка обратно.