Масивні ВСТАВКИ, що блокують SELECT


14

У мене проблема з величезною кількістю ВСТУП, які блокують мої операції 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 блискавично працює, якщо екземпляр виконує швидке вставлення, SELECTs стає дійсно повільним або навіть скасовується.

Спроби

Я зробив декілька SELECTs на [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 = Спільний. Засідання сесії надає спільний доступ до ресурсу.

Питання

Чому 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 командування

  • dd hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • ЦП: 198
  • blocking_session_id: 146
  • читає: 99,368
  • пише: 0
  • статус: призупинено
  • open_tran_count: 0

[InsertOrUpdateInverterData]Команда блокування

  • dd hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • ЦП: 3972
  • blocking_session_id: NULL
  • читає: 376,95
  • пише: 126
  • статус: сон
  • open_tran_count: 1

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

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

Відповіді:


12

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

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

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

Основне питання

Чому SELECTsблокується процедура [InsertOrUpdateInverterData], яка використовує лише MERGEкоманди?

Під блокуванням за замовчуванням рівень ізоляції, здійснений зчитуванням, блокування спільних (S) приймаються під час читання даних і, як правило, (хоча і не завжди) звільняються незабаром після завершення зчитування. Деякі спільні блокування зберігаються до кінця заяви.

А MERGEзаяву модифікує дані, тому він отримає S або оновлення (U) блокується при розміщенні даних на зміни, які перетворюються у винятковому (X) блокування безпосередньо перед виконанням фактичної зміни. І U, і X замок повинні бути утримувані до кінця транзакції.

Це справедливо для всіх рівнів ізоляції, за винятком "оптимістичної" ізоляції знімків (СІ), яку не слід плутати з читанням версій, також відомим як ізоляція знімків, зроблених зчитуванням (RCSI).

Ніщо у вашому запитанні не показує сеанс очікування блокування блокування S сеансом, що містить блокування U Ці замки сумісні . Будь-яке блокування майже напевно спричинено блокуванням утримуваного блокування X. Це може бути дещо складно зафіксувати, коли велика кількість короткочасних блокувань робиться, перетворюється та звільняється за короткий проміжок часу.

open_tran_count: 1За командою InsertOrUpdateInverterData варто досліджувати. Незважаючи на те, що команда виконується не дуже довго, слід переконатися, що у вас немає зайвої транзакції (у програмі або збереженій процедурі вищого рівня), яка зайва. Найкраща практика - тримати операції якомога коротше. Це може бути нічого, але ви обов'язково повинні перевірити.

Потенційне рішення

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

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

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

Примітка. На відміну від RCSI (який колись увімкнено, застосовується до всіх транзакцій, що виконуються при здійсненні читання), SI потрібно чітко запитувати, використовуючи SET TRANSACTION ISOLATION SNAPSHOT;.

Тонка поведінка, яка залежить від читачів, що блокують авторів (в тому числі і в тригерному коді!), Робить тестування важливим. Докладні відомості див. У моїх пов'язаних серіях статей та "Книги онлайн". Якщо ви все-таки вирішите вирішити RCSI, перегляньте Модифікації даних, зокрема, в розділі "Прочитати виконаний знімок знімка" .

Нарешті, ви повинні переконатися, що ваш примірник є виправленим до пакета оновлень 4 для SQL Server 2008.


0

Покірно я б не використовував злиття. Я б поїхав із IF Exists (UPDATE) ELSE (INSERT) - у вас є кластерний ключ з двома стовпцями, які ви використовуєте для ідентифікації рядків, тому це простий тест.

Ви згадуєте МАСИВНІ вставки, але все-таки робите 1 на 1 ... думали про групування даних у таблиці інсценізації та використанні потужності набору даних 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.