В Sql Server есть способ проверить, заблокирована ли выбранная группа строк или нет?


21

Мы пытаемся обновить / удалить большое количество записей в многомиллиардной таблице строк. Поскольку это популярная таблица, в разных разделах этой таблицы много активности. Любая крупная операция по обновлению / удалению блокируется на длительные периоды времени (так как она ожидает блокировки всех строк, или блокировки страницы, или блокировки таблицы), что приводит к тайм-аутам или занимает несколько дней для выполнения задачи.

Таким образом, мы меняем подход к удалению небольших партий строк одновременно. Но мы хотим проверить, заблокированы ли выбранные (скажем, 100, 1000 или 2000 строк) другим процессом или нет.

  • Если нет, тогда приступайте к удалению / обновлению.
  • Если они заблокированы, перейдите к следующей группе записей.
  • В конце вернитесь к началу и попробуйте обновить / удалить пропущенные.

Это выполнимо?

Спасибо, ToC


2
Вы рассматривали READPAST как часть оператора delete или 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 Интересный подход. Я тоже попробую.
ToC

2
Чтобы ответить, нет, нет более простого способа увидеть, заблокированы ли строки, если в приложении не сделано что-то определенное. По сути, вы могли бы сначала сделать выбор с помощью HOLDLOCK и XLOCK с установленным значением lock_timeout (именно об этом NOWAIT в моем исходном комментарии, устанавливая тайм-аут на 0). Если вы этого не получите, значит, что-то заблокировано. Нет ничего легкодоступного, чтобы сказать «Является ли строка X в таблице Y, использующая индекс Z, чем-то заблокированной». Мы можем видеть, есть ли у таблицы блокировки или какие-либо блокировки у страниц / строк / ключей / и т. Д., Но преобразовать это в определенные строки в запросе будет нелегко.
Шон говорит Удалить Сару Чиппс

Ответы:


10

Если я правильно понимаю запрос, цель состоит в том, чтобы удалить пакеты строк, в то же время операции DML выполняются со строками по всей таблице. Цель состоит в том, чтобы удалить партию; однако, если какие-либо нижележащие строки, содержащиеся в диапазоне, определенном упомянутым пакетом, заблокированы, то мы должны пропустить этот пакет и перейти к следующему пакету. Затем мы должны вернуться к любым пакетам, которые ранее не были удалены, и повторить нашу первоначальную логику удаления. Мы должны повторять этот цикл до тех пор, пока все требуемые пакеты строк не будут удалены.

Как уже упоминалось, разумно использовать подсказку READPAST и уровень изоляции READ COMMITTED (по умолчанию), чтобы пропустить прошлые диапазоны, которые могут содержать заблокированные строки. Я сделаю еще один шаг и порекомендую использовать уровень изоляции SERIALIZABLE и удаления нибблингов.

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

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

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

Несколько служебных заметок:

  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

На этом этапе нам понадобится один или несколько индексов, по которым могут действовать механизмы блокировки уровня изоляции SERIALIZABLE.


-- 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;

Давайте начнем обе операции, начиная с вставки и заканчивая удалением. Мы видим замки с ключом и эксклюзивные замки.

Диапазон и эксклюзивные замки

Вставка сгенерировала эти блокировки:

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

Ликвидировать удалить / выбрать удерживает эти блокировки:

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

Наша вставка блокирует удаление, как и ожидалось:

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

Теперь давайте передадим транзакцию вставки и посмотрим, что происходит.

Зафиксировать удаление

И, как и ожидалось, все транзакции завершены. Теперь мы должны проверить, была ли вставка фантомной, или операция удаления также удаляла ее.


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- см кб / 308886 , SQL Server для чтения Консистенция Проблемы Ицик Бен-Ган , Положив NOLOCK везде - Аарон Бертрана и SQL Server NOLOCK HiNT и другие плохие идеи .

READPASTподсказка поможет в вашем сценарии. Суть READPASTподсказки такова: если есть блокировка на уровне строк, SQL-сервер не будет ее читать.

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

Во время моего ограниченного тестирования, я нашел очень хорошие пропускную способность при использовании DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)и настройки уровня изоляции сеанса запроса на READ COMMITTEDиспользование SET TRANSACTION ISOLATION LEVEL READ COMMITTEDкоторой является уровень изоляции по умолчанию в любом случае.


2

Обобщая другие подходы, первоначально предложенные в комментариях к вопросу.


  1. Используйте, NOWAITесли желаемым поведением является сбой всего чанка, как только обнаружится несовместимая блокировка.

    Из NOWAITдокументации :

    Указывает компоненту Database Engine возвращать сообщение, как только на столе обнаружена блокировка. 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

подключение / сеанс 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.