Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 22 мин
Средний
sqllambdahigher-order-functionsarray

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 функциональный стиль обработки коллекций — и сделали его частью декларативного запроса.

Функция высшего порядка принимает lambda
МассивВходной ARRAY, например ARRAY[1, 2, 3, 4]
+ lambda x -> ...
Функция высшего порядкаtransform, filter или reduce. Применяет переданную lambda к элементам массива по своим правилам
результат
Новый массив или значениеtransform и filter возвращают массив, reduce — одно скалярное значение

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 способна выразить любую агрегацию по массиву: сумму, максимум, конкатенацию, подсчёт по условию, и более сложные свёртки, где состояние — не число, а структура.

reduce: накопление состояния по элементам
Начальное состояние: 0Второй аргумент reduce — стартовое значение аккумулятора
элемент 1: state + 1
Состояние: 1Комбинатор применён к первому элементу: 0 + 1
элемент 2: state + 2
Состояние: 3Комбинатор применён ко второму элементу: 1 + 2
... остальные элементы
Финальная lambda -> результатЧетвёртая lambda преобразует итоговое состояние в результат, часто тождественно

Композиция и почему это эффективнее 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. Когда задача — преобразовать массив, а не присоединить его элементы к другим таблицам, функции высшего порядка и быстрее, и читаются как единое выражение.

TIP

Ориентир выбора. Если элементы массива нужно соединить с другими таблицами или сгруппировать вместе со строками из разных массивов — нужен UNNEST: задача реляционная. Если массив нужно преобразовать, отфильтровать или свернуть «в себе», не выходя за пределы строки, — берите transform / filter / reduce: меньше материализации, нет лишних стадий, и выражение остаётся компактным.

DuckDB: list_transform, list_filter и lambda-синтаксисSQL: функции над массивами в PostgreSQL

Другие функции высшего порядка

Кроме трёх ключевых 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):

  1. Выполните 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 и почему она именно такая.

  2. Разберите reduce пошагово. Выполните SELECT reduce(ARRAY[5,1,8,3], 0, (s,x) -> greatest(s,x), s -> s);. Распишите на бумаге, какое значение принимает состояние s после каждого из четырёх элементов, и какой результат вернёт финальная lambda.

  3. Рассуждение в двух абзацах. Дана таблица заказов с массивом-полем item_prices ARRAY(DECIMAL(10,2)). Задача А: посчитать сумму цен внутри каждого заказа, оставив одну строку на заказ. Задача Б: получить плоскую таблицу «заказ, цена позиции» по строке на позицию. Для какой задачи возьмёте reduce, для какой — UNNEST, и почему — свяжите ответ с тем, плодит ли операция строки и нужна ли сборка обратно.


Проверка знанийKnowledge check
Что такое функция высшего порядка и lambda-выражение в Trino, чем отличаются transform, filter и reduce, и почему обработка массива функциями высшего порядка эффективнее UNNEST, когда массив надо преобразовать «в себе»?
ОтветAnswer
Функция высшего порядка — это функция, которая принимает в качестве аргумента другую функцию. Функция-аргумент записывается lambda-выражением — анонимной функцией вида параметр -> выражение, которая не вызывается сама по себе, а передаётся в функцию высшего порядка. Три ключевые функции различаются тем, что делают с массивом. transform применяет lambda к каждому элементу и возвращает новый массив той же длины с преобразованными элементами — это map. filter применяет lambda-предикат, возвращающий BOOLEAN, и возвращает новый массив только из элементов, для которых предикат истинен, — длина результата меньше или равна входу. reduce сворачивает массив в одно скалярное значение: она берёт начальное состояние и lambda-комбинатор (состояние, элемент) -> новое состояние, применяет комбинатор к каждому элементу по очереди, накапливая состояние, и финальной lambda преобразует итог в результат — это fold. Эффективнее UNNEST они потому, что UNNEST физически разворачивает массив в строки: одна строка с массивом из 100 элементов становится 100 строками — это материализация новых строк в Page'ах, а если результат нужно собрать обратно в массив через array_agg с GROUP BY, добавляется ещё стадия группировки с exchange. Функции высшего порядка обрабатывают массив на месте, внутри значения, не покидая строку и не плодя строк: меньше материализации, нет лишнего exchange. Поэтому когда задача — преобразовать, отфильтровать или свернуть массив «в себе», а не соединить его элементы с другими таблицами, функции высшего порядка и быстрее, и компактнее.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое функция высшего порядка в Trino?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7