StarRocks 4.0 официально выпущен! Эта версия приносит целый ряд ключевых обновлений. В данном материале мы сосредоточимся на системном ускорении запросов к JSON: благодаря новому механизму колоночного хранения и оптимизациям исполнения FlatJSON, StarRocks 4.0 обеспечивает для JSON в задачах анализа в реальном времени производительность, близкую к нативным столбцам.
Для логов, трекинговых событий и данных IoT пользователям больше не нужен отдельный ETL: можно сразу выполнять высокопроизводительный анализ JSON и, наконец, совместить «гибкость структуры данных» с «эффективностью аналитики».
В сценариях анализа в реальном времени логи, кликстрим, трекинг и пользовательские профили встречаются повсеместно. Эти данные обычно хранятся в формате JSON — он гибкий, универсальный, не требует предварительного моделирования; особенно удачен для быстро меняющихся бизнес‑сценариев: поля можно добавлять и удалять в любой момент, а разные системы легко обмениваются такими объектами.
Именно поэтому JSON стал наиболее распространенным форматом данных в интернет‑сервисах. Но как только такая гибкость попадает в СУБД, всё меняется.
Даже при корректно написанном SQL запросы в логовой или поведенческой аналитике могут выполняться очень долго.
SELECT
get_json_string(event, '$.type') AS event_type,
COUNT(DISTINCT user_id)
FROM events_log
WHERE
get_json_string(event, '$.region') = 'US' AND
to_datetime(get_json_int(dt, '$.event_ts')) BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY event_type;
Почему такой простой SQL работает медленно? Потому что для базы данных JSON — это «черный ящик»:
Подсистема хранения должна считывать в память целый JSON каждой строки.
Даже если SQL обращается к нескольким полям, приходится читать весь объект.
Фильтры не могут эффективно использовать индексы и вынуждены выполнять полное сканирование (Full Scan).
Строковые вычисления дороги и не позволяют применять оптимизации вроде кодирования словарём (dictionary encoding).
На первый взгляд, запрос лишь фильтрует region='US', группирует по event_type и делает DISTINCT по пользователям, но время исполнения может доходить до десятков секунд. CPU «зашкаливает», задержки растут — дело не в сложности SQL, а в том, что JSON изначально не проектировался для аналитики.
Когда JSON становится основой анализа, его достоинства быстро превращаются в недостатки:
Раздутый объём хранения: имена полей и значения повторяются, сжатие работает хуже.
Высокая стоимость чтения: извлечение полей требует обхода и поиска внутри JSON, потребляя CPU.
Быстрая эволюция схемы: поля добавляются/исчезают, исторические данные неоднородны, моделирование затруднено.
Неоднородные типы: одно и то же поле может иметь разные типы в разных записях, усложняя запросы.
Много полей: отдельный JSON нередко содержит сотни полей, хотя запросы обычно используют лишь малую их часть.
Подходы СУБД к решению
Много лет инженеры пробуют разные подходы — их можно разделить на три класса:
Бинарная сериализация. Типичный пример — JSONB в PostgreSQL. JSON парсится при записи и сериализуется в двоичный формат, чтобы сократить строковый парсинг при чтении. Это частично ускоряет запросы, но по‑прежнему недостаточно для OLAP. Ранние версии StarRocks использовали схожую идею.
Ручная экстракция полей. Традиционные DWH предпочитают на этапе ETL разворачивать JSON в отдельные столбцы. Это даёт производительность, близкую к колоночному хранению, но сложно в сопровождении. В StarRocks пользователи могут делать это через Generated Column (генерируемые столбцы), например:
ALTER TABLE tbl ADD COLUMN json_event_type STRING AS get_json_string(event, '$.event_type')
Автоматическое извлечение в колонки. Идеальный путь — чтобы система сама распознавала часто используемые поля и «незаметно для пользователя» представляла их в виде отдельных столбцов.
Но реализация этого крайне сложна: системе нужно умно обрабатывать неоднородные схемы и их эволюцию. Поэтому глубина оптимизаций у разных продуктов сильно различается.

От Binary JSON к производительности, близкой к колоночному хранению
Чтобы решить перечисленные проблемы, StarRocks в версии 2.3 добавил Binary JSON — двоично сериализованный формат, который избавляет от повторного парсинга строк и позволяет при поиске по полям использовать двоичный поиск, снижая нагрузку на CPU. Однако и этого недостаточно. На примере запроса из начала статьи:
Индексы для фильтрации использовать нельзя, приходится читать все JSON‑данные, что создаёт значительный рост I/O‑нагрузки.
Для каждой строки выполняются многократные get_json_* для извлечения region, event_type, user_id.
Фильтр region='US' требует посимвольного сравнения; «горячие» функции тратят 80–90% CPU на парсинг и сравнения.
На этапе агрегации снова происходят строковые операции — хэширование и DISTINCT — без возможности оптимизировать через кодирование словарём.
Даже при десятках миллионов строк такой запрос может выполняться десятки секунд.
Эволюция поддержки JSON в StarRocks
StarRocks последовательно прошёл путь от «строкового хранения» к «колоночному»:
До 2.3: нет нативного JSON, только хранение как строк.
2.3: добавлен тип JSON, двоичная сериализация и богатый набор JSON‑функций.
3.3: представлен FlatJSON — колоночное хранение, которое автоматически выносит частые поля в отдельные столбцы; производительность уровня хранения впервые приблизилась к колоночной.
4.0: на уровне исполнения добавлены индексы, глобальные словари и отложенная материализация, доведя производительность запросов к JSON до уровня, близкого к нативным столбцам.
Наглядная разница в производительности
Если запустить один и тот же запрос из начала статьи на разных реализациях JSON, разница будет очевидна: от десятков секунд — до субсекундного времени отклика при FlatJSON. Далее разберём, как это работает изнутри.

FlatJSON: колоночное хранение

Чтобы понять устройство FlatJSON, вспомним базовую структуру хранения в StarRocks:
Segment — минимальная файловая единица хранения (обычно ~1 ГБ). При каждой загрузке данных создаётся минимум один файл Segment.
Segment использует колоночный формат; каждый столбец хранится отдельно как набор страниц (Page). На уровне Page поддерживаются encoding и compression.
Внутри Segment поддерживается несколько индексов: по умолчанию создаются легковесные (например, ZoneMap, индекс по ключу сортировки — Sort Key Index), а при необходимости — более сложные (битмап‑индекс — Bitmap Index; индекс Bloom‑фильтра — Bloom Filter Index).
При записи данные сортируются в соответствии с ORDER BY, заданным пользователем.
Опираясь на эту архитектуру, FlatJSON выполняет «колоннизацию» уже на этапе загрузки:
Сканирует множество ключей JSON, считает частоты полей и выявляет «частые» (например, поле "region", присутствующее почти в каждой записи).
Автоматически выводит типы полей: всё, что можно хранить как число, не сохраняется строкой; для многотипных полей выбирается совместимый общий тип.
Выявленные поля сохраняются как обычные столбцы (INT, STRING, DOUBLE и т. п.).
Редкие или нестабильные поля записываются в «резервный столбец», который хранится как JSON, — запасной вариант на случай низкой частоты.
В результате JSON, который раньше требовал глубокого парсинга, физически превращается в «полуструктурированную таблицу». Внутри Segment такие поля ничем не отличаются от обычных столбцов: их можно читать по требованию и строить по ним индексы.
Почему FlatJSON работает быстрее:
Эффективнее сжатие: поля низкой кардинальности (например, region) кодируются словарём, уменьшая объём.
Нет избыточности: ключи JSON не дублируются.
Меньше I/O: запрос читает только столбцы, вынесенные из JSON.
Исполнение без парсинга: на этапе запроса JSON‑строки не парсятся — сразу считываются нужные столбцы из Segment.
SELECT
get_json_string(event, '$.type') AS event_type,
COUNT(DISTINCT user_id)
FROM events_log
WHERE
to_datetime(get_json_int(dt, '$.event_ts'))
BETWEEN '2024-01-01' AND '2024-12-31'
AND get_json_string(event, '$.region') = 'US'
GROUP BY event_type;
Возвращаясь к примеру:
Подсистема хранения считывает только .type,.type, .type,.event_ts, $.region — те поля, которые были вынесены в столбцы, — без чтения полного JSON.
Для выражения get_json_string(event, '$.type') система не парсит строку JSON, а напрямую читает соответствующий столбец из Segment.
Благодаря колоночному представлению производительность запросов к JSON существенно возрастает.
FlatJSON: колоночные запросы
После преобразования хранения в «столбцы» критичны оптимизации и на уровне исполнения. Ниже — четыре ключевые техники FlatJSON и их вклад в ускорение запросов.
Индексы (Index): из «иголки в стоге сена» — в направленный поиск

Чтобы избежать полного сканирования, FlatJSON поддерживает индексацию вынесенных полей (например, region, event_time, event_type), в частности ZoneMap. ZoneMap хранит минимум и максимум для каждой страницы (Page) и при запросе быстро определяет, какие страницы потенциально удовлетворяют предикатам, позволяя пропускать нерелевантные данные.
Основной выигрыш — резкое сокращение объёма сканирования:
Используя селективность предикатов, система переходит от «сканирования всей таблицы» к «частичному чтению». Это особенно эффективно при высокой селективности (например, region='US' AND dt BETWEEN ...). Если партиционирование и сортировка согласованы с условиями, выгода ещё выше.
При низкой селективности или «размытых» предикатах (например, LIKE '%xx') эффект индексного фильтра ослабевает.
В типичных аналитических запросах ZoneMap всё равно снижает I/O на порядок, ускоряя выполнение запроса.
Эффективное декодирование (Dictionary Decoding)

Для строковых полей низкой кардинальности Segment автоматически применяет кодирование словарём. Для поля region система хранит словарь и массив кодов, устраняя повтор строк.
На этапе исполнения StarRocks переписывает предикаты над словарно‑кодированными данными: например, region='US' преобразуется в region=1, после чего индекс сразу отфильтровывает соответствующие блоки. Это устраняет частые строковые сравнения, облегчает вычисления и повышает эффективность использования памяти и I/O. После фильтрации коды словаря декодируются обратно в строки для последующих вычислений.
Отложенная материализация (Late Materialization)

Традиционно СУБД декодируют данные до фильтрации — т. е. платят цену за парсинг и создание объектов даже для записей, которые позже будут отбракованы. Отложенная материализация делает наоборот: на начальном этапе используются лёгкие структуры (например, номера строк/row id), а после применения всех фильтров считываются только нужные столбцы по отобранным row id.
Для условия region='US' StarRocks сначала читает и фильтрует столбец region, сохраняя номера подходящих строк. Затем по этим номерам выбирается столбец user_id — без лишних чтений и декодирования.
Суть — отложить ненужные вычисления и декодирование: это уменьшает объём I/O, повышает локальность кэша и фокусирует CPU на действительно нужных данных. Поэтому при низкой селективности такой механизм значительно снижает стоимость пустых вычислений.
Глобальный словарь (Global Dictionary): сводим строки к целочисленным вычислениям

Строки — один из главных факторов, тормозящих запросы: и фильтрация, и агрегация требуют посимвольных сравнений и строкового хэширования, что расходует CPU и ухудшает локальность кэша. Для решения StarRocks поверх локальных словарей уровня Segment вводит глобальный словарь (Global Dictionary) — единое отображение, агрегирующее локальные словари разных узлов.
С глобальным словарём словарное кодирование выходит за рамки одного Segment и применяется к агрегациям, сортировкам, соединениям и т. д. Дорогие строковые операции «понижаются» до операций над целыми числами: CPU выполняет лёгкие сравнения/хэширование по int, локальность кэша растёт, хэш‑таблицы становятся компактнее и эффективнее, а уровень коллизий при агрегациях существенно снижается.
Например, GROUP BY region обычно выполняет частые строковые поиска и обновления в хэш‑таблице. С глобальным словарём план переписывается в GROUP BY region_code, и агрегация идёт по целым числам; декодирование в строку происходит только на этапе финального вывода.
Краткое сравнение путей исполнения

Традиционный запрос к JSON: чтение полного JSON → разбор путей → приведение типов → фильтрация и агрегация.
Запрос FlatJSON: прямое обращение к столбцам → индексы и словари для фильтрации → вычисления по глобальному словарю → отложенное декодирование.
По сравнению с традиционным подходом FlatJSON резко сокращает затраты на парсинг, сравнения и полные сканирования.
В практических тестах при одинаковом агрегирующем запросе по 1 млрд строк JSON традиционный способ занимает около 30 секунд, тогда как FlatJSON выполняет его примерно за 500 мс, снижая затраты на I/O и CPU на несколько порядков.
Практическая ценность в бизнес‑сценариях
Ценность FlatJSON — не только в производительности, но и в балансе гибкости и простоты эксплуатации.
Аналитика трекинговых логов
Проблема: дашборды в реальном времени должны агрегировать и группировать свежие трекинговые события — зачастую это десятки миллиардов строк JSON. Традиционно запросы парсят JSON построчно, ответ занимает десятки секунд и более; при этом поля трекинга часто меняются, появление нового поля требует правок ETL.
Решение FlatJSON: на этапе загрузки система автоматически выносит часто используемые поля (user_id, event_time, region, event_type и т. п.) в отдельные столбцы. Появление новых полей не ломает текущие запросы — их можно читать через JSON‑функции; система сама решит, стоит ли выносить эти поля в столбцы по частоте обращений.
Результат: задержки запросов снижаются с десятков секунд до сотен миллисекунд; динамическая схема трекинга поддерживается без постоянных правок схемы таблиц и ETL.
Формирование отчётов в e‑commerce
Проблема: заказы часто хранятся в JSON: SKU, цена, промо, регион, доставка и сотни других полей. Традиционное решение требует сложного ETL, что дорого в разработке и поддержке; при добавлении полей или несовпадении типов (например, price как строка и как число) пайплайн часто ломается.
Решение FlatJSON: при загрузке FlatJSON автоматически выявляет и выносит частые поля в столбцы, сохраняя при этом полный Binary JSON. Даже при динамической схеме и неоднородных типах запросы выполняются корректно, а производительность доступа к популярным полям остаётся близкой к нативным столбцам.
Результат: время генерации отчётов уменьшается с минут до секунд; разработчики и эксплуатация не тратят ресурсы на постоянные правки ETL, аналитики быстрее реагируют на изменения бизнеса.
Реальное‑времени мониторинг IoT (Интернет вещей)
Проблема: данные с IoT‑устройств приходят в JSON с десятками метрик; при этом поля сильно различаются между моделями (неоднородные схемы — обычное явление). В традиционном подходе СУБД парсит полный JSON; при отсутствии поля или несоответствии типов запросы деградируют или падают.
Решение FlatJSON: на этапе загрузки FlatJSON выносит частые метрики (например, температуру, влажность) в отдельные столбцы, а остальные поля сохраняет в Binary JSON. При запросах система пропускает отсутствующие поля, избегая ошибок исполнения; для неоднородных типов применяет унификацию на уровне выполнения или отложенную материализацию.
Результат: поддерживается агрегация в реальном времени для данных десятков миллионов устройств; мониторинговые отчёты формируются за секунды. Динамика и неоднородность схем IoT обрабатываются автоматически, что существенно снижает затраты на ручную очистку и моделирование.
Итог: FlatJSON не только решает проблему производительности, но и превращает динамическую и неоднородную схему в контролируемую системную стоимость. Разработчикам не нужно беспокоиться о добавлении/отсутствии полей и несогласованных типах, а аналитики свободно пишут SQL, не опасаясь частых изменений низкоуровневой структуры данных.
Как использовать FlatJSON
Включение FlatJSON очень простое и почти не отличается от работы с обычным типом JSON.
-- Минимальная таблица с JSON-столбцом и включённым FlatJSON
CREATE TABLE events_log (
dt DATE,
event_id BIGINT,
event JSON
)
DUPLICATE KEY(`dt`, event_id)
PARTITION BY date_trunc('DAY', dt)
DISTRIBUTED BY HASH(dt, event_id)
PROPERTIES (
"flat_json.enable" = "true", -- Включить FlatJSON для этой таблицы
"flat_json.null.factor" = "0.3" -- Необязательно: не извлекать слишком разреженные поля
);
FlatJSON включается и выключается через свойства таблицы; новые загруженные JSON‑данные автоматически будут представлены в виде столбцов (если подходят под критерии извлечения).
Дополнительно можно задать порог через параметр flat_json.null.factor, чтобы избегать вынесения чрезмерно разреженных полей.
Вставим несколько строк данных:
INSERT INTO events_log VALUES
('2025-09-01', 1001, PARSE_JSON('{
"user_id": 12345, "region": "US", "event_type": "click", "ts": 1710000000
}')),
('2025-09-01', 1002, PARSE_JSON('{
"user_id": 54321, "region": "CA", "event_type": "purchase", "ts": 1710000300,
"experiment_flag": "A" -- редкое поле; останется в резервном JSON
}'));
Используйте JSON‑функции в запросах как обычно:
-- Фильтрация и группировка по полям, которые FlatJSON, вероятно, вынес в столбцы (часто встречающиеся)
SELECT
get_json_string(event, '$.event_type') AS event_type,
COUNT(*) AS cnt
FROM events_log
WHERE
get_json_string(event, '$.region') = 'US'
AND get_json_int(event, '$.ts') BETWEEN 1710000000 AND 1710003600
GROUP BY event_type;
Итоги
FlatJSON предлагает инженерно выверенный путь обработки JSON в СУБД. На уровне хранения он автоматически выносит частые поля в отдельные столбцы, повышая степень сжатия и заметно снижая стоимость сканирования. На уровне выполнения он сочетает индексы, глобальный словарь (Global Dictionary) и отложенную материализацию (Late Materialization), минимизируя расходы на парсинг и декодирование.
Практические тесты показывают: при одном и том же SQL задержки падают с десятков секунд до субсекундного уровня. Пользователям больше не нужно выбирать между «гибкостью» и «производительностью». Данные с динамической схемой — из трекинга, логов и IoT — можно напрямую записывать в StarRocks, одновременно поддерживая быстро меняющиеся требования бизнеса и обеспечивая стабильную предсказуемую производительность запросов. Это снижает затраты на моделирование и ETL и позволяет командам сосредоточиться на самой бизнес‑аналитике.
Хотите узнать больше и пообщаться с практиками? Присоединяйтесь к сообществу StarRocks и обсуждайте решения вместе с другими разработчиками.