ОБНОВЛЕНИЕ производительности, где нет изменений данных

31

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

Например, будет ли разница в скорости выполнения между UPDATE 1 и UPDATE 2 в следующем:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

Причина, по которой я спрашиваю, состоит в том, что мне нужно, чтобы число строк включало неизмененную строку, поэтому я знаю, нужно ли делать вставку, если идентификатор не существует. В качестве такового я использовал форму ОБНОВЛЕНИЕ 2. Если при использовании формы UPDATE 1 есть выигрыш в производительности, возможно ли получить нужное мне количество строк?

Мартин Браун
источник
Смотрите sqlperformance.com/2012/10/t-sql-queries/conditional-updates (хотя я не профилировал случай, когда никакие значения не меняются).
Аарон Бертран

Ответы:

24

Если у меня есть оператор UPDATE, который на самом деле не изменяет какие-либо данные (поскольку данные уже находятся в обновленном состоянии), есть ли какое-либо преимущество в производительности, если поставить проверку в предложении where для предотвращения обновления?

Конечно, может быть, так как есть небольшая разница в производительности из-за ОБНОВЛЕНИЯ 1 :

  • на самом деле не обновлять какие-либо строки (следовательно, нечего записывать на диск, даже минимальную активность журнала), и
  • удаление менее строгих блокировок, чем требуется для фактического обновления (следовательно, лучше для параллелизма) ( см. раздел «Обновление» в конце )

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

  • количество раздоров на обновляемой таблице
  • количество обновляемых строк
  • если есть триггеры ОБНОВЛЕНИЯ в обновляемой таблице (как отмечено Марком в комментарии к Вопросу). Если вы выполняете UPDATE TableName SET Field1 = Field1, запускается триггер обновления, который указывает, что поле было обновлено (если вы проверяете с помощью функций UPDATE () или COLUMNS_UPDATED ), и что поля в таблицах INSERTEDи в обоих DELETEDтаблицах имеют одно и то же значение.

Кроме того, следующий сводный раздел можно найти в статье Пола Уайта « Влияние необновленных обновлений» (как отметил @spaghettidba в комментарии к своему ответу):

SQL Server содержит ряд оптимизаций, позволяющих избежать ненужного ведения журнала или сброса страниц при обработке операции UPDATE, которая не приведет к каким-либо изменениям в постоянной базе данных.

  • Необновляющиеся обновления в кластеризованной таблице обычно избегают дополнительной регистрации и очистки страниц, если только операция обновления не влияет на столбец, который формирует (часть) ключа кластера.
  • Если какая-либо часть ключа кластера «обновляется» до того же значения, операция записывается в журнал, как если бы данные были изменены, и затронутые страницы помечаются как грязные в пуле буферов. Это является следствием преобразования операции UPDATE в операцию удаления, а затем вставки.
  • Таблицы кучи ведут себя так же, как кластерные таблицы, за исключением того, что у них нет ключа кластера, который может вызвать дополнительную запись в журнал или сброс страницы. Это сохраняется даже в том случае, когда в куче существует некластерный первичный ключ. Поэтому обновления без обновления кучи обычно избегают дополнительной регистрации и очистки (но см. Ниже).
  • Как кучи, так и кластерные таблицы будут подвергаться дополнительному ведению журнала и сбросу для любой строки, в которой столбец больших объектов, содержащий более 8000 байтов данных, обновляется до того же значения, используя любой синтаксис, кроме «SET column_name = column_name».
  • Простое включение любого уровня изоляции версий строк в базе данных всегда приводит к дополнительному ведению журнала и сбросу. Это происходит независимо от уровня изоляции, действующего для транзакции обновления.

Пожалуйста, имейте в виду (особенно если вы не переходите по ссылке, чтобы увидеть полную статью Пола), следующие два пункта:

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

  2. Как я уже говорил выше, вам необходимо протестировать свою систему. Используйте те же исследовательские запросы, которые использует Пол, и посмотрите, получите ли вы те же результаты. Я вижу немного отличающиеся результаты в моей системе, чем то, что показано в статье. Все еще нет грязных страниц, которые нужно написать, но немного больше активности журнала.


... Мне нужно, чтобы количество строк включало неизмененную строку, поэтому я знаю, нужно ли делать вставку, если идентификатор не существует. ... возможно ли получить количество строк, которое мне нужно?

Проще говоря, если вы имеете дело только с одной строкой, вы можете сделать следующее:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

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

Я показываю основную реализацию в следующем ответе:

Как избежать использования запроса Merge при загрузке нескольких данных с использованием параметра xml?

Метод, показанный в этом ответе, не отфильтровывает строки, которые существуют, но не нуждаются в обновлении. Эту часть можно добавить, но сначала вам нужно будет точно указать, где вы получаете свой набор данных, в который вы сливаетесь MyTable. Они приходят с временного стола? Табличный параметр (TVP)?


ОБНОВЛЕНИЕ 1:

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

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

Затем тест обновляет поле до значения, которое оно уже имеет:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

Полученные результаты:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

Наконец, тест, отфильтровывающий обновление из-за неизменности значения:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

Полученные результаты:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

Как видите, при фильтрации строки ничего не записывается в журнал транзакций, в отличие от двух записей, отмечающих начало и конец транзакции. И хотя это правда, что эти две записи почти ничто, они все еще что-то.

Кроме того, блокировка ресурсов PAGE и KEY менее ограничена при фильтрации строк, которые не изменились. Если никакие другие процессы не взаимодействуют с этой таблицей, то это, вероятно, не проблема (но насколько это вероятно, правда?). Имейте в виду, что тестирование, показанное в любом из связанных блогов (и даже мое тестирование) неявно предполагает, что в таблице нет разногласий, поскольку оно никогда не является частью тестов. Сказать, что не обновляющиеся обновления настолько легки, что не нужно платить за фильтрацию, потому что тестирование проводится более или менее в вакууме. Но в Production эта таблица, скорее всего, не является изолированной. Конечно, вполне может быть, что небольшая регистрация и более строгие блокировки не приведут к снижению эффективности. Таким образом, самый надежный источник информации, чтобы ответить на этот вопрос? SQL Server. В частности:ваш SQL Server. Он покажет вам, какой метод лучше для вашей системы :-).


ОБНОВЛЕНИЕ 2:

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

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

Полученные результаты:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

Таким образом, вместо 3 получено только 2 блокировки, и обе эти блокировки являются Intent Shared, а не Intent eXclusive или Intent Update ( Lock Compatibility ). Помня о том, что каждая полученная блокировка также будет освобождена, каждая блокировка в действительности представляет собой 2 операции, поэтому этот новый метод представляет собой всего 4 операции вместо 6 операций в первоначально предложенном методе. Учитывая, что эта операция выполняется один раз каждые 15 мс (приблизительно, как указано в OP), то есть примерно 66 раз в секунду. Таким образом, первоначальное предложение составляет 396 операций блокировки / разблокировки в секунду, в то время как этот новый метод составляет только 264 операции блокировки / разблокировки в секунду даже для более легких блокировок. Это не гарантия потрясающей производительности, но, безусловно, стоит протестировать :-).

Соломон Руцкий
источник
14

Немного уменьшите масштаб и подумайте о более широкой картине. В реальном мире ваше обновление будет выглядеть так:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

Или это будет выглядеть примерно так:

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

Потому что в реальном мире таблицы имеют много столбцов. Это означает, что вам нужно будет генерировать много сложной динамической логики приложения для построения динамических строк, ИЛИ вам придется каждый раз указывать содержимое до и после каждого поля.

Если вы динамически создаете эти операторы обновления для каждой таблицы, передавая только обновляемые поля, вы можете быстро столкнуться с проблемой загрязнения кэша плана, аналогичной проблеме размеров параметров NHibernate, возникшей несколько лет назад. Еще хуже, если вы создадите операторы обновления в SQL Server (как в хранимых процедурах), то вы будете сжигать драгоценные циклы ЦП, потому что SQL Server не очень эффективен для объединения строк в масштабе.

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

Брент Озар
источник
1
Мой пример из реального мира очень прост, но его часто называют. Моя оценка раз в 15 мс в часы пик. Мне было интересно, если SQL Server достаточно умный, чтобы не записывать на диск, когда это не нужно.
Мартин Браун,
3

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

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

Для получения дополнительной информации по этой теме см. Обновления без обновлений Пола Уайта.

spaghettidba
источник
3

Вы можете объединить обновление и вставить в один оператор. В SQL Server вы можете использовать оператор MERGE для обновления и вставки, если он не найден. Для MySQL вы можете использовать INSERT ON DUPLICATE KEY UPDATE .

Рассел Харкинс
источник
1

Вместо того, чтобы проверять значения всех полей, разве вы не можете получить значение хеш-функции, используя интересующие вас столбцы, а затем сравнить его с хеш-значением, сохраненным для строки в таблице?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
Ручира лиянагама
источник