ClickHouse для аналітики веб-додатків
ClickHouse — це колоночна база даних для аналітичних запитів (OLAP). Агрегує мільярди рядків за секунди там, де PostgreSQL витрачає хвилини. Не заміна транзакційної БД — доповнення: PostgreSQL для операційних даних, ClickHouse для аналітики.
Коли потрібен ClickHouse
Типові запити, які PostgreSQL переважує погано:
- "Щоденна аудиторія за останні 90 днів за джерелами трафіку"
- "Воронка конверсії за кроками за квартал"
- "Топ-1000 товарів за дохідністю за рік з розбивкою за регіонами"
- "Когортний аналіз: утримання користувачів за місяцями реєстрації"
Для таблиці подій > 100 млн рядків ClickHouse забезпечує прискорення в 10-100 разів.
Схема таблиць
ClickHouse використовує спеціалізовані рушії. MergeTree — основний:
-- Таблиця подій (page views, clicks, purchases)
CREATE TABLE events (
event_date Date,
event_time DateTime,
event_type LowCardinality(String), -- enum-like: 'pageview', 'click', 'purchase'
user_id UInt64,
session_id String,
tenant_id UInt32,
page_url String,
referrer String,
country LowCardinality(String),
device_type LowCardinality(String), -- 'desktop', 'mobile', 'tablet'
properties String -- JSON для довільних атрибутів
) ENGINE = MergeTree()
ORDER BY (tenant_id, event_date, event_type, user_id)
PARTITION BY toYYYYMM(event_date);
-- Індекси (min/max для швидкого пропуску блоків)
ALTER TABLE events ADD INDEX idx_session session_id TYPE bloom_filter(0.01) GRANULARITY 4;
ORDER BY — ключ сортування, за яким ClickHouse зберігає дані. Запити з фільтрами за tenant_id та event_date використовують його для pruning.
LowCardinality — оптимізація для колонок з малою кількістю унікальних значень (~10k). Зберігається як dictionary encoding.
Вставка даних
Ніколи не вставляйте по одному рядку — ClickHouse оптимізований для пакетних вставок:
// Node.js + @clickhouse/client
import { createClient } from '@clickhouse/client';
const client = createClient({
host: process.env.CLICKHOUSE_HOST,
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD,
database: 'analytics',
});
// Буфер подій — скидайте кожні N секунд або M подій
const eventBuffer = []
function flushEvents() {
if (eventBuffer.length === 0) return
await client.insert({
table: 'events',
values: eventBuffer,
format: 'JSONEachRow'
})
eventBuffer.length = 0
}
setInterval(flushEvents, 10000) // 10 секунд
Аналітичні запити
-- Щоденні активні користувачи за країною
SELECT
event_date,
country,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_date >= today() - 90
GROUP BY event_date, country
ORDER BY event_date DESC, dau DESC;
-- Когортна утримання: % користувачів з когорти все ще активні
SELECT
registration_cohort,
days_since_registration,
COUNT(DISTINCT user_id) AS active_users
FROM (
SELECT
user_id,
toDate(first_event) AS registration_cohort,
dateDiff('day', first_event, event_date) AS days_since_registration
FROM events
)
GROUP BY registration_cohort, days_since_registration;
-- Воронка доходу
SELECT
'view_product' AS step,
COUNT(DISTINCT user_id) AS users
FROM events
WHERE event_type = 'page_view' AND page_url LIKE '/product/%'
UNION ALL
SELECT
'add_to_cart' AS step,
COUNT(DISTINCT user_id) AS users
FROM events
WHERE event_type = 'add_to_cart'
Інтеграція з веб-додатками
// Надсилайте события в ClickHouse через API endpoint
async function trackEvent(eventData) {
const event = {
event_date: new Date().toISOString().split('T')[0],
event_time: new Date().toISOString(),
event_type: eventData.type,
user_id: userId,
session_id: sessionId,
tenant_id: tenantId,
page_url: window.location.pathname,
properties: JSON.stringify(eventData)
}
await fetch('/api/events', {
method: 'POST',
body: JSON.stringify(event)
})
}
// Відслідковуйте при завантаженні сторінки, кліках, поданні форм
trackEvent({ type: 'pageview' })
document.addEventListener('click', e => {
if (e.target.closest('[data-track]')) {
trackEvent({ type: 'click', element: e.target.id })
}
})
Час виконання
Налаштування кластера ClickHouse, дизайн схеми, конвеєр даних — 3–5 робочих днів.







