Чи є на сервері Sql спосіб перевірити, заблокована чи вибрана група рядків?


21

Ми намагаємось оновити / видалити велику кількість записів у багатомільярдній таблиці рядків. Оскільки це популярна таблиця, в різних розділах цієї таблиці є велика активність. Будь-яка велика активізація оновлення / видалення блокується протягом тривалого періоду часу (оскільки вона чекає отримання блоку в усіх рядках або блокуванні сторінки або блокуванні таблиці), що призводить до затримки часу або потребує декількох днів для виконання завдання.

Отже, ми змінюємо підхід, щоб видалити невелику партію рядків за раз. Але ми хочемо перевірити, чи вибрані (скажімо, 100 чи 1000 чи 2000 рядків) наразі заблоковані іншим процесом чи ні.

  • Якщо ні, то продовжуйте видалення / оновлення.
  • Якщо вони заблоковані, перейдіть до наступної групи записів.
  • Зрештою, поверніться до початку та спробуйте оновити / видалити залишені.

Це можливо?

Спасибі, ToC


2
Ви розглядали READPAST як частину оператора видалення або NOWAIT (для відмови всієї групи)? Одне з них може працювати для вас. msdn.microsoft.com/en-us/library/ms187373.aspx
Шон каже Видалити Сару Чіпс

@SeanGallardy Я не розглядав цю ідею, але зараз буду. Але чи є простіший спосіб перевірити, заблоковано чи не певний рядок? Спасибі.
ToC

3
Ви також можете заглянути в LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ). Наприклад, таким чином sp_whoisactive Адама Маханіка гарантує, що процедура не чекатиме занадто довго, якщо її заблокують при спробі зібрати план виконання. Ви можете встановити короткий час очікування або навіть використовувати значення 0 ("0 означає взагалі не чекати і повертати повідомлення, як тільки виявиться блокування.") Ви можете комбінувати це з TRY / CATCH, щоб виявити помилку 1222 ( "Перевищено період очікування блокування запиту") та переходите до наступної партії.
Джефф Паттерсон

@gpatterson Цікавий підхід. Я теж спробую це.
ТЗ

2
Щоб відповісти, ні, не існує більш простого способу дізнатися, чи заблоковані рядки, якщо в програмі щось конкретно не зроблено. В основному ви можете спершу зробити вибір з HOLDLOCK та XLOCK з набором lock_timeout (про що йдеться у NOWAIT в моєму первісному коментарі, встановивши час очікування на 0). Якщо ви цього не отримаєте, знаєте, що щось заблоковано. Немає нічого просто сказати "Чи рядок X у таблиці Y з використанням індексу Z чимось заблокований". Ми можемо побачити, чи є таблиця із замками чи якісь сторінки / рядки / ключі / тощо мають блокування, але перевести це у конкретні рядки в запиті було б непросто.
Шон каже «Видалити Сару Чіпс»

Відповіді:


10

Якщо я правильно розумію запит, мета - видалити партії рядків, в той же час операції з DML відбуваються на рядках по всій таблиці. Мета - видалити партію; однак, якщо будь-які базові рядки, що містяться в діапазоні, визначеному зазначеною партією, заблоковані, ми повинні пропустити цю партію і перейти до наступної партії. Потім ми повинні повернутися до будь-яких партій, які раніше не були видалені, і спробувати нашу початкову логіку видалення. Ми повинні повторювати цей цикл, поки всі необхідні партії рядків не будуть видалені.

Як було зазначено, доцільно використовувати підказку READPAST та рівень ізоляції READ COMMITTED (за замовчуванням), щоб пропустити минулі діапазони, які можуть містити заблоковані рядки. Я піду на крок далі і порекомендую використовувати рівень ізоляції СЕРІАЛІЗАЦІЙНОГО та нанизування делетів.

SQL Server використовує блокування Key-Range для захисту діапазону рядків, неявно включених у набір запису, який читається оператором Transact-SQL під час використання рівня ізоляції транзакційних серійних операцій ... докладніше тут: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

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

Перш ніж продемонструвати своє рішення, я хотів би додати, що ні я не рекомендую переключити рівень ізоляції вашої бази даних за замовчуванням на СЕРІАЛЬНИЙ, а також не рекомендую, щоб моє рішення було найкращим. Я просто хотів би представити його і подивитися, куди ми можемо піти звідси.

Кілька домашніх записок:

  1. Версія SQL Server, яку я використовую, - це Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. У моїй тестовій базі даних використовується повна модель відновлення

Для початку свого експерименту я встановлю тестову базу даних, зразок таблиці, і я заповнять таблицю 2 000 000 рядків.


USE [master];
GO

SET NOCOUNT ON;

IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
    ALTER DATABASE [test] SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [test];
END
GO

-- Create the test database
CREATE DATABASE [test];
GO

-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;

-- Create a FULL database backup
-- in order to ensure we are in fact using 
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO

USE [test];
GO

-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
    DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
      c1 BIGINT IDENTITY (1,1) NOT NULL
    , c2 INT NOT NULL
) ON [PRIMARY];
GO

-- Insert 2,000,000 rows 
INSERT INTO dbo.tbl
    SELECT TOP 2000
        number
    FROM
        master..spt_values
    ORDER BY 
        number
GO 1000

На цьому етапі нам знадобляться один або кілька показників, за якими можуть діяти механізми блокування рівня ізоляції СЕРІАЛІЗАЦІЙНО.


-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
    ON dbo.tbl (c1);
GO

-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2 
    ON dbo.tbl (c2);
GO

Тепер перевіримо, чи було створено 2 000 000 рядків


SELECT
    COUNT(*)
FROM
    tbl;

введіть тут опис зображення

Отже, у нас є наша база даних, таблиця, індекси та рядки. Отже, давайте налагодимо експеримент для обкулювання делетів. По-перше, ми повинні вирішити, як найкраще створити типовий механізм видалення кусання.


DECLARE
      @BatchSize        INT    = 100
    , @LowestValue      BIGINT = 20000
    , @HighestValue     BIGINT = 20010
    , @DeletedRowsCount BIGINT = 0
    , @RowCount         BIGINT = 1;

SET NOCOUNT ON;
GO

WHILE  @DeletedRowsCount <  ( @HighestValue - @LowestValue ) 
BEGIN

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION

        DELETE 
        FROM
            dbo.tbl 
        WHERE
            c1 IN ( 
                    SELECT TOP (@BatchSize)
                        c1
                    FROM
                        dbo.tbl 
                    WHERE 
                        c1 BETWEEN @LowestValue AND @HighestValue
                    ORDER BY 
                        c1
                  );

        SET @RowCount = ROWCOUNT_BIG();

    COMMIT TRANSACTION;

    SET @DeletedRowsCount += @RowCount;
    WAITFOR DELAY '000:00:00.025';
    CHECKPOINT;

END;

Як бачите, явну транзакцію я розмістив всередині циклу while. Якщо ви хочете обмежити залишки журналу, тоді сміливо розміщуйте його поза циклом. Крім того, оскільки ми перебуваємо у моделі ПОВНОГО відновлення, ви, можливо, бажаєте частіше створювати резервні копії журналу транзакцій під час виконання операцій видалення, щоб гарантувати, що ваш журнал транзакцій може не допустити шаленого зростання.

Отже, у мене є пара цілей з цією установкою. По-перше, я хочу, щоб були мої блокування ключа; так, я намагаюся зберегти партії якомога менше. Я також не хочу негативно впливати на паралельність мого "гігантського" столу; тому я хочу взяти свої замки і залишити їх якнайшвидше. Тож рекомендую зробити розмір партії невеликим.

Тепер я хочу навести дуже короткий приклад цього режиму видалення в дії. Ми повинні відкрити нове вікно в SSMS та видалити один рядок із нашої таблиці. Я зроблю це в рамках неявної транзакції, використовуючи стандартний рівень ізоляції READ COMMITTED.


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

Чи справді цей рядок видалено?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;

Так, це було видалено.

Доказ видаленого рядка

Тепер, щоб побачити наші замки, давайте відкриємо нове вікно в SSMS та додамо фрагмент коду чи два. Я використовую sp_whoisactive Адама Механіка, який можна знайти тут: sp_whoisactive


SELECT
    DB_NAME(resource_database_id) AS DatabaseName
  , resource_type
  , request_mode
FROM
    sys.dm_tran_locks
WHERE
    DB_NAME(resource_database_id) = 'test'
    AND resource_type = 'KEY'
ORDER BY
    request_mode;

-- Our insert
sp_lock 55;

-- Our deletions
sp_lock 52;

-- Our active sessions
sp_whoisactive;

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

Код вставки:


BEGIN TRANSACTION

    SET IDENTITY_INSERT dbo.tbl ON;

    INSERT  INTO dbo.tbl
            ( c1 , c2 )
    VALUES
            ( 20005 , 1 );

    SET IDENTITY_INSERT dbo.tbl OFF;

--COMMIT TRANSACTION;

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

Замки діапазону та eXclusive

Вкладиш генерував ці блокування:

Вставте замки

Уключення видалення / вибору утримує ці блокування:

введіть тут опис зображення

Наша вставка блокує наше видалення, як очікувалося:

Вставити блоки Видалити

Тепер давайте здійснимо операцію з вставкою і подивимося, що відбувається.

Введіть Видалити

І як очікувалося, всі транзакції завершені. Тепер ми повинні перевірити, чи вставка була фантомом чи операція видалення також її видалила.


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

Фактично вставку було видалено; тому жодна фантомна вставка не була дозволена.

Без вставки фантома

Отже, на закінчення, я думаю, що справжній намір цієї вправи полягає в тому, щоб не намагатися відслідковувати кожен окремий рядок, сторінку або блокування на рівні таблиці та намагатися визначити, чи заблокований елемент партії, і тому вимагатиме операція видалення для чекати. Можливо, це був намір запитуючих; однак це завдання є геркулесовим і в основному непрактичним, якщо не неможливим. Справжня мета полягає в тому, щоб не виникнути небажаних явищ, як тільки ми виділили діапазон нашої партії із власними замками, а потім передуємо видаленню партії. Рівень ізоляції СЕРІАЛІЗАЦІЙНО досягає цієї мети. Ключ полягає в тому, щоб тримати свої дрібниці, журнал транзакцій під контролем та усунути небажані явища.

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

Будь ласка, дайте мені знати, що ви думаєте.

Я створив декілька подальших прикладів рівня ізоляції СЕРІАЛІЗАЦІЙНОЇ дії. Вони повинні бути доступні за посиланнями нижче.

Операція видалення

Вставка операції

Операції рівності - Блокування діапазонів ключових значень на наступних ключових значеннях

Операції з рівності - Односильний вибір існуючих даних

Операції з рівності - Односильний вибір неіснуючих даних

Операції нерівності - блокування ключових діапазонів у діапазоні та наступних ключових значеннях


9

Отже, ми змінюємо підхід, щоб видалити невелику партію рядків за раз.

Це дійсно гарна ідея видалити невеликими обережними партіями або шматками . Я додав би невеликий waitfor delay '00:00:05'і залежно від моделі відновлення бази даних - якщо FULL, тоді зробіть a log backupі якщо SIMPLEтоді зробіть a, manual CHECKPOINTщоб уникнути здуття журналу транзакцій - між партіями.

Але ми хочемо перевірити, чи вибрані (скажімо, 100 чи 1000 чи 2000 рядків) наразі заблоковані іншим процесом чи ні.

Те, що ви говорите, не зовсім можливе (не маючи на увазі свої 3 кулі). Якщо вищенаведена пропозиція - small batches + waitfor delayне працює (за умови правильного тестування), ви можете скористатися query HINT.

Не використовуйте NOLOCK- див. Kb / 308886 , Проблеми з читанням послідовності SQL Server від Ітзіка Бен-Гана , скрізь виставляючи NOLOCK - Аарон Бертран та підказки SOL Server NOLOCK та інші погані ідеї .

READPASTпідказка допоможе у вашому сценарії. Суть READPASTпідказки - якщо є блокування рівня рядків, то SQL-сервер не зможе його прочитати.

Вказує, що двигун бази даних не читає рядки, які заблоковані іншими транзакціями. Коли READPASTзазначено, блокування на рівні рядків пропускаються. Тобто, Database Engine пропускає повз рядки, а не блокує поточну транзакцію до тих пір, поки блоки не будуть звільнені.

Під час мого обмеженого тестування я виявив дійсно гарну пропускну спроможність при використанні DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)та встановленні рівня ізоляції сеансу запиту до READ COMMITTEDвикористання, у SET TRANSACTION ISOLATION LEVEL READ COMMITTEDбудь-якому випадку, рівень ізоляції за замовчуванням.


2

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


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

    З NOWAITдокументації :

    Доручає двигуну баз даних повертати повідомлення, як тільки на столі трапляється блокування. NOWAITеквівалентна вказівці SET LOCK_TIMEOUT 0для конкретної таблиці. NOWAITПідказка не працює , коли TABLOCKпідказка також включена. Щоб припинити запит, не чекаючи при використанні TABLOCKпідказки, попередньо запросіть запит SETLOCK_TIMEOUT 0;замість нього.

  2. Використовуйте SET LOCK_TIMEOUTдля досягнення аналогічного результату, але з настроюваним тайм-аутом:

    З SET LOCK_TIMEOUTдокументації

    Визначає кількість мілісекунд, на яку оператор очікує звільнення блокування.

    Коли очікування блокування перевищує значення тайм-ауту, повертається помилка. Значення 0 означає взагалі не чекати і повертати повідомлення, як тільки виявляється блокування.


0

Припустимо, у нас є 2 запити на паралелі:

підключення / сеанс 1: заблокує рядок = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

connect / session 2: ігнорує заблокований рядок = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

АБО підключення / сеанс 2: викине виняток

DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
  THROW 51000, 'Hi, a record is locked or does not exist.', 1;

-1

Спробуйте фільтрувати щось подібне - це може ускладнитися, якщо ви хочете отримати дійсно, дійсно конкретні. Подивіться в BOL для опису sys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

просто цікаво - навіщо голова?
rottengeek

-11

Ви можете використовувати NoLOCK під час видалення, і якщо рядки заблоковані, вони не будуть видалені. Це не ідеально, але може зробити трюк для вас.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

7
Якщо я намагаюся що на моїй локальній машині я отримую Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., застарілий з 2005
Том V - Team Моніці
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.