Masywne WSTAWKI blokujące WYBÓR

14

Mam problem z ogromną liczbą INSERTów, które blokują moje operacje SELECT.

Schemat

Mam taki stół:

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

Mam również tę małą procedurę pomocniczą, która pozwala mi wstawiać lub aktualizować (aktualizować w przypadku konfliktu) za pomocą polecenia 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

Stosowanie

Teraz uruchomiłem instancje usług na wielu serwerach, które wykonują masowe aktualizacje [InsertOrUpdateInverterData], szybko wywołując procedurę.

Istnieje również strona internetowa, która wykonuje zapytania SELECT na [InverterData]stole.

Problem

Jeśli wybiorę zapytania w [InverterData]tabeli, będą one wykonywane w różnych przedziałach czasowych, w zależności od użycia INSERT moich wystąpień usługi. Jeśli zatrzymam wszystkie wystąpienia usługi, SELECT jest błyskawicznie szybki, jeśli wystąpienie wykonuje szybkie wstawianie, SELECT staje się naprawdę wolny, a nawet czas oczekiwania zostaje anulowany.

Próbowanie

Skończyłem kilka instrukcji SELECT na [sys.dm_tran_locks]stole, aby znaleźć procesy blokowania, takie jak to

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

Oto wynik:

wprowadź opis zdjęcia tutaj

S = wspólny. Sesja wstrzymująca ma wspólny dostęp do zasobu.

Pytanie

Dlaczego SELECTY są blokowane przez [InsertOrUpdateInverterData]procedurę, która używa tylko komend MERGE?

Czy muszę korzystać z jakiejś transakcji ze zdefiniowanym trybem izolacji w środku [InsertOrUpdateInverterData]?

Aktualizacja 1 (związana z pytaniem z @Paul)

Oprzyj się na wewnętrznych raportach serwera MS-SQL o [InsertOrUpdateInverterData]następujących statystykach:

  • Średni czas pracy procesora: 0,12 ms
  • Średnie procesy odczytu: 5,76 na sekundę
  • Średnie procesy zapisu: 0,4 na sekundę

Na tej podstawie wygląda na to, że polecenie POŁĄCZ jest głównie zajęte operacjami odczytu, które zablokują tabelę! (?)

Aktualizacja 2 (związana z pytaniem z @Paul)

[InverterData]Stół jak ma następujące statystyki przechowywania:

  • Przestrzeń danych: 26 901,86 MB
  • Liczba wierszy: 131 827 749
  • Partycjonowany: prawda
  • Liczba partycji: 62

Oto (prawie) kompletny zestaw wyników sp_WhoIsActive :

SELECT Komenda

  • dd hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • Procesor: 198
  • blocking_session_id: 146
  • brzmi: 99,368
  • pisze: 0
  • status: zawieszony
  • open_tran_count: 0

[InsertOrUpdateInverterData]Polecenie blokowania

  • dd hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • Procesor: 3972
  • blocking_session_id: NULL
  • brzmi: 376,95
  • pisze: 126
  • status: spanie
  • open_tran_count: 1
Steffen Mangold
źródło
Do ([TimeStamp] DESC, [InverterID] ASC)wygląda dziwny wybór dla indeksu klastrowego. Mam na myśli tę DESCczęść.
ypercubeᵀᴹ
Rozumiem twój punkt widzenia: indeks klastrowy wstawianie danych DESC wymusiłoby przebudowę tabeli raczej dołączając do końca ... pies wydajności; blokuje stół podczas przebudowy ... tak. przez Jove, masz to. Struktura jest przyczyną blokowania więcej niż zamków.
Alocyte,

Odpowiedzi:

12

Po pierwsze, choć nieco niezwiązane z głównym pytaniem, twoje MERGEstwierdzenie jest potencjalnie zagrożone błędami z powodu warunków wyścigu . W skrócie problem polega na tym, że wiele współbieżnych wątków może stwierdzić, że wiersz docelowy nie istnieje, co powoduje kolizję prób wstawienia. Główną przyczyną jest to, że nie można wziąć blokady współdzielonej lub aktualizacji w wierszu, który nie istnieje. Rozwiązaniem jest dodanie podpowiedzi:

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

Serializable poziom izolacji wskazówkę zapewnia kluczowy zakres gdzie rząd pójdzie jest zablokowana. Masz unikalny indeks, który obsługuje blokowanie zasięgu, więc ta podpowiedź nie będzie miała negatywnego wpływu na blokowanie, po prostu zyskasz ochronę przed potencjalnymi warunkami wyścigu.

Główne pytanie

Dlaczego są SELECTsblokowane przez procedurę [InsertOrUpdateInverterData], która używa tylko MERGEpoleceń?

Poniżej domyślnego poziomu izolacji zatwierdzonego odczytu blokującego , współdzielone (S) blokady są podejmowane podczas odczytu danych i zwykle (choć nie zawsze) zwalniane wkrótce po zakończeniu odczytu. Niektóre udostępnione blokady są wstrzymywane do końca instrukcji.

A MERGEdane modyfikuje komunikat, więc będzie on pozyskać S lub aktualizacji (U) blokuje lokalizowanie, kiedy dane do zmian, które są przekształcane w wyłącznym (X) blokuje przed wykonaniem żądanej zmiany. Zarówno blokady U, jak i X muszą być utrzymywane do końca transakcji.

Odnosi się to do wszystkich poziomów izolacji, z wyjątkiem „optymistycznej” izolacji migawki (SI), której nie należy mylić z wersjonowaniem zatwierdzonego odczytu zatwierdzonego, znanego również jako izolowana migawka izolowana (RCSI).

Nic w twoim pytaniu nie pokazuje sesji oczekującej na zablokowanie blokady S przez sesję z blokadą U. Te zamki są kompatybilne . Każde blokowanie jest prawie na pewno spowodowane blokowaniem zablokowanego zamka X. Może to być nieco trudne do uchwycenia, gdy duża liczba krótkoterminowych blokad jest pobierana, konwertowana i zwalniana w krótkim odstępie czasu.

Warto open_tran_count: 1zapoznać się z poleceniem InsertOrUpdateInverterData. Chociaż polecenie nie działało zbyt długo, należy sprawdzić, czy nie zawiera niepotrzebnie długiej transakcji zawierającej transakcję (w aplikacji lub w procedurze przechowywanej wyższego poziomu). Najlepszą praktyką jest, aby transakcje były jak najkrótsze. To może być nic, ale zdecydowanie powinieneś sprawdzić.

Potencjalne rozwiązanie

Jak zasugerował Kin w komentarzu, możesz włączyć poziom izolacji wersji wiersza (RCSI lub SI) w tej bazie danych. Najczęściej używane jest RCSI, ponieważ zazwyczaj nie wymaga tylu zmian aplikacji. Po włączeniu domyślny poziom izolacji zatwierdzonego odczytu używa wersji wiersza zamiast pobierania blokad S dla odczytów, więc blokowanie SX jest zmniejszone lub wyeliminowane. Niektóre operacje (np. Kontrole klucza obcego) nadal uzyskują blokady S na podstawie RCSI.

Pamiętaj jednak, że wersje wierszy zajmują miejsce w tempdb, mówiąc ogólnie proporcjonalnie do szybkości zmian zmian i długości transakcji. Będziesz musiał dokładnie przetestować swoją implementację pod obciążeniem, aby zrozumieć i zaplanować wpływ RCSI (lub SI) w twoim przypadku.

Jeśli chcesz zlokalizować użycie wersjonowania zamiast włączania go dla całego obciążenia, SI może nadal być lepszym wyborem. Używając SI do transakcji odczytu, unikniesz rywalizacji między czytelnikami a pisarzami, kosztem czytelników, którzy zobaczą wersję wiersza przed rozpoczęciem jakichkolwiek równoczesnych modyfikacji (bardziej poprawnie, operacja odczytu w SI zawsze zobaczy zatwierdzony stan wiersz w momencie rozpoczęcia transakcji SI). Korzystanie z SI do zapisywania transakcji jest niewielkie lub nie ma żadnej korzyści, ponieważ blokady zapisu będą nadal pobierane i będziesz musiał obsługiwać wszelkie konflikty zapisu. Chyba że tego właśnie chcesz :)

Uwaga: W przeciwieństwie do RCSI (które raz włączone dotyczy wszystkich transakcji uruchomionych w momencie zatwierdzenia odczytu), należy wyraźnie zażądać SI za pomocą SET TRANSACTION ISOLATION SNAPSHOT;.

Subtelne zachowania, które zależą od czytelników blokujących pisarzy (w tym w kodzie wyzwalającym!) Sprawiają, że testowanie jest niezbędne. Szczegółowe informacje można znaleźć w mojej połączonej serii artykułów i Książkach online. Jeśli zdecydujesz się na RCSI, koniecznie przejrzyj Modyfikacje danych, w szczególności w sekcji Przeczytaj zatwierdzoną izolację migawki .

Na koniec należy upewnić się, że instancja została załatana do dodatku Service Pack 4 dla programu SQL Server 2008.

Paul White 9
źródło
0

Pokornie nie użyłbym scalania. Poszedłbym z IF Exists (UPDATE) ELSE (INSERT) - masz klastrowany klucz z dwiema kolumnami, których używasz do identyfikacji wierszy, więc jest to łatwy test.

Wspominasz MASYWNE wstawki, a jednak robisz 1 na 1 ... pomyślałeś o grupowaniu danych w tabeli pomostowej i wykorzystaniu mocy zestawu danych SQL OVERWHELMING POWER, aby wykonać więcej niż 1 aktualizację / wstawić na raz? Na przykład rutynowe testowanie zawartości w tabeli pomostowej i pobieranie najlepszych 10000 na raz zamiast 1 na raz ...

Zrobiłbym coś takiego w mojej aktualizacji

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

Prawdopodobnie możesz uruchomić wiele zadań, usuwając partie aktualizacji, i potrzebujesz osobnego zadania, w którym będzie przeprowadzane usuwanie zraszania

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

wyczyścić stół pomostowy.

Alocyt
źródło