Типы данных и движки в ClickHouse: Фундамент для производительности. Урок 2

Типы данных и движки таблиц в ClickHouse - https://bigdataschool.ru

Приветствуем вас во второй части нашего курса по основам ClickHouse (далее CH)! В первой статье мы разобрались, что такое ClickHouse, почему он так хорош для аналитики и как запустить его локально или в облаке. Теперь пришло время углубиться в две ключевые концепции, которые определяют, как CH хранит и обрабатывает ваши данные: типы данных ClickHouse и движки таблиц (Table Engines).

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

Типы Данных ClickHouse: Точность и Эффективность Хранения

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

Числовые типы данных используемые в ClickHouse

ClickHouse предоставляет широкий спектр числовых типов, что позволяет выбрать оптимальный для памяти диапазон значений.

Целые числа: Это типы от Int8/UInt8 до Int256/UInt256. Правило простое: всегда выбирайте наименьший тип, который может вместить ваши значения. Например, для возраста человека идеально подойдет UInt8.

Числа с плавающей точкой: Float32 и Float64 используются для вычислений, где требуется высокая точность.

Десятичные числа (Decimal): Типы Decimal32, Decimal64 и другие незаменимы там, где потеря точности недопустима, например, при работе с денежными суммами.

 

Строковые типы

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

String: Строка произвольной длины, самый универсальный тип.

FixedString(N): Строка фиксированной длины. Идеальна для данных вроде хэшей или кодов стран.

LowCardinality(String): Это “секретное оружие” для оптимизации. Используется для столбцов с небольшим количеством уникальных значений. ClickHouse создает словарь уникальных значений и для каждой строки хранит лишь короткий числовой индекс. Это драматически улучшает сжатие и ускоряет фильтрацию и группировку.

LowCardinality in ClickHouse

Дата и время

Date: Хранит только дату.

DateTime: Хранит дату и время с точностью до секунды.

DateTime64(precision): Хранит дату и время с точностью до милли-, микро- или наносекунд.

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

 

Прочие важные типы

Boolean: Хранится как UInt8 (0 или 1).

UUID: Для хранения универсальных уникальных идентификаторов.

IPv4, IPv6: Специализированные и эффективные типы для IP-адресов.

Array(T): Массив значений одного типа.

Nullable(T): Оборачивает любой тип, позволяя ему хранить значение NULL. Используйте его только при необходимости.

 

Практический пример: Выбор типов данных для ClickHouse таблиц

Давайте пересмотрим таблицу access_logs из первой статьи и посмотрим, как типы данных влияют на нее:

CREATE TABLE access_logs (
    timestamp DateTime64(3),          -- Точность до миллисекунд для детального анализа событий
    event_type LowCardinality(String),-- Ограниченное число уникальных типов событий -> эффективно
    user_id UInt64,                   -- Большое количество пользователей, ID всегда положительный
    ip_address IPv4,                  -- Оптимизировано для хранения IP-адресов
    url String,                       -- Произвольная длина, высокая кардинальность -> String
    duration_ms UInt32,               -- Продолжительность всегда положительная, до нескольких миллиардов миллисекунд
    is_mobile Nullable(UInt8)         -- Добавим новое поле: может быть NULL, если неизвестно
) ENGINE = MergeTree()
ORDER BY (timestamp, user_id);

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

 

Движки Таблиц (Table Engines): Сердце Хранения Данных

Движок таблицы (Table Engine) определяет, как данные хранятся, читаются, записываются, индексируются и реплицируются. Это одна из самых мощных и отличительных особенностей CH . Выбор движка критически важен, так как он определяет производительность, надежность и функциональность вашей таблицы.

CH предлагает множество движков, но для аналитических задач наиболее важными являются движки семейства MergeTree. Они оптимизированы для сценариев OLAP (Online Analytical Processing) и обеспечивают высокую производительность на больших объемах данных.

Семейство движков MergeTree: ваш рабочий инструмент

Движки MergeTree спроектированы для хранения огромных объемов данных и обеспечивают молниеносное выполнение аналитических запросов. Ключевые особенности MergeTree:

Колоночное хранение: Как мы уже обсуждали, данные хранятся по столбцам.

Партиционирование (Partitioning): Данные могут быть разбиты на логические части (партиции) по заданному критерию (часто по дате или месяцу). Это позволяет ClickHouse читать только те данные, которые релевантны запросу, и эффективно удалять старые данные.

Сортировка (Ordering): Данные внутри каждой партиции отсортированы по указанному ключу сортировки (ORDER BY). Это ускоряет фильтрацию и агрегацию.

Индексирование (Indexing): Поверх отсортированных данных создается разреженный индекс (primary index), который позволяет быстро находить блоки данных.

Слияние (Merging): Данные записываются в небольшие, отсортированные части (data parts). В фоновом режиме ClickHouse периодически объединяет эти части в более крупные, что оптимизирует хранение и производительность.

MergeTree engines ClickHouse

Синтаксис базового движка MergeTree:
CREATE TABLE my_table (
    col1 DataType,
    col2 DataType,
    -- ...
) ENGINE = MergeTree()
PARTITION BY expression -- (Опционально) Выражение для партиционирования
ORDER BY (col1, col2)  -- Ключ сортировки (ОБЯЗАТЕЛЕН для MergeTree)
PRIMARY KEY (col1)     -- (Опционально) Выборка подмножества из ORDER BY для первичного индекса
TTL expression         -- (Опционально) Время жизни данных
SETTINGS setting = value -- (Опционально) Дополнительные настройки

PARTITION BY: Определяет, как данные будут разбиты на партиции. Чаще всего используется по дате или части даты (toYYYYMM(timestamp)). Это наиболее важная оптимизация для работы с большими временными рядами. При запросах по диапазону дат CH будет читать только нужные партиции, игнорируя остальные.

ORDER BY: Определяет, как данные отсортированы внутри каждой партиции. Это ваш основной индекс. Запросы с WHERE или GROUP BY по столбцам из ORDER BY будут выполняться быстрее.

PRIMARY KEY: В отличие от традиционных СУБД, PRIMARY KEY в CH не гарантирует уникальность. Он просто определяет, какие столбцы используются для построения разреженного первичного индекса. Если не указан, по умолчанию используется ORDER BY.

 

Вариации MergeTree

Помимо базового MergeTree, существуют специализированные движки, наследующие его функциональность и добавляющие специфическое поведение:

ReplacingMergeTree:

Назначение: Обработка дубликатов. При слиянии частей данных, если есть строки с одинаковым значением ключа сортировки (ORDER BY), ReplacingMergeTree оставляет только одну (последнюю по времени вставки или указанному столбцу-версии).

Пример: Для таблицы пользователей, где нужно хранить только актуальную информацию о каждом пользователе.

ENGINE = ReplacingMergeTree([ver_column])ver_column опционален и указывает на столбец, по которому определяется “самая свежая” запись.

SummingMergeTree:

Назначение: Агрегация данных “на лету” при слиянии частей. Все числовые столбцы (кроме тех, что в ORDER BY) суммируются, если строки имеют одинаковое значение ключа сортировки.

Пример: Для агрегации метрик (просмотры, клики, суммы транзакций) по измерениям (дата, кампания, пользователь). Значительно уменьшает объем хранимых данных.

ENGINE = SummingMergeTree([columns_to_sum]) – опционально можно указать, какие столбцы суммировать. Если не указаны, суммируются все числовые.

AggregatingMergeTree:

Назначение: Хранение предварительно агрегированных данных. Используется совместно с агрегатными функциями ClickHouse engine, которые возвращают промежуточные состояния (AggregateFunction).

Пример: Если вам нужно часто считать uniqCombined или quantiles по большому объему данных, вы можете предварительно агрегировать их.

Это более продвинутый движок, требующий понимания работы с AggregateFunction.

CollapsingMergeTree:

Назначение: Удаление “парных” строк (например, событие “вход” и “выход”) или хранение только последнего состояния записи, используя специальный столбец Sign.

Пример: Отслеживание сессий или состояний сущностей, где важно фиксировать изменения и убирать промежуточные состояния.

GraphiteMergeTree:

Назначение: Оптимизирован для хранения данных временных рядов, подобных метрикам Graphite.

Replicated*MergeTree: (Например, ReplicatedMergeTree, ReplicatedReplacingMergeTree и т.д.)

Назначение: Обеспечивает репликацию данных между серверами в кластере, используя Apache ZooKeeper (или ClickHouse Keeper) для координации. Это ваш выбор для production-систем, где нужна отказоустойчивость.

 

Практический Пример: Использование ClickHouse MergeTree engine с Партиционированием

Вернемся к нашей таблице access_logs. Добавим партиционирование по месяцу, что очень распространено для временных рядов. Удалим старую таблицу (если она существует):

DROP TABLE IF EXISTS my_first_db.access_logs;

-- Создадим новую таблицу с партиционированием

CREATE TABLE my_first_db.access_logs (
timestamp DateTime64(3),
event_type LowCardinality(String),
user_id UInt64,
ip_address IPv4,
url String,
duration_ms UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp) -- Партиционируем по году и месяцу из поля timestamp
ORDER BY (timestamp, user_id);

Здесь toYYYYMM(timestamp) извлекает год и месяц из временной метки (например, 202406 для июня 2024 года). ClickHouse автоматически создаст отдельные директории для каждой партиции на диске.

Вставим данные (включая данные за разные месяцы):

INSERT INTO my_first_db.access_logs (timestamp, event_type, user_id, ip_address, url, duration_ms) VALUES
('2024-05-15 10:00:00.123', 'page_view', 100, '192.168.1.1', '/old_home', 150),
('2024-05-15 10:00:01.456', 'click', 100, '192.168.1.1', '/old_button_a', 20),
('2024-06-19 10:00:00.123', 'page_view', 101, '192.168.1.1', '/home', 150),
('2024-06-19 10:00:01.456', 'click', 101, '192.168.1.1', '/button_a', 20),
('2024-06-19 10:00:02.789', 'page_view', 102, '10.0.0.5', '/products', 300),
('2024-06-20 10:00:03.000', 'page_view', 101, '192.168.1.1', '/contact', 100),
('2024-06-20 10:00:04.111', 'click', 102, '10.0.0.5', '/product_details', 50),
('2024-07-01 08:00:05.222', 'page_view', 103, '172.16.0.10', '/home', 200),
('2024-07-01 08:01:05.222', 'page_view', 104, '172.16.0.11', '/home', 250);

Проверим партиции (только для локальной инсталляции или через системные таблицы). В clickhouse-client вы можете посмотреть, какие партиции создались:

system.parts для получения информации по партициям

Вы видите отдельные партиции для 202405, 202406 и 202407. Выполним запрос с фильтрацией по партиции. Когда вы запрашиваете данные за конкретный месяц, CH будет читать только соответствующую партицию:

Запрос с фильтрацией по партиции

Этот запрос будет очень быстрым, так как CH сразу поймет, что ему нужно работать только с партицией 202406.

Другие категории движков (краткий обзор)

Хотя MergeTree – ваш основной инструмент для аналитики, стоит знать о других категориях движков:

Лог-движки (Log, TinyLog, StripeLog):

Простые движки для небольших объемов данных, которые записываются последовательно и не изменяются. Не поддерживают ORDER BY, PARTITION BY.

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

Движки для внешних систем (Kafka, MySQL, PostgreSQL, ODBC, JDBC, S3, URL, File):

Позволяют ClickHouse напрямую работать с данными из внешних источников, не импортируя их.

Когда использовать: Для чтения данных из стриминговых платформ ( Kafka), PostgreSQL, S3 и других источников (более подробно посмотрим на них в Уроке 7 посвященном интеграции)

Специальные движки (Dictionary, Distributed, Buffer):

Dictionary: Для работы со словарями (маппингами), которые загружаются в оперативную память для быстрого сопоставления.

Distributed: Не хранит данные сам по себе, а позволяет выполнять распределенные запросы по нескольким серверам CH.

Buffer: Для временного буферизации данных перед их записью в другой движок (обычно MergeTree).

 

Практическое Задание для Самостоятельного Изучения

Чтобы закрепить материал, предлагаю следующее упражнение. Выполните его в вашей локальной инсталляции Docker или в ClickHouse Cloud.

  • Создайте новую базу данных с именем my_app_data.
  • Создайте таблицу user_profiles в этой базе данных с использованием ReplacingMergeTree (чтобы гарантировать уникальность профилей по user_id при наличии дубликатов). Включите следующие столбцы:
user_id (UInt64)
username (String)
email (String)
registration_date (Date)
last_login_timestamp (DateTime)
is_premium (UInt8 или Nullable(UInt8))
region (LowCardinality(String))
version (UInt64) - используйте его как столбец версии для ReplacingMergeTree.

Определите ORDER BY для этой таблицы по user_id и registration_date.

Вставьте несколько записей в user_profiles, включая:

Несколько уникальных пользователей.

Несколько записей для одного и того же user_id, но с разными version и last_login_timestamp, чтобы увидеть, как ReplacingMergeTree работает. Убедитесь, что последняя запись для пользователя имеет наибольший version.

Проверьте данные с помощью SELECT * FROM user_profiles.

Попробуйте вставить еще одну запись для существующего пользователя с более высоким version и измененными данными (например, новый email).

Снова проверьте данные и убедитесь, что ReplacingMergeTree оставил только самую свежую версию записи для этого пользователя (возможно, вам придется подождать несколько секунд, пока произойдет слияние, или принудительно выполнить его с помощью OPTIMIZE TABLE user_profiles FINAL; — не используйте OPTIMIZE в продакшене без понимания его работы).

Это упражнение поможет вам на практике понять, как работают типы данных и движки MergeTree.

Заключение

Типы данных и движки таблиц — это не просто теоретические концепции в CH; это фундаментальные строительные блоки, которые напрямую влияют на производительность, стоимость хранения и эффективность ваших аналитических решений. Правильный выбор и понимание их работы позволят вам раскрыть весь потенциал СУБД ClickHouse.

В следующей статье ( Урок 3) мы углубимся в выполнение запросов: рассмотрим базовые DML операции (вставка, выборка) и продвинутые SQL-функции, которые помогут вам извлекать максимум информации из ваших данных.


Использованные референсы и материалы

  1. Официальная документация ClickHouse по типам данных и движкам таблиц.
  2. Документация Apache Kafka и Apache ZooKeeper.

SQL-блокнот к Уроку 1 бесплатного курса доступен в нашем репозитории на GitHub