Заява злиття заходить у глухий кут


22

У мене є така процедура (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey формують складовий ключ для цільової таблиці. CompanyId - це зовнішній ключ батьківської таблиці. Крім того, існує некластеризований індекс на CompanyId asc, UserId asc.

Він викликається з багатьох різних потоків, і я послідовно отримую тупики між різними процесами, називаючи це одне і те ж твердження. Моє розуміння полягало в тому, що "з (затримкою)" необхідно для запобігання вставки / оновлення помилок стану гонки.

Я припускаю, що два різні потоки фіксують рядки (або сторінки) в різних порядках, коли вони перевіряють обмеження, і, таким чином, є тупиком.

Це правильне припущення?

Який найкращий спосіб вирішити цю ситуацію (тобто відсутність тупиків, мінімальний вплив на багатопотокові характеристики)?

Зображення плану запиту (Якщо ви переглядаєте зображення на новій вкладці, воно читається. Вибачте за невеликий розмір.)

  • У @datatable є щонайбільше 28 рядків.
  • Я простежив код, і я ніде не бачу, як ми тут починаємо транзакцію.
  • Іноземний ключ встановлюється для каскаду лише при видаленні, і не було жодних видалень з батьківської таблиці.

Відповіді:


12

Гаразд, переглянувши все кілька разів, я думаю, що ваше основне припущення було правильним. Тут, мабуть, відбувається таке:

  1. Частина MATCH у MERGE перевіряє індекс на відповідність, читаючи-фіксуючи ці рядки / сторінки в міру його проходження.

  2. Коли у нього є рядок без збігу, він спробує спочатку вставити новий рядок Index, щоб він запитав блокування запису рядка / сторінки ...

Але якщо інший користувач також перейшов до кроку 1 у тому ж рядку / сторінці, то перший користувач буде заблокований з Оновлення, і ...

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

AFAIK, існує лише один (простий) спосіб бути 100% впевненим, що ви не зможете отримати тупик із цією процедурою, і це було б додати підказку TABLOCKX до MERGE, але це, ймовірно, має дуже поганий вплив на продуктивність.

Цілком можливо, що замість цього замість підказки TABLOCK буде достатньо для вирішення проблеми, не впливаючи на ефективність.

Нарешті, ви також можете спробувати додати PAGLOCK, XLOCK або PAGLOCK та XLOCK. Знову ж таки, що може працювати і продуктивність може бути не надто жахливою. Вам доведеться спробувати це, щоб побачити.


Думаєте, рівень ізоляції знімків (версія версії) може бути тут корисним?
Мікаел Ерікссон

Можливо. Або це може перетворити винятки з тупикових ситуацій у винятки одночасності.
RBarryYoung

2
Визначення підказки TABLOCK для таблиці, яка є цільовим висловом INSERT, має той же ефект, що й указання підказки TABLOCKX. (Джерело: msdn.microsoft.com/en-us/library/bb510625.aspx )
втп

31

Проблеми не виникло б, якби змінна таблиці містила лише одне значення. З декількома рядками з'являється нова можливість для тупикової ситуації. Припустимо, два одночасні процеси (A & B) виконуються із табличними змінними, що містять (1, 2) та (2, 1) для однієї компанії.

Процес A зчитує призначення, не знаходить рядка та вставляє значення '1'. Він містить ексклюзивне блокування рядків зі значенням "1". Процес B зчитує призначення, не знаходить рядка та вставляє значення '2'. Він містить ексклюзивне блокування рядків зі значенням "2".

Тепер процес A повинен обробити рядок 2, а процес B повинен обробити рядок 1. Жоден процес не може досягти прогресу, оскільки він вимагає блокування, несумісного з ексклюзивним блокуванням, що утримується іншим процесом.

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

Існуючий план

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

Перевизначення TYPEзмінної таблиці, щоб включити кластер PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

План виконання тепер показує сканування кластерного індексу, і гарантія унікальності означає, що оптимізатор може безпечно видалити котушку таблиці:

З первинним ключем

У тестах з 5000 ітераціями MERGEоператора на 128 потоках жодних затримок із змінною кластерної таблиці не сталося. Я мушу наголосити, що це лише на основі спостереження; змінна кластерна таблиця також може ( технічно ) створювати свої рядки в різних порядках, але шанси на послідовне замовлення дуже сильно підвищуються. Спостережувану поведінку потрібно, звичайно, перевірити для кожного нового накопичувального оновлення, пакета обслуговування чи нової версії SQL Server.

Якщо визначення змінної таблиці неможливо змінити, існує інша альтернатива:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Це також дозволяє домогтися усунення котушки (і послідовності послідовності порядку) ціною введення явного сортування:

План сортування

Цей план також не створив тупиків, використовуючи той же тест. Сценарій відтворення нижче:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;

8

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

Є три інші варіанти:

  1. Ви можете серіалізувати свої вставки, щоб вони не стикалися: ви можете викликати sp_getapplock на початку транзакції та придбати ексклюзивний замок перед виконанням MERGE. Звичайно, вам все-таки потрібно пройти стрес-тест.

  2. Ви можете мати один потік обробляти всі вставки, щоб ваш сервер додатків обробляв одночасність.

  3. Ви можете автоматично спробувати після тупикових ситуацій - це може бути найповільніший підхід, якщо конкурентоспроможність висока.

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

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

Ми в основному використовуємо підхід 1 в нашій системі. Це працює дуже добре для нас.


-1

Ще один можливий підхід - я виявив, що злиття іноді представляє проблеми блокування та продуктивності - можливо, варто грати з опцією запиту Option (MaxDop x)

У тьмяному та далекому минулому SQL Server мав варіант блокування рівня вставки рядків - але це, здається, померло смертю, проте кластеризований ПК з посвідченням повинен зробити вставки чистими.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.