сервер sql: оновлення полів на величезній таблиці невеликими шматками: як отримати прогрес / статус?


10

У нас дуже велика (100 мільйонів рядків) таблиця, і нам потрібно оновити пару полів на ній.

Для доставки журналів і т. Д. Ми, очевидно, хочемо зберегти транзакції за розміром укусів.

  • Чи вдасться нижче зробити трюк?
  • І як ми можемо змусити його надрукувати певний вихід, щоб ми побачили прогрес? (ми намагалися додати оператор PRINT туди, але під час циклу нічого не виходило)

Код такий:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END

Відповіді:


12

Мені не було відомо про це запитання, коли я відповів на відповідне запитання ( чи потрібні явні транзакції в цьому циклі? ), Але заради повноти я вирішу це питання тут, оскільки воно не було частиною моєї пропозиції в тій пов'язаній відповіді .

Оскільки я пропоную запланувати це за допомогою роботи агента SQL (зрештою, це 100 мільйонів рядків), я не думаю, що будь-яка форма надсилання повідомлень про стан клієнту (тобто SSMS) буде ідеальною (хоча якщо це колись виникає потреба в інших проектах, тоді я погоджуюся з Володимиром, що використання RAISERROR('', 10, 1) WITH NOWAIT;- це шлях).

У цьому конкретному випадку я створив би таблицю статусу, яку можна оновити для кожного циклу, кількість оновлених до цього рядків. І не завадить кинутись у поточний час, щоб серце билося на цьому процесі.

Зважаючи на те, що ви хочете мати можливість скасувати та перезапустити процес, Мені стомлено обгортати ОНОВЛЕННЯ головної таблиці ОНОВЛЕННЯМ таблиці статусу в явній транзакції. Однак якщо ви відчуваєте, що таблиця стану не синхронізована через скасування, легко оновити поточне значення, просто оновивши його вручну за допомогою COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL.і ОНОВЛЮЄТЬСЯ дві таблиці (тобто головна таблиця та таблиця статусу), ми повинні використовувати явну транзакцію для синхронізації цих двох таблиць, але ми не хочемо ризикувати осиротілою транзакцією, якщо ви скасуєте процес у пункт після того, як вона розпочала транзакцію, але не здійснила її. Це слід безпечно робити, доки ви не зупините роботу агента SQL.

Як можна зупинити процес, не зупинившись? Попросивши зупинити :-). Так. Відправляючи процесу "сигнал" (подібний до kill -3Unix), ви можете вимагати, щоб він зупинився в наступний зручний момент (тобто, коли немає активної транзакції!) І дозволити йому очистити всі приємні та охайні.

Як можна спілкуватися з запущеним процесом в іншому сеансі? Використовуючи той самий механізм, який ми створили для нього, щоб повідомити вам про його поточний стан: таблицю стану. Нам просто потрібно додати стовпчик, який процес перевірятиме на початку кожного циклу, щоб він знав, чи слід продовжувати чи скасовувати. А оскільки наміром є запланувати це як завдання SQL Agent (запускати кожні 10 або 20 хвилин), ми також повинні перевірити на самому початку, оскільки немає сенсу заповнювати таблицю темпів на 1 мільйон рядків, якщо процес просто йде щоб піти на мить пізніше і не використовувати жоден із цих даних.

DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

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

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 

Потім ви можете перевірити стан у будь-який час за допомогою наступного запиту:

SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;

Хочете призупинити процес, незалежно від того, працює він у роботі SQL Agent або навіть у SSMS на чужому комп'ютері? Просто запустіть:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;

Хочете, щоб процес міг запускатись знову? Просто запустіть:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;

ОНОВЛЕННЯ:

Ось кілька додаткових речей, які можна спробувати, які можуть покращити ефективність цієї операції. Жоден не гарантовано допоможе, але, ймовірно, варто перевірити. І на 100 мільйонів рядків для оновлення у вас є достатньо часу / можливості перевірити деякі варіанти ;-).

  1. Додайте TOP (@UpdateRows)до запиту UPDATE так, щоб верхній рядок виглядав так:
    UPDATE TOP (@UpdateRows) ht
    Іноді це допомагає оптимізатору дізнатися, на скільки буде задіяно максимум рядків, щоб він не витрачав час на пошуки більше.
  2. Додайте ПЕРШИЙ КЛЮЧ до #CurrentSetтимчасової таблиці. Ідея тут полягає в тому, щоб допомогти оптимізатору приєднатися до таблиці 100 мільйонів рядків.

    І щоб це було зазначено так, щоб це не було неоднозначним, не повинно бути жодних причин додавати ПК до #FullSetтимчасової таблиці, оскільки це просто проста таблиця черг, де порядок не має значення.

  3. У деяких випадках це допомагає додати відфільтрований індекс, щоб допомогти тим, SELECTхто подається в #FullSetтаблицю темпів. Ось кілька міркувань, пов’язаних із додаванням такого індексу:
    1. Умови WHERE повинні відповідати умові WHERE вашого запиту, отже WHERE deleted is null or deletedDate is null
    2. На початку процесу більшість рядків відповідатиме вашому умові WHERE, тому індекс не так корисний. Можливо, ви захочете зачекати, поки десь біля позначки 50% перед додаванням цього. Звичайно, наскільки це допомагає і коли найкраще додати індекс, змінюється через декілька факторів, тому це трохи спроб та помилок.
    3. Можливо, вам доведеться вручну ОНОВЛЮВАТИ ДЕРЖАВИ та / або ВІДНОВИТИ індекс, щоб оновити його в актуальному стані, оскільки базові дані змінюються досить часто
    4. Не забудьте пам’ятати, що індекс, допомагаючи SELECT, завдає шкоди, UPDATEоскільки це ще один об’єкт, який необхідно оновити під час цієї операції, отже, більше вводу / виводу. Це грає як з використанням відфільтрованого індексу (який скорочується, коли ви оновлюєте рядки, оскільки менше рядків відповідає фільтру), так і трохи чекаєте, щоб додати індекс (якщо він не буде дуже корисним на початку, то немає причин для виникнення сумнівів додатковий введення / виведення).

1
Це чудово. Я запускаю його зараз, і курить, що ми можемо виконувати його в режимі дня, протягом дня. Дякую!
Jonesome Reinstate Monica

@samsmith Перегляньте розділ «ОНОВЛЕННЯ», який я щойно додав, оскільки існують деякі ідеї, які потенційно роблять процес ще швидшим.
Соломон Руцький

Без вдосконалень UPDATE ми отримуємо близько 8 мільйонів оновлень / годину ... з @BatchRows встановлено 10000000 (десять мільйонів)
Jonesome Reinstate Monica

@samsmith Це чудово :) правда? Майте на увазі дві речі: 1) Процес буде сповільнюватися все менше і менше рядків , відповідні Інеко, отже , чому це було б час добре додати відфільтрований індекс, але ви вже додали нефільтрованого індекс на Почніть, тому я не впевнений, чи допоможе це чи зашкодить, але все ж я б очікував, що пропускна здатність зменшиться в міру наближення до виконання, і 2) ви можете збільшити пропускну здатність, зменшивши WAITFOR DELAYдо півсекунди або близько того, але це компроміс з одночасністю і, можливо, скільки надсилається через доставку журналів.
Соломон Руцький

Ми задоволені 8 мільйонами рядків / годину. Так, ми можемо бачити, як він сповільнюється. Ми вагаємося створити ще якісь індекси (оскільки таблиця заблокована для всієї збірки). Те, що ми зробили пару разів, - це зробити реорганізацію існуючого індексу (тому що це прямо).
Jonesome Reinstate Monica

4

Відповідь на другу частину: як надрукувати деякий вихід під час циклу.

У мене є кілька тривалих процедур обслуговування, які інколи доводиться запускати системному адміністратору.

Я запускаю їх із SSMS і також помічаю, що PRINTоператор відображається в SSMS лише після закінчення всієї процедури.

Отже, я використовую RAISERRORз низькою суворістю:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

Я використовую SQL Server 2008 Standard та SSMS 2012 (11.0.3128.0). Ось повний робочий приклад для запуску в SSMS:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Коли я коментую RAISERRORі залишаю лише PRINTповідомлення на вкладці Повідомлення в SSMS, з’являються лише після закінчення всієї партії, через 6 секунд.

Коли я коментую PRINTта використовую RAISERRORповідомлення на вкладці Повідомлення в SSMS, вони з’являються не чекаючи 6 секунд, але по мірі просування циклу.

Цікаво, що коли я використовую і те, RAISERRORі інше PRINT, я бачу обидва повідомлення. Спочатку надходить повідомлення спочатку RAISERROR, потім затримується на 2 секунди, потім перше PRINTі друге RAISERRORтощо.


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

Поки довгий процес періодично запускається SELECTз logтаблиці, щоб побачити, що відбувається.

Це, очевидно, має певні накладні витрати, але це залишає журнал (або історію журналів), яку я можу пізніше вивчити у власному темпі.


У SQL 2008/2014 ми не бачимо результатів підняття .... чого нам не вистачає?
Jonesome Reinstate Monica

@samsmith, я додав повний приклад. Спробуй це. Яку поведінку ви отримуєте на цьому простому прикладі?
Володимир Баранов

2

Ви можете відстежувати це з іншого з'єднання з чимось на зразок:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

щоб побачити, скільки залишилося зробити. Це може бути корисно, якщо програма викликає процес, а не запускає його вручну в SSMS або подібному, і йому потрібно продемонструвати прогрес: запустити основний процес асинхронно (або на іншій потоці), а потім циклічно викликуючи "скільки залишилось" "перевіряти кожен раз, поки виклик асинхронізації (або потік) не завершиться.

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

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