ADG Оставить заявку
Блог Данные и аналитика 4 мин чтения

ClickHouse 25.1: нативный JSON выходит из беты - переводим продуктовые столбцы и смотрим что меняется

ClickHouse 25.1 объявил нативный тип JSON General Availability. Переводим String-столбцы с JSON на нативный тип в продуктовой DWH-базе и замеряем разницу в скорости и объёме.

Контекст момента

ClickHouse 25.1: нативный тип JSON стал GA, расширение функций оконных функций

В ClickHouse 25.1, вышедшем в конце января, нативный тип JSON перешёл в статус GA. Для тех, кто следил за этой историей с 2023 года: бета была долгой, флаг allow_experimental_object_type держали включённым на свой страх и риск, а теперь тип стал production-ready без оговорок. Плюс в этом же релизе заметно расширили набор оконных функций - но об этом ниже.

Мы не стали ждать и на прошлой неделе перевели несколько столбцов в продуктовой DWH-базе клиента с String-хранения на нативный JSON. Результаты записали.

Зачем вообще хранить JSON в ClickHouse

Контекст для тех, кто не в теме: ClickHouse - колонночная база, заточенная под аналитику. Хранить в ней произвольный JSON - это исторически был компромисс. Паттерн «положи всё в String, потом вытаскивай через JSONExtract*» работает, но платишь за это дважды: строка занимает место целиком, и каждый запрос с JSONExtractString(col, 'field') парсит JSON в рантайме на лету.

У клиента ровно такая картина: несколько таблиц с событийными данными, где одна из колонок - String с payload размером от 200 байт до нескольких килобайт. На эту колонку приходится основная аналитическая нагрузка - фильтры по полям внутри JSON, агрегации, группировки.

Что изменилось с нативным JSON

Нативный тип JSON в ClickHouse хранит данные иначе. Движок разбирает JSON при вставке и раскладывает поля по субколонкам - то есть хранение становится колоночным, как и должно быть в ClickHouse. Тип поля в субколонке выводится автоматически. При запросе поле вытаскивается как нативный тип, а не как строка после парсинга.

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

Миграция на уровне DDL несложная:

ALTER TABLE events
    MODIFY COLUMN payload JSON;

Данные при этом переписываются на диске - операция не мгновенная, на таблице объёмом несколько сотен миллионов строк заняла у нас около 40 минут.

Что получили на практике

Замеры делали на одном и том же наборе запросов до и после конвертации. Тестовая среда - те же данные, тот же кластер, запросы прогревали перед замером.

Объём на диске. Сжатие сразу ощутимое - колонка с payload ужалась примерно вдвое по сравнению со String-хранением. Точные цифры зависят от структуры конкретного JSON и от кардинальности значений, но порядок такой. ClickHouse применяет колоночное сжатие к каждой субколонке отдельно, и это работает значительно лучше, чем сжимать строку с JSON целиком.

Скорость фильтрующих запросов. Запросы вида WHERE payload.user_id = 123 ускорились заметно - движок читает только субколонку user_id, не трогая остальные поля. До перевода JSONExtractString(payload, 'user_id') = '123' читало всю строку целиком. На длинных временных диапазонах разница в elapsed time хорошо видна.

Агрегации по полям JSON. GROUP BY payload.event_type тоже стало быстрее - по той же причине: читается одна субколонка вместо полного scan'а строки с последующим парсингом.

Запросы с разреженными полями. Это интересный случай. Если поле присутствует только в части строк, нативный JSON хранит его отдельно и эффективно пропускает при чтении. String-хранение этого не умеет - JSONExtract всегда читал полную строку.

Где прирост менее выражен - в запросах, которые вытаскивают весь payload целиком. Здесь нативный JSON должен собрать его обратно из субколонок, и это не бесплатно. Таких запросов у нас немного.

Оконные функции в 25.1

Отдельная тема в этом релизе - расширение оконных функций. Добавили несколько новых: first_value, last_value, nth_value с нормальными семантиками. До этого в ClickHouse с оконными функциями было немного стыдно по сравнению с тем же PostgreSQL - базовые функции были, но граничные случаи с FRAME и неочевидными аргументами вели себя неожиданно.

Для нашей аналитики оконные функции используются в отчётах по сессиям и в расчёте retention. После обновления пара запросов, которые мы раньше обходили через CTE и самосоединения именно из-за ограничений оконок, теперь пишутся прямо.

Ограничения, которые стоит знать

Нативный JSON пока не поддерживает часть операций, которые доступны для обычных колонок. В частности, создание материализованных представлений прямо по субколонкам JSON требует явного указания типов. Первичный ключ из поля внутри JSON тоже нельзя сделать напрямую - нужна отдельная материализованная колонка. Эти ограничения задокументированы, просто надо знать заранее.

Статус

Перевели три таблицы из пяти. Оставшиеся две - с более сложной структурой payload и с материализованными представлениями, там сначала разберёмся со схемой MV. По результатам всего перевода напишем итог.

Пока впечатление такое: для аналитики на ClickHouse переход на нативный JSON из String-хранения - это не косметика, а реальная экономия ресурсов. Если у вас есть String-столбцы с JSON в продуктиве - апгрейд на 25.1 и конвертация выглядят разумно.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.