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 и конвертация выглядят разумно.
- Tantor SE 17 в продуктиве: карта несовместимостей при миграции с MS SQL · 3 февраля 2025
- LLM-агент на IT-хелпдеске: первый месяц в проде · 30 января 2025