Обновления и удаления

Режим Unique

Режим Unique — это специализированная модель записи в MARS3, предназначенная для преобразования операций «обновление записи» в «вставку новой записи с тем же уникальным ключом». Движок хранения автоматически обрабатывает замену старых версий новыми версиями для одного и того же значения ключа.

Основная ценность этого режима заключается в возможности выражать семантику обновления с помощью более простого и унифицированного пути записи (INSERT) в сценариях с высокой частотой записи. Это снижает накладные расходы и сложность явных операторов UPDATE, сохраняя при этом лучший контроль над организацией данных при непрерывной записи.

Области применения:

  • Непрерывное обновление состояния по ключу сущности: Сильные измерения сущностей (например, устройства, транспортные средства, пользователи, заказы), где repeatedly записывается последнее значение для конкретного ключа.
  • Высокочастотная запись небольшими пакетами: Сценарии, требующие простого, стабильного и устойчивого пути записи.
  • Запросы последнего состояния: Рабочие нагрузки, ориентированные преимущественно на запрос последнего значения или снимка данных, такие как панели мониторинга в реальном времени, состояния последних оповещений или панели текущих метрик.
  • Отсутствие зависимости от семантики физического удаления: Бизнес-логика не требует частых операций DELETE (или может выражать недействительность/истечение срока действия другими средствами).

Различия между режимом Unique и UPSERT

В режиме Unique уникальный ключ определяется ключом сортировки таблицы (ORDER BY (...)). Когда вставляется новая запись с Unique Key, совпадающим с существующей записью, движок трактует это как обновление данного ключа. Явный оператор UPDATE не требуется; прямая команда INSERT реализует семантику обновления.

В отличие от традиционного UPSERT (INSERT ... ON CONFLICT), режим Unique выполняет слияние во время чтения.

  • Режим Unique: Данные немедленно записываются в хранилище. Во время чтения движок использует цепочки версий и правила видимости, чтобы гарантировать возврат только последних данных. Дублирующиеся данные удаляются позже в процессе COMPACT.
  • UPSERT: Выполняет слияние во время записи. Перед записью выполняется проверка на наличие потенциальных конфликтов, что создает дополнительные накладные расходы на запись.

Следовательно, эффективность записи в режиме Unique значительно выше, чем у традиционного UPSERT. Бенчмарки показывают, что производительность режима Unique примерно в 1,5 раза выше, чем у традиционного UPSERT.

Обработка JSONB

В сценариях режима Unique,涉及ющих перезапись одного и того же ключа:

  • Скалярные столбцы: Поведение по умолчанию — перезапись старого значения новым.
  • Столбцы JSONB: Движок поддерживает семантику инкрементального слияния. Вместо прямой перезаписи новое значение JSONB выводится по правилу: new_jsonb = jsonb_concat(old_jsonb, incoming_jsonb). База данных внутренне вызывает jsonb_concat для выполнения слияния.

Примеры:

postgres=# SELECT jsonb_concat('[1,2]'::jsonb, '[2,3]'::jsonb);
 jsonb_concat 
--------------
 [1, 2, 2, 3]
(1 строка)

postgres=# SELECT jsonb_concat('{"k":1,"x":2}'::jsonb, '{"k":null}'::jsonb);
    jsonb_concat    
---------------------
 {"k": null, "x": 2}
(1 строка)

postgres=# SELECT jsonb_concat('{"a":1,"b":2}'::jsonb, '{"b":99,"c":3}'::jsonb);
       jsonb_concat       
---------------------------
 {"a": 1, "b": 99, "c": 3}
(1 строка)

Это позволяет приложениям, использующим JSONB для динамических атрибутов, расширяемых полей или тегов, записывать только инкрементальные изменения. Движок берет на себя слияние, избегая сложности и конфликтов параллелизма, связанных с чтением старых значений, их слиянием на уровне приложения и обратной записью.

Риски и лучшие практики

  • Риск идемпотентности при слиянии массивов: Добавление элементов в массив JSONB не устраняет дубликаты. Если записи повторяются или воспроизводятся, могут накапливаться дублирующиеся элементы.
    • Рекомендация: Избегайте долгосрочного добавления деталей событий в массив. Для обеспечения идемпотентности введите event_id и обеспечьте уникальность на уровне приложения или используйте структуру объекта/карты для перезаписи по ключу.
  • Управление ростом JSONB: Слияние JSONB приводит к постепенному росту полей, что потенциально увеличивает затраты на чтение и нагрузку на фоновое обслуживание.
    • Рекомендация: Внедрите управление емкостью для полей JSONB (например, обрезка, разделение таблиц, бакетизация или хранение только последних N записей).
  • NULL не равно удалению: Установка ключа в null (например, {"k": null}) обновляет значение до null; это не удаляет ключ.
    • Рекомендация: Если требуется семантика удаления, разработайте специальный механизм (например, выделенное поле-маркер, отдельную таблицу удалений или соглашение на уровне приложения).

Стратегия реализации UPDATE/DELETE

Зачем нужна UpdateChain? Для обеспечения корректности при параллельных обновлениях одной и той же строки.

В стандартных таблицах Heap PostgreSQL обновление не является изменением на месте. Вместо этого старый кортеж помечается как удаленный, и вставляется новый кортеж. Поле ctid связывает старую версию с новой, формируя цепочку обновлений (update chain).

Image

В MARS3 маркеры удаления/обновления не хранятся в заголовке кортежа (как xmax в Heap). Вместо этого они записываются в Delta-файлы. Чтобы гарантировать сохранение этой информации после уплотнения (compaction) или сброса, используются Link-файлы.

  • Heap: Цепочки версий полагаются на заголовки кортежей и ссылки ctid (указатели версий встроены в сами данные).
  • MARS3: Версионирование и семантика удаления вынесены во вспомогательные структуры (Delta и Link-файлы). Следовательно, корректность зависит не только от файлов данных, но и от обеспечения целостности, согласованности и непрерывности Delta/Link-файлов во время операций сброса, уплотнения и вакуумирования.

Ранние версии MARS3 выдавали ошибку при параллельных обновлениях одной и той же строки из-за отсутствия поддержки UpdateChain. С внедрением UpdateChain MARS3 теперь использует TupleLock для блокировки логических строк и проходит по цепочке обновлений для поиска последней версии. Это обеспечивает последовательное выполнение параллельных обновлений, гарантируя корректную семантику UPDATE.

Сборка мусора и освобождение места

Подобно PostgreSQL, YMatrix реализует обновления и удаления через управление многоверсионным параллелизмом (MVCC), а не путем изменения данных на месте:

  • Обновления и удаления: MARS3 не изменяет данные на месте. Вместо этого используются DELTA-файлы и информация о версиях для скрытия старых данных, контролируя видимость данных.
  • Удаления: Операция DELETE записывает факт удаления в Delta-файл соответствующего Run. Физическое удаление данных происходит только при слиянии Run.
  • Обновления: Операция UPDATE фактически удаляет исходные данные и вставляет новую запись.

Мертвые кортежи (невидимые runs), образовавшиеся в результате обновлений и удалений, очищаются фоновым процессом autovacuum. Также можно выполнить ручной VACUUM. Кроме того, в процессе COMPACT, когда меньшие Run объединяются в большие, невидимые Run удаляются.

VACUUM FULL идет дальше, объединяя несколько небольших Run в один большой. Если новых записей не поступает, выполнение VACUUM FULL будет агрессивно сливать пакеты до тех пор, пока дальнейшее слияние станет невозможным. В конечном итоге размер каждого оставшегося пакета будет приближаться к настроенному значению max_runsize. Обратите внимание, что сам процесс VACUUM FULL может генерировать некоторые невидимые Run в ходе выполнения.

Резюме:

  1. VACUUM: Сбрасывает данные, удаляет невидимые Run и записывает данные rowstore вниз.
  2. VACUUM FULL: Выполняет все действия VACUUM плюс агрессивное слияние Run.

Вернуться к предыдущему разделу: Принципы движка хранения