commit ed11050a75ff83b8972aa7e74a36ad6c885ca625 Author: Rinsvent Date: Mon Jun 12 09:33:17 2023 +0700 Добавил гайд по изучению базовых навыков clickhouse Если такой формат зайдет, то буду расширять текущее описание. Например про кластер + добавлю гайды по другим базам diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..678b837 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db-data +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..113ed8a --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ +# Clickhouse guide + +Clickhouse - очень мощный инструмент. Это высокопроизводительная база данных, которая обладает большим спектром возможностей. +Хорошо подходит для построения отчетов и хранения истории. + +## Запустить контейнер +```bash +docker compose up -d +``` + +## Подключаемся через PhpStorm к базе +### Ищем список драйверов +![Ищем список клиентов](images/1.png) +### Выбираем драйвер +![Выбираем клиент](images/2.png) +### Указываем доступы до базы +Доступы можно взять из docker-compose.yml +- Логин - dbuser +- Пароль - dbuser +- Имя базы данных - dbname + +![Указываем доступы до базы](images/3.png) +### Переходим в терминал +![Переходим в терминал](images/4.png) +### Выбираем дефолтную консоль +![Выбираем дефолтную консоль](images/5.png) + +## Создание таблиц +В целом clickhouse реализует sql и запросы похожи не те, которые используются при работе в mysql, но есть ряд отличий(с ними познакомимся дальше). +Создание таблицы может выглядеть так +```clickhouse +CREATE TABLE history +( + uuid UUID, + entity ENUM('user', 'device'), + entity_id String, + action Int16, + field String, + value Nullable(String), + created_by Int32, + created_at DateTime +) +Engine MergeTree() +PARTITION BY toYYYYMM(created_at) +PRIMARY KEY (created_at, uuid) +ORDER BY (created_at, uuid); +``` +Нужно скопировать запрос в терминал и выполнить (ctrl + enter) + +В данном случае мы создали простую таблицу для хранения истории +В clickhouse нет auto increment поэтому в качестве primary key используется uuid. +В данном случае создана партицированная таблица с разбивкой по месяцам, поэтому мы обязаны при создании добавить поле в PRIMARY KEY и ORDER BY +Партиции позволяют ускорить запрос за счет того что выборка получается меньше по размерам. Вместо 1 большой таблицы на 10 миллионов записей, можно получить 10 виртуальных таблиц по 1 миллиону. +При запросе к такой таблице clickhouse может перенаправить запрос только в нужные партиции тем самым избежав обхода лишнего колличества данных. +С точки зрения использования запрос не меняется. Мы по прежнему работаем с 1 таблицей. Всю магию по определению партиция и объединению результатов выборки clickhouse реализует под капотом. +За счет этого, запросы получаются быстрее по сранению с классической single таблицей. + +По sql выше мы видим, что определение типов в запросе выглядит иначе по сранению с mysql. +Информацию по типам можно найти в оффициальной документации в этом разделе https://clickhouse.com/docs/ru/sql-reference/data-types/int-uint. +Наиболее явное отличие это определение nullable - оно связано с типом в отличие от mysql. + +Дальше в запросе видно что мы указали engine для таблицы. В отличие от mysql, где на проекте обычно используется всегда один движок, в clickhouse нужно иметь понятие об их существовании и отличиях. +Официальная документация тут https://clickhouse.com/docs/ru/engines/table-engines. +Каждый движок несет свои уникальные возможности. +Например: +- MergeTree - позволяет использовать партиции и быстро обрабатывает insert запросы +- MySql - позволяет делать запросы к mysql через clickhouse и тем самым избежать дублирования данных +- File - позволяет писать / читать из конкретного файла. Может быть удобно для просмотра логов, либо для быстрого импорта данных в основную таблицу на основе которой уже выводится статистика. + +## Модификация полей таблицы (DDL) +Модификация полей в целом аналогична mysql +Выполним запрос и описание таблицы +```clickhouse +ALTER TABLE history MODIFY COLUMN created_by Nullable(Int32) +``` +Посмотрим DDL + +![img.png](images/ddl.png) +Видим такой результат + +```clickhouse +-- auto-generated definition +create table history +( + uuid UUID, + entity Enum8('user' = 1, 'device' = 2), + entity_id String, + action Int16, + field String, + value Nullable(String), + created_by Nullable(Int32), + created_at DateTime +) +engine = MergeTree PARTITION BY toYYYYMM(created_at) +PRIMARY KEY (created_at, uuid) +ORDER BY (created_at, uuid) +SETTINGS index_granularity = 8192; +``` +Текущая схема соответствует ожидаемой структуре. + +## Вставка данных +Вставка данных похожа на mysql. +Выполните в терминале +```clickhouse +insert into history SELECT generateUUIDv4(), 'user', generateUUIDv4(), rand32(), 'first_name', 'Anakin', 3, now() - interval 100 day; +insert into history SELECT generateUUIDv4(), 'user', generateUUIDv4(), rand32(), 'last_name', 'Skywalker', 2, now() - interval 50 day; +insert into history SELECT generateUUIDv4(), 'user', generateUUIDv4(), rand32(), 'age', '33', 1, now() - interval 10 day; +insert into history SELECT generateUUIDv4(), 'user', generateUUIDv4(), rand32(), 'first_name', 'Denis', 2, now(); +``` +Данные должны успешно вставиться и быть доступны на чтение +Сейчас можно посмотреть какие партиции созданы +Выполним запрос +```clickhouse +SELECT + partition, + name, + active +FROM system.parts +WHERE table = 'history'; +``` +В ответе будет примерно такое + +![img.png](images/partitions.png) + +Здесь видно, что данные разбиты на 5 частей, не смотря на то что у нас три записи попадают в один месяц. При вставке clickhouse вставляет данные в отдельные куски и затем в фоне может их обрабатывать и объединять для оптимизации. +По окончанию мы увидим следующий результат + +![img.png](images/partitions2.png) + +Сейчас можно попробовать встаить такие данные +```clickhouse +insert into history SELECT generateUUIDv4(), 'device', generateUUIDv4(), rand32(), 'first_name', 'Denis', null, now(); +``` +Вставка происходит успешно. Несмотря на то, что мы передаем null в not nullable поле created_by. В данном случае значение привелось к 0 и сохранилось в базе +А теперь попробуем вставить такие данные +```clickhouse +insert into history SELECT generateUUIDv4(), 'device2', generateUUIDv4(), rand32(), 'first_name', 'Denis', 3, now(); +``` +clickhouse выдает ошибку что значение enum нам не известно и возвращает ошибку. + +## Модификация данных +В clickhouse нет классического редактирования данных. Отказ от update позволяет clickhouse быть таким быстрым. +Но не все безнадежно. Если нельзя но очень хочется, то есть механизм мутаций. +Подробнее тут https://clickhouse.com/docs/ru/sql-reference/statements/alter#mutations +В целом это выглядит как редактирование на mysql, но через alter table + +```clickhouse +alter table history update value='Dionis' where value='Denis'; +``` + +В данный момент возможно посмотреть список мутаций так + +```clickhouse +SELECT + mutation_id, + command, + is_done +FROM system.mutations +WHERE table = 'history'; +``` + +В ответе будет такое + +![img.png](images/mutations.png) + +Из таблицы видно, что наш запрос был зарегистрирован в списке мутаций и что он уже выполнился. +Особо сложные запросы могут выполняться часами. +Такое обновление происходит асинхронно в фоне и не блочит работу таблицы. +Также можно заметить что изменения типа полей таблицы также были зарегистрированы в мутациях. +Несмотря на то, что есть возможность редактировать данные - не стоит этим злоупотреблять. Если единовременно будет много мутаций в обработке, то это плохо скажется на производительности системы и может привести к ошибкам накатки мутаций. +Мутации следует использовать когда одним запросом можно привести данные к ожидаемому результату. +Например, на основе одного поля рассчитать значения другого или скопировать значения поля из соседней таблицы в указанную. + +Вероятно, что может понадобиться возможность частого обновления данных. В этом случае есть несколько лайфхаков. +Подробнее тут - https://habr.com/ru/companies/just_ai/articles/589621/ +Один из простых способов достичь нужного нам результата выглядит так. +- Создаем еще одну таблицу где мы будем хранить более свежие значения полей +- Строим запрос с проверкой на наличие свежей версии значения +- Раз в определенный период (например раз в месяц) выполняем мутацию на перенос значений в основную таблицу и чистим значения из дополнительной + +Создаем таблицу +```clickhouse +CREATE TABLE history_value +( + uuid UUID, + value Nullable(String), + timestamp Int64 +) +Engine ReplacingMergeTree() +ORDER BY uuid; +``` +Обновляем значение +```clickhouse +insert into history_value (uuid, value, timestamp) VALUES ((select uuid from history where field='age' and value='33'), '44', toUnixTimestamp(now())); +``` +Получаем строки с актуальными значениями +```clickhouse +select + uuid, + entity, + entity_id, + action, + field, + coalesce(hv.value, h.value), + created_by, + created_at +from history h left join history_value hv on h.uuid = hv.uuid; +``` + +Обновляем значение повторно +```clickhouse +insert into history_value (uuid, value, timestamp) VALUES ((select uuid from history where field='age' and value='33'), '55', toUnixTimestamp(now())); +``` +После повторно операции видим, что у нас дублируются записи + +![img.png](images/override_value.png) + +Можно было бы остановиться на первом варианте запроса с актуальными значениями, если допустимо, что какое-то время данные будут дублироваться. +Например, если выполнить обновление ночью, то к утру, вероятно, что лишние данные уже будут дедублицированы. +Чтобы ускорить процесс слияния кусков, можно принудительно запустить внеочередное слияние так: + +Предлагаю пока не выполнять этот запрос, а продолжить изучение дальше. +По окончанию практики можете повторить выполнить вставку нескольких строк данных и оптимизацию таблицы самостоятельно. +```clickhouse +OPTIMIZE TABLE history_value DEDUPLICATE; +``` + +Если же запрос выполнили, то вставьте еще несколько строк +```clickhouse +insert into history_value (uuid, value, timestamp) VALUES ((select uuid from history where field='age' and value='33'), '66', toUnixTimestamp(now())); +insert into history_value (uuid, value, timestamp) VALUES ((select uuid from history where field='age' and value='33'), '77', toUnixTimestamp(now())); +``` + +Подбедить дубли в realtime можно, например так: +```clickhouse +select + h.uuid, + entity, + entity_id, + action, + field, + coalesce(hv.value, h.value), + created_by, + created_at +from history h + left join history_value hv on h.uuid = hv.uuid + left join history_value hv2 on h.uuid = hv2.uuid +where empty(hv.uuid) or (hv2.timestamp < hv.timestamp); +``` +Так как для таблицы использовался движок ReplacingMergeTree, то дубль значения будет удален автоматически. + +Раз в определенный период можно запускать миграции для чистки лишних значений. +Синхронизируем значения +```clickhouse +alter table history update value=( + select coalesce(hv.value, h.value) + from history h + left join history_value hv on h.uuid = hv.uuid + left join history_value hv2 on h.uuid = hv2.uuid + where history.uuid = h.uuid and (hv2.timestamp < hv.timestamp) +) +where uuid in (select distinct history_value.uuid from history_value); +``` +И после этого удаляем лишние значения +```clickhouse +alter table history_value +delete where timestamp <= ( + select max(create_time) + from system.mutations + where command like '%UPDATE history%' and is_done = 1 +); +``` + +## Удаление записей +Здесь ситуация аналогична update. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c11d6f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + clickhouse: + image: clickhouse/clickhouse-server:23.4 + container_name: clickhouse + mem_limit: 2048m + restart: always + environment: + - TZ=${WORKSPACE_TIMEZONE:-Asia/Novosibirsk} + - CLICKHOUSE_USER=dbuser + - CLICKHOUSE_PASSWORD=dbuser + - CLICKHOUSE_DB=dbname + - PUID=${WORKSPACE_PUID:-1000} + - PGID=${WORKSPACE_PGID:-1000} + volumes: + - ./db-data/clickhouse:/var/lib/clickhouse + ports: + - "8123:8123" + ulimits: + nofile: + soft: 262144 + hard: 262144 + networks: + - general + +networks: + general: + external: + name: general diff --git a/images/1.png b/images/1.png new file mode 100644 index 0000000..7e58528 Binary files /dev/null and b/images/1.png differ diff --git a/images/2.png b/images/2.png new file mode 100644 index 0000000..79671c7 Binary files /dev/null and b/images/2.png differ diff --git a/images/3.png b/images/3.png new file mode 100644 index 0000000..5d1d752 Binary files /dev/null and b/images/3.png differ diff --git a/images/4.png b/images/4.png new file mode 100644 index 0000000..b093d58 Binary files /dev/null and b/images/4.png differ diff --git a/images/5.png b/images/5.png new file mode 100644 index 0000000..ee24a19 Binary files /dev/null and b/images/5.png differ diff --git a/images/ddl.png b/images/ddl.png new file mode 100644 index 0000000..c7bcd65 Binary files /dev/null and b/images/ddl.png differ diff --git a/images/mutations.png b/images/mutations.png new file mode 100644 index 0000000..9248757 Binary files /dev/null and b/images/mutations.png differ diff --git a/images/override_value.png b/images/override_value.png new file mode 100644 index 0000000..a865d56 Binary files /dev/null and b/images/override_value.png differ diff --git a/images/partitions.png b/images/partitions.png new file mode 100644 index 0000000..71d76e5 Binary files /dev/null and b/images/partitions.png differ diff --git a/images/partitions2.png b/images/partitions2.png new file mode 100644 index 0000000..afb56e5 Binary files /dev/null and b/images/partitions2.png differ