Чи потрібні явні транзакції в цьому циклі?


11

SQL Server 2014:

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

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

Якщо ми дозволимо нижче виконати деякий час, а потім скасувати / припинити запит, чи буде виконана робота, яка виконується поки що, чи нам потрібно додати явні BEGIN TRANSACTION / END TRANSACTION заяви, щоб ми могли скасувати будь-який час?

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

Відповіді:


13

Окремі заяви - DML, DDL тощо - самі по собі є транзакціями. Так, так, після кожної ітерації циклу (технічно: після кожного оператора), будь-яке UPDATEзміна цього твердження було зроблено автоматично.

Звичайно, завжди є виняток, правда? Можна ввімкнути непрямі транзакції за допомогою SET IMPLICIT_TRANSACTIONS , і в цьому випадку перший UPDATEвислів розпочне транзакцію, яку вам доведеться COMMITабо ROLLBACKв кінці. Це налаштування рівня сеансу, яке за замовчуванням вимкнено у більшості випадків.

чи потрібно нам додавати явні BEGIN TRANSACTION / END TRANSACTION заяви, щоб ми могли будь-коли скасувати?

Ні. І насправді, враховуючи, що ви хочете мати змогу зупинити процес і перезапустити, додавання явної транзакції (або ввімкнення неявних транзакцій) було б поганою ідеєю, оскільки зупинення процесу може зловити його до того, як це зробити COMMIT. У такому випадку вам потрібно буде вручну видати COMMIT(якщо ви перебуваєте в SSMS) або якщо ви запускаєте це з завдання SQL Agent, то у вас немає такої можливості і може закінчитися транзакцією-сиротою.


Також ви можете встановити @CHUNK_SIZEменшу кількість. Ескалація блокування зазвичай відбувається у 5000 замків, придбаних на одному об’єкті. Залежно від розміру рядків і якщо це робиться Lock Lock vs Page Locks, можливо, ви будете перевищувати цю межу. Якщо розмір рядка такий, що на кожну сторінку розміщується лише 1 або 2 рядки, то ви завжди можете це робити, навіть якщо це робить блокування сторінки.

Якщо таблиця розділена, то у вас є можливість встановити LOCK_ESCALATIONпараметр (введений у SQL Server 2008) для таблиці AUTOтаким чином, щоб після ескалації він блокував лише розділ, а не всю таблицю. Або для будь-якої таблиці ви можете встановити той самий варіант DISABLE, хоча вам слід бути дуже обережним. Докладні відомості див. У розділі ALTER TABLE .

Ось деяка документація, яка розповідає про ескалацію блокування та порогові значення: Ескалація блокування (вона говорить, що стосується "версій SQL Server 2008 R2 та новіших версій"). І ось повідомлення в блозі, яке стосується виявлення та виправлення ескалації блокування: Блокування в Microsoft SQL Server (Частина 12 - Ескалація блокування) .


Не пов’язане з точним запитанням, але пов’язане із запитом у запитанні, тут можна внести декілька вдосконалень (або, принаймні, це виглядає просто з огляду на нього):

  1. Для вашого циклу робити WHILE (@@ROWCOUNT = @CHUNK_SIZE)це трохи краще, оскільки якщо кількість рядків, оновлених за останню ітерацію, менша за суму, яку потрібно провести ОНОВЛЕННЯ, то роботи не залишається.

  2. Якщо deletedполе є BITтипом даних, то чи не це значення визначається тим, чи deletedDateє 2000-01-01? Навіщо тобі обоє?

  3. Якщо ці два поля є новими, і ви додали їх NULLтак, як це може бути операція в режимі он-лайн / не блокує, і тепер хочуть оновити їх до їх "за замовчуванням" значення, тоді це було не потрібно. Починаючи з SQL Server 2012 (лише для Enterprise Edition), додавання NOT NULLстовпців із обмеженням DEFAULT - це не блокуючі операції, поки значення DEFAULT є постійним. Отже, якщо ви ще не використовуєте поля, просто опустіть і додайте знову як NOT NULLі з обмеженням DEFAULT.

  4. Якщо жоден інший процес не оновлює ці поля, поки ви робите це ОНОВЛЕННЯ, тоді буде швидше, якби ви поставили в чергу записи, які хотіли оновити, а потім просто відпрацюйте цю чергу. У поточному методі є показник продуктивності, оскільки вам потрібно кожного разу перепитувати таблицю, щоб отримати набір, який потрібно змінити. Натомість ви можете зробити наступне, що сканує таблицю лише один раз у цих двох полях, а потім видає лише дуже націлені оператори UPDATE. Також не передбачено жодного штрафу зупинити процес і запустити його пізніше, оскільки початкова сукупність черги просто знайде записи, що залишилися для оновлення.

    1. Створіть тимчасову таблицю (#FullSet), яка містить лише ключові поля з кластерного індексу в ній.
    2. Створіть другу тимчасову таблицю (#CurrentSet) тієї самої структури.
    3. вставити в #FullSet через SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      TOP(n)Знаходиться там з - за розміру таблиці. Маючи 100 мільйонів рядків у таблиці, вам дійсно не потрібно заповнювати таблицю черг цілим набором клавіш, особливо якщо ви плануєте так часто зупиняти процес і перезапускати його пізніше. Тож можливо встановити n1 мільйон і нехай це закінчиться. Ви завжди можете запланувати це в роботі SQL Agent, яка виконує набір в 1 мільйон (а може бути, і менше), а потім чекає, коли наступний запланований час знову підійде. Потім ви можете запланувати запуск кожні 20 хвилин, щоб між наборами була певна кімната дихання n, але він все одно закінчить весь процес без нагляду. Тоді просто потрібно видалити себе, коли більше нічого робити :-).

    4. в циклі, зробіть:
      1. Населяйте поточну партію через щось подібне DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. Зробіть ОНОВЛЕННЯ, використовуючи щось на кшталт: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. Очистити поточний набір: TRUNCATE TABLE #CurrentSet;
  5. У деяких випадках це допомагає додати відфільтрований індекс, щоб допомогти тим, SELECTхто подається в #FullSetтаблицю темпів. Ось кілька міркувань, пов’язаних із додаванням такого індексу:
    1. Умови WHERE повинні відповідати умові WHERE вашого запиту, отже WHERE deleted is null or deletedDate is null
    2. На початку процесу більшість рядків відповідатиме вашому умові WHERE, тому індекс не так корисний. Можливо, ви захочете зачекати, поки десь біля позначки 50% перед додаванням цього. Звичайно, наскільки це допомагає і коли найкраще додати індекс, змінюється через декілька факторів, тому це трохи спроб та помилок.
    3. Можливо, вам доведеться вручну ОНОВЛЮВАТИ ДЕРЖАВИ та / або ВІДНОВИТИ індекс, щоб оновити його в актуальному стані, оскільки базові дані змінюються досить часто
    4. Не забудьте пам’ятати, що індекс, допомагаючи SELECT, завдає шкоди, UPDATEоскільки це ще один об’єкт, який необхідно оновити під час цієї операції, отже, більше вводу / виводу. Це грає як з використанням відфільтрованого індексу (який зменшується, коли ви оновлюєте рядки, оскільки менше рядків відповідає фільтру), так і трохи чекаєте, щоб додати індекс (якщо на початку це не буде корисним, то немає причин для виникнення сумнівів додатковий введення / виведення).

ОНОВЛЕННЯ: Будь ласка, дивіться мою відповідь на питання, пов’язане з цим питанням, для повного втілення запропонованого вище, включаючи механізм відстеження стану та чистого скасування: sql сервер: оновлення полів на величезній таблиці невеликими шматками: як отримати прогрес / статус?


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

@BaconBits Домовились про початок простого. Справедливості, ці пропозиції мали на меті не для всіх сценаріїв. Питання стосується розгляду дуже великої (100 мільйонів + рядків) таблиці.
Соломон Руцький
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.