РАЗРАБОТКА

ClickHouse FINAL убивает производительность — разбираем альтернативы

Оператор FINAL в ClickHouse решает проблему дубликатов, но превращает быструю OLAP-систему в медленную транзакционную. Разбираем альтернативы.

✍️ Редакция iTech News | 03.03.2026 | ⏱ 2 мин | 👁 4 | Источник: DEV Community
🔗

Если вы регулярно используете FINAL в ClickHouse, проблема в архитектуре схемы данных. FINAL кажется спасением — видите дубликаты, добавляете оператор, и всё выглядит корректно. Но за удобство приходится платить производительностью.

Проблема не в самом операторе, а в том, что он превращает быструю колоночную OLAP-систему в медленную транзакционную базу данных.

Откуда берутся дубликаты

ClickHouse (особенно семейство движков MergeTree) работает по принципу асинхронного слияния:

  • Каждая вставка создаёт новую часть (part)
  • Части неизменяемы после создания
  • Фоновые процессы объединяют части асинхронно
  • Дедупликация в ReplacingMergeTree происходит только во время слияний

Строки с одинаковым первичным ключом временно сосуществуют в разных частях. Удаляются только после фонового слияния. До этого момента дубликаты видны в запросах — это не баг, а особенность архитектуры.

ClickHouse жертвует немедленной консистентностью ради скорости записи.

Что делает FINAL

Когда вы пишете SELECT * FROM table FINAL, вы говорите ClickHouse: «Игнорируй состояние фонового слияния. Восстанови финальный результат во время выполнения запроса».

Внутри FINAL:

  • Читает все релевантные части
  • Применяет логику дедупликации во время SELECT
  • Выполняет процесс слияния в памяти
  • Отключает определённые оптимизации чтения

FINAL перемещает фоновую работу в ваш запрос. На больших таблицах это болезненно.

Почему FINAL убивает производительность

Больше частей для чтения. Без FINAL ClickHouse эффективно использует индексы. С FINAL должен учитывать все строки, участвующие в дедупликации.

Дополнительная нагрузка на CPU. Логика дедупликации, которая обычно выполняется в фоне, теперь работает во время запроса.

Повышенное использование памяти. Слияние во время запроса требует буферизации строк для сравнения. На больших партициях критично.

Плохое масштабирование. На маленьких датасетах разница незаметна. На больших аналитических таблицах получаете систему, которая ведёт себя как транзакционная СУБД с построчной сверкой.

Альтернативы FINAL

Используйте колонку версии правильно. В ReplacingMergeTree определите колонку версии и проектируйте запросы через агрегацию:

SELECT id, argMax(value, version) AS value FROM table GROUP BY id

Это избегает полного слияния через FINAL.

Моделируйте неизменяемые события. Вместо перезаписи строк сохраняйте события в append-only режиме. Вычисляйте «последнее состояние» через представления или агрегации.

Предварительная агрегация. Используйте материализованные представления, AggregatingMergeTree или SummingMergeTree. Переносите вычисление состояния на время вставки данных.

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

Поделиться: Telegram X LinkedIn