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:
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
źródło
([TimeStamp] DESC, [InverterID] ASC)
wygląda dziwny wybór dla indeksu klastrowego. Mam na myśli tęDESC
część.Odpowiedzi:
Po pierwsze, choć nieco niezwiązane z głównym pytaniem, twoje
MERGE
stwierdzenie 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: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
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
MERGE
dane 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: 1
zapoznać 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.
źródło
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
Prawdopodobnie możesz uruchomić wiele zadań, usuwając partie aktualizacji, i potrzebujesz osobnego zadania, w którym będzie przeprowadzane usuwanie zraszania
wyczyścić stół pomostowy.
źródło