Вложенные структуры данных
ADQM/ClickHouse предоставляет тип данных Nested для хранения и обработки вложенных структур данных, который может служить альтернативой типам Tuple и Array(Tuple). Он позволяет моделировать сложные структуры данных без необходимости создания/хранения дополнительных таблиц и использования сложных запросов.
Вложенная структура данных Nested представляет собой таблицу внутри ячейки основной таблицы. Каждой строке основной таблицы может соответствовать любое количество строк во вложенной структуре данных.
Обзор
Чтобы описать в таблице столбец типа Nested, необходимо указать параметры вложенной структуры — имена столбцов и соответствующие типы данных.
Ниже приведен пример синтаксиса запроса CREATE TABLE для создания таблицы с вложенной структурой данных в столбце (с целью упрощения в примере используются краткие названия столбцов и конкретные типы данных):
CREATE TABLE test_table (
a String,
b UInt32,
...
n Nested (
c String,
d Int64,
...
)
)
ENGINE = MergeTree ORDER BY tuple();
К столбцам вложенной структуры данных можно обращаться в запросах, используя формат <nested_structure>.<nested_column> (где <nested_structure> — название столбца таблицы типа Nested, <nested_column> — название столбца вложенной структуры).
Формат данных столбцов вложенной структуры определяется настройкой flatten_nested, значение которой может быть равным 1 (по умолчанию) или 0.
flatten_nested = 1
Столбцы вложенной структуры имеют тип Array(Type), то есть хранят массивы значений соответствующих типов данных одинаковой длины. В этом случае при чтении и обработке значений вложенных столбцов можно использовать функции для работы с массивами, в том числе выражение ARRAY JOIN.
Посмотреть, какие типы данных определены для столбцов созданной таблицы, можно с помощью запроса SHOW CREATE TABLE:
SHOW CREATE TABLE test_table;
┌─statement─────────────────────────┐ 1. │ CREATE TABLE default.test_table │ │ ( │ │ `a` String, │ │ `b` UInt32, │ │ `n.c` Array(String), │ │ `n.d` Array(Int64) │ │ ) │ │ ENGINE = MergeTree │ │ ORDER BY tuple() │ │ SETTINGS index_granularity = 8192 │ └───────────────────────────────────┘
Вставка данных
Для вставки данных в столбец типа Nested необходимо отдельно передать массив значений для каждого столбца вложенной структуры (система будет автоматически проверять, что все предаваемые массивы значений имеют одинаковую длину). Например:
INSERT INTO test_table VALUES ('a', 1, ['c1', 'c2', 'c3'], [100, 200, 300]);
В таблицу со столбцами, которые содержат вложенные структуры, можно также вставлять данные JSON с той же структурой. Например, используйте для этого в запросах формат JSONEachRow:
INSERT INTO test_table FORMAT JSONEachRow {"a":"a1", "b":2, "n.c":["c4", "c5"], "n.d":[101, 202]};
Чтобы передать данные как иерархический объект JSON, предварительно установите input_format_import_nested_json=1:
SET input_format_import_nested_json=1;
INSERT INTO test_table FORMAT JSONEachRow
{
"a":"a2",
"b":"3",
"n": {
"c": ["c6", "c7"],
"d": [111, 222]
}
};
Чтение данных
Запрос всех данных из таблицы выводит отдельно каждый столбец вложенной структуры:
SELECT * FROM test_table;
┌─a──┬─b─┬─n.c──────────────┬─n.d───────────┐ 1. │ a │ 1 │ ['c1','c2','c3'] │ [100,200,300] │ 2. │ a1 │ 2 │ ['c4','c5'] │ [101,202] │ 3. │ a2 │ 3 │ ['c6','c7'] │ [111,222] │ └────┴───┴──────────────────┴───────────────┘
flatten_nested = 0
Данные всех столбцов вложенной структуры хранятся как один массив кортежей, то есть фактически столбец Nested имеет тип Array(Tuple). В этом случае поддерживается произвольный уровень вложенности.
Если пересоздать таблицу test_table, установив перед этим flatten_nested = 0, в выводе запроса SHOW CREATE TABLE для столбца n будет показан тип Nested, хотя фактически это Array(Tuple(…)):
┌─statement──────────────────────────┐ 1. │ CREATE TABLE default.test_table │ │ ( │ │ `a` String, │ │ `b` UInt32, │ │ `n` Nested(c String, d Int64) │ │ ) │ │ ENGINE = MergeTree │ │ ORDER BY tuple() │ │ SETTINGS index_granularity = 8192 │ └────────────────────────────────────┘
Вставка данных
Соответственно данные во вложенную структуру необходимо вставлять как массив кортежей. Например:
INSERT INTO test_table VALUES ('a', 1, [('c1', 100), ('c2', 200), ('c3', 300)]);
Для вставки в таблицу с Nested-столбцом иерархического объекта JSON при flatten_nested = 0 не обязательно активировать опцию input_format_import_nested_json, при этом данные для вложенных столбцов нужно передавать как массив:
INSERT INTO test_table FORMAT JSONEachRow
{
"a":"a1",
"b":"2",
"n": [
{"c": "c4", "d": 101},
{"c": "c5", "d": 202}
]
};
Чтение данных
Запрос всех данных из таблицы выводит столбец типа Nested, где каждая строка — массив кортежей значений столбцов вложенной структуры:
SELECT * FROM test_table;
┌─a──┬─b─┬─n──────────────────────────────────┐
1. │ a │ 1 │ [('c1',100),('c2',200),('c3',300)] │
2. │ a1 │ 2 │ [('c4',101),('c5',202)] │
└────┴───┴────────────────────────────────────┘
Данные отдельных вложенных столбцов можно также запрашивать, используя нотацию с точкой:
SELECT n.c, n.d FROM test_table;
┌─n.c──────────────┬─n.d───────────┐ 1. │ ['c1','c2','c3'] │ [100,200,300] │ 2. │ ['c4','c5'] │ [101,202] │ └──────────────────┴───────────────┘
Пример
Создание таблицы
Создайте таблицу orders для хранения данных о заказах в столбцах соответствующих типов:
-
order_id(UInt64) — идентификатор заказа; -
customer_id(UInt32) — идентификатор покупателя; -
order_date(Date) — дата заказа.
Добавьте также в таблицу столбец для хранения информации о продуктах, которые включает каждый заказ — столбец items типа Nested с описанием следующих полей вложенной структуры:
-
product_id(UInt32) — идентификатор продукта; -
product_name(String) — название продукта; -
quantity(UInt16) — количество единиц продукта в заказе; -
price_per_unit(Decimal) — стоимость единицы продукта.
Значение параметра flatten_nested — 1 (по умолчанию).
CREATE TABLE orders (
order_id UInt64,
customer_id UInt32,
order_date Date,
items Nested (
product_id UInt32,
product_name String,
quantity UInt16,
price_per_unit Decimal(10, 2)
)
) ENGINE = MergeTree()
ORDER BY (order_date, order_id);
Вставка данных
Вставьте в таблицу тестовые данные (например, несколько заказов за два дня):
INSERT INTO orders VALUES
(1001, 42, '2025-08-07',
[101, 102, 103],
['Laptop', 'Mouse', 'Keyboard'],
[1, 2, 1],
[999.99, 19.99, 49.99]),
(1002, 77, '2025-08-07',
[104, 105],
['Monitor', 'Headphones'],
[2, 1],
[199.99, 89.99]),
(1003, 55, '2025-08-07',
[101, 104, 102],
['Laptop', 'Monitor', 'Mouse'],
[1, 1, 1],
[999.99, 199.99, 19.99]),
(1004, 24, '2025-08-08',
[102, 105],
['Mouse', 'Headphones'],
[3, 4],
[19.99, 89.99]),
(1005, 38, '2025-08-08',
[101, 103, 104],
['Laptop', 'Keyboard', 'Monitor'],
[1, 1, 1],
[999.99, 49.99, 199.99]);
Чтение данных
Запросите все данные таблицы:
SELECT * FROM orders;
┌─order_id─┬─customer_id─┬─order_date─┬─items.product_id─┬─items.product_name──────────────┬─items.quantity─┬─items.price_per_unit──┐ 1. │ 1001 │ 42 │ 2025-08-07 │ [101,102,103] │ ['Laptop','Mouse','Keyboard'] │ [1,2,1] │ [999.99,19.99,49.99] │ 2. │ 1002 │ 77 │ 2025-08-07 │ [104,105] │ ['Monitor','Headphones'] │ [2,1] │ [199.99,89.99] │ 3. │ 1003 │ 55 │ 2025-08-07 │ [101,104,102] │ ['Laptop','Monitor','Mouse'] │ [1,1,1] │ [999.99,199.99,19.99] │ 4. │ 1004 │ 24 │ 2025-08-08 │ [102,105] │ ['Mouse','Headphones'] │ [3,4] │ [19.99,89.99] │ 5. │ 1005 │ 38 │ 2025-08-08 │ [101,103,104] │ ['Laptop','Keyboard','Monitor'] │ [1,1,1] │ [999.99,49.99,199.99] │ └──────────┴─────────────┴────────────┴──────────────────┴─────────────────────────────────┴────────────────┴───────────────────────┘
Чтобы вывести данные в обычном табличном виде, развернув Nested-структуру в несколько строк, используйте в запросе ARRAY JOIN:
SELECT * FROM orders ARRAY JOIN items;
┌─order_id─┬─customer_id─┬─order_date─┬─items.product_id─┬─items.product_name─┬─items.quantity─┬─items.price_per_unit─┐
1. │ 1001 │ 42 │ 2025-08-07 │ 101 │ Laptop │ 1 │ 999.99 │
2. │ 1001 │ 42 │ 2025-08-07 │ 102 │ Mouse │ 2 │ 19.99 │
3. │ 1001 │ 42 │ 2025-08-07 │ 103 │ Keyboard │ 1 │ 49.99 │
4. │ 1002 │ 77 │ 2025-08-07 │ 104 │ Monitor │ 2 │ 199.99 │
5. │ 1002 │ 77 │ 2025-08-07 │ 105 │ Headphones │ 1 │ 89.99 │
6. │ 1003 │ 55 │ 2025-08-07 │ 101 │ Laptop │ 1 │ 999.99 │
7. │ 1003 │ 55 │ 2025-08-07 │ 104 │ Monitor │ 1 │ 199.99 │
8. │ 1003 │ 55 │ 2025-08-07 │ 102 │ Mouse │ 1 │ 19.99 │
9. │ 1004 │ 24 │ 2025-08-08 │ 102 │ Mouse │ 3 │ 19.99 │
10. │ 1004 │ 24 │ 2025-08-08 │ 105 │ Headphones │ 4 │ 89.99 │
11. │ 1005 │ 38 │ 2025-08-08 │ 101 │ Laptop │ 1 │ 999.99 │
12. │ 1005 │ 38 │ 2025-08-08 │ 103 │ Keyboard │ 1 │ 49.99 │
13. │ 1005 │ 38 │ 2025-08-08 │ 104 │ Monitor │ 1 │ 199.99 │
└──────────┴─────────────┴────────────┴──────────────────┴────────────────────┴────────────────┴──────────────────────┘
По столбцам вложенной структуры можно применять агрегирование. Например, следующий запрос вычисляет сколько единиц каждого продукта было продано за месяц (количество по всем заказам):
SELECT
formatDateTime(order_date, '%M') as month,
items.product_name as product,
SUM(items.quantity) as units_sold
FROM orders
ARRAY JOIN items
GROUP BY
formatDateTime(order_date, '%M'),
items.product_name;
┌─month──┬─product────┬─units_sold─┐ 1. │ August │ Keyboard │ 2 │ 2. │ August │ Headphones │ 5 │ 3. │ August │ Laptop │ 3 │ 4. │ August │ Mouse │ 6 │ 5. │ August │ Monitor │ 4 │ └────────┴────────────┴────────────┘
По полю Nested-структуры можно также фильтровать данные. Например, следующий запрос выводит продажи продукта Laptop за каждый день:
SELECT
order_date,
items.product_name AS product ,
SUM(items.quantity) AS quantity
FROM orders
ARRAY JOIN items
WHERE items.product_name = 'Laptop'
GROUP BY
order_date,
items.product_name;
┌─order_date─┬─product─┬─quantity─┐ 1. │ 2025-08-07 │ Laptop │ 2 │ 2. │ 2025-08-08 │ Laptop │ 1 │ └────────────┴─────────┴──────────┘
Чтобы вернуть только одну строку с количеством проданных единиц каждого продукта для каждой даты, можно использовать подзапрос и функцию groupArray:
SELECT
order_date,
groupArray((product, units_sold))
FROM
(
SELECT
order_date,
product,
sum(product_sales) AS units_sold
FROM orders
ARRAY JOIN
items.product_name AS product,
items.quantity AS product_sales
GROUP BY
order_date,
product
)
GROUP BY order_date;
┌─order_date─┬─groupArray((product, units_sold))────────────────────────────────────────┐
1. │ 2025-08-07 │ [('Laptop',2),('Mouse',3),('Monitor',3),('Keyboard',1),('Headphones',1)] │
2. │ 2025-08-08 │ [('Keyboard',1),('Mouse',3),('Monitor',1),('Headphones',4),('Laptop',1)] │
└────────────┴──────────────────────────────────────────────────────────────────────────┘