Стратегии дедупликации
Дедупликация относится к процессу удаления дублирующихся строк в наборе данных. В OLTP базе данных это делается легко, поскольку каждая строка имеет уникальный первичный ключ, но это выполняется за счет более медленных вставок. Каждая вставленная строка сначала должна быть найдена, и, если она найдена, её нужно заменить.
ClickHouse разработан для высокой скорости при вставке данных. Файлы хранения неизменяемы, и ClickHouse не проверяет существующий первичный ключ перед вставкой строки, поэтому дедупликация требует немного больше усилий. Это также означает, что дедупликация не является моментальной – она будет выполнена в будущем, что имеет несколько побочных эффектов:
- В любой момент времени в вашей таблице все еще могут быть дубликаты (строки с одинаковым ключом сортировки)
- Фактическое удаление дублирующихся строк происходит во время слияния частей
- Ваши запросы должны допускать возможность наличия дубликатов
![]() | ClickHouse предлагает бесплатное обучение по дедупликации и многим другим темам. Модуль обучения по Удалению и обновлению данных - хорошее место для старта. |
Опции для дедупликации
Дедупликация реализуется в ClickHouse с использованием следующих движков таблиц:
-
Движок таблиц
ReplacingMergeTree
: с этим движком дублирующие строки с одинаковым ключом сортировки удаляются во время слияний.ReplacingMergeTree
является хорошим вариантом для имитации поведения upsert (когда вы хотите, чтобы запросы возвращали последнюю вставленную строку). -
Схлопывание строк: движки таблиц
CollapsingMergeTree
иVersionedCollapsingMergeTree
используют логику, при которой существующая строка "отменяется", а новая строка вставляется. Они сложнее в реализации, чемReplacingMergeTree
, но ваши запросы и агрегаты могут быть проще для написания, не беспокоясь о том, произошла ли агрегация данных. Эти два движка таблиц полезны, когда вам нужно часто обновлять данные.
Мы рассмотрим обе эти техники ниже. Для получения более подробной информации ознакомьтесь с нашим бесплатным модулем обучения Удаление и обновление данных.
Использование ReplacingMergeTree для Upserts
Рассмотрим простой пример, где таблица содержит комментарии Hacker News с колонкой views, представляющей количество раз, которое комментарий был просмотрен. Допустим, мы вставляем новую строку, когда статья публикуется, и обновляем новую строку раз в день с общим количеством просмотров, если значение увеличивается:
Давайте вставим две строки:
Чтобы обновить колонку views
, вставляем новую строку с тем же первичным ключом (обратите внимание на новые значения в колонке views
):
Теперь в таблице 4 строки:
Отдельные блоки выше в выводе показывают две части, которые происходят за кадром – эти данные еще не были объединены, поэтому дублирующиеся строки еще не были удалены. Давайте используем ключевое слово FINAL
в запросе SELECT
, что приведет к логическому объединению результата запроса:
Результат содержит только 2 строки, и возвращается последняя вставленная строка.
Использование FINAL
нормально, если у вас небольшой объем данных. Если вы имеете дело с большим объемом данных, использование FINAL
вероятно, не лучший вариант. Давайте обсудим лучший вариант для получения последнего значения столбца...
Избегание FINAL
Давайте снова обновим колонку views
для обеих уникальных строк:
В таблице теперь 6 строк, потому что фактическое слияние еще не произошло (только слияние во время запроса, когда мы использовали FINAL
).
Вместо использования FINAL
, давайте применим некоторую бизнес-логику - мы знаем, что колонка views
всегда увеличивается, поэтому мы можем выбрать строку с наибольшим значением, используя функцию max
после группировки по нужным колонкам:
Группировка, представленная в запросе выше, может быть даже более эффективной (с точки зрения производительности запросов), чем использование ключевого слова FINAL
.
Наш модуль обучения по удалению и обновлению данных расширяет этот пример, включая то, как использовать колонку version
с ReplacingMergeTree
.
Использование CollapsingMergeTree для Частого Обновления Колонок
Обновление колонки предполагает удаление существующей строки и замену её новыми значениями. Как вы уже видели, данный тип мутации в ClickHouse происходит в будущем - во время слияний. Если вам нужно обновить множество строк, может быть более эффективно избежать ALTER TABLE..UPDATE
и вместо этого просто вставить новые данные рядом с существующими данными. Мы могли бы добавить колонку, указывающую, является ли данные устаревшими или новыми... и на самом деле существует движок таблиц, который уже реализует это поведение очень хорошо, особенно учитывая, что он автоматически удаляет устаревшие данные за вас. Давайте посмотрим, как это работает.
Допустим, мы отслеживаем количество просмотров комментария Hacker News с использованием внешней системы, и каждые несколько часов мы загружаем данные в ClickHouse. Мы хотим, чтобы старые строки были удалены, а новые строки представляли новое состояние каждого комментария Hacker News. Мы можем использовать CollapsingMergeTree
для реализации этого поведения.
Давайте определим таблицу для хранения количества просмотров:
Обратите внимание, что в таблице hackernews_views
есть колонка Int8
с названием sign, которая обозначается как колонка знака. Имя колонки знака произвольное, но тип данных должен быть Int8
, и обратите внимание, что имя колонки было передано в конструктор движка CollapsingMergeTree
.
Что такое колонка знака в таблице CollapsingMergeTree
? Она представляет состояние строки, и колонка знака может быть только 1 или -1. Вот как это работает:
- Если две строки имеют одинаковый первичный ключ (или порядок сортировки, если он отличается от первичного ключа), но разные значения колонок знака, то последняя вставленная строка с +1 становится строкой состояния, а другие строки отменяют друг друга
- Строки, которые отменяют друг друга, удаляются во время слияний
- Строки, для которых нет соответствующей пары, сохраняются
Давайте добавим строку в таблицу hackernews_views
. Поскольку это единственная строка для этого первичного ключа, мы устанавливаем её состояние в 1:
Теперь предположим, что мы хотим изменить колонку просмотров. Вы вставляете две строки: одну, которая отменяет существующую строку, и одну, которая содержит новое состояние строки:
Теперь в таблице 3 строки с первичным ключом (123, 'ricardo')
:
Обратите внимание, что добавление FINAL
возвращает текущую строку состояния:
Но, конечно, использование FINAL
не рекомендуется для больших таблиц.
Значение, которое передается в колонку views
в нашем примере, на самом деле не требуется, и оно не должно совпадать с текущим значением views
старой строки. На самом деле, вы можете отменить строку только с помощью первичного ключа и -1:
Обновления в реальном времени из нескольких потоков
С таблицей CollapsingMergeTree
строки отменяют друг друга, используя колонку знака, и состояние строки определяется последней вставленной строкой. Но это может быть проблематично, если вы вставляете строки из разных потоков, когда строки могут быть вставлены в неправильном порядке. Использование "последней" строки не работает в этой ситуации.
Здесь на помощь приходит VersionedCollapsingMergeTree
- он схлопывает строки так же, как CollapsingMergeTree
, но вместо того, чтобы сохранять последнюю вставленную строку, он сохраняет строку с наивысшим значением указанных вами версии.
Рассмотрим пример. Предположим, мы хотим отслеживать количество просмотров комментариев Hacker News, и данные обновляются часто. Мы хотим, чтобы отчеты использовали последние значения без принуждения или ожидания слияний. Мы начинаем с таблицы, аналогичной CollapsedMergeTree
, за исключением того, что добавляем колонку для хранения версии состояния строки:
Обратите внимание, что таблица использует VersionsedCollapsingMergeTree
в качестве движка и передает колонку знака и колонку версии. Вот как работает таблица:
- Она удаляет каждую пару строк, которые имеют одинаковый первичный ключ и версию и разные знаки
- Порядок вставки строк не имеет значения
- Обратите внимание, что если колонка версии не является частью первичного ключа, ClickHouse добавляет её в первичный ключ неявно в качестве последнего поля
Вы используете ту же логику при написании запросов - группируете по первичному ключу и используете логику, чтобы избежать строк, которые были отменены, но ещё не удалены. Давайте добавим несколько строк в таблицу hackernews_views_vcmt
:
Теперь мы обновляем две строки и удаляем одну из них. Чтобы отменить строку, обязательно указывайте предыдущее значение версии (поскольку оно является частью первичного ключа):
Мы выполним тот же запрос, что и раньше, который ловко добавляет и вычитает значения на основании колонки знака:
Результат – две строки:
Давайте принудительно проведем слияние таблицы:
В результате должно быть всего две строки:
Таблица VersionedCollapsingMergeTree
очень полезна, когда вы хотите реализовать дедупликацию, вставляя строки из нескольких клиентов и/или потоков.
Почему мои строки не дедуплицируются?
Одной из причин, по которой вставленные строки могут не дедуплицироваться, является использование функции или выражения, не обладающих идемпотентностью, в вашем выражении INSERT
. Например, если вы вставляете строки со столбцом createdAt DateTime64(3) DEFAULT now()
, ваши строки гарантированно будут уникальными, поскольку каждая строка будет иметь уникальное значение по умолчанию для столбца createdAt
. Движок таблиц MergeTree / ReplicatedMergeTree не будет знать о необходимости дедупликации строк, так как каждая вставленная строка будет генерировать уникальную контрольную сумму.
В этом случае вы можете указать свой собственный insert_deduplication_token
для каждой партии строк, чтобы гарантировать, что множественные вставки одной и той же партии не приведут к повторной вставке одних и тех же строк. Пожалуйста, ознакомьтесь с документацией по insert_deduplication_token
для получения дополнительной информации о том, как использовать эту настройку.