EvApps
192 subscribers
1.24K photos
52 videos
2 files
252 links
IT-aутстафферы из Тулы💚
https://evapps.ru/

Здесь пишем про веб- и мобильную разработку

▶️ Наш чат для системных аналитиков: https://t.me/pro_sa_evapps

▶️ Посмотреть, как мы живём: https://vk.com/evapps
Download Telegram
А тем временем в новом эпизоде ITToLкового подкаста наши бессменные ведущие вывели на чистую воду человека, который привык оставаться за кадром (но у истоков💪) всех наших PR-активностей😎

Поговорили с CMO EvApps Юлией Резниченко о том, как продвигать IT-компанию, сколько дают "на маркетинг" и причем здесь тульские пряники😉

🚀Что выяснили?
🔗 Смотри скорее по ссылке: https://vkvideo.ru/video-78780379_456239303
👍1
Продолжаем серию постов про ClickHouse! Ранее мы разобрали основные движки таблиц.
Сегодня углубимся в сердце производительности ClickHouse — ключи и индексы.

Важное уточнение: всё, о чём поговорим сегодня, работает только для семейства движков MergeTree.

Первичный ключ (Primary Key)
Индексы ClickHouse основаны на разреженном индексировании (Sparse Indexing) — альтернативе B-деревьям, используемым традиционными СУБД.
В B-деревьях индексируется каждая строка, что хорошо подходит для точечных запросов (point queries), характерных для OLTP-задач.
Однако это приводит к низкой скорости вставки больших объемов данных и высокому потреблению памяти и дискового пространства.

Напротив, разреженный индекс разбивает данные на несколько частей, каждая из которых группируется в фиксированные порции — гранулы.
ClickHouse создает индекс для каждой гранулы (группы данных), а не для каждой строки, отсюда и название "разреженный индекс".
При запросе с фильтром по первичным ключам ClickHouse находит соответствующие гранулы и загружает их параллельно в память.
Кроме того, данные хранятся в столбцах в нескольких файлах, что позволяет их сжимать и значительно экономить место на диске.

Для наглядности создадим таблицу пользовательских логов и вставим в нее данные:
CREATE TABLE user_access_logs
(
user_id UInt32,
page_url String,
access_time DateTime,
ip_address String,
session_duration UInt32
)
ENGINE = MergeTree
ORDER BY (user_id, access_time);

INSERT INTO user_access_logs
SELECT
number % 5000 as user_id,
concat('https://site.com/page', toString(rand() % 100)) as page_url,
now() - (rand() % 2592000) as access_time,
concat('192.168.', toString(rand() % 255), '.', toString(rand() % 255)) as ip_address,
rand() % 300 as session_duration
FROM numbers(1000000);

Важно: Если отдельно не указать первичные ключи, ClickHouse использует ключи сортировки (ORDER BY) в качестве первичных ключей. В этой таблице user_id и access_time будут первичными ключами.
При каждой вставке данных они будут сортироваться сначала по user_id, затем по access_time.

Фильтрация по первому первичному ключу
Посмотрим, что происходит при фильтрации по user_id (первый ключ):
EXPLAIN indexes=1
SELECT * FROM user_access_logs WHERE user_id = 100;

Результат анализа индексов: Система определила user_id как первичный ключ и исключила большинство гранул с его помощью!

Фильтрация по второму первичному ключу

Теперь попробуем фильтрацию по access_time (второй ключ):
EXPLAIN indexes=1
SELECT * FROM user_access_logs
WHERE access_time >= '2024-01-15 00:00:00'
AND access_time < '2024-01-16 00:00:00';

Результат анализа индексов: База данных определила access_time как первичный ключ, но не смогла эффективно исключить гранулы. Почему?
Потому что ClickHouse использует бинарный поиск только для первого ключа, а для остальных ключей — общий исключающий поиск, который гораздо менее эффективен.

Решение: правильный порядок ключей
Если мы поменяем порядок ключей в ORDER BY, поместив access_time на первое место (так как временные метки часто используются для диапазонных запросов), то получим лучшие результаты:
CREATE TABLE user_access_logs_optimized
(
`user_id` UInt32,
`page_url` String,
`access_time` DateTime,
`ip_address` String,
`session_duration` UInt32
)
ENGINE = MergeTree
ORDER BY (toStartOfDay(access_time), user_id, access_time);

Теперь при фильтрации по user_id (который стал вторым ключом):
EXPLAIN indexes=1
SELECT * FROM user_access_logs_optimized WHERE user_id = 100;

Результат: ClickHouse всё равно сможет эффективно фильтровать данные, используя комбинированную стратегию поиска.

Ключевое правило - всегда старайтесь упорядочивать первичные ключи от низкой к высокой кардинальности:
— Сначала ключи с малым количеством уникальных значений
— Затем ключи с большим количеством уникальных значений
Это обеспечит максимальную эффективность индексов для различных типов запросов.

#ClickHouse #БазыДанных #Аналитика #Индексы #Производительность #Оптимизация #OLAP
Всем привет!
Сегодня углубимся в детали ключей, партиций и дополнительных индексов — всё, что нужно для максимальной производительности.

Order Key vs Primary Key
Ранее я упоминал: если не указать PRIMARY KEY явно, ClickHouse использует ключи сортировки (ORDER BY) как первичные ключи.
Но вы можете задать PRIMARY KEY отдельно — он должен быть подмножеством ORDER BY.
CREATE TABLE ecommerce_events
(
    `customer_id` UInt32,
    `action_type` String,
    `event_date` Date,
    `product_id` UInt32,
    `session_token` String
)
ENGINE = MergeTree
PRIMARY KEY (event_date, customer_id)  -- Для индекса
ORDER BY (event_date, customer_id, action_type, product_id);  -- Для сортировки

Что здесь происходит:
— event_date и customer_id — используются и для индекса, и для сортировки
— action_type и product_id — только для сортировки (помогают в ORDER BY запросах)
— session_token — вообще не участвует в сортировке
Зачем это нужно? 
Если вы часто используете ORDER BY в запросах, включение этих столбцов в ORDER BY таблицы ускоряет выполнение — ClickHouse не будет тратить время на дополнительную сортировку.

Partition Key — разбиваем данные
Партиционирование в ClickHouse — это логическое разделение данных на части. По умолчанию все данные в одной партиции, но вы можете изменить это:
CREATE TABLE server_logs_partitioned
(
`server_id` UInt16,
`log_message` String,
`log_timestamp` DateTime
)
ENGINE = MergeTree
PARTITION BY toDate(log_timestamp)
ORDER BY (log_timestamp, server_id);

Что даёт партиционирование:
— Быстрое удаление старых данных — можно удалить целую партицию
— Оптимизация запросов — ClickHouse читает только нужные партиции
— Управление данными — перемещение, копирование партиций
Важно: 
Партиционирование — не для ускорения запросов!

Skip Index — когда ORDER BY не помогает:
Что делать, если нужно искать по столбцу, которого нет в ORDER BY? Например, найти все логи с определённым типом ошибки:
-- Добавляем ngram bloom filter для поиска подстрок
ALTER TABLE server_logs
ADD INDEX error_idx log_message
TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 2;

-- Применяем индекс к существующим данным
ALTER TABLE server_logs
MATERIALIZE INDEX error_idx;

Типы Skip Index:
— bloom_filter — для точного совпадения строк
— minmax — для диапазонов (даты, числа)
— ngrambf_v1 — для поиска подстрок
— tokenbf_v1 — для поиска отдельных слов

Case Study: оптимизация метрик IoT-устройств

Допустим, у нас есть данные с 50K IoT-устройств. Мы хотим:
1. Быстро искать метрики по времени и device_id
2. Фильтровать по типу сенсора
3. Искать по статусу ошибки
CREATE TABLE iot_metrics
(
`device_id` UInt32,
`sensor_type` String,
`metric_time` DateTime,
`value` Float32,
`error_flag` UInt8
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(metric_time) -- Для архивации старых данных
PRIMARY KEY (metric_time, device_id) -- Основные фильтры
ORDER BY (metric_time, device_id, sensor_type) -- Сортировка + фильтры
SETTINGS index_granularity = 8192;

-- Добавляем skip index для поиска ошибок
ALTER TABLE iot_metrics
ADD INDEX error_sk error_flag TYPE minmax GRANULARITY 1;

Результат:
— По времени+устройству — мгновенно (первичный ключ)
— По типу сенсора — быстро (часть ORDER BY)
— По ошибкам — эффективно (skip index)
— Архивация данных — DROP PARTITION для старых месяцев

Главные правила проектирования:
1.ORDER BY — сначала столбцы для самых частых фильтров
2.PRIMARY KEY — подмножество ORDER BY (обычно первые 2-3 столбца)
3.PARTITION BY — только для управления данными, не для скорости
4.Skip Index — для столбцов вне ORDER BY, но с фильтрами

Что дальше?
В следующем посте мы проведём детальное сравнение производительности ClickHouse и MySQL на практических примерах

#ClickHouse #БазыДанных #Производительность #Индексы #Партиционирование #Оптимизация #MySQL
👍3
Друзья, с наступающим 2026 годом! 🚀

Еще один год кода, багов, бессонных деплоев и триумфальных "все работает!" позади. Год, в котором требования менялись быстрее, чем кеш, а в продакшене всегда находился тот самый крайний случай. Но мы выдержали нагрузку, отрефакторили хаос и выкатили фичи - потому что наше дело именно такое.

Так пусть же в новом году:

🔥 Мержи проходят без конфликтов, а код-ревью длится не дольше чашки кофе.

🔥 Продакшен-баги обходят ваши сервисы десятой дорогой, а если и появляются — то в понедельник утром.

🔥 Тесты покрывают все, что нужно, и никогда не падают из-за кривых моков.

🔥 Документация существует (!), будет актуальной и в ней можно будет найти ответы.

🔥 Технический долг будет-таки закрыт, а не станет легаси, о котором все боятся вспоминать.

Желаем вам в 2026-м легких задач, быстрых компиляций, стабильных зависимостей и того чувства, когда после долгой работы код компилируется и запускается с первого раза. Пусть баланс между "багом" и "фичей" всегда будет в вашу пользу!

Отдыхайте, набирайтесь сил и вдохновения. Новый год готовит нам свежие вызовы, крутые технологии и гору интересной работы. И мы с вами точно со всем справимся - потому что лучшая команда разработчиков собрана именно здесь. 💻❤️

С Новым годом! 🎄
🔥2🎉1
ClickHouse vs MySQL: Когда что выбирать? 🤔
Ни одна БД не идеальна для всех задач. Нельзя ожидать, что одна СУБД будет максимально производительна для каждого запроса.
Ключевой навык разработчика — понимать сильные и слабые стороны разных инструментов и уметь выбирать подходящий инструмент для каждой задачи. 🛠

В следующем посте мы сравним ClickHouse как представителя OLAP-баз данных и MySQL как представителя OLTP.
Это поможет принимать лучшие решения при проектировании систем. Перед тем как переходить к сравнению, разберемся с основными понятиями. 📚

OLTP - Online Transaction Processing 💳
Это обработка транзакций в реальном времени.
Используется для повседневных операций: обработка заказов, обновление информации о клиентах, финансовые операции.
Оптимизирован для коротких транзакций с минимальным временем отклика.
Ключевые требования — точность данных и их согласованность.

OLAP - Online Analytical Processing 📊
Это аналитическая обработка данных.
Используется для анализа больших объёмов информации, выявления тенденций и закономерностей.
Оптимизирован для сложных аналитических запросов, которые обрабатывают миллионы строк.
Позволяет получать insights, недоступные при использовании традиционных отчётных инструментов.

MySQL 🐬
Популярная открытая система управления базами данных.
Это реляционная СУБД, которая хранит данные в таблицах и позволяет выполнять запросы к ним.
Используется веб-сайтами и приложениями для хранения и управления информацией.
Предоставляет такие функции как триггеры, хранимые процедуры и представления.
Проста в использовании и имеет широкий набор функций для создания мощных и эффективных приложений.

ClickHouse 🚀
Это колоночная OLAP-СУБД с открытым исходным кодом, разработанная в Yandex.
Создана для обеспечения высокой производительности аналитических запросов.
Использует SQL-подобный язык запросов и поддерживает различные типы данных, включая целые числа, строки, даты и числа с плавающей точкой.
Предлагает такие функции как кластеризация, распределённая обработка запросов и отказоустойчивость.
Также поддерживает репликацию и шардирование данных.

В следующем посте проведём детальное сравнение производительности ClickHouse и MySQL на примерах и покажем, в каких сценариях эти базы данных показывают себя наилучшим образом. 🎯
Всем привет! 👋
Прежде чем продолжить сравнивать базы - вернемся к бенчмаркам разработчиков Clickhouse

Эти тесты охватывают наиболее распространённые сценарии работы с данными:
- Анализ кликов и веб-трафика
- Веб-аналитика и пользовательское поведение
- Обработка машинно-генерируемых данных
- Работа со структурированными логами и событиями
- Ad-hoc аналитика и дашборды реального времени

Методология сравнения🧪
Для объективного сопоставления ClickHouse и MySQL мы взяли 10 миллионов строк из этого датасета и отобрали запросы, которые наиболее наглядно демонстрируют разницу между двумя подходами.
Хотя бенчмарк и ограничен этими двумя базами данных, вы можете обобщить концепцию на другие строчные и колоночные СУБД.

Процесс тестирования⚙️
- Создаётся база данных
- Создаётся таблица с определённым DDL
- Данные (hits.tsv) загружаются в таблицу, и измеряется время загрузки
- Выполняются запросы, и измеряется время выполнения каждого запроса

Тестовые запросы:📋
1. SELECT COUNT(*) FROM analytics_events; [OLAP]
2. SELECT SUM(event_value), COUNT(*), AVG(processing_time) FROM analytics_events; [OLAP]
3. Сложный GROUP BY с фильтрами по дате и категории события [OLAP]
4. Агрегация по 2 полям с сортировкой по частоте [OLAP]
5. Точечный SELECT по ID пользователя [OLTP]
6. SELECT нескольких полей по ID транзакции [OLTP]
7. UPDATE одной строки в таблице пользователей [OLTP]

1. Загрузка данных⬇️
• ClickHouse: 65 секунд
• MySQL: 11 минут 35 секунд
• Соотношение: MySQL медленнее в 10.7 раза

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

2. Размер таблицы на диске💾
ClickHouse: 1.3 GiB
MySQL: 6.32 GiB
Соотношение: MySQL занимает в 4.86 раза больше места
Колоночная структура обеспечивает возможность сжатия данных, что недоступно в строчных базах данных.

3. Выполнение запросов на чтение📖
Запрос 1: SELECT COUNT(*) FROM analytics_events;
ClickHouse: 0.005 сек
MySQL: 7.79 сек
Соотношение: ×1558 - ClickHouse быстрее

Запрос 2: SELECT SUM(event_value), COUNT(*), AVG(processing_time) FROM analytics_events;
ClickHouse: 0.030 сек
MySQL: 16.0 сек
Соотношение: ×533 - ClickHouse быстрее

Запрос 3: Сложный GROUP BY с фильтрами по дате
ClickHouse: 0.193 сек
MySQL: 4.35 сек
Соотношение: ×22.5 - ClickHouse быстрее

Запрос 4: Агрегация по двум полям с сортировкой
ClickHouse: 2.600 сек
MySQL: 180.93 сек (≈3 минуты)
Соотношение: ×69.6 - ClickHouse быстрее

Запрос 5: Точечный SELECT по конкретным ID
ClickHouse: 0.01 сек
MySQL: <0.001 сек
Для точечных запросов MySQL показывает лучшее время

Запрос 6: SELECT нескольких полей по конкретным ID
ClickHouse: 0.011 сек
MySQL: <0.001 сек
Для точечных запросов MySQL показывает лучшее время

Разреженные индексы и колоночная структура ClickHouse превзошли MySQL во всех OLAP-запросах (номера 1-4). Вот почему BI-аналитики и аналитики данных были бы более чем довольны ClickHouse для своих ежедневных отчетов.
Однако MySQL выигрывает битву, когда речь идет об OLTP-запросах (номера 5 и 6). B-деревья лучше для точечных запросов, где требуются короткие транзакции с небольшим количеством строк.

4. Выполнение обновлений🔄
Для запроса на обновление в ClickHouse нужно выполнить другой запрос, он не поддерживает обновления в традиционном смысле, будем использовать ALTER:
Кроме того, ClickHouse применяет обновление асинхронно. Чтобы получить результат немедленно, нужно выполнить команду оптимизации
ALTER TABLE user_sessions UPDATE is_active = 0 WHERE user_id = 12345 AND session_date = '2024-01-15' AND session_token = 'abc123' AND device_id = 'device001';
OPTIMIZE TABLE user_sessions FINAL;

Запрос: 7
ClickHouse: 26 сек
MySQL: <0.001 сек
ClickHouse снова проигрывает в реальных обновлениях (и аналогично удалениях) по сравнению с MySQL.

Ну и напоследок у нас остается тема - применение CDC из MySQL в ClickHouse
Ждите апдейтов :)👀

#ClickHouse #БазыДанных #Производительность #Сравнение #Бенчмарки #Оптимизация #MySQL
Предположим, у вас уже есть транзакционная база данных, обслуживающая основную работу приложения (OLTP-нагрузка).
Со временем появляется новая задача — строить сложные отчёты, считать метрики, анализировать поведение клиентов.
Для этого вы поднимаете отдельное аналитическое хранилище, например ClickHouse.

Дальше возникает логичный вопрос: как организовать стабильную и быструю передачу данных между этими системами?
И какие подводные камни ждут при попытке связать транзакционную и аналитическую базы?

Подобные сценарии встречаются практически в каждом продукте, где есть серьёзная работа с данными.
Сегодня для этого часто применяют подход Change Data Capture (CDC) и стриминговые платформы вроде Apache Kafka.
Они позволяют передавать изменения не батчами, а потоком, почти в реальном времени.
Но когда источник — классическая OLTP-БД (например, MySQL), а приёмник — колоночное OLAP-хранилище (ClickHouse), появляются особенности, которые нельзя игнорировать: разные модели данных, подходы к обновлениям, удалению строк и консистентности.
В этом завершающей линейке постов разберём один из практических вариантов реализации репликации изменений из MySQL в ClickHouse.

Несмотря на конкретный стек, описанный подход легко адаптируется под другие комбинации технологий.

Общая схема решения

Используется событийная архитектура:
1.MySQL фиксирует изменения в бинарном логе
2.Debezium превращает их в поток событий
3.Apache Kafka выступает транспортным слоем
4.ClickHouse читает события и сохраняет данные у себя
5. Аналитическая база постепенно синхронизируется с источником

MySQL → Debezium → Kafka → ClickHouse

Пусть в MySQL хранится таблица заказов интернет-магазина:
CREATE TABLE `orders` (
  `order_id` int NOT NULL AUTO_INCREMENT,
  `customer_id` int NOT NULL,
  `status` enum('new','paid','shipped','cancelled','completed') NOT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  `currency` char(3) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT NULL,
  `delivery_city` varchar(100) DEFAULT NULL,
  `payment_method` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`order_id`),
  KEY `idx_customer` (`customer_id`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Что происходит с данными:
📦 постоянно создаются новые заказы (INSERT)
🔄 меняется статус и сумма (UPDATE)
часть заказов отменяется и удаляется (DELETE)
⚡️ высокая частота операций в рабочее время

Наша цель:
Нужно без потерь передавать все изменения по заказам в ClickHouse и использовать их для:
- аналитики продаж
- расчёта конверсий
- финансовых отчётов
- мониторинга бизнес-метрик

Для этого:
применяется Debezium 2.1 как CDC-инструмент

В следующем посте будем глубже погружаться в нюансы реализации.
Следите за обновлениями! 🚀

#ClickHouse #MySQL #CDC #Debezium #Kafka #DataSync #OLTP #OLAP #DataEngineering #АналитикаДанных
⚙️ Реализация: шаг 1 — настраиваем CDC через Debezium

Почти любая современная СУБД ведёт журнал изменений, куда сначала записываются все операции, а уже потом они применяются к данным.
Это механизм Write Ahead Log 📝
В MySQL таким журналом является binlog.
Если читать этот журнал, интерпретировать изменения и передавать их в другую систему, мы фактически реализуем подход Change Data Capture (CDC) 🔄

Почему CDC удобен для синхронизации:
- Работает потоково, почти без задержек
- Обеспечивает конечную согласованность
- Не требует тяжёлых батч-процедур
- Сохраняет порядок изменений
- Масштабируется под высокую нагрузку

Для работы с binlog чаще всего используют Debezium. Он подключается к MySQL, отслеживает изменения и публикует их в Kafka через Kafka Connect.
Дальше эти события может забирать ClickHouse 🧩

Ниже — только те настройки Debezium, которые важны именно для корректной работы с ClickHouse.

1. Оставляем только актуальное состояние записи
По умолчанию Debezium отправляет событие в таком виде:
- Cостояние до изменения
- Cостояние после изменения
Плюс при удалении формируется «пустое» сообщение

Для Kafka это нормально, а вот для ClickHouse — неудобно: таблицы Kafka Engine ожидают плоскую структуру.

Поэтому включаем трансформацию:
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState"


Что это даёт:
- Для INSERT и UPDATE остаётся только новое состояние строки
- Поле before отбрасывается
- Структура сообщения упрощается

2. Корректно обрабатываем удаления

После упрощения структуры Debezium перестаёт передавать удаления. Чтобы это исправить, добавляем:
"transforms.unwrap.delete.handling.mode": "rewrite"


Теперь:
- Удалённые записи не пропадают
- В сообщении появляется поле __deleted = true
- Остальные операции получают __deleted = false

Это позволяет:
- Хранить историю изменений
- Фильтровать удалённые строки уже на стороне ClickHouse (через view)

3. Проблема обновлений неключевых полей
В нашем примере:
- В MySQL первичный ключ — id
- В ClickHouse таблица отсортирована по (id, status)
Если обновляется поле status, ClickHouse видит новую комбинацию ключей и создаёт ещё одну строку → появляются дубликаты 😬

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

Настройка:
"message.key.columns": "inventory.orders:id;inventory.orders:status"


Теперь при изменении этих полей:

- Сначала генерируется событие удаления старой версии
- Затем событие вставки новой
- ClickHouse корректно «заменяет» строку через ReplacingMergeTree

Итоговая конфигурация Debezium
{
  "name": "mysql-connector",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "database.hostname": "mysql",
    "database.port": "3306",
    "database.user": "root",
    "database.password": "mypassword",
    "database.server.id": "2",
    "database.server.name": "dbz.inventory.v2",

    "database.include.list": "inventory",
    "table.include.list": "inventory.orders",

    "message.key.columns": "inventory.orders:id;inventory.orders:status",

    "schema.history.internal.kafka.bootstrap.servers": "broker:9092",
    "schema.history.internal.kafka.topic": "dbz.inventory.history.v2",

    "snapshot.mode": "schema_only",
    "topic.prefix": "dbz.inventory.v2",

    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.delete.handling.mode": "rewrite"
  }
}

⚠️ Важный момент: как выбирать message.key.columns

После задания message.key.columns:
- Эти поля используются как ключ сообщения в Kafka
- По ним распределяются данные по партициям
- Нарушенный порядок событий = риск рассинхронизации в ClickHouse

Практическое правило:
1️⃣ Определите ключ сортировки таблицы в ClickHouse
2️⃣ Поймите, из каких колонок источника он формируется
3️⃣ Объедините все эти колонки
4️⃣ Укажите их в message.key.columns
5️⃣ Убедитесь, что они входят в ORDER BY в ClickHouse

#ClickHouse #MySQL #CDC #Debezium #Kafka #DataSync #OLTP #OLAP #DataEngineering #АналитикаДанных
🗄 Шаг 2: таблицы в ClickHouse
ClickHouse умеет читать сообщения напрямую из Kafka через Kafka Engine.
Для этого настраивается цепочка из трёх таблиц и одного представления.

📥 Kafka-таблица
Описывает структуру сообщений и Kafka-топик, из которого будут читаться данные.
CREATE TABLE default.kafka_orders
(
    `id` Int32,
    `status` String,
    `price` String,
    `__deleted` Nullable(String)
)
ENGINE = Kafka('broker:9092', 'inventory.orders', 'clickhouse', 'AvroConfluent')
SETTINGS format_avro_schema_registry_url = 'http://schema-registry:8081';


🔁Материализатор данных из Kafka
Kafka-таблица читает сообщения только один раз — смещения коммитаются в consumer group.
Поэтому каждую запись нужно сразу перекладывать в постоянную таблицу.
CREATE MATERIALIZED VIEW default.consumer__orders
TO default.stream_orders
(
    `id` Int32,
    `status` String,
    `price` String,
    `__deleted` Nullable(String)
) AS
SELECT
    id,
    status,
    price,
    __deleted
FROM default.kafka_orders;


🧱 Основная таблица
Хранит все версии строк и пометки об удалении.
Для корректной замены старых записей используется ReplacingMergeTree.
CREATE TABLE default.stream_orders
(
    `id` Int32,
    `status` String,
    `price` String,
    `__deleted` String
)
ENGINE = ReplacingMergeTree
ORDER BY (id, price)
SETTINGS index_granularity = 8192;


👀 Витрина данных
Скрывает удалённые строки и возвращает только актуальное состояние данных.
CREATE VIEW default.orders
(
    `id` Int32,
    `status` String,
    `price` String
) AS
SELECT
    id,
    status,
    price
FROM default.stream_orders
FINAL
WHERE __deleted = 'false';

Важно:

постоянное использование FINAL дорого по ресурсам.
В production лучше:
1. Агрегации, - last value
2. Фоновые merge, - ожидание схлопывания данных
3. Материализованные витрины, - предрасчитанные представления

Заключение
Мы собрали полноценный конвейер синхронизации между MySQL и ClickHouse через CDC.
Ключевые элементы:
1. Debezium, - читает binlog MySQL
2. Kafka, - гарантирует доставку и порядок событий
3. Kafka Engine, - потоковая загрузка в ClickHouse
4. ReplacingMergeTree, - устранение дубликатов
5. Поле __deleted, - корректная обработка удалений

В результате получается аналитическая копия боевой OLTP-базы:
MySQL продолжает обслуживать транзакции,ClickHouse — тяжёлую аналитику и отчёты,
оба без взаимных блокировок и деградации производительности.

На этом мы завершаем линейку постов про ClickHouse.Мы разобрали, на мой взгляд, все ключевые аспекты — дальше только практика-практика и еще раз практика

Ещё услышимся 👋
#ClickHouse #MySQL #CDC #Debezium #Kafka #DataSync #OLTP #OLAP #DataEngineering #АналитикаДанных
Media is too big
VIEW IN TELEGRAM
А на досуге можно глянуть новый эпизод нашего подкаста - в нем ребята поговорили с совладельцем EvApps Русланом Ишмухамедовым - предпринимателем, яхтсменом и не выспавшимся отцом - о бизнесе, путешествиях, воспитании детей и будущем аутстаффинга😉

Смотреть в VK Видео
🔥2
Мы вездесущи😈
В том смысле, что теперь ты можешь смотреть и слушать наш подкаст ITToLк там, где тебе удобно:

в Яндекс Музыке: clck.ru/3RiYL9
в VK Видео: clck.ru/3RidoV
в VK Подкастах: clck.ru/3RiduC

Делись в комментариях, какие еще подкасты про IT и на каких площадках ты слушаешь. Мы доберемся и туда😉
🔥2
🤖Сегодня большинство цифровых продуктов рано или поздно получают AI-функциональность

AI-агенты, NLP, автоматические решения — постепенно становятся привычной частью продуктовой разработки.

И вместе с этим появился новый рефлекс:
- Ответ получился странным — переписываем промпт
- Модель ошиблась — добавляем больше контекста.
- Поведение непредсказуемо — промпт недостаточно точный.

Со временем складывается впечатление, что качество работы AI зависит от prompt engineering. На практике это не так.

🔍Что происходит на самом деле

AI редко существует сам по себе. Чаще он встроен в продукт:
Получает данные из backend-сервисов, использует API, опирается на модели данных и выполняет действия внутри системы.
Здесь и возникает главная проблема.
Когда AI начинает работать плохо, команда пытается исправить поведение модели, и промпт превращается в инструмент компенсации архитектурных пробелов.

🛠 Почему всё сводится к промптам
Потому что их изменить проще.
Не нужно наводить порядок в данных, вводить единый источник правды или пересобирать бизнес-логику.
Достаточно добавить инструкции вроде «если данные неполные — сделай разумное предположение» или «если значения конфликтуют — выбери логичное», и вроде бы начинает работать лучше.
Но часть архитектурных решений просто переносится внутрь запроса к модели.

⚡️Важно понимать:
AI не знает, как устроен продукт.
Он видит только ту картину мира, которую открыли — кусок данных, ограниченный контекст, описание правил.
Если бизнес-логика не формализована, модель будет угадывать.
Если не определила источник истины, будет интерпретировать.

Когда AI начинает не только отвечать, но и выполнять действия, ситуация становится ещё хуже.
AI-агент — это распределённая система с вероятностным компонентом: запросы выполняются асинхронно, состояние восстанавливается из контекста, операции повторяются, ошибки сложно воспроизводятся.
Если архитектура продукта не рассчитана на такие сценарии, появляются знакомые эффекты: действия выполняются дважды, решения принимаются на неполных данных, система уверенно делает неправильные шаги.
И снова кажется, что проблема в AI, хотя он лишь усиливает системные слабости.

Ограничения, проверки и дополнительные инструкции - помогают снизить количество ошибок.
Но чаще это выглядит так: ещё больше правил в промпте, дополнительные проверки после ответа, просьбы к модели уточнять действия у пользователя.
Снижаются симптомы, но не устраняется причина — отсутствие чёткой структуры взаимодействия между AI и продуктом.

🕵️Непопулярная правда
Со временем становится заметна одна неприятная закономерность: AI не упрощает архитектуру продукта и не компенсирует слабые места.
Наоборот, делает их очевидными.
Когда архитектура изначально спроектирована аккуратно — с понятными границами, согласованными данными и формализованной бизнес-логикой — AI внутри неё ведёт себя спокойно и предсказуемо.

Но если продукт изначально построен на неявных договорённостях, разрозненных данных и логике, размазанной по частям системы, AI начинает создавать противоположное впечатление.
На демо выглядит впечатляюще — модель угадывает намерения, достраивает недостающую информацию и будто бы компенсирует несовершенство.
Однако при реальной эксплуатации эта иллюзия быстро исчезает

Индустрия регулярно попадает в эту ситуацию по вполне понятной причине
Изменение промпта даёт мгновенный результат: поведение модели меняется сразу, улучшение легко показать, прогресс ощущается буквально после следующего запроса.
Архитектурные же изменения требуют времени, редко выглядят впечатляюще и становятся заметны только спустя месяцы.
Поэтому естественно возникает желание улучшать то, что быстрее приносит видимый эффект, даже если это временное решение.

🚀В итоге
Prompt engineering остаётся важным инструментом работы с AI
Но не исправляет продуктовую архитектуру, AI ускоряет момент, когда архитектурные проблемы становятся заметны и раньше, чем это происходило в системах без AI.

#AI #web #development #NLP #architecture #tech
🔥2
В прошлом посте я писал, что prompt engineering не исправит архитектуру.💻
И сразу получил ожидаемый ответ:
«Окей, но умение писать промпты — не менее важная часть работы с AI‑системами».

И… да.
Как и умение писать SQL, который компенсирует плохо спроектированную базу.
Полезно? Конечно.
Но это не отменяет того, что проблема — глубже.

Для тех, кто хочет разобраться в том, как писать запросы лучше, есть книга Prompt Engineering (Lee Boonstra, 2025). 

Давайте разберёмся, что в книге действительно полезно, а что стоит читать с поправкой на реальность.

Что в книге действительно ценно🧩
Книга честно проговаривает то, что хайп вокруг AI обычно замалчивает:
LLM — не разум. Это предсказательная машина.  
Она угадывает следующий токен. Вежливо. Последовательно. Без понимания.

Всё остальное — «мышление», «рассуждение», «принятие решений» — это надстройки, которые мы сами прикручиваем.

Техники, описанные в книге:
- Chain of Thought
- Step‑back prompting
- Self‑consistency
- ReAct
- JSON‑схемы
- Ограничения вывода

Это не магия. Это механизмы контроля.

Если вы когда‑то:
- Оборачивали нестабильный API ретраями,
- Добавляли идемпотентность,
- Заставляли ответ соответствовать схеме, чтобы он не ломал систему,

то вы уже занимались prompt engineering.
Просто не называли это так.

Ключевая мысль книги⚙️
Prompt engineering — это middleware для вероятностных систем.

Все техники в книге решают одни и те же проблемы:
- Недетерминизм,
- Отсутствие структуры,
- Отсутствие контрактов,
- Непредсказуемые ретраи,
- Побочные эффекты.

Классические проблемы распределённых систем.

Только вместо логов — абзацы.
Вместо stack trace — уверенность.

Когда промпт‑инжиниринг действительно уместен🎯
Он отлично работает, когда:
- Задача по природе размытая (язык, суммирование, классификация),
- Цена ошибки низкая,
- Вывод носит рекомендательный характер,
- Можно безопасно ретраить,
- Никто не притворяется, что система детерминирована.

Но если вы используете его, чтобы:
- Применять бизнес‑правила,
- Принимать финансовые решения,
- Менять состояние продакшена,
- Заменять доменную логику,

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

Главный урок книги ( о котором явно не говорят)📘
Лучшие промпты в книге имеют общие черты:
- Чёткие входные форматы,
- Явные схемы,
- Узкие обязанности,
- Детерминированные ожидания,
- Скучные, предсказуемые ответы.

Настоящий урок — не:
Стань гуру промптов
а:
«Твоя система наконец должна иметь границы. И AI больше не позволит тебе их игнорировать».

Вывод🧠
Книгу действительно стоит прочитать — не как набор трюков и не как быстрый способ «прокачать» навыки.
Она полезна, если смотреть на неё глазами инженера, который разбирается с тем, как ведут себя вероятностные системы

Prompt engineering не исправит архитектуру.
Он делает важную вещь:
Помогает увидеть те проблемы, которые раньше было легко не замечать.
Возможно, именно поэтому создаётся ощущение, что это такой мощный инструмент.
2
🌐Браузер сегодня — это уже не просто «рендер HTML».
Проблема в том, что мы часто по привычке тянем библиотеки… даже когда нужный инструмент уже встроен:
1) Structured Clone API 🧬
Раньше вопрос «как правильно скопировать объект?» был классикой.
Кто-то вспоминал про ссылки, кто-то — про Object.assign, кто-то — про JSON, а кто-то начинал гуглить 😉
Сегодня всё стало проще:
const copy = structuredClone(original);

Почему это удобно:
- Работает с Map, Set, Date, Blob, File, ArrayBuffer
- Корректно обрабатывает циклические ссылки
- Поддерживается всеми современными браузерами

2) Performance API ⏱️
Мы часто спорим об оптимизациях, но редко честно измеряем результат.
А между тем, браузер уже даёт простой инструмент:
performance.mark("start");
// код
performance.mark("end");
performance.measure("test", "start", "end");
console.log(performance.getEntriesByName("test"));

Это полезно для:
- Микро-бенчмарков
- Сравнения разных реализаций
- Проверки, имеет ли смысл Worker или WASM
Работает стабильно во всех современных браузерах.

3) Page Visibility API 👀
Этот API сообщает, активна ли вкладка.
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
video.pause();
}
});

Реальный мир выглядит так:
Пользователь открыл ваше приложение — и ушёл в другую вкладку на полчаса.
Или вообще забыл вернуться.
Можно:
- Cтавить видео и анимации на паузу
- Останавливать polling
- Снижать нагрузку на CPU
И серверу станет легче жить.

4) ResizeObserver 📐
Наконец-то можно следить за размером элемента, а не только окна.
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
console.log(entry.contentRect.width);
}
});
ro.observe(element);

Если вы делали адаптивные компоненты или графики, вы точно писали костыли под resize.

5) IntersectionObserver 📍
Этот API отвечает на вопрос: попал ли элемент в область видимости?
const io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log("Элемент виден");
}
});
});
io.observe(element);

Идеально подходит для:
- Lazy loading
- Infinite scroll
- Анимаций при скролле
Любой, кто реализовывал infinite scroll вручную, знает, насколько это «весёлое» занятие 😄

6) AbortController 🛑
Чаще всего его используют с fetch, но на самом деле он подходит для любых отменяемых операций.
const controller = new AbortController();
fetch(url, { signal: controller.signal });
// позже
controller.abort();

Плюсы:
- Можно отменять несколько операций
- Подходит для fetch, стримов, обработчиков событий и т.д.

7) Idle Detection API 🧍
Если Page Visibility говорит о вкладке, то Idle Detection говорит активен ли пользователь за устройством
const detector = new IdleDetector();
await detector.start();
detector.addEventListener("change", () => {
console.log(detector.userState);
});

Пользователь может держать вкладку открытой, но в реальности — уйти на созвон.
Примеры применения:
- Авто-выход из аккаунта
- Статус «away»
- Оптимизация фоновых задач
Поддержка в основном в Chromium и требует разрешения.

8) BroadcastChannel API 📡
Простой способ общаться между вкладками одного сайта.
const channel = new BroadcastChannel("app");
channel.postMessage("logout");
channel.onmessage = e => {
console.log(e.data);
};

Подходит для:
- Cинхронизации логаута
- Cостояния авторизации
Особенно актуально, когда у пользователя открыты пара вкладок.

9) Web Locks API🔐
Нужен, чтобы не делать одну и ту же работу в нескольких вкладках.
navigator.locks.request("data-lock", async () => {
await fetchData();
});

Например:
- Только одна вкладка опрашивает сервер
- Меньше лишних запросов

10) File System Access API 📁
Да, браузер умеет работать с файлами напрямую.
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();

Это открывает дорогу для:
- Веб-редакторов
- Инструментов импорта/экспорта
Поддержка в основном у Chromium-браузеров.

А какими вы уже пользовались? 😊

#web #frontend #javascript #dev
🤖 AI убил рынок джунов?
Сейчас много разговоров о том, что AI убил рынок джунов.
Статистика вакансий действительно выглядит пугающе.

Но проблема немного в другом.
Роль junior-разработчика не умерла — просто сломалась карьерная лестница.

Раньше путь выглядел довольно понятно:

junior делал скучную работу — писал тесты, фиксил мелкие баги, делал CRUD, конвертировал схемы.
Для сеньоров это была рутина.
Для джунов — лучший способ понять, как на самом деле работают системы.
Через эту “скучную работу” постепенно появлялось главное:
интуиция — как ломаются системы, где появляются баги, как течёт data flow.
Сегодня эту работу делает AI.
Boilerplate, тесты, схемы, простые функции — всё это модель генерирует быстрее и дешевле.
То есть нижняя ступенька лестницы просто исчезла.

В результате рынок начинает выглядеть как странная “гантеля”:
• На одном конце — супер-сеньоры, которые с AI работают в 10 раз быстрее
• На другом — люди, которые умеют писать промпты
А моста между ними почти нет
И именно это сейчас ломает карьерный pipeline.
Но из этого не следует, что роль junior исчезла.Скорее она трансформировалась.

Новый junior — это не кодер, а аудитор
AI может генерировать код.
Но кто проверяет, что он правильный?
Именно здесь появляется новая роль.
Новый ключевой навык джуна — verification.

То есть способность:
— Читать код, который написал AI
— Находить архитектурные проблемы
— Замечать баги, которые проходят тесты
— Понимать, где AI ошибается
Проблема в том, что проверить можно только то, что понимаешь.
А значит от джунов теперь требуют почти сеньорские навыки анализа —но без нормального пути, как до них дойти.
Раньше обучение происходило через практику.Теперь эту практику всё чаще делает AI.

🔎Исследования это подтверждают
В эксперименте Anthropic джуны с AI выполняли задачи быстрее, но хуже понимали код.
Разница составила –17% по тестам на знание.
Разница не в инструменте.
А в том, как его использовали:
— одни делегировали задачу AI
— другие задавали вопросы и пытались понять решение
Те, кто пытался разобраться, учились.
Те, кто копировал ответ — нет.

🧠Почему это происходит

Во многом мы начинаем понимать идею именно в процессе её формулирования.
Когда этот процесс полностью отдаётся AI, результат появляется — но мышление может так и не сформироваться.
Если junior делегирует решение модели, он получает функцию.
Но пропускает сам процесс рассуждения, который позволил бы заметить, почему это решение может сломаться

⚠️Плохие ответы на Stack Overflow, странные баги, неожиданные проблемы —всё это заставляло сомневаться и проверять решения.
Иногда ответ выглядел правильным, но через пару часов становилось понятно, что он ломает половину системы.
Так появлялось настоящее понимание.
AI убирает этот опыт.
Он всегда отвечает уверенно и выглядит правым.
Поэтому легко просто скопировать решение —
и не разбираться, почему оно работает.

🧑‍💻В 2026 портфолио джуна — это уже не todo-app.

AI делает такой проект за 30 секунд.
Гораздо ценнее показать мышление и суждение:
— “Вот код, который предложил AI — и почему я его отклонил”— “Вот баг, который AI пропустил”
— “Вот как я проверил архитектурное решение”
— “Вот почему чистое решение AI ломается на масштабе”
Сегодня ценится не столько генерация кода, сколько способность разобраться, где он неправильный.
Не генерация. Суждение.

В Если компании перестанут нанимать джунов, потому что “AI дешевле” —откуда возьмутся сеньоры через 5–10 лет?
Пока хорошего ответа ни у кого нет.
Но компании, которые смогут заново построить эту лестницу, получат огромный кадровый запас.

А как вы думаете?
#ai #webdev #career #junior #programming #discussion
🔥5
🎨CSS функции в 2026: насколько далеко они зашли

Когда-то в CSS было всего две функции:

- calc()
- rgb()
И даже calc() многие использовали с осторожностью

Сегодня в CSS появились новые функции:
- anchor()
- anchor-size()
- exp()
- hypot()
- sign()
- shape()
- xywh()
- image-set()
CSS постепенно получает всё больше возможностей для работы с layout, вычислениями и адаптивностью

Разберём несколько интересных функций.
⚓️1. anchor() — позиционирование относительно элемента
Раньше для позиционирования tooltip или popover часто использовали JavaScript:
getBoundingClientRect()
+ scroll offsets
+ ResizeObserver

Теперь часть подобных задач можно решить средствами CSS
.tooltip {
position-anchor: --btn;
left: anchor(left);
top: anchor(bottom);
}

CSS может позиционировать элемент относительно другого элемента-якоря
Это упрощает реализацию tooltip, popover и похожих интерфейсных элементов

📏2. anchor-size() — размеры относительно другого элемента
Теперь элемент может брать размер от другого элемента, а не только от родителя или viewport
.popover {
width: anchor-size(width);
}

Это полезно для:
- Dropdown меню
- Popover
- Контекстных панелей
- Floating UI
Например, когда ширина выпадающего меню должна совпадать с шириной кнопки

📈3. exp() — экспонента в CSS
CSS получил функцию для экспоненциальных вычислений
width: calc(exp(2) * 10px);

Это может быть полезно для:
- Систем масштабирования
- Типографических шкал
- Плавных кривых анимации
Пример:
font-size: calc(1rem * exp(var(--scale)));

📐4. hypot() — вычисление расстояния
Функция возвращает длину гипотенузы по теореме Пифагора
width: hypot(30px, 40px);

Она может использоваться, например, при вычислениях расстояния или диагонали
Иногда применяется в сложных layout или анимациях:
transform: translate(
hypot(10px, 20px)
);

5. sign() — работа со знаком числа
Функция возвращает:
-1
0
1
в зависимости от значения
Пример:
opacity: calc(sign(var(--value)) * 1);

Это позволяет строить простые условные зависимости через CSS-вычисления

🖼6. image-set() — изображения для разных плотностей экранов
Позволяет указывать несколько версий изображения
background-image: image-set(
"image.png" 1x,
"image@2x.png" 2x
);

Браузер сам выберет подходящую версию в зависимости от плотности пикселей экрана
Плюсы:
- Оптимальная загрузка
- Экономия трафика
- Более чёткое отображение на retina-экранах

🔺7. shape() — работа с геометрией
Функция позволяет задавать геометрические формы для clipping
clip-path: shape(
from 0 0,
line to 100% 0,
line to 50% 100%,
close
);

Это упрощает создание нестандартных форм без SVG

📦8. xywh() — понятный синтаксис
Функция задаёт прямоугольник через координаты и размеры
Раньше:
clip-path: inset(10px 20px 30px 40px);

Теперь:
clip-path: xywh(10px 20px 200px 100px);

x, y, width, height
Такой синтаксис легче читать и поддерживать

🧩Итог
Современные функции CSS позволяют решать больше задач прямо на уровне стилей
Многие задачи интерфейса, которые раньше решались с помощью JavaScript, теперь иногда можно реализовать напрямую в CSS.

💬 Как вы относитесь к тому, что часть задач интерфейса постепенно переходит из JavaScript в CSS?
Это упрощает разработку или наоборот усложняет поддержку?


#css #webdev #frontend #webdevelopment #dev #programming
Готовь сани летом, а архитектуру - на старте проекта! 🚀

Если не хотите через пару лет плакать над контроллерами на 2000+ строк и молиться на God Class - приходите на наш вебинар.

2 апреля в 18:30 МСК встречаемся с ведущим fullstack-разработчиком EvApps Михаилом Прохоровым, чтобы поговорить по делу:

Почему «просто MVC» - это путь к архитектурному долгу?
Где проходит грань, когда DDD реально окупается, а не просто «усложняет ради понтов»?
Как тактические паттерны (Value Objects, Aggregates) вытаскивают проект из болота?

Участие бесплатное, но места надо занять😉
🔗 Регистрация по ссылке: clck.ru/3SaKJV
Media is too big
VIEW IN TELEGRAM
Врываемся в твою рабочую неделю с новым эпизодом подкаста IT ToLк🚀

В новом выпуске - Андрей Карпов, сооснователь проекта PVS-Studio: поиск ошибок в коде программ. Андрей 17 лет в IT, у него есть бэкграунд CTO, а сегодня он является амбассадором статического анализа кода, автором целого сборника "вредных советов" о C#, а еще строит DevRel в своей компании.

Что в подкасте?

⭐️ Скучают ли топ-разработчики по C++ после перехода в управление?

⭐️ Rust вытеснит C++ или это хайп?

⭐️ Почему AI-код - это новая головная боль для SAST-инструментов?

⭐️ И главное: что посоветовать себе в 2007 году, чтобы писать код без багов?

Гость честный, вопросы интересные. Поехали 🎧

Смотрим по ссылке: https://clck.ru/3Si2SR
1
Стартуем через полчаса🚀

EvApps приглашает вас на запланированную конференцию: Zoom.
Подключиться к конференции Zoom
https://us06web.zoom.us/j/86917612389?pwd=g9Lpv9yy4Jj4xNP9VqL2BNO5zjqm3u.1

Идентификатор конференции: 869 1761 2389
Код доступа: 874163
1
Architecture Evolution.pdf
1.6 MB
Всем, кто очень хотел, но не успел на вчерашний вебинар по эволюции архитектуры, дарим ссылочку на запись и прочие материалы😊

🔗 Запись вебинара здесь
Презентацию прикрепили👌
Ссылка на код здесь
2