Unique Key
Кратко Модель Unique Key в Apache Doris представляет собой модель таблиц с первичным ключом: одна строка на первичный ключ и поддержка SQL
UPDATEиDELETE, как в транзакционной базе данных. Операции upsert с частичным обновлением столбцов поступают из Stream Load непосредственно в таблицу. Начиная с версии 2.1 модель Unique Key по умолчанию использует Merge-on-Write, поэтому дубликаты устраняются при записи, а запросам не требуется слияние версий при чтении.

Зачем использовать модель Unique Key в Apache Doris?
Модель Unique Key в Apache Doris обеспечивает OLAP-движку полноценную семантику первичного ключа: на каждый ключ приходится одна строка, уникальность гарантируется сразу при записи, а SQL UPDATE/DELETE работают без перезаписи партиций. Большинство аналитических движков рассматривают таблицы как структуры, допускающие только добавление данных. Но затем возникает задача CDC. В MySQL обновляется строка, пользователь запрашивает удаление данных по GDPR, запоздавшее событие должно перезаписать более старое. Модель таблиц, допускающая только добавление, позволяет зафиксировать эти изменения, но не позволяет удалить из хранилища старую версию без перезаписи всей партиции.
- Задание Flink CDC отслеживает изменения в MySQL, и каждое обновление по первичному ключу должно попадать в таблицу за несколько секунд.
- В таблице статусов заказов состояния сменяются с
createdнаpaid, затем наshippedиdelivered, а запросы должны видеть только последнее состояние. - Таблица профилей пользователей используется в соединениях в реальном времени, и при соединении никогда не должен появляться вчерашний адрес электронной почты.
Для этого и предназначена модель Unique Key. Новая запись с существующим ключом заменяет старую строку, UPDATE и DELETE выполняются как обычные операции SQL, а аналитические запросы по-прежнему работают со скоростью колоночного движка. Ближайший аналог в других OLAP-движках, ReplacingMergeTree в ClickHouse, не обеспечивает немедленную уникальность: строки-дубликаты остаются видимыми до выполнения фонового слияния, и даже после этого движок гарантирует лишь устранение дубликатов со временем. Запрос, выполненный сразу после записи, все еще может увидеть старую версию, а обходной путь с SELECT ... FINAL принудительно устраняет дубликаты при чтении, создавая ощутимые накладные расходы. Unique Key избавляет от этого компромисса. Новая строка заменяет старую при записи, и следующий запрос видит одну строку на ключ.
Что такое модель Unique Key в Apache Doris?
Модель Unique Key в Apache Doris хранит ровно одну строку на первичный ключ; движок Merge-on-Write обеспечивает это при записи. Предложение UNIQUE KEY(...) в CREATE TABLE задает ключевые столбцы; все остальные являются столбцами значений. Две строки с одним и тем же ключом не могут существовать одновременно. Новая запись с существующим ключом заменяет строку.
... UNIQUE KEY(order_id) ...
... UNIQUE KEY(tenant_id, user_id) ...
В Catalog FE эта модель записывается как KeysType.UNIQUE_KEYS. Это задает контракт, который движок хранения соблюдает с этого момента. Модель фиксируется при создании таблицы и не может быть изменена позже.
Основные термины
- Первичный ключ: столбцы, указанные в
UNIQUE KEY(...). Они определяют порядок сортировки данных на диске и обеспечивают уникальность. - Столбцы значений: все остальные столбцы. Данные хранятся в том виде, в котором записаны, без агрегации.
- Merge-on-Write (MoW): механизм хранения по умолчанию начиная с 2.1. При новой записи выполняется поиск существующей строки, ее ID отмечается в битовой карте удалений соответствующего rowset, а новая строка добавляется в новый rowset.
- Merge-on-Read (MoR): прежний механизм хранения. При записи данные добавляются; при чтении несколько версий каждого ключа сливаются на лету. По-прежнему доступен при
enable_unique_key_merge_on_write = false. - Битовая карта удалений: битовая карта Roaring с ключом
(rowset_id, segment_id, version), содержащая ID строк, которые запросы должны пропускать. Структура MoW, заменяющая слияние при чтении. - Столбец последовательности (
function_column.sequence_col): столбец значений, определяющий, какая запись имеет приоритет, когда два обновления содержат один и тот же ключ. События CDC, поступающие не по порядку, используют его для сохранения более нового состояния. - Частичное обновление столбцов: операция upsert, записывающая только часть столбцов. Apache Doris считывает остальные из существующей строки и записывает обратно полную строку.
__DORIS_DELETE_SIGN__: скрытый столбец типа tinyint. Задайте ему значение1при загрузке любым способом, чтобы логически удалить строку.
Как работает модель Unique Key в Apache Doris?
Модель Unique Key в Apache Doris выполняет каждую операцию upsert в пять этапов: буферизует пакет, ищет ключи в индексах первичного ключа сегментов, устанавливает биты удалений для старых строк, определяет порядок с помощью столбца последовательности и публикует новый rowset.
Рассмотрим операцию upsert в таблице Unique Key с MoW.
- Буферизация пакета. Узлы BE собирают поступающие строки в memtable и сортируют их по ключу.
- Поиск каждого ключа. Для каждого ключа в пакете BE обращается к индексу первичного ключа сегмента (одно короткое чтение на ключ), чтобы найти существующий rowset и ID строки, если они есть. Индекс представляет собой отсортированную структуру с разбивкой на страницы, которая строится при сбросе каждого сегмента и концептуально напоминает партиционированный индекс RocksDB.
- Пометка старых строк. В битовой карте удалений каждого затронутого rowset устанавливаются биты для ID старых строк. Карта ведется для каждой комбинации
(rowset_id, segment_id, version), а старые данные остаются на диске, пока compaction не освободит занимаемое ими место. - Определение порядка с помощью столбца последовательности. Если задан
function_column.sequence_col, MoW сравнивает значение последовательности поступающей строки с__DORIS_SEQUENCE_COL__текущей строки. Побеждает большее значение. При равных значениях используется порядок загрузки. Тип последовательности должен быть целочисленным илиDATE/DATETIME. - Публикация нового rowset. Транзакция фиксируется, новая версия становится видимой, и с этого момента запросы фильтруют строки по объединенной битовой карте удалений. Слияние версий при чтении не требуется.
DELETE FROM ... WHERE следует тому же процессу, но без нового rowset. Частичные обновления добавляют еще один шаг: перед записью BE считывает для каждого ключа столбцы, которые не изменяются, чтобы сохранить полную строку.
Быстрый старт
CREATE TABLE orders (
order_id BIGINT,
status VARCHAR(20),
amount DECIMAL(10, 2),
updated DATETIME
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 4
PROPERTIES ("function_column.sequence_col" = "updated");
INSERT INTO orders VALUES
(1, 'created', 99.50, '2026-05-08 10:00:00'),
(1, 'paid', 99.50, '2026-05-08 10:05:00'),
(1, 'created', 99.50, '2026-05-08 09:00:00'); -- late event
SELECT * FROM orders;
Ожидаемый результат
+----------+--------+--------+---------------------+
| order_id | status | amount | updated |
+----------+--------+--------+---------------------+
| 1 | paid | 99.50 | 2026-05-08 10:05:00 |
+----------+--------+--------+---------------------+
Три вставки, одна строка. Второй INSERT перезаписал первую строку, поскольку ключи совпали. Третья вставка попыталась снова перезаписать строку, но ее значение updated было более ранним, чем текущее значение последовательности, поэтому MoW сохранил существующую строку. Если убрать свойство function_column.sequence_col, третья строка получила бы приоритет только за счет порядка загрузки, что нежелательно для CDC.
Когда стоит использовать модель Unique Key в Apache Doris?
Модель Unique Key в Apache Doris подходит для приемников CDC, таблиц измерений и справочников, обновляемых в реальном времени, часто меняющихся таблиц заказов, пользователей и балансов, точечных запросов в стиле KV в сочетании с store_row_column, построчного удаления данных по GDPR и сборки широких таблиц из нескольких источников. Она не подходит для потоков событий, в которые данные только добавляются, частых последовательных обновлений отдельных строк, случайных или очень широких первичных ключей, а также нагрузок с интенсивным использованием count(*) на прежнем механизме Merge-on-Read.
Подходящие сценарии
- Приемники CDC для MySQL, Postgres или любого источника с первичным ключом. Используйте столбец последовательности на основе временной метки фиксации транзакции источника или LSN. См. Интеграция с Kafka CDC.
- Таблицы измерений и справочники, обновляемые в реальном времени и присоединяемые к таблицам фактов в запросах.
- Таблицы заказов, пользователей, балансов или состояний сеансов, которые часто изменяются и используются в аналитических запросах.
- Точечные запросы в стиле KV при сочетании MoW с
store_row_columnи подготовленным запросом. См. Точечные запросы при высокой конкурентной нагрузке. - Построчное удаление данных по ID пользователя в соответствии с GDPR, включая логическое удаление через
__DORIS_DELETE_SIGN__при любом способе загрузки. - Сборка широких таблиц, в которых каждый источник отвечает за группу столбцов и записывает только свои столбцы через Stream Load с
partial_columns: true. См. Частичное обновление столбцов.
Неподходящие сценарии
- Потоки событий, допускающие только добавление (логи, потоки кликов, показания IoT). При каждой записи приходится искать ключ в индексе ради возможности, которой вы не пользуетесь. Выберите модель Duplicate Key. См. Модель данных.
- Частые последовательные
UPDATEотдельных строк. Каждый оператор представляет собой отдельную транзакцию, поэтому основную долю времени занимают накладные расходы на фиксацию. Объединяйте изменения в пакеты через Stream Load сpartial_columns: trueили используйте Group Commit. - Случайные или очень широкие первичные ключи (UUID, длинные составные ключи). Каждый загружаемый пакет обращается к индексу PK, а случайные ключи нарушают локальность обращений к его кэшу. Предпочитайте целые числа, делайте ключ коротким и используйте наименьший подходящий тип. Типы float, double и сложные типы, такие как
ARRAY,MAP,STRUCT,JSONиVARIANT, вообще не допускаются в ключевых столбцах. - Использование ключа партиционирования как надмножества первичного ключа. Столбцы партиционирования должны быть подмножеством уникального ключа, и ни в коем случае не наоборот. Иначе один и тот же ключ может попасть в две партиции, и устранение дубликатов перестанет работать.
- Нагрузки с интенсивным использованием
count(*)в варианте MoR. MoR восстанавливает версии во время выполнения запроса, что делает подсчет дорогим. Либо включите Merge-on-Write (используется по умолчанию с 2.1), либо, если данные действительно только добавляются, перейдите на Duplicate.
Дополнительные материалы
- Модель Unique Key: подробное описание синтаксиса SQL, различий между Merge-on-Write и Merge-on-Read, правил партиционирования и свойств создания таблиц.
- Обновление и удаление данных: возможности SQL (
UPDATE,DELETE, операции upsert с частичным обновлением столбцов, атомарные замены партиций) и внутренние механизмы Merge-on-Write, которые их обеспечивают. - Модель данных: сравнение Unique Key с Duplicate и Aggregate и нагрузки, для которых подходит каждая модель.
- Управление конкурентным доступом с помощью столбца последовательности: допустимые типы, обработка NULL и разрешение конфликтов записей, поступающих не по порядку.
- Точечные запросы при высокой конкурентной нагрузке: план short-circuit, который превращает таблицу Unique Key в KV-хранилище.
- Apache Doris 2.1.0: Merge-on-Write становится режимом по умолчанию для Unique Key