Если существует до вставки, обновления, удаления для оптимизации - PullRequest
34 голосов
/ 16 февраля 2010

Нередко возникает ситуация, когда вам необходимо выполнить оператор INSERT, UPDATE или DELETE на основании какого-либо условия. И мой вопрос заключается в том, влияет ли на производительность запроса добавление IF EXISTS перед командой.

Пример

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
    UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

А как насчет ВСТАВКИ или УДАЛЕНИЯ?

Ответы [ 12 ]

70 голосов
/ 16 февраля 2010

Я не совсем уверен, но у меня складывается впечатление, что этот вопрос действительно касается upsert, который является следующей атомарной операцией:

  • Если строка существует и в источнике, и в цели, UPDATE цель;
  • Если строка существует только в источнике, INSERT строка в цели;
  • (Необязательно) Если строка существует в цели, но не источник, DELETE строка из цели.

Разработчики, ставшие администраторами баз данных, часто наивно пишут это построчно, например:

-- For each row in source
IF EXISTS(<target_expression>)
    IF @delete_flag = 1
        DELETE <target_expression>
    ELSE
        UPDATE target
        SET <target_columns> = <source_values>
        WHERE <target_expression>
ELSE
    INSERT target (<target_columns>)
    VALUES (<source_values>)

Это самое худшее, что вы можете сделать по нескольким причинам:

  • Состояние гонки. Строка может исчезнуть между IF EXISTS и последующими DELETE или UPDATE.

  • Это расточительно. Для каждой транзакции выполняется дополнительная операция; может быть, это тривиально, но это полностью зависит от того, насколько хорошо вы проиндексировали.

  • Хуже всего - это следовать итерационной модели, думая об этих проблемах на уровне одной строки. Это будет иметь самое большое (худшее) влияние из всех на общую производительность.

Одна очень незначительная (и я подчеркиваю незначительная) оптимизация - это просто попытаться все равно UPDATE; если строка не существует, @@ROWCOUNT будет 0, и вы можете "безопасно" вставить:

-- For each row in source
BEGIN TRAN

UPDATE target
SET <target_columns> = <source_values>
WHERE <target_expression>

IF (@@ROWCOUNT = 0)
    INSERT target (<target_columns>)
    VALUES (<source_values>)

COMMIT

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

Но реальная проблема заключается в том, что это все еще делается для каждой строки в источнике.

До SQL Server 2008 вам приходилось использовать неудобную трехэтапную модель, чтобы справиться с этим на заданном уровне (все же лучше, чем строка за строкой):

BEGIN TRAN

INSERT target (<target_columns>)
SELECT <source_columns> FROM source s
WHERE s.id NOT IN (SELECT id FROM target)

UPDATE t SET <target_columns> = <source_columns>
FROM target t
INNER JOIN source s ON t.d = s.id

DELETE t
FROM target t
WHERE t.id NOT IN (SELECT id FROM source)

COMMIT

Как я уже сказал, производительность была довольно паршивой, но все же намного лучше, чем подход «один ряд за раз». Однако в SQL Server 2008 наконец-то появился синтаксис MERGE , поэтому теперь все, что вам нужно сделать, это:

MERGE target
USING source ON target.id = source.id
WHEN MATCHED THEN UPDATE <target_columns> = <source_columns>
WHEN NOT MATCHED THEN INSERT (<target_columns>) VALUES (<source_columns>)
WHEN NOT MATCHED BY SOURCE THEN DELETE;

Вот и все. Одно утверждение. Если вы используете SQL Server 2008 и вам нужно выполнить любую последовательность INSERT, UPDATE и DELETE в зависимости от того, существует ли уже строка - , даже если это только одна строка - там нет извините, чтобы не использовать MERGE.

Вы можете даже OUTPUT строк, затронутых MERGE в табличной переменной, если вам нужно выяснить, что было сделано позже. Просто, быстро и без риска. Сделай это.

8 голосов
/ 16 февраля 2010

Это бесполезно только для одного обновления / удаления / вставки.
Возможно добавляет производительность, если несколько операторов после условия if.
В последнем случае лучше написать

update a set .. where ..
if @@rowcount > 0 
begin
    ..
end
4 голосов
/ 16 февраля 2010

Ни

UPDATE … IF (@@ROWCOUNT = 0) INSERT

ни

IF EXISTS(...) UPDATE ELSE INSERT

шаблоны работают, как и ожидалось, при высоком параллелизме. Оба могут потерпеть неудачу. Оба могут очень часто терпеть неудачу. MERGE - король - он гораздо лучше справляется со своими задачами. Давайте проведем стресс-тестирование и убедитесь сами.

Вот таблица, которую мы будем использовать:

CREATE TABLE dbo.TwoINTs
    (
      ID INT NOT NULL PRIMARY KEY,
      i1 INT NOT NULL ,
      i2 INT NOT NULL ,
      version ROWVERSION
    ) ;
GO

INSERT  INTO dbo.TwoINTs
        ( ID, i1, i2 )
VALUES  ( 1, 0, 0 ) ;    

ЕСЛИ СУЩЕСТВУЕТ (…), ЭТОТ шаблон часто дает сбой при высоком параллелизме.

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

-- hit Ctrl+T to execute in text mode

SET NOCOUNT ON ;

DECLARE @ID INT ;

SET @ID = 0 ;
WHILE @ID > -100000
    BEGIN ;
        SET @ID = ( SELECT  MIN(ID)
                    FROM    dbo.TwoINTs
                  ) - 1 ;
        BEGIN TRY ;

            BEGIN TRANSACTION ;
            IF EXISTS ( SELECT  *
                        FROM    dbo.TwoINTs
                        WHERE   ID = @ID )
                BEGIN ;
                    UPDATE  dbo.TwoINTs
                    SET     i1 = 1
                    WHERE   ID = @ID ;
                END ;
            ELSE
                BEGIN ;
                    INSERT  INTO dbo.TwoINTs
                            ( ID, i1, i2 )
                    VALUES  ( @ID, 0, 0 ) ;
                END ;
            COMMIT ; 
        END TRY
        BEGIN CATCH ;
            ROLLBACK ; 
            SELECT  error_message() ;
        END CATCH ;
    END ; 

Когда мы запустим этот скрипт одновременно на двух вкладках, мы сразу получим огромное количество нарушений первичного ключа на обеих вкладках. Это показывает, насколько ненадежен шаблон IF EXISTS, когда он выполняется с высоким уровнем параллелизма.

Примечание: этот пример также демонстрирует, что небезопасно использовать SELECT MAX (ID) +1 или SELECT MIN (ID) -1 в качестве следующего доступного уникального значения, если мы делаем это в параллельном режиме.

4 голосов
/ 16 февраля 2010

Вы не должны делать это для UPDATE и DELETE, как будто влияет на производительность, оно не является положительным .

Для INSERT могут быть ситуации, когда ваш INSERT вызовет исключение (нарушение UNIQUE CONSTRAINT и т. Д.), И в этом случае вы можете предотвратить его с помощью IF EXISTS и обработать его более изящно.

3 голосов
/ 16 февраля 2010

IF EXISTS в основном сделает SELECT - тот же, что и UPDATE.

Таким образом, это снизит производительность - если обновлять нечего, вы проделали тот же объем работы (UPDATE запросил бы тот же недостаток строк, что и ваш выбор), и если есть что обновить, Вы сделали ненужный выбор.

3 голосов
/ 16 февраля 2010

Вы не должны делать это в большинстве случаев. В зависимости от уровня транзакции вы создали условие гонки, и теперь в вашем примере это не будет иметь большого значения, но данные могут быть изменены с первого выбора до обновления. И все, что вы сделали, это заставили SQL выполнять больше работы

Лучший способ узнать наверняка - это проверить два отличия и посмотреть, какое из них дает вам соответствующую производительность.

2 голосов
/ 16 февраля 2010

Это в значительной степени повторяет предыдущие (по времени) пять (нет, шесть) (нет, семь) ответов, но:

Да, структура ЕСЛИ СУЩЕСТВУЕТ, что вы в целом удвоите выполненную работупо базе данных.Хотя IF EXISTS «остановится», когда найдет первую подходящую строку (ему не нужно находить их все), это все же дополнительные и в конечном итоге бессмысленные усилия - для обновлений и удалений.

  • Еслитаких строк не существует, ЕСЛИ EXISTS выполнит полное сканирование (таблицу или индекс), чтобы определить это.
  • Если существует одна или несколько таких строк, ЕСЛИ EXISTS прочитает достаточно таблицы / индекса, чтобы найтисначала один, а затем UPDATE или DELETE затем перечитают эту таблицу, чтобы найти ее снова и обработать - и она прочитает «остальную часть» таблицы, чтобы увидеть, есть ли еще какие-либо для обработки также.(Достаточно быстро, если правильно проиндексирован, но все же.)

Так или иначе, вы в конечном итоге прочитаете всю таблицу или индекс хотя бы один раз.Но зачем вообще беспокоиться о IF EXISTS?

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 

или подобное DELETE будет нормально работать независимо от того, найдены ли строки для обработки .Нет строк, таблица отсканирована, ничего не изменено, все готово;1+ строк, таблица отсканирована, все, что должно быть изменено, сделано снова.Один проход, без суеты, без суеты, не нужно беспокоиться о том, «изменилась ли база данных другим пользователем между моим первым запросом и моим вторым запросом».

INSERT - это ситуация, когда это может быть полезно -перед добавлением проверьте, есть ли строка, чтобы избежать нарушений первичного или уникального ключа.Конечно, вам нужно беспокоиться о параллелизме - что если кто-то еще попытается добавить эту строку одновременно с вами?Оборачивая все это в один INSERT, мы будем обрабатывать все это в неявной транзакции (помните ваши свойства ACID!):

INSERT Contacs (col1, col2, etc) values (val1, val2, etc) where not exists (select 1 from Contacs where col1 = val1)
IF @@rowcount = 0 then <didn't insert, process accordingly>
2 голосов
/ 16 февраля 2010

Есть небольшой эффект, так как вы делаете одну и ту же проверку дважды, по крайней мере, в вашем примере:

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)

Требуется запрос, посмотрите, есть ли они, если true, то:

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

Нужно запросить, посмотреть, какие ... та же самая проверка дважды без причины. Теперь, если искомое условие проиндексировано, оно должно быть быстрым, но для больших таблиц вы можете увидеть некоторую задержку только потому, что запускаете select.

2 голосов
/ 16 февраля 2010

Производительность оператора IF EXISTS:

IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue)

зависит от имеющихся индексов для удовлетворения запроса.

1 голос
/ 16 февраля 2010

Да, это повлияет на производительность (степень влияния на производительность будет зависеть от ряда факторов). По сути, вы делаете один и тот же запрос «дважды» (в вашем примере). Спросите себя, нужно ли вам проявлять осторожность в своем запросе и в каких ситуациях не будет строки? Кроме того, с помощью оператора update затронутые строки, вероятно, являются лучшим способом определить, обновлено ли что-либо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...