Если вы регулярно используете FINAL в ClickHouse, скорее всего, ваша схема базы этих спроектирована неправильно. Эта операция превращает быструю аналитическую систему в медленную транзакционную — и убивает производительность на больших объёмах данных.
Проблема в том, что FINAL кажется волшебным решением. Видите дубликаты — добавляете FINAL, и всё выглядит правильно. Но за этим «исправлением» скрывается серьёзная архитектурная проблема.
Почему в ClickHouse вообще появляются дубликаты
ClickHouse работает не как обычная OLTP-база. В движках семейства MergeTree каждая вставка создаёт новую неизменяемую часть данных. Фоновые процессы асинхронно объединяют эти части, и только тогда происходит дедупликация.
В ReplacingMergeTree строки с одинаковыми первичными ключами могут временно сосуществовать в разных частях. Они исчезают только после фонового слияния — до этого момента дубликаты видны в запросах.
Это не баг, а особенность архитектуры. ClickHouse жертвует мгновенной согласованностью ради скорости записи и асинхронного сжатия.
Как FINAL убивает производительность
Когда вы пишете SELECT * FROM table FINAL, вы заставляете ClickHouse выполнить слияние этих прямо во время запроса. Система должна:
- Прочитать все релевантные части данных
- Применить логику дедупликации в памяти
- Выполнить процесс, похожий на слияние
- Отключить определённые оптимизации чтения
На небольших датасетах вы этого не заметите. Но на крупных аналитических таблицах FINAL превращает OLAP-систему в нечто, подозрительно похожее на транзакционную базу с построчной сверкой данных.
А для этого ClickHouse не оптимизирована.
Правильные альтернативы FINAL
Вместо регулярного использования FINAL лучше пересмотреть архитектуру:
Используйте колонку версии правильно. В ReplacingMergeTree определите версию и стройте запросы через агрегацию: SELECT id, argMax(value, version) FROM table GROUP BY id.
Моделируйте неизменяемые события. Вместо перезаписи строк сохраняйте события в append-only формате. Вычисляйте актуальное состояние через представления или агрегации.
Используйте предварительную агрегацию. Материализованные представления, AggregatingMergeTree и SummingMergeTree переносят вычисления на момент вставки данных.
Пересмотрите стратегию партицирования. Иногда дубликаты накапливаются из-за слишком больших партиций или неоптимальных настроек слияния.
Когда FINAL всё-таки оправдан
FINAL имеет смысл использовать для отладочных запросов, работы с небольшими таблицами или валидации данных. Но если это становится частью регулярных запросов к продакшн-системе — стоит пересмотреть дизайн схемы.
Помните: лучше потратить время на правильную архитектуру этих сейчас, чем бороться с проблемами производительности потом.


