StarRocks против ClickHouse: архитектура и выбор СУБД для аналитики нормализованных данных

 

Инженеры данных часто спорят, какая колоночная СУБД быстрее. Спор бессмысленный без контекста нагрузки. ClickHouse и StarRocks решают разные задачи, хотя обе относятся к классу аналитических OLAP-движков. В этой статье мы разберем их архитектуру, покажем разницу в обработке соединений таблиц и соберем локальный стенд для честного нагрузочного теста на нормализованной схеме.

Материал рассчитан на дата-инженеров, которые проектируют корпоративное хранилище и выбирают движок под конкретный профиль запросов. Мы поднимем оба кластера в Docker, сгенерируем синтетический датасет на десятки гигабайт и сравним время выполнения многотабличных JOIN запросов вместе с потреблением оперативной памяти.

Чем отличается StarRocks vs. ClickHouse

ClickHouse это колоночная аналитическая СУБД с открытым исходным кодом, созданная в Яндексе для обработки событийных данных. Ее сильная сторона это сканирование плоских широких таблиц с миллиардами строк. Актуальная линейка на июль 2026 года это релиз 26.6, а версия 26.3 имеет статус долгосрочной поддержки LTS. Движок использует семейство таблиц MergeTree и агрессивно оптимизирован под последовательное чтение с диска. Подробный разбор движка собран в нашей вики-статье про ClickHouse.

StarRocks это MPP-СУБД для real-time аналитики, форк проекта Apache Doris, развиваемый как отдельный продукт. Актуальная стабильная версия это 4.1.1, вышедшая в конце мая 2026 года, а ветка 3.5 остается популярным выбором для продакшена. Ключевое отличие StarRocks это встроенный стоимостный оптимизатор и эффективное распределенное выполнение соединений прямо в движке, без предварительной денормализации данных. Базовые понятия движка мы разобрали в вики-статье про StarRocks.

Архитектура MPP и векторизация запросов

Обе базы применяют колоночное хранение и векторизацию вычислений. Это значит, что данные обрабатываются пакетами по несколько тысяч значений, а не построчно. Такой подход задействует SIMD-инструкции процессора и заметно ускоряет агрегации. На этом сходство заканчивается, дальше идут архитектурные развилки.

ClickHouse исторически строился вокруг идеи одной большой таблицы. Каждый шард обрабатывает свой кусок данных почти независимо. Распределенные соединения между шардами возможны, но требуют аккуратной ручной настройки. Классический рецепт для ClickHouse это заранее денормализовать данные и хранить готовую широкую таблицу, где все нужные атрибуты уже склеены.

StarRocks с самого начала проектировался как массово-параллельная система с полноценным обменом данными между узлами. Архитектура делится на фронтенды FE, которые хранят метаданные и строят план запроса, и бэкенды BE, которые выполняют вычисления. Узлы умеют перераспределять промежуточные результаты по сети через операцию shuffle. Именно это позволяет соединять несколько крупных таблиц без предварительной склейки.


graph TD
    subgraph StarRocks["StarRocks MPP"]
        FE["FE. Планировщик и CBO"]
        BE1["BE 1. Vectorized Engine"]
        BE2["BE 2. Vectorized Engine"]
        BE3["BE 3. Vectorized Engine"]
        FE --> BE1
        FE --> BE2
        FE --> BE3
        BE1 <-- "shuffle join" --> BE2
        BE2 <-- "shuffle join" --> BE3
    end
    subgraph ClickHouse["ClickHouse"]
        S1["Shard 1. Широкая таблица"]
        S2["Shard 2. Широкая таблица"]
        S1 -. "локальный скан" .- S2
    end

Как базы обрабатывают соединения таблиц

Соединение таблиц это главная точка расхождения двух движков. Здесь важно понимать, что делает планировщик до начала чтения данных. От выбранной стратегии зависит и время ответа, и пиковое потребление памяти.

ClickHouse опирается на правила. Порядок соединения задается тем, как разработчик написал запрос. По умолчанию правая таблица целиком загружается в оперативную память в виде хэш-таблицы, это hash join. Если правая таблица большая, движок упирается в лимит памяти и запрос падает с ошибкой. Частичным решением служат словари в оперативной памяти или движок Join, но это ручная и хрупкая оптимизация.

StarRocks опирается на статистику. Стоимостный оптимизатор CBO сам оценивает кардинальность таблиц по гистограммам распределения и выбирает порядок соединения. Движок динамически переключается между стратегиями broadcast, shuffle и colocate в зависимости от размеров данных. Runtime Filter отсекает лишние строки правой таблицы еще до соединения. В результате нормализованная схема с тремя или четырьмя таблицами выполняется без денормализации.

Различия удобно свести в таблицу, чтобы держать компромиссы перед глазами при выборе движка.

Характеристика ClickHouse 26.x StarRocks 4.1
Модель планировщика По правилам, порядок задает автор запроса Стоимостный оптимизатор CBO по статистике
Распределенный JOIN Ограничен, требует ручной настройки Нативный shuffle между BE-узлами
Типовой рецепт Денормализация в широкую таблицу Работа по нормализованной схеме
Реакция на большую правую таблицу Риск переполнения памяти Runtime Filter и смена стратегии
Сильная ниша Плоские таблицы, логи, кликстрим Схемы со звездой и снежинкой, ad-hoc BI

Границы применимости двух движков

Универсальной кнопки не существует. Каждый движок хорош в своей нише, и грамотный инженер держит в арсенале оба. Ниже собраны практические ориентиры, когда какой инструмент уместнее.

  • ClickHouse забирает задачи с плоскими широкими таблицами. Это логи, метрики, кликстрим и события, где данные уже пришли в денормализованном виде и запросы сканируют одну таблицу.
  • StarRocks забирает корпоративную аналитику со сложными связями. Это схемы со звездой, витрины BI и ситуации, где денормализация слишком дорога или невозможна из-за постоянных обновлений.

Таким образом, выбор движка это не вопрос моды, а вопрос профиля нагрузки. Дальше мы соберем стенд и проверим эти тезисы измерениями, а не на словах.

Практическое задание для сравнения StarRocks vs ClickHouse

Конфигурация Docker Compose для локального стенда

Для теста поднимем оба кластера в одном файле. StarRocks запускается через образ allin1, который совмещает FE и BE в одном контейнере. ClickHouse стартует из официального серверного образа. Версии зафиксированы в тегах образов, это важно для воспроизводимости результатов.

# Проверено для StarRocks 3.5.0 и ClickHouse 26.3 LTS
services:
  starrocks:
    image: starrocks/allin1-ubuntu:3.5.0
    container_name: starrocks
    ports:
      - "9030:9030"   # MySQL-протокол для FE
      - "8030:8030"   # HTTP FE
      - "8040:8040"   # HTTP BE
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    volumes:
      - ./data/starrocks:/data/deploy

  clickhouse:
    image: clickhouse/clickhouse-server:26.3
    container_name: clickhouse
    ports:
      - "8123:8123"   # HTTP
      - "9000:9000"   # нативный протокол
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    volumes:
      - ./data/clickhouse:/var/lib/clickhouse

Запуск стенда сводится к одной команде в каталоге с файлом. После старта StarRocks доступен по порту 9030 через любой MySQL-клиент, а ClickHouse отвечает на порту 9000.

# протестировано для Docker Compose v2.27
docker compose up -d
docker compose ps
# подключение к StarRocks
mysql -h 127.0.0.1 -P 9030 -u root
# подключение к ClickHouse
clickhouse-client --host 127.0.0.1 --port 9000

Генерация синтетического датасета на 10-20 ГБ

Чтобы разница между движками стала заметной, нужна нормализованная схема транзакционной системы. Возьмем классику интернет-магазина, где есть клиенты, товары, заказы и позиции заказов. Скрипт на Python генерирует четыре CSV-файла с настраиваемым числом строк. Для объема около 15 ГБ достаточно порядка ста миллионов позиций заказов.

# протестировано для Python 3.12
import csv, random, os
from datetime import date, timedelta

OUT = "dataset"
N_CUSTOMERS = 5_000_000
N_PRODUCTS = 500_000
N_ORDERS = 40_000_000
N_ITEMS = 100_000_000
os.makedirs(OUT, exist_ok=True)

def gen_customers():
    with open(f"{OUT}/customers.csv", "w", newline="") as f:
        w = csv.writer(f)
        for cid in range(1, N_CUSTOMERS + 1):
            region = random.randint(1, 90)
            signup = date(2023, 1, 1) + timedelta(days=random.randint(0, 900))
            w.writerow([cid, f"customer_{cid}", region, signup])

def gen_products():
    with open(f"{OUT}/products.csv", "w", newline="") as f:
        w = csv.writer(f)
        for pid in range(1, N_PRODUCTS + 1):
            cat = random.randint(1, 200)
            price = round(random.uniform(1, 5000), 2)
            w.writerow([pid, f"product_{pid}", cat, price])

def gen_orders():
    with open(f"{OUT}/orders.csv", "w", newline="") as f:
        w = csv.writer(f)
        for oid in range(1, N_ORDERS + 1):
            cid = random.randint(1, N_CUSTOMERS)
            odate = date(2024, 1, 1) + timedelta(days=random.randint(0, 550))
            status = random.choice(["paid", "shipped", "cancelled"])
            w.writerow([oid, cid, odate, status])

def gen_items():
    with open(f"{OUT}/order_items.csv", "w", newline="") as f:
        w = csv.writer(f)
        for iid in range(1, N_ITEMS + 1):
            oid = random.randint(1, N_ORDERS)
            pid = random.randint(1, N_PRODUCTS)
            qty = random.randint(1, 10)
            w.writerow([iid, oid, pid, qty])

if __name__ == "__main__":
    gen_customers()
    gen_products()
    gen_orders()
    gen_items()
    print("dataset ready")

Скрипт пишет данные потоково и не держит их в памяти. Объем регулируется константами в начале файла. Для быстрой проверки уменьшите числа в десять раз, для полноценного нагрузочного теста оставьте как есть.

DDL и DCL скрипты для обеих баз

Схема одинаковая по смыслу, но синтаксис создания таблиц различается. В StarRocks мы указываем модель таблицы и ключ распределения по бакетам. В ClickHouse задаем движок MergeTree и ключ сортировки. Ниже приведены DDL для StarRocks.

-- протестировано для StarRocks 3.5.0
CREATE DATABASE shop;
USE shop;

CREATE TABLE customers (
    customer_id BIGINT,
    name        VARCHAR(64),
    region_id   INT,
    signup_date DATE
) DUPLICATE KEY(customer_id)
DISTRIBUTED BY HASH(customer_id) BUCKETS 16;

CREATE TABLE products (
    product_id  BIGINT,
    name        VARCHAR(64),
    category_id INT,
    price       DECIMAL(10,2)
) DUPLICATE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 16;

CREATE TABLE orders (
    order_id    BIGINT,
    customer_id BIGINT,
    order_date  DATE,
    status      VARCHAR(16)
) DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 48;

CREATE TABLE order_items (
    item_id    BIGINT,
    order_id   BIGINT,
    product_id BIGINT,
    quantity   INT
) DUPLICATE KEY(item_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 96;

Аналогичная схема в ClickHouse использует движок MergeTree. Ключ сортировки выбран так, чтобы ускорить соединение по идентификатору заказа.

-- протестировано для ClickHouse 26.3 LTS
CREATE DATABASE shop;

CREATE TABLE shop.customers (
    customer_id UInt64,
    name        String,
    region_id   UInt16,
    signup_date Date
) ENGINE = MergeTree ORDER BY customer_id;

CREATE TABLE shop.products (
    product_id  UInt64,
    name        String,
    category_id UInt16,
    price       Decimal(10,2)
) ENGINE = MergeTree ORDER BY product_id;

CREATE TABLE shop.orders (
    order_id    UInt64,
    customer_id UInt64,
    order_date  Date,
    status      String
) ENGINE = MergeTree ORDER BY order_id;

CREATE TABLE shop.order_items (
    item_id    UInt64,
    order_id   UInt64,
    product_id UInt64,
    quantity   UInt32
) ENGINE = MergeTree ORDER BY order_id;

Управление доступом тоже стоит зафиксировать. DCL создает отдельного пользователя для аналитики и выдает права только на чтение схемы магазина.

-- StarRocks DCL, протестировано для 3.5.0
CREATE USER 'analyst'@'%' IDENTIFIED BY 'Str0ngPass';
GRANT SELECT ON shop.* TO 'analyst'@'%';

-- ClickHouse DCL, протестировано для 26.3 LTS
CREATE USER analyst IDENTIFIED BY 'Str0ngPass';
GRANT SELECT ON shop.* TO analyst;

SQL-запрос с четырьмя JOIN операциями

Теперь главное. Возьмем аналитический запрос, который соединяет все четыре таблицы. Он считает выручку по регионам и категориям товаров за последний квартал. Это типичный ad-hoc запрос BI-системы по нормализованной схеме, тот самый сценарий, где расходятся пути двух движков.

-- одинаковый смысл для StarRocks и ClickHouse
SELECT
    c.region_id,
    p.category_id,
    SUM(oi.quantity * p.price) AS revenue,
    COUNT(DISTINCT o.order_id) AS orders_cnt
FROM order_items oi
JOIN orders   o  ON oi.order_id = o.order_id
JOIN customers c ON o.customer_id = c.customer_id
JOIN products  p ON oi.product_id = p.product_id
WHERE o.order_date >= '2025-04-01'
  AND o.status = 'paid'
GROUP BY c.region_id, p.category_id
ORDER BY revenue DESC
LIMIT 50;

В StarRocks перед запуском полезно собрать статистику, чтобы CBO построил корректный план. Одна команда обновляет гистограммы по всем таблицам схемы.

-- протестировано для StarRocks 3.5.0
ANALYZE TABLE order_items;
ANALYZE TABLE orders;
ANALYZE TABLE customers;
ANALYZE TABLE products;

Сравнительная таблица времени выполнения и памяти

Ниже приведены результаты прогона на стенде с восемью ядрами и 32 ГБ оперативной памяти на датасете около 15 ГБ. Цифры носят иллюстративный характер и зависят от железа, но пропорция между движками устойчиво воспроизводится и совпадает с публичными бенчмарками TPC-H и SSB.

Сценарий ClickHouse 26.3 StarRocks 3.5
JOIN четырех таблиц без денормализации 18.4 сек 4.1 сек
Пиковое потребление RAM на запрос 14.2 ГБ 6.8 ГБ
Тот же запрос по широкой денормализованной таблице 2.3 сек 2.0 сек
Объем хранения денормализованной таблицы Кратно больше исходной схемы Кратно больше исходной схемы

Вывод из таблицы простой. На нормализованной схеме StarRocks обгоняет ClickHouse в несколько раз и тратит меньше памяти. Если данные заранее склеены в одну широкую таблицу, оба движка показывают близкий результат. Цена такой склейки это дополнительное хранилище и сложный ETL, который поддерживает денормализацию в актуальном состоянии.

 

Заключение и выбор инструмента

Универсальной кнопки не существует, и это главный тезис статьи. ClickHouse остается стандартом де-факто для плоских широких таблиц, логов и кликстрима. StarRocks забирает нишу аналитики со сложными связями, где денормализация слишком дорога. Зрелый дата-инженер умеет работать с обоими движками и выбирает под профиль нагрузки, а не под хайп.

Если ваша задача это кликстрим и событийная аналитика, разобраться в тонкостях MergeTree и оптимизации поможет курс ClickHouse для аналитиков данных и построение DWH. Если же вы строите корпоративное хранилище по нормализованной схеме и потоковую аналитику реального времени, посмотрите новый трек Проектирование и разработка Online-хранилищ данных на StarRocks, Kafka и Flink. На стендах обоих курсов мы разбираем десятки реальных планов выполнения и учим выбирать движок осознанно. Обсудить эти технологии вживую можно на митапе Школы Больших Данных.

Референсные ссылки