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

9

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

Теперь я пытаюсь добиться этого в хранимой процедуре, которая принимает параметр XML. Причина, по которой я использую XML, а не параметр с табличным значением, заключается в том, что в последнем случае мне придется создавать пользовательский тип в SQL и связывать этот тип с хранимой процедурой. Если бы я когда-нибудь что-то изменил в своей хранимой процедуре или в моей схеме БД, мне пришлось бы повторить как хранимую процедуру, так и пользовательский тип. Я хочу избежать этой ситуации. Кроме того, преимущество, которое TVP имеет над XML, не полезно для моей ситуации, потому что размер моего массива данных никогда не будет превышать 1000. Это означает, что я не могу использовать предложенное здесь решение: Как вставить несколько записей с использованием XML в SQL Server 2008

Кроме того, подобное обсуждение здесь ( UPSERT - есть ли лучшая альтернатива MERGE или @@ rowcount? ) Отличается от того, что я спрашиваю, потому что я пытаюсь добавить несколько строк в таблицу.

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

begin tran
   update table with (serializable) set select * from xml_param
   where key = @key

   if @@rowcount = 0
   begin
      insert table (key, ...) values (@key,..)
   end
commit tran

Следующей альтернативой является использование исчерпывающего IF EXISTS или одного из его вариантов следующей формы. Но я отвергаю это из-за неоптимальной эффективности:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

Следующим вариантом было использование оператора Merge, как описано здесь: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Но затем я прочитал о проблемах с запросом Merge здесь: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . По этой причине я пытаюсь избежать слияния.

Итак, теперь мой вопрос: есть ли какой-либо другой вариант или лучший способ добиться множественного восстановления с использованием параметра XML в хранимой процедуре SQL Server 2008?

Обратите внимание, что данные в параметре XML могут содержать некоторые записи, которые не должны быть UPSERTed из-за того, что они старше, чем текущая запись. В ModifiedDateтаблице XML и в таблице назначения есть поле, которое необходимо сравнить, чтобы определить, должна ли запись быть обновлена ​​или отброшена.

GMalla
источник
Попытка избежать внесения изменений в proc в будущем - не очень хорошая причина не использовать TVP. если данные передаются в изменениях, вы в любом случае вносите изменения в код.
Макс Вернон
1
@MaxVernon Сначала у меня была та же мысль, и я почти сделал очень похожий комментарий, потому что это само по себе не повод избегать TVP. Но они требуют немного больше усилий, и с предостережением «никогда не более 1000 строк» ​​(подразумевается иногда, или, может быть, даже часто?) Это немного путаница. Тем не менее, я полагаю, что должен уточнить свой ответ, заявив, что <1000 строк за раз не слишком сильно отличается от XML, если он не вызывается 10k раз подряд. Тогда небольшие различия в производительности, безусловно, складываются.
Соломон Руцкий
Проблемы, на MERGEкоторые указывает Бертран, - это, в основном, крайние случаи и неэффективность, а не пробки - MS не выпустила бы это, если бы это было настоящее минное поле. Вы уверены, что извилины, которые вы проходите, чтобы избежать MERGE, не создают больше потенциальных ошибок, чем они сохраняют?
Джон на все руки
@JonofAllTrades Если честно, то, что я предложил, на самом деле не так запутанно по сравнению с MERGE. Шаги INSERT и UPDATE в MERGE по-прежнему обрабатываются отдельно. Основным отличием в моем подходе является переменная таблицы, которая содержит обновленные идентификаторы записей, и запрос DELETE, который использует эту переменную таблицы для удаления этих записей из временной таблицы входящих данных. И я полагаю, что SOURCE может быть прямым из @ XMLparam.nodes () вместо дампа во временную таблицу, но, тем не менее, это не так уж много лишних вещей, чтобы не беспокоиться о том, что вы окажетесь в одном из этих крайних случаев; ).
Соломон Руцкий

Ответы:

11

Является ли источник XML или TVP, не имеет большого значения. Общая операция по существу:

  1. ОБНОВИТЬ существующие строки
  2. ВСТАВИТЬ пропущенные строки

Вы делаете это в таком порядке, потому что если вы сначала ВСТАВИТЕ, то все строки существуют, чтобы получить ОБНОВЛЕНИЕ, и вы будете выполнять повторную работу для всех строк, которые были только что вставлены.

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

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

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

Оттуда мы делаем ОБНОВЛЕНИЕ, а затем вставляем:

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

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

  1. захватить @@ ROWCOUNT вставки в временную таблицу и сравнить с @@ ROWCOUNT ОБНОВЛЕНИЯ. Если они одинаковые, тогда мы можем пропустить ВСТАВКУ

  2. записать значения идентификаторов, обновленные с помощью предложения OUTPUT, и УДАЛИТЬ значения из временной таблицы. Тогда вставка не нужнаWHERE NOT EXISTS(...)

  3. Если во входных данных есть какие-либо строки, которые не следует синхронизировать (т.е. не вставлять и не обновлять), то эти записи должны быть удалены перед выполнением ОБНОВЛЕНИЯ

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

Я несколько раз использовал эту модель в Imports / ETL, которые либо имеют более 1000 строк, либо, может быть, 500 в пакете из общего набора в 20 КБ - более миллиона строк. Однако я не проверял разницу в производительности между DELETE обновленных строк из временной таблицы и простым обновлением поля [IsUpdate].


Обратите внимание на решение об использовании XML поверх TVP, поскольку в каждый момент времени необходимо импортировать не более 1000 строк (упомянуто в вопросе):

Если это вызывается несколько раз здесь и там, то вполне возможно, что незначительное увеличение производительности в TVP может не стоить дополнительных затрат на обслуживание (необходимость отменить процедуру перед изменением пользовательского типа таблицы, изменений кода приложения и т. Д.) , Но если вы импортируете 4 миллиона строк, отправляя по 1000 за раз, то есть 4000 выполнений (и 4 миллиона строк XML для анализа независимо от того, как он разбит), и даже небольшая разница в производительности при выполнении всего несколько раз приведет к добавьте к заметной разнице.

При этом метод, как я описал, не меняется, за исключением замены SELECT FROM @XmlInputParam на SELECT FROM @TVP. Поскольку TVP доступны только для чтения, вы не сможете удалить их. Я полагаю, вы можете просто добавить WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)к этому окончательному SELECT (привязанному к INSERT) вместо простого WHERE IsUpdate = 0. Если бы вы использовали @UpdateIDsпеременную таблицы таким образом, то вы могли бы даже избежать сброса входящих строк во временную таблицу.

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