Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 23 мин
Средний
sqlarraymaprowjson

Сложные типы: ARRAY, MAP, ROW, JSON

Реляционная модель в чистом виде говорит: ячейка таблицы — это одно скалярное значение. Но данные в data lake выглядят иначе. Событие из Kafka — это вложенный JSON: список товаров в заказе, словарь атрибутов, объект пользователя внутри объекта события. Parquet и ORC умеют хранить такую вложенность нативно. И Trino, чтобы работать с этими источниками честно, поддерживает сложные (контейнерные) типы: ARRAY, MAP, ROW, плюс полуструктурированный JSON.

Массивы в PostgreSQL: когда допустимо нарушить 1NF JSONB в PostgreSQL: гибкая схема внутри строгой Parquet: вложенные данные и Dremel

Этот урок — про то, что это за типы, как они представлены в памяти, как к ним обращаться и в чём принципиальная разница между типизированными контейнерами и нетипизированным JSON.


Три контейнера и одна карта

Сложные типы — это типы, чьё значение содержит другие значения. Их три, и они покрывают три разные формы вложенности.

ТипФормаАналогияПример объявления
ARRAYупорядоченный список однотипных элементовсписок / массивARRAY(INTEGER)
MAPнабор пар ключ-значениесловарь / хэш-таблицаMAP(VARCHAR, INTEGER)
ROWфиксированный набор именованных типизированных полейструктура / запись / кортежROW(name VARCHAR, age INTEGER)

Принципиальное различие между ними — в том, что фиксировано. У ARRAY фиксирован тип элемента, но не длина: элементов может быть ноль или миллион, все одного типа. У MAP фиксированы типы ключа и значения, но не набор ключей: ключи произвольны и определяются данными. У ROW фиксирована вся схема — имена и типы полей заданы в типе, как у строки таблицы; нельзя «добавить поле» в значение, не сменив тип.

Три контейнера: что в них фиксировано
ARRAYУпорядоченный список. Фиксирован тип элемента, длина произвольна. Доступ по индексу с 1
MAPНабор пар ключ-значение. Фиксированы типы ключа и значения, набор ключей задаётся данными. Доступ по ключу
ROWФиксированный набор именованных полей. Фиксирована вся схема: имена и типы. Доступ по имени поля или позиции

ARRAY: упорядоченный список

ARRAY — список однотипных элементов с сохранением порядка. Литерал — ARRAY[...], доступ к элементу — квадратные скобки.

SELECT ARRAY[10, 20, 30]            AS arr,
       ARRAY[10, 20, 30][2]         AS second,
       cardinality(ARRAY[10, 20, 30]) AS len;
     arr      | second | len
--------------+--------+-----
 [10, 20, 30] |     20 |   3

Критически важная деталь: индексация массивов в Trino начинается с 1, а не с 0. Это требование SQL-стандарта. arr[1] — первый элемент; arr[0] — ошибка. Программист, пришедший из Python или Java, спотыкается тут регулярно — зафиксируйте сразу.

Для извлечения элементов в строки служит UNNEST — он разворачивает массив в набор строк, по строке на элемент, и применяется в FROM как источник:

SELECT id, tag
FROM (VALUES (1, ARRAY['a', 'b', 'c'])) AS t(id, tags)
CROSS JOIN UNNEST(tags) AS u(tag);
 id | tag
----+-----
  1 | a
  1 | b
  1 | c

Одна строка с массивом из трёх элементов превратилась в три строки. UNNEST — основной мост между вложенными данными и плоской реляционной формой; полноценная работа с массивами через функции высшего порядка — тема четвёртого урока модуля.


MAP: пары ключ-значение

MAP — набор ассоциаций ключ-значение, аналог словаря. Литерал собирается функцией MAP(...) из двух массивов — ключей и значений; доступ к значению — по ключу в квадратных скобках.

SELECT m, m['b'], cardinality(m)
FROM (VALUES MAP(ARRAY['a','b','c'], ARRAY[1,2,3])) AS t(m);
       m        | _col1 | _col2
----------------+-------+-------
 {a=1, b=2, c=3}|     2 |     3

Тонкость доступа: обращение m['нет_такого_ключа'] к отсутствующему ключу возвращает ошибку. Чтобы безопасно достать значение с подстановкой по умолчанию, используют element_at(map, key) — он возвращает NULL, если ключа нет, вместо падения запроса. Различие между «бросить ошибку» и «вернуть NULL» — повторяющийся мотив в работе со сложными типами: для контролируемых данных годится прямой доступ, для грязных внешних — безопасные функции.

MAP тоже разворачивается через UNNEST — но в две колонки, ключ и значение:

SELECT k, v
FROM (VALUES MAP(ARRAY['x','y'], ARRAY[10,20])) AS t(m)
CROSS JOIN UNNEST(m) AS u(k, v);
 k | v
---+----
 x | 10
 y | 20

ROW: именованная структура

ROW — фиксированный набор именованных типизированных полей, ближайший аналог структуры или записи. Это естественное представление вложенного объекта: «пользователь» внутри «события».

SELECT u,
       u.name        AS by_name,
       u[2]          AS by_position
FROM (VALUES CAST(ROW('Анна', 30) AS ROW(name VARCHAR, age INTEGER))) AS t(u);
       u        | by_name | by_position
----------------+---------+-------------
 {Анна, 30}     | Анна    |          30

К полю ROW обращаются двумя способами: по имени через точку (u.name) или по позиции в квадратных скобках (u[2], нумерация с 1). Доступ по имени читаемее и устойчивее к перестановке полей — предпочитайте его.

Почему ROW важен с точки зрения «до железа». Когда таблица Parquet содержит вложенный столбец-структуру, и запрос обращается лишь к одному полю этой структуры, Trino умеет прочитать из файла только это поле, не материализуя весь объект. Эта оптимизация называется dereference pushdown — она тема пятого урока восьмого модуля, но связь зафиксируем здесь: типизированный ROW со статически известной схемой делает её возможной. Движок знает раскладку полей и может попросить у коннектора ровно нужное.

Доступ к вложенному ROW: dereference
Столбец event: ROWВложенный столбец-структура в Parquet, например ROW(user ROW(id, name), ts TIMESTAMP)
запрос просит event.user.id
DereferenceTrino знает статическую схему ROW и обращается только к нужному вложенному полю
pushdown в коннектор
Чтение только idКоннектор читает из файла лишь нужное поле, не материализуя весь объект — экономия I/O

JSON: полуструктурированный тип-исключение

JSON стоит особняком. ARRAY, MAP, ROWтипизированы: их структура известна движку статически, на этапе планирования. JSONнетипизированный контейнер: значение типа JSON хранит произвольный JSON-документ, чья форма заранее не известна. Это полуструктурированные данные в чистом виде.

Отсюда вытекает практическое различие. Из JSON значения извлекаются в рантайме, специальными функциями навигации по пути:

  • json_extract(json, path) — достаёт фрагмент по JSONPath, результат снова JSON.
  • json_extract_scalar(json, path) — достаёт скалярное значение как VARCHAR.
  • json_query / json_value — SQL-стандартные функции навигации; json_value извлекает скаляр, json_query — фрагмент.
  • json_parse / json_format — разбор строки в JSON и обратная сериализация.
SELECT json_extract_scalar(j, '$.user.city') AS city,
       json_value(j, 'lax $.items[0].price') AS first_price
FROM (VALUES JSON '{"user":{"city":"Берлин"},"items":[{"price":42}]}') AS t(j);
  city  | first_price
--------+-------------
 Берлин | 42

Когда что брать. Если форма данных стабильна и известна — задавайте типизированные ARRAY/MAP/ROW: движок проверит типы на этапе планирования, применит pushdown полей, и читается это эффективнее. Если форма данных динамична, разрежена или меняется от записи к записи — JSON честнее: он не навязывает несуществующую схему, а навигация по пути разбирает документ в рантайме.

NOTE

В релизе 480-481 у Trino есть и третий путь для полуструктурированных данных — тип VARIANT коннектора Iceberg v3. VARIANT — это эффективное двоичное представление полуструктурированных данных с разбором без полной материализации, более производительное, чем хранение и парсинг сырого JSON. Это перспективное направление для лейкхауса на Iceberg. В рамках этого урока удерживайте базовое различие: типизированные контейнеры (ARRAY/MAP/ROW) — со статической схемой, JSON — нетипизированный, разбирается в рантайме.


Вложенность складывается

Сложные типы комбинируются произвольно: ARRAY(ROW(...)), MAP(VARCHAR, ARRAY(INTEGER)), ROW с полем-ARRAY из ROW. Так выглядит реальная схема Parquet-таблицы из data lake — массив товаров, где каждый товар структура, а у структуры есть поле-словарь атрибутов.

SELECT order_id, item.sku, item.qty
FROM (VALUES (
        1001,
        ARRAY[
          CAST(ROW('SKU-1', 2) AS ROW(sku VARCHAR, qty INTEGER)),
          CAST(ROW('SKU-2', 5) AS ROW(sku VARCHAR, qty INTEGER))
        ]
      )) AS t(order_id, items)
CROSS JOIN UNNEST(items) AS u(item);
 order_id |  sku  | qty
----------+-------+-----
     1001 | SKU-1 |   2
     1001 | SKU-2 |   5

Здесь ARRAY(ROW(...)) сначала развёрнут UNNEST в строки, затем у каждой структуры через точку взяты поля. Это типовой паттерн распаковки вложенных данных лейкхауса: UNNEST снимает уровень массива, dereference через точку — уровень структуры. Складывая их, разбирают вложенность любой глубины.


Попробуй сам

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

  1. Выполните SELECT ARRAY[5,10,15][1], ARRAY[5,10,15][3]; и затем SELECT ARRAY[5,10,15][0];. Объясните, почему второй запрос — ошибка, и сформулируйте правило индексации массивов в Trino одной фразой.

  2. Сравните прямой доступ и безопасный: выполните SELECT element_at(MAP(ARRAY['a'],ARRAY[1]), 'нет');, затем SELECT MAP(ARRAY['a'],ARRAY[1])['нет'];. Опишите разницу в поведении и укажите, какой вариант выбрать для разбора грязных внешних данных и почему.

  3. Рассуждение в двух абзацах. Дано: одна таблица — события с полем attributes, где набор ключей у каждого события свой и заранее не известен; другая — события с полем geo, всегда содержащим ровно latitude и longitude. Для какого поля разумен JSON, а для какого — типизированный ROW? В ответе свяжите выбор со статической схемой и с тем, на каком этапе (планирование или рантайм) разбирается структура.


Проверка знанийKnowledge check
В чём принципиальная разница между типизированными контейнерами (ARRAY, MAP, ROW) и типом JSON, и как этот выбор влияет на то, когда разбирается структура данных?
ОтветAnswer
Типизированные контейнеры ARRAY, MAP и ROW имеют схему, известную движку статически — на этапе планирования запроса. У ARRAY зафиксирован тип элемента, у MAP — типы ключа и значения, у ROW — вся схема: имена и типы полей. JSON, наоборот, нетипизированный контейнер: значение хранит произвольный JSON-документ, чья форма заранее не известна, — это полуструктурированные данные. Разница влияет на момент разбора структуры. У типизированных контейнеров структура разбирается на этапе планирования: движок проверяет типы заранее, может применить dereference pushdown (прочитать из Parquet только нужное вложенное поле ROW, не материализуя весь объект), и обработка идёт эффективнее. Из JSON значения извлекаются в рантайме специальными функциями навигации по пути — json_extract, json_value, json_query — которые разбирают документ при исполнении. Практический выбор: если форма данных стабильна и известна, задают типизированные ARRAY/MAP/ROW ради статической проверки и pushdown; если форма динамична, разрежена или меняется от записи к записи, JSON честнее, потому что не навязывает несуществующую схему.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что фиксировано у типа ROW, в отличие от ARRAY и MAP?

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

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

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

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