Learning Platform
Глоссарий Troubleshooting
Урок 04.06 · 22 мин
Средний
friendly-sqllistsstructslambda

List comprehensions, slicing и dot-оператор

SQL исторически плохо работает с коллекциями внутри строки. Если в колонке лежит список чисел, преобразовать каждый элемент стандартными средствами — это UNNEST, обработка развёрнутых строк и обратная сборка через агрегат. Три шага ради операции, которая в любом языке программирования — одна строка. То же со вложенными структурами: достать поле из STRUCT без удобного синтаксиса доступа неудобно.

DuckDB заимствует решение прямо из Python. List comprehensions (списочные выражения), slicing (срезы с отрицательными индексами) и dot-оператор для доступа к полям делают работу с LIST и STRUCT такой же лаконичной, как в Python. Это не отдельный «процедурный» язык поверх SQL — это выражения, которые движок исполняет векторизованно, как и любые другие. В этом уроке разберём все три и заодно зафиксируем актуальный синтаксис лямбд.


List comprehensions

Списочное выражение строит новый список из существующего: применяет преобразование к каждому элементу и опционально отфильтровывает часть. Синтаксис скопирован у Python почти дословно:

-- Удвоить каждый элемент списка
SELECT [x * 2 for x in [1, 2, 3, 4]] AS doubled;
-- результат: [2, 4, 6, 8]

-- С фильтром: только чётные, затем удвоить
SELECT [x * 2 for x in [1, 2, 3, 4, 5, 6] if x % 2 = 0] AS r;
-- результат: [4, 8, 12]

Структура — [выражение for переменная in список if условие]. for ... in перебирает элементы, выражение слева вычисляется для каждого, if (опционально) оставляет только подходящие. Читается как «список из x*2 для каждого x из исходного списка, где x чётное».

Применяется это к колонкам-спискам так же, как к литералам:

-- Таблица baskets: id INT, prices INT[]
-- Применить скидку 10% к каждой цене в каждой корзине
SELECT
    id,
    [round(p * 0.9, 2) for p in prices] AS discounted
FROM baskets;

Под капотом list comprehension — это синтаксический сахар над функциями list_transform и list_filter. Запрос выше эквивалентен list_transform(prices, lambda p: round(p * 0.9, 2)). DuckDB разворачивает comprehension в эти функции на этапе биндинга, а исполняются они векторизованно — без построчного интерпретатора, без UNNEST и обратной сборки.

List comprehension разворачивается в list-функции
[x*2 for x in xs if x>0]Питоновская запись списочного выражения: компактно и читаемо, привычно для любого, кто знает Python.
binder разворачивает
list_transform + list_filterDuckDB переписывает comprehension в вызовы встроенных list-функций с лямбдами. if становится list_filter, выражение слева — list_transform.
векторизованное исполнение
новый списокФункции исполняются движком векторизованно, по батчам значений, без построчного интерпретатора.

Синтаксис лямбд: актуальная форма

Лямбды нужны для list-функций (list_transform, list_filter, list_reduce) и для COLUMNS(). С версии DuckDB 1.5 актуальный синтаксис лямбды — питоновский, со словом lambda и двоеточием:

-- Актуальная форма (DuckDB 1.5+): lambda параметр: тело
SELECT list_transform([1, 2, 3], lambda x: x + 1) AS r;
-- результат: [2, 3, 4]

SELECT list_filter([1, -2, 3, -4], lambda x: x > 0) AS positives;
-- результат: [1, 3]

Раньше DuckDB использовал стрелочную форму x -> x + 1. В версии 1.5 стрелочный синтаксис объявлен устаревшим (deprecated). Причина — конфликт: стрелка -> уже занята оператором извлечения из JSON (json_col -> '$.field'), и совмещение двух смыслов одного оператора создавало неоднозначность парсинга. Питоновская форма lambda x: устраняет конфликт и заодно знакома большинству. Поведением управляет настройка lambda_syntax, но в новом коде стоит сразу писать lambda-форму — это то, что будет жить дальше.

-- Лямбда с двумя параметрами: list_reduce сворачивает список
SELECT list_reduce([1, 2, 3, 4], lambda acc, x: acc + x) AS total;
-- результат: 10
WARNING

Стрелочная форма x -> x + 1 всё ещё может работать в зависимости от настройки lambda_syntax, но она устарела с DuckDB 1.5 и будет удалена. В новом коде используйте только питоновскую форму lambda x: x + 1. Если видите стрелочную форму в старых примерах из интернета — это признак, что пример написан до 1.5.


Slicing: срезы и отрицательные индексы

Slicing достаёт подсписок (или подстроку) по диапазону позиций. Синтаксис — квадратные скобки с двоеточием, как в Python: list[начало:конец].

Первая особенность, к которой надо привыкнуть: в DuckDB индексация списков начинается с 1, а не с 0. list[1] — первый элемент. Срез list[start:end] включает обе границы.

-- Элементы со 2-го по 4-й включительно
SELECT ([10, 20, 30, 40, 50])[2:4] AS slice;
-- результат: [20, 30, 40]

Вторая особенность — отрицательные индексы, отсчёт с конца. [-1] — последний элемент, [-2] — предпоследний:

-- Последний элемент
SELECT ([10, 20, 30, 40, 50])[-1] AS last_elem;
-- результат: 50

-- Последние три элемента: от -3 до конца
SELECT ([10, 20, 30, 40, 50])[-3:] AS last_three;
-- результат: [30, 40, 50]

-- Всё, кроме последнего элемента
SELECT ([10, 20, 30, 40, 50])[:-2] AS without_last;
-- результат: [10, 20, 30, 40]

Пропущенная граница означает «до конца» ([2:]) или «с начала» ([:3]). Отрицательные индексы убирают типовую боль SQL: «достать последний элемент» без них требует знать длину списка (list[len(list)]), а [-1] работает независимо от длины.

Тот же синтаксис среза работает для строк — строка ведёт себя как список символов:

-- Первые 3 символа и последние 4
SELECT ('analytics')[1:3] AS prefix, ('analytics')[-4:] AS suffix;
-- prefix: 'ana', suffix: 'tics'
СрезЧто возвращает
xs[2:4]элементы со 2-го по 4-й включительно
xs[-1]последний элемент
xs[-3:]последние три элемента
xs[:3]первые три элемента
xs[:-1]все, кроме последнего

Dot-оператор для struct и chaining

STRUCT — это запись с именованными полями (детально разбирается в модуле про систему типов). Доступ к полю STRUCT в DuckDB — через точку, как к атрибуту объекта в Python:

-- Колонка address типа STRUCT(city VARCHAR, zip VARCHAR)
SELECT
    address.city AS city,
    address.zip  AS zip
FROM customers;

address.city достаёт поле city из структуры address. Точечный доступ можно вкладывать для структур внутри структур: event.payload.user_id спускается на два уровня. Это намного читаемее, чем функция извлечения struct_extract(address, 'city') — хотя под капотом это она и есть.

Второе применение точки — chaining функций. Любую функцию f(x, ...) можно вызвать как x.f(...) — значение слева от точки становится первым аргументом функции. Это превращает вложенные вызовы в читаемую цепочку:

-- Вложенная запись: трудно читать изнутри наружу
SELECT trim(upper(replace(name, '_', ' ')));

-- Цепочка через точку: читается слева направо, в порядке применения
SELECT name.replace('_', ' ').upper().trim();

Обе строки делают одно и то же. name.replace('_', ' ') — это replace(name, '_', ' '). Результат идёт дальше в .upper(), то есть upper(...), и так далее. Цепочка читается в том порядке, в котором операции выполняются — слева направо, — а не «изнутри наружу», как вложенные вызовы.

Chaining: вложенные вызовы как цепочка
nameИсходное значение — первое звено цепочки. Стоит слева, дальше операции применяются по порядку.
.replace()
без подчёркиванийx.replace(...) эквивалентно replace(x, ...). Значение слева от точки — первый аргумент функции.
.upper()
верхний регистрРезультат предыдущего звена становится входом следующего вызова через точку.
.trim()
результатФинальное значение цепочки. Порядок чтения совпадает с порядком исполнения.
TIP

Chaining и точечный доступ к struct используют один и тот же оператор ., и DuckDB различает их по контексту: если слева STRUCT и справа имя его поля — это доступ к полю; если справа вызов функции — это chaining. Сомневаетесь — всегда можно вернуться к явной форме struct_extract(s, 'field') и f(x, ...), она полностью эквивалентна.

Все четыре конструкции — comprehensions, lambda-форма, slicing, dot-оператор — объединяет одно: они приносят в SQL эргономику языка программирования, не превращая его в процедурный язык. Это по-прежнему декларативные выражения, которые оптимизатор видит насквозь и исполняет векторизованно. Вы просто пишете их так же компактно, как написали бы в Python.


Попробуй сам

Создайте таблицу:

CREATE TABLE baskets AS
  SELECT * FROM (VALUES
    (1, [100, 200, 50, 300]),
    (2, [80, 120]),
  ) t(id, prices);

Задания:

  1. Через list comprehension постройте для каждой корзины список цен с 10-процентной скидкой.
  2. Через list comprehension с if оставьте в каждой корзине только цены больше 100.
  3. Перепишите задание 1 явно через list_transform с лямбдой в актуальной форме lambda x: .... Убедитесь в идентичности результата.
  4. Для каждой корзины через slicing достаньте последние две цены (prices[-2:]) и всё, кроме первой цены (prices[2:]).
  5. Возьмите строку 'data_engineering' и через chaining приведите её к виду 'DATA ENGINEERING': замените подчёркивание на пробел и переведите в верхний регистр. Запишите то же самое вложенными вызовами и сравните читаемость.
Массивы в PostgreSQL: UNNEST и работа с коллекциями
Проверка знанийKnowledge check
Какая форма синтаксиса лямбд актуальна в DuckDB начиная с версии 1.5 и почему, и во что разворачивается list comprehension?
ОтветAnswer
С DuckDB 1.5 актуальна питоновская форма лямбды — со словом lambda и двоеточием: lambda x: x + 1, lambda acc, x: acc + x для нескольких параметров. Прежняя стрелочная форма x -> x + 1 объявлена устаревшей (deprecated) и будет удалена. Причина — конфликт: оператор -> уже занят извлечением поля из JSON (json_col -> '$.field'), и один и тот же символ с двумя смыслами создавал неоднозначность парсинга. Питоновская форма lambda x: устраняет конфликт и привычна большинству разработчиков. Поведением управляет настройка lambda_syntax, но в новом коде нужно сразу писать lambda-форму. List comprehension вида [выражение for x in список if условие] — это синтаксический сахар: DuckDB на этапе биндинга разворачивает его в вызовы встроенных list-функций — list_transform для преобразующего выражения и list_filter для части if. Получившиеся вызовы с лямбдами исполняются движком векторизованно, по батчам значений, без построчного интерпретатора и без UNNEST с обратной сборкой. Поэтому списочные выражения остаются декларативными и быстрыми, просто записываются так же компактно, как в Python.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какая форма синтаксиса лямбд актуальна в DuckDB начиная с версии 1.5?

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

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

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

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