ClickHouse — где заканчивается стандартный SQLClickHouse — where standard SQL stops
ClickHouse говорит на SQL, но это язык для OLAP, а не для OLTP: нет полноценных транзакций и foreign key, зато есть массивы как полноправный тип, десятки специализированных агрегатных функций и комбинаторы, превращающие любую функцию в условную или в state-накопитель.ClickHouse speaks SQL, but it's a language for OLAP, not OLTP: no real transactions or foreign keys, but arrays as a first-class type, dozens of specialized aggregate functions, and combinators that turn any function conditional or into a state accumulator.
Нет foreign key constraints — ссылочная целостность не проверяется движком вообще, это ответственность приложения; в OLAP-нагрузке это осознанный выбор (данные обычно приходят уже согласованными из источника, а не создаются интерактивно). Нет полноценных многострочных транзакций с откатом — вставка атомарна на уровне одного `INSERT`, но нет `BEGIN`/`COMMIT`/`ROLLBACK` через несколько независимых операций, как в PostgreSQL. `JOIN` работает, но по умолчанию требует, чтобы правая таблица помещалась в память (`hash join`) — для по-настоящему больших таблиц с обеих сторон нужны специальные алгоритмы (`partial_merge`, чекнуть `join_algorithm`), иначе запрос упадёт по памяти.No foreign key constraints — referential integrity isn't checked by the engine at all, that's the application's responsibility; a deliberate choice for OLAP workloads (data typically arrives already consistent from a source, rather than being created interactively). No full multi-statement transactions with rollback — an insert is atomic at the level of a single INSERT, but there's no BEGIN/COMMIT/ROLLBACK spanning several independent operations the way PostgreSQL has. JOIN works, but by default requires the right-hand table to fit in memory (a hash join) — genuinely large tables on both sides need special algorithms (partial_merge, check join_algorithm), or the query fails on memory.
Массив (`Array(T)`) в ClickHouse — не сериализованный JSON-блоб, а полноценная колонка со своим сжатием, по которой можно фильтровать и агрегировать напрямую функциями типа `arrayMap`, `arrayFilter`, `arraySum`, без разворачивания в отдельные строки. `Nested` — способ смоделировать "таблицу внутри колонки" (набор параллельных массивов с общим префиксом имени), удобный для one-to-many связей, которые не хочется выносить в отдельную join-таблицу — например, список тегов ордера или лог событий внутри одной сессии.An array (Array(T)) in ClickHouse isn't a serialized JSON blob but a genuine column with its own compression, which can be filtered and aggregated directly via functions like arrayMap, arrayFilter, arraySum, without unrolling into separate rows. Nested is a way to model "a table inside a column" (a set of parallel arrays sharing a name prefix), convenient for one-to-many relationships you don't want to break out into a separate join table — say, an order's list of tags, or the event log within a single session.
ARRAY JOIN разворачивает массив в отдельные строки прямо в запросе (аналог `UNNEST` в PostgreSQL) — нужен, когда агрегация должна идти по элементам массива, а не по массиву целиком.ARRAY JOIN unrolls an array into separate rows right within the query (analogous to UNNEST in PostgreSQL) — needed when aggregation must happen over array elements, not over the array as a whole.
SELECT
order_id,
tag
FROM orders
ARRAY JOIN tags AS tag // разворачивает Array(String) tags в строки// unrolls Array(String) tags into rows
WHERE tag = 'urgent';
Квантильные функции (`quantile`, `quantileExact`, `quantileTDigest`) считают перцентили за один проход по данным без сортировки всего набора — `quantile` (по умолчанию) использует приближённый алгоритм (reservoir sampling), что на порядки быстрее точного расчёта и вполне достаточно для мониторинга/аналитики; `quantileExact` даёт точный ответ ценой полной сортировки в памяти. `uniqCombined`/`uniq` считают приближённую кардинальность (аналог HyperLogLog) — точный `COUNT(DISTINCT ...)` на миллиардах строк требует держать в памяти все уникальные значения, приближённый алгоритм даёт погрешность около 1% при постоянной памяти.Quantile functions (quantile, quantileExact, quantileTDigest) compute percentiles in a single pass over the data without sorting the whole set — quantile (the default) uses an approximate algorithm (reservoir sampling), orders of magnitude faster than an exact computation and plenty good for monitoring/analytics; quantileExact gives an exact answer at the cost of a full in-memory sort. uniqCombined/uniq compute approximate cardinality (a HyperLogLog analogue) — an exact COUNT(DISTINCT ...) over billions of rows requires holding every unique value in memory, the approximate algorithm gives roughly 1% error at constant memory.
Комбинаторы-суффиксы превращают любую агрегатную функцию в вариант с другим поведением: `-If` добавляет условие без отдельного `WHERE`/`CASE` (`sumIf(qty, side = 'buy')` — сумма только по покупкам в одном проходе вместе с другими агрегатами), `-Array` применяет агрегат к элементам массива, `-State`/`-Merge` — те самые парные функции для инкрементальных агрегатов в материализованных представлениях (см. проектирование схемы).Suffix combinators turn any aggregate function into a variant with different behavior: -If adds a condition without a separate WHERE/CASE (sumIf(qty, side = 'buy') — sum only over buys, in the same pass as other aggregates), -Array applies the aggregate to array elements, -State/-Merge are the same paired functions for incremental aggregates in materialized views (see schema design).
SELECT
symbol,
sumIf(qty, side = 'buy') AS buy_volume,
sumIf(qty, side = 'sell') AS sell_volume,
quantile(0.99)(latency_ms) AS p99_latency
FROM trades
GROUP BY symbol;
Оконные функции (`OVER (PARTITION BY ... ORDER BY ...)`) в ClickHouse синтаксически совпадают со стандартным SQL и покрывают типовые задачи — ранжирование, running total, `LAG`/`LEAD` между соседними строками. Ключевое отличие в производительности: оконная функция требует держать в памяти и сортировать целое окно, что на распределённой таблице (`Distributed`) может означать пересылку и досортировку данных между узлами — там, где эквивалентный результат достижим через `GROUP BY` с обычными агрегатами (которые считаются инкрементально при слиянии кусков), `GROUP BY` почти всегда быстрее на больших объёмах.Window functions (OVER (PARTITION BY ... ORDER BY ...)) in ClickHouse match standard SQL syntax and cover the usual cases — ranking, running totals, LAG/LEAD between neighboring rows. The key difference is performance: a window function needs the whole window held in memory and sorted, which on a distributed table (Distributed) can mean shuffling and re-sorting data across nodes — where an equivalent result is reachable via GROUP BY with plain aggregates (computed incrementally during part merges), GROUP BY is almost always faster at scale.
Практическое правило: если задача сводится к "агрегат на группу" — использовать `GROUP BY`; если действительно нужен доступ к соседним строкам в упорядоченном окне (сравнение с предыдущей ценой, running total именно по порядку, а не по группе) — оконная функция неизбежна, но масштаб данных, на котором она применяется, стоит держать в уме.Rule of thumb: if the task reduces to "an aggregate per group," use GROUP BY; if you genuinely need access to neighboring rows within an ordered window (comparing to the previous price, a running total by order rather than by group), a window function is unavoidable — but the data scale it's applied to is worth keeping in mind.
Дефолтный `hash`-алгоритм джойна строит хэш-таблицу из правой стороны в памяти одного узла — если она большая (сотни миллионов строк), запрос падает по памяти независимо от того, насколько хорошо спроектирована схема. Правило "правая сторона джойна — маленькая справочная таблица" не опционально в ClickHouse, как в PostgreSQL с его умным планировщиком, а требование к запросу: располагайте большую таблицу слева.The default hash join algorithm builds a hash table from the right side in a single node's memory — if it's large (hundreds of millions of rows), the query fails on memory regardless of how well the schema is designed. The rule "the join's right side is a small lookup table" isn't optional in ClickHouse the way it can be with PostgreSQL's smart planner — it's a query requirement: put the large table on the left.
`uniq`/`quantile` дают приближённый результат по умолчанию — на небольших выборках погрешность может быть заметна, и разработчики, ожидающие точного `COUNT(DISTINCT)` как в реляционных БД, получают "неправильные" на вид числа. Если точность критична (финансовая отчётность, а не дашборд), нужно явно использовать `uniqExact`/`quantileExact`, отдавая себе отчёт в цене по памяти и времени.`uniq`/`quantile` give an approximate result by default — on small samples the error can be noticeable, and developers expecting an exact COUNT(DISTINCT) as in relational DBs get numbers that look "wrong." If precision is critical (financial reporting, not a dashboard), explicitly use `uniqExact`/`quantileExact`, aware of the memory and time cost.
Один `INSERT` атомарен, но если приложение отправляет несколько независимых `INSERT`, которые логически должны быть согласованы (например, факт сделки и обновление агрегата отдельными запросами), сбой между ними оставляет данные в промежуточном состоянии — откатить нечем, `ROLLBACK` не существует. Проектировать вставку так, чтобы вся логически согласованная единица данных уходила одним `INSERT` — единственная защита на уровне схемы.A single INSERT is atomic, but if the application sends several independent inserts that logically need to stay consistent (say, a trade fact and an aggregate update as separate requests), a failure between them leaves data in an intermediate state — there's nothing to roll back, ROLLBACK doesn't exist. Designing inserts so a whole logically-consistent unit of data goes out as one INSERT is the only schema-level defense.