Массивные вставки, блокирующие SELECT


14

У меня проблема с огромным количеством INSERT, которые блокируют мои операции SELECT.

схема

У меня есть такая таблица:

CREATE TABLE [InverterData](
    [InverterID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,    
    [ValueA] [decimal](18, 2) NULL,
    [ValueB] [decimal](18, 2) NULL
    CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED 
    (
        [TimeStamp] DESC,
        [InverterID] ASC
    ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
    , IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON)
)

У меня также есть эта маленькая вспомогательная процедура, которая позволяет мне вставлять или обновлять (обновлять при конфликте) с помощью команды MERGE:

CREATE PROCEDURE [InsertOrUpdateInverterData]
    @InverterID bigint, @TimeStamp datetime
    , @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
    MERGE [InverterData] AS TARGET
        USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
        AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
        ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
    WHEN MATCHED THEN
        UPDATE
        SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]              
    WHEN NOT MATCHED THEN
        INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB]) 
        VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END

использование

Теперь я запускаю экземпляры служб на нескольких серверах, которые выполняют масштабные обновления, [InsertOrUpdateInverterData]быстро вызывая процедуру.

Существует также веб-сайт, который выполняет запросы SELECT на [InverterData]столе.

проблема

Если я выполняю запросы SELECT к [InverterData]таблице, они выполняются в разные промежутки времени, в зависимости от использования INSERT моих экземпляров службы. Если я приостанавливаю все экземпляры службы, SELECT работает молниеносно, если экземпляр выполняет быструю вставку, SELECT становятся очень медленными или даже отменяют тайм-аут.

попытки

Я сделал несколько SELECT на [sys.dm_tran_locks]столе, чтобы найти процессы блокировки, как это

SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode

FROM sys.dm_tran_locks AS tl

INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

Это результат:

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

S = Shared. Для проведения сеанса предоставляется общий доступ к ресурсу.

Вопрос

Почему SELECT блокируется [InsertOrUpdateInverterData]процедурой, которая использует только команды MERGE?

Нужно ли использовать какую-то транзакцию с определенным режимом изоляции внутри [InsertOrUpdateInverterData]?

Обновление 1 (связано с вопросом от @Paul)

База данных внутреннего сервера MS-SQL о [InsertOrUpdateInverterData]следующей статистике:

  • Среднее время процессора: 0,12 мс
  • Среднее число процессов чтения: 5,76 чел / с
  • Среднее число процессов записи: 0,4 за / с

Исходя из этого, похоже, что команда MERGE в основном занята операциями чтения, которые блокируют таблицу! (?)

Обновление 2 (связано с вопросом от @Paul)

[InverterData]Таблица , как имеет следующие хранения статистики:

  • Объем данных: 26 901,86 МБ
  • Количество строк: 131 827 749
  • Разделен: правда
  • Количество разделов: 62

Вот (почти все) полный набор результатов sp_WhoIsActive :

SELECT команда

  • дд чч: мм: сс.мсс: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629мс) LCK_M_S
  • Процессор: 198
  • blocking_session_id: 146
  • читает: 99,368
  • пишет: 0
  • статус: приостановлено
  • open_tran_count: 0

[InsertOrUpdateInverterData]Команда блокировки

  • дд чч: мм: сс.мсс: 00 00: 00: 00.330
  • идентификатор_сессии: 146
  • wait_info: NULL
  • Процессор: 3972
  • blocking_session_id: NULL
  • читает: 376,95
  • пишет: 126
  • статус: спит
  • open_tran_count: 1

В ([TimeStamp] DESC, [InverterID] ASC)выглядит как странный выбор для кластерного индекса. Я имею в виду DESCчасть.
ypercubeᵀᴹ

Я понимаю вашу точку зрения: кластеризованный индекс DESC, вставляющий данные, заставил бы перестраивать таблицу, скорее добавляя в конец ... performance dog; заблокирует таблицу, пока происходит восстановление ... да. Jove, у вас есть это. Структура является причиной для блокировки больше, чем замки.
Алоцит

Ответы:


12

Во-первых, хотя ваше MERGEутверждение и не имеет никакого отношения к основному вопросу, оно потенциально может быть подвержено ошибкам из-за состояния гонки . Проблема, в двух словах, заключается в том, что несколько параллельных потоков могут сделать вывод, что целевой строки не существует, что приводит к конфликтным попыткам вставки. Основная причина заключается в том, что невозможно установить общую или обновленную блокировку для несуществующей строки. Решение состоит в том, чтобы добавить подсказку:

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

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

Главный вопрос

Почему SELECTsблокируется процедурой [InsertOrUpdateInverterData], которая использует только MERGEкоманды?

При установленном по умолчанию уровне изоляции фиксации блокировки чтения общие (S) блокировки снимаются при чтении данных и обычно (хотя и не всегда) снимаются вскоре после завершения чтения. Некоторые общие блокировки удерживаются до конца оператора.

А MERGEзаявление модифицирует данные, поэтому он получит S или обновление (U) блокируется при размещении данных на изменения, которые преобразуются в исключительном (X) блокировка непосредственно перед выполнением фактического изменения. Блокировки U и X должны удерживаться до конца транзакции.

Это справедливо для всех уровней изоляции, кроме «оптимистической» изоляции моментальных снимков (SI), которая не должна быть перепутана с версионным подтверждением чтения с фиксацией , также известным как изоляция моментального снимка с фиксацией чтения (RCSI).

Ничто в вашем вопросе не показывает сеанс, ожидающий блокировки S, когда сеанс удерживает блокировку U. Эти замки совместимы . Любая блокировка почти наверняка вызвана блокировкой удерживаемой блокировки X. Это может быть немного сложно для захвата, когда большое количество краткосрочных блокировок взято, преобразовано и выпущено за короткий промежуток времени.

Команда open_tran_count: 1InsertOrUpdateInverterData заслуживает изучения. Хотя команда не выполнялась слишком долго, вы должны убедиться, что у вас нет транзакции (в приложении или хранимой процедуре более высокого уровня), которая неоправданно длинна. Лучшая практика - делать транзакции максимально короткими. Это может быть ничего, но вы должны обязательно проверить.

Потенциальное решение

Как предложил Кин в комментарии, вы можете включить уровень изоляции строк (RCSI или SI) в этой базе данных. RCSI является наиболее часто используемым, так как обычно он не требует столько изменений приложения. После включения уровень изоляции по умолчанию для фиксации чтения использует версии строк, а не блокировку S для чтения, поэтому блокировка SX уменьшается или устраняется. Некоторые операции (например, проверка внешнего ключа) все еще получают блокировки S под RCSI.

Имейте в виду, что версии строк занимают пространство в базе данных tempdb, в целом пропорционально скорости изменения активности и продолжительности транзакций. Вам нужно будет тщательно протестировать свою реализацию под нагрузкой, чтобы понять и спланировать влияние RCSI (или SI) в вашем случае.

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

Примечание. В отличие от RCSI (который после включения применяется ко всем транзакциям, выполняемым при подтверждении чтения), SI должен запрашиваться явно с помощью SET TRANSACTION ISOLATION SNAPSHOT;.

Тонкое поведение, которое зависит от читателей, блокирующих писателей (в том числе в коде триггера!), Делает тестирование необходимым. См. Мою серию связанных статей и Books Online для деталей. Если вы решите выбрать RCSI, обязательно ознакомьтесь с изменениями данных, в частности, в разделе «Обязательная изоляция моментального снимка» .

Наконец, вы должны убедиться, что ваш экземпляр исправлен в SQL Server 2008 с пакетом обновления 4.


0

Смиренно, я бы не использовал слияние. Я бы пошел с IF Exists (UPDATE) ELSE (INSERT) - у вас есть кластерный ключ с двумя столбцами, которые вы используете для идентификации строк, так что это простой тест.

Вы упомянули МАССИВНЫЕ вставки и все же делали 1 на 1 ... задумывались о пакетировании данных в промежуточной таблице и использовании мощности набора данных SQL POWER OVERWHELMING для выполнения более 1 обновления / вставки за раз? Например, проведите рутинное тестирование контента в промежуточной таблице и получите 10000 за раз вместо 1 за раз ...

Я бы сделал что-то подобное в моем обновлении

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

Вы могли бы, вероятно, запустить несколько заданий, выпуская пакеты обновлений, и вам понадобится отдельное задание, выполняющее ручное удаление

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

очистить промежуточный стол.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.