Як уникнути використання запиту на об'єднання під час введення кількох даних за допомогою параметра xml?


10

Я намагаюся оновити таблицю з масивом значень. Кожен елемент масиву містить інформацію, яка відповідає рядку в таблиці в базі даних SQL Server. Якщо рядок уже існує в таблиці, ми оновлюємо цей рядок інформацією в даному масиві. В іншому випадку ми вставляємо новий ряд у таблицю. Я в основному описав прихильність.

Тепер я намагаюся досягти цього в збереженій процедурі, яка приймає параметр XML. Причина, що я використовую параметр XML, а не табличний параметр, полягає в тому, що, виконуючи останнє, мені доведеться створити власний тип у SQL і пов’язати цей тип із збереженою процедурою. Якщо я колись щось міняв у своїй збереженій процедурі або моїй схемі db по дорозі, мені доведеться повторити як збережену процедуру, так і спеціальний тип. Я хочу уникнути цієї ситуації. Крім того, перевага, яку має TVP над XML, не корисна для моєї ситуації, тому що розмір мого масиву даних ніколи не перевищить 1000. Це означає, що я не можу використовувати запропоноване тут рішення: Як вставити кілька записів за допомогою XML на SQL-сервер 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 . Але потім я читав про проблеми із запитом на об’єднання тут: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . З цієї причини я намагаюся уникати Злиття.

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

Зверніть увагу, що дані в параметрі XML можуть містити деякі записи, які не повинні бути UPSERTed через те, що вони старші за поточний запис. ModifiedDateІ в XML, і в цільовій таблиці є поле, яке необхідно порівняти, щоб визначити, чи слід оновити або відкинути запис.


Намагатися уникати змін у програмі в майбутньому насправді не є вагомим приводом не використовувати TVP. якщо дані, передані в змінах, ви в будь-якому випадку внесете зміни в код.
Макс Вернон

1
@MaxVernon Я спочатку мав таку ж думку, і майже зробив дуже схожий коментар, оскільки це само по собі не є причиною уникати TVP. Але вони забирають трохи більше зусиль, і з застереженням "ніколи не перевищуючи 1000 рядів" (мається на увазі іноді, а може, навіть часто?) Це трохи підкидання. Однак, я думаю, я повинен кваліфікувати свою відповідь, констатуючи, що <1000 рядків одночасно не надто відрізняється від XML, доки він не називається 10k разів підряд. Тоді незначні відмінності у виконанні, безумовно, складаються.
Соломон Руцький

Проблеми, на MERGEякі вказує Бертран, - це переважно кращі випадки та неефективність, а не пробки - MS не випустили б його, якби це було справжнє мінне поле. Ви впевнені, що згортки, через які ви переживаєте MERGE, не створюють більше потенційних помилок, ніж заощаджують?
Йон усіх торгів

@JonofAllTrades Справедливо кажучи, те, що я запропонував, насправді не таке перекручене порівняно з MERGE. Етапи INSERT та UPDATE MERGE все ще обробляються окремо. Основна відмінність мого підходу - це змінна таблиця, яка містить оновлені ідентифікатори запису, і запит DELETE, який використовує цю змінну таблиці для видалення цих записів із таблиці темпів вхідних даних. І я гадаю, ДЖЕРЕЛО може бути безпосередньо з @ XMLparam.nodes () замість того, щоб скидати до темп-таблиці, але все-таки, це не багато зайвих матеріалів, щоб не потрібно було турбуватися про те, щоб коли-небудь опинитися в одному з цих крайових випадків; - ).
Соломон Руцький

Відповіді:


12

Незалежно від того, джерело 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;

Я декілька разів використовував цю модель в Імпорті / ETL, які мають понад 1000 рядків або, можливо, 500 у партії із загального набору 20 к - понад мільйон рядків. Однак я не перевіряв різницю продуктивності між DELETE оновлених рядків із таблиці темп порівняно з лише оновленням поля [IsUpdate].


Зверніть увагу на рішення про використання XML через TVP через те, що одночасно буде імпортуватися не більше 1000 рядків (згаданих у запитанні):

Якщо це викликає кілька разів тут і там, то, можливо, незначний приріст продуктивності в TVP може бути не вартим додаткових витрат на обслуговування (потрібно скинути прок перед тим, як змінити визначений користувачем тип таблиці, зміни коду програми тощо). . Але якщо ви імпортуєте 4 мільйони рядків, надсилаючи одночасно 1000, тобто 4000 виконань (і 4 мільйони рядків XML для розбору незалежно від того, як він розбитий), і навіть незначна різниця в продуктивності при виконанні лише кілька разів додайте до помітної різниці.

Як було сказано, метод, як я описав, не змінюється за межами заміни SELECT FROM @XmlInputParam на SELECT FROM @TVP. Оскільки ТВП доступні лише для читання, ви не зможете їх видалити. Я думаю, ви можете просто додати WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)до цього остаточного SELECT (прив’язаного до ВСТАВКИ) замість простого WHERE IsUpdate = 0. Якби ви використовували @UpdateIDsзмінну таблиці таким чином, ви могли б навіть уникнути, не скидаючи вхідні рядки в таблицю темп.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.