Learning Platform
Глоссарий Troubleshooting
Урок 08.06 · 23 мин
Средний
sqlmergedmltable-functions

MERGE, UPDATE, DELETE и полиморфные табличные функции

Долгое время Trino воспринимали как движок только для чтения: подключи источники, читай и федеративно джойни. Это давно не так. Trino полноценно меняет данные в источниках, которые это поддерживают: INSERT, DELETE, UPDATE и сводящий их воедино MERGE. И есть отдельный, мощный механизм — полиморфные табличные функции (PTF), которые расширяют сам набор того, что может стоять в FROM.

Этот урок — про DML-операции в Trino, про то, почему их возможность зависит от коннектора и формата таблицы, и про PTF как точку расширяемости движка.


Почему DML зависит от коннектора

Сначала принципиальное. Trino — движок без своего хранилища. Когда вы пишете DELETE FROM, Trino сам ничего не удаляет — он переводит операцию в действия коннектора над источником. А источники по-разному способны менять данные.

Простой INSERT поддерживают почти все пишущие коннекторы. А вот построчные UPDATE и DELETE — операция нетривиальная для файловых форматов. Parquet и ORC иммутабельны: файл нельзя «отредактировать», изменив одну строку. Поэтому row-level изменения требуют, чтобы формат таблицы умел их выражать поверх иммутабельных файлов.

Современные форматы таблиц это умеют. Iceberg и Delta Lake поддерживают row-level операции через механизм delete-файлов (delete files): вместо переписывания файла данных создаётся отдельный файл, помечающий, какие строки считать удалёнными; при чтении движок применяет эти пометки. Для Iceberg это требует формата таблицы версии v2 или выше. Hive поддерживает UPDATE/DELETE только для транзакционных ACID-таблиц формата ORC.

Вывод, который надо держать в голове: возможность MERGE/UPDATE/DELETE — это свойство пары «коннектор + формат таблицы», а не самого Trino. На обычной нетранзакционной Hive-таблице в Parquet построчный DELETE работать не будет; на таблице Iceberg v2 — будет.

DML проходит через коннектор
SQL: DELETE / UPDATE / MERGEКоманда DML, отправленная клиентом в Trino
Trino планирует
КоннекторTrino переводит операцию в действия коннектора. Сам ничего не хранит и не удаляет
зависит от формата
ИсточникIceberg v2+, Delta — row-level через delete-файлы. Hive — только ACID ORC. Обычный Parquet в Hive — построчный DELETE недоступен

DELETE и UPDATE

DELETE удаляет строки, удовлетворяющие условию WHERE:

DELETE FROM iceberg.shop.orders
WHERE orderstatus = 'CANCELLED' AND orderdate < DATE '2024-01-01';

Есть важный частный случай. Если условие WHERE совпадает с границами партиций — например, удаляются целиком партиции за старые даты, — коннектор может выполнить metadata delete: не трогать файлы данных вообще, а лишь обновить метаданные таблицы, исключив целые файлы или партиции. Это операция уровня метаданных, мгновенная независимо от объёма данных в удаляемых партициях. Если же условие отсекает строки внутри файлов, включается row-level механизм с delete-файлами — он дороже.

UPDATE меняет значения столбцов в строках по условию:

UPDATE iceberg.shop.orders
SET orderstatus = 'ARCHIVED'
WHERE orderdate < DATE '2023-01-01';

Под капотом для иммутабельных форматов UPDATE — это, по сути, «удалить старую версию строки и записать новую»: затронутые строки помечаются удалёнными через delete-файл, а их обновлённые версии дописываются новыми файлами данных.


MERGE: upsert одной командой

MERGE — самая мощная DML-команда. Она сопоставляет целевую таблицу с источником по условию и в зависимости от того, нашлось совпадение или нет, выполняет разные действия: обновить, удалить или вставить. Это каноничный upsert (update-or-insert) и основа инкрементальной загрузки данных.

MERGE INTO iceberg.shop.customers AS t
USING staging.customer_updates AS s
  ON t.customer_id = s.customer_id
WHEN MATCHED AND s.is_deleted = true
  THEN DELETE
WHEN MATCHED
  THEN UPDATE SET name = s.name, email = s.email, updated_at = s.updated_at
WHEN NOT MATCHED
  THEN INSERT (customer_id, name, email, updated_at)
       VALUES (s.customer_id, s.name, s.email, s.updated_at);

Разберём структуру. MERGE INTO target USING source ON условие задаёт цель, источник и правило сопоставления. Дальше — ветки WHEN:

  • WHEN MATCHED — строка цели нашла пару в источнике. Действие — UPDATE или DELETE. Веток WHEN MATCHED может быть несколько с дополнительными AND-условиями: в примере первая ветка удаляет помеченные на удаление, вторая обновляет остальные совпавшие.
  • WHEN NOT MATCHED — строка источника не нашла пары в цели. Действие — INSERT.

Один MERGE заменяет связку из отдельных UPDATE, DELETE и INSERT, и делает это согласованно, за один проход сопоставления. Именно MERGE — рабочая лошадка инкрементального обновления таблиц лейкхауса: в источнике лежит «дельта» изменений, MERGE вмёрживает её в большую таблицу.

dbt: incremental-модели используют MERGE/DELETE+INSERT под капотом Apache Iceberg: delete-файлы как основа row-level операций
MERGE: одно сопоставление, три исхода
target JOIN source ON условиеMERGE сопоставляет целевую таблицу и источник по ON-условию
для каждой пары/строки
MATCHED -> UPDATEСтрока цели нашла пару в источнике: обновить её значения
MATCHED -> DELETEСовпало и выполнено доп. условие AND: удалить строку цели
NOT MATCHED -> INSERTСтрока источника не нашла пары в цели: вставить новую строку
WARNING

MERGE физически выполняет всю работу row-level операций: для иммутабельных форматов совпавшие строки помечаются удалёнными через delete-файлы, а обновлённые и вставленные дописываются новыми файлами данных. Частый и интенсивный MERGE мелкими порциями быстро накапливает множество мелких файлов данных и delete-файлов — это потом замедляет чтение. Поэтому MERGE в лейкхаусе идёт в паре с регулярным обслуживанием таблицы: компакцией мелких файлов через ALTER TABLE ... EXECUTE optimize. Планируя инкрементальную загрузку через MERGE, сразу планируйте и обслуживание таблицы.


Полиморфные табличные функции

Теперь другая тема — расширяемость. Обычно в FROM стоит таблица или подзапрос. Но что если нужно нечто, чего реляционная модель не выражает: выполнить «родной» запрос прямо в источнике, сгенерировать данные, разобрать нестандартный формат? Для этого есть табличные функции — функции, которые возвращают не скаляр, а целую таблицу, и потому стоят в FROM.

Большинство полезных табличных функций — полиморфные (PTF, polymorphic table functions). «Полиморфная» означает: схема результата (имена и типы столбцов) не фиксирована заранее, а определяется в момент вызова — по аргументам функции. Одна и та же PTF при разных аргументах вернёт таблицы с разной схемой. Обычная функция так не умеет — у неё фиксированный тип результата.

Самый важный практический пример PTF — query pass-through через функцию query. Она позволяет отправить запрос на «родном» диалекте источника прямо в источник, а Trino получает результат как таблицу:

SELECT *
FROM TABLE(
  postgresql.system.query(
    query => 'SELECT region, count(*) FROM analytics.users GROUP BY region'
  )
);

Зачем это нужно. Trino транслирует SQL в вызовы коннектора и проталкивает в источник то, что умеет (pushdown — тема восьмого модуля). Но не всё: специфичная для PostgreSQL функция, хитрая агрегация, фича, которой нет в SPI, в источник не протолкнутся — Trino вытянет сырые данные и посчитает сам. query обходит это ограничение: текст в query => '...' исполняется источником как есть, его родным движком, со всеми его возможностями. PTF здесь полиморфна по необходимости — схема результата зависит от того, что вернёт переданный SQL, и известна только в момент вызова.

Синтаксис вызова любой табличной функции — TABLE(имя_функции(аргументы)) в позиции FROM. Аргументы передаются по имени через =>. Кроме query существуют и другие табличные функции — например, генераторы данных и функции разбора файлов в объектном хранилище; конкретный набор зависит от коннектора.

NOTE

Query pass-through через query — мощный, но осознанный инструмент. Запрос внутри query Trino не анализирует и не оптимизирует — он целиком отдаётся источнику. Из этого следует две вещи. Плюс: можно использовать любые родные возможности источника, которые не выражаются через SPI. Минус: Trino не сможет применить к такому подзапросу свои оптимизации, dynamic filtering, не проверит его на этапе планирования. Берите query, когда действительно нужна фича источника, недоступная иначе, — а не как замену обычным запросам через Trino.


Попробуй сам

На песочнице курса (Trino 481):

  1. На песочнице есть каталог memory (коннектор Memory, поддерживает запись). Создайте таблицу: CREATE TABLE memory.default.t AS SELECT 1 AS id, 'a' AS val;. Выполните INSERT, затем UPDATE, затем DELETE и проверяйте содержимое через SELECT после каждого шага. Зафиксируйте, что все три DML-операции отработали, потому что коннектор Memory это поддерживает.

  2. Рассуждение в двух абзацах. Первый: объясните, почему построчный DELETE по условию, отсекающему строки внутри файлов, на таблице Iceberg v2 возможен, а на обычной нетранзакционной Hive-таблице в Parquet — нет. Второй: в чём разница между metadata delete (удаление целых партиций) и row-level delete по стоимости и почему первый практически мгновенный.

  3. Объясните своими словами, что значит «полиморфная» в названии PTF и почему функция query обязана быть полиморфной. Приведите пример запроса к источнику, который имеет смысл выполнить через TABLE(...query(...)), а не обычным SELECT через Trino, и обоснуйте выбор.


Проверка знанийKnowledge check
Почему возможность MERGE/UPDATE/DELETE в Trino зависит от пары «коннектор + формат таблицы», что делает команда MERGE, и что означает «полиморфная» в названии PTF на примере функции query?
ОтветAnswer
Trino — движок без своего хранилища: при DML он не меняет данные сам, а переводит операцию в действия коннектора над источником, а источники по-разному способны менять данные. Простой INSERT поддерживают почти все пишущие коннекторы, но построчные UPDATE и DELETE нетривиальны для файловых форматов: Parquet и ORC иммутабельны, файл нельзя отредактировать, изменив одну строку. Поэтому row-level операции требуют, чтобы формат таблицы умел их выражать: Iceberg (формат v2 и выше) и Delta делают это через delete-файлы, помечающие удалённые строки поверх иммутабельных файлов данных; Hive поддерживает UPDATE/DELETE только для транзакционных ACID-таблиц ORC. Значит, возможность MERGE/UPDATE/DELETE — свойство пары «коннектор плюс формат таблицы», а не самого Trino: на обычной нетранзакционной Hive-таблице в Parquet построчный DELETE недоступен, на Iceberg v2 — доступен. MERGE сопоставляет целевую таблицу с источником по ON-условию и в зависимости от наличия совпадения выполняет разные действия: ветки WHEN MATCHED обновляют или удаляют совпавшие строкии (их может быть несколько с AND-условиями), ветка WHEN NOT MATCHED вставляет строки источника, не нашедшие пары. Это каноничный upsert и основа инкрементальной загрузки: MERGE заменяет связку отдельных UPDATE, DELETE и INSERT, делая всё за один проход сопоставления. «Полиморфная» в названии PTF означает, что схема результата — имена и типы столбцов — не фиксирована заранее, а определяется в момент вызова по аргументам функции: одна PTF при разных аргументах вернёт таблицы разной схемы. Функция query (query pass-through) обязана быть полиморфной, потому что отправляет переданный текст запроса в источник как есть, и схема результата зависит от того, что вернёт этот SQL, — она известна только в момент вызова.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Почему возможность построчного UPDATE и DELETE в Trino зависит от коннектора и формата таблицы?

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

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

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

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