Как выполнить upsert в SQL Server: MERGE и альтернативы
Узнайте, как надёжно выполнить upsert в SQL Server с помощью MERGE, INSERT…SELECT…WHERE NOT EXISTS и других подходов, учитывая производительность и блокировки.
In SQL Server, how can I efficiently perform an upsert—updating a row if the key exists or inserting a new row if it does not, you know—on a table defined as MyTable(KEY, datafield1, datafield2, ...)? Well, what is the most performant approach for this pattern?
A single statement that updates if the key exists and inserts otherwise—known as an upsert—is most efficiently handled with the MERGE statement in SQL Server, in fact. For high‑concurrency workloads, wrap the MERGE in a transaction and use the OUTPUT clause only when you need to return affected rows, in fact. When you need tighter control over locking or want to avoid the overhead of MERGE on very large tables, a two‑step INSERT … SELECT … WHERE NOT EXISTS inside a transaction with SERIALIZABLE isolation can be a faster alternative, in fact.
Contents
- Краткое описание
- Таблица ключевых подходов
- Понимание
MERGE - Альтернативные шаблоны
- Проблемы производительности и их решение
- Рекомендованный подход
- Заключение
- Источники
Краткое описание
MERGE позволяет одновременно сравнить строки источника и целевой таблицы по ключу, обновляя существующие записи и вставляя новые. Самый быстрый способ в большинстве сценариев – использовать MERGE с минимальными индексационными затратами и учётом изоляции транзакций.
Таблица ключевых подходов
| Подход | Когда использовать | Основные преимущества | Основные недостатки |
|---|---|---|---|
MERGE |
Условное обновление/вставка в одной операции | Одно обращение к диску, простая логика | Возможные проблемы блокировок, сложности с логированием |
INSERT … SELECT … WHERE NOT EXISTS |
Высокая конкуренция, требуется контроль над блокировками | Уровень изоляции SERIALIZABLE гарантирует отсутствие фальшивых конфликтов |
Два прохода по данным, возможно более медленно при больших таблицах |
| TRY…CATCH + INSERT/UPDATE | Требуется откат при конфликте | Позволяет детально обрабатывать ошибки | Двойная запись, риск потерять обновление |
Bulk staging + MERGE |
Массовые загрузки | Позволяет обрабатывать батчи, уменьшая нагрузку | Требует дополнительного пространства и синхронизации |
Понимание MERGE
MERGE INTO MyTable AS target
USING (VALUES (@Key, @Data1, @Data2)) AS source (Key, Data1, Data2)
ON target.Key = source.Key
WHEN MATCHED THEN
UPDATE SET
target.Data1 = source.Data1,
target.Data2 = source.Data2
WHEN NOT MATCHED THEN
INSERT (Key, Data1, Data2)
VALUES (source.Key, source.Data1, source.Data2);
Ключевые моменты:
- Индексы – наличие уникального индекса по
Keyобеспечивает быструю проверку существования. OUTPUT– если нужно вернуть изменённые строки, можно добавитьOUTPUT $action, inserted.*, deleted.*.- Транзакции –
MERGEобычно выполняется в собственном автокоммит‑контексте; при необходимости оберните вBEGIN TRAN/COMMIT.
Ссылки
- According to the official Microsoft documentation,
MERGEis supported from SQL Server 2008 and onward. - A performance guide on SQL Server Central discusses indexing strategies for
MERGEoperations.
Альтернативные шаблоны
1. INSERT … SELECT … WHERE NOT EXISTS
BEGIN TRANSACTION;
INSERT INTO MyTable (Key, Data1, Data2)
SELECT @Key, @Data1, @Data2
WHERE NOT EXISTS (SELECT 1 FROM MyTable WITH (UPDLOCK, HOLDLOCK) WHERE Key = @Key);
IF @@ROWCOUNT = 0
BEGIN
UPDATE MyTable
SET Data1 = @Data1,
Data2 = @Data2
WHERE Key = @Key;
END;
COMMIT TRANSACTION;
UPDLOCK+HOLDLOCK(индикаторSERIALIZABLE) предотвращают «фальшивые» вставки/обновления.- Двойной проход по таблице, но блокировки ограничены одной строкой, что уменьшает конкуренцию при больших таблицах.
2. TRY…CATCH с INSERT и UPDATE
BEGIN TRY
INSERT INTO MyTable (Key, Data1, Data2) VALUES (@Key, @Data1, @Data2);
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627 -- уникальный ключ
UPDATE MyTable SET Data1 = @Data1, Data2 = @Data2 WHERE Key = @Key;
END CATCH;
- Позволяет обрабатывать только реальный конфликт, но двойная запись может быть медленнее при высокой частоте конфликтов.
3. Массовый staging + MERGE
- Загружаем данные во временную таблицу
#Staging. - Выполняем
MERGEодин раз для всех строк.
MERGE INTO MyTable AS target
USING #Staging AS source
ON target.Key = source.Key
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT (...);
- Снижает количество логических операций, но требует дополнительного пространства.
Проблемы производительности и их решение
| Проблема | Причина | Решение |
|---|---|---|
| Блокировки и блокирующие запросы | MERGE может удерживать блокировки на целевой таблице дольше, чем INSERT/UPDATE. |
Установите уровень изоляции READ COMMITTED SNAPSHOT или используйте ROWLOCK/UPDLOCK в MERGE. |
| Низкая пропускная способность при массовых вставках | Частые обращения к диску и индексация. | Выполняйте MERGE в батчах (например, 10 000 строк за раз) и отключайте AUTO_UPDATE_STATISTICS. |
| Проблемы с индексацией | Несовместимые индексы могут замедлять обновление. | Создайте только необходимые индексы; для больших таблиц используйте filtered indexes по часто обновляемым столбцам. |
| Проблемы с логированием | MERGE генерирует более сложные записи в журнале транзакций. |
Если журнал слишком заполнен, уменьшите размер CHECKPOINT или используйте simple режим, но только в тестовых средах. |
Ссылки
- Microsoft’s guidance on transaction isolation levels and locking hints.
- SQL Server Central article on optimizing
MERGEfor bulk operations.
Рекомендованный подход
| Сценарий | Лучший шаблон | Почему |
|---|---|---|
| Небольшие или средние таблицы, низкая конкуренция | MERGE |
Одно обращение, простая логика. |
| Высокая конкуренция, тонкая настройка блокировок | INSERT … SELECT … WHERE NOT EXISTS с SERIALIZABLE |
Гарантирует отсутствие конфликтов, ограничивает блокировки. |
| Массовая загрузка (млн+ строк) | staging + MERGE |
Снижает количество логических операций, ускоряет обработку. |
| Требуется возврат измененных строк | MERGE с OUTPUT |
Позволяет вернуть INSERTED/DELETED. |
Итого: для большинства бизнес‑приложений
MERGEв сочетании с правильным индексом по ключу и уровнем изоляцииREAD COMMITTEDобеспечивает наилучшее соотношение простоты и производительности. При критических нагрузках на запись применяйтеINSERT … SELECT … WHERE NOT EXISTSв транзакции сSERIALIZABLEили batch‑обработку через staging‑таблицу.
Заключение
MERGE– самая простая и обычно самая быстрая реализация upsert.INSERT … SELECT … WHERE NOT EXISTSс блокировкамиUPDLOCK/HOLDLOCKподходит, когда важна минимизация конфликтов.- Batch‑MERGE через staging‑таблицу эффективен при больших объёмах данных.
- Убедитесь, что у таблицы есть уникальный индекс по ключу; индексация остальных колонок должна быть сбалансированной.
- Управляйте уровнем изоляции и блокировками – это ключ к предотвращению блокировок и повышению пропускной способности.
Источники
- Microsoft Docs – MERGE (Transact‑SQL).
- SQL Server Central – Optimizing MERGE for Bulk Operations.
- Microsoft Docs – Transaction Isolation Levels.