Nietypowe blokowanie zapytania przez transakcję (?)

2

Mam niezłego zonka z MSSQL (2014). Zapytanie (bardzo długa i brzydka procedura składowana używająca innych równie długich i brzydkich sp) zakleszcza mi się. Robi to konsekwentnie dla różnych zestawów danych tylko na moim komputerze. Debugując namierzyłem dokładnie jeden insert z zagnieżdżonym selectem, na którym sp się zakleszcza. I tu oczy otworzyły mi się tak szeroko, że aż pomarszczyła mi się skóra z tyłu szyi. Konkrety:
blokujący się kod:

	INSERT INTO core.root_seats
		SELECT
			@se_id + ROW_NUMBER() OVER (ORDER BY se_id),
			se_name,
			se_description,
			se_number,
			se_x,
			se_y,
			se_quality,
			se_capacity,
			se_group,
			se_flags,
			r.r_id,
			se_sv_id,
			se_seat_size,
			se_drawing_objects,
			SYSDATETIMEOFFSET(),
			se_id,
			0 ------------
		FROM
			ss_events.core.se_seats AS se
			INNER JOIN core.root_rows AS r ON (r.r_root_id = se.se_row_id)
			INNER JOIN core.root_sectors AS ON (s.s_id = r.r_sector_id)
		WHERE
			(s.s_a_id = @a_id);

Można go uprościć do:

SELECT *
FROM
	ss_events.core.se_seats AS se
	INNER JOIN core.root_rows AS r ON r.r_root_id = se.se_row_id
	INNER JOIN core.root_sectors AS s ON s.s_id = r.r_sector_id
WHERE
	s.s_a_id = 12;

Powyższe uproszczone zapytanie blokuje się tak samo, jak oryginalny insert. Puszczam je w ten sposób, że pauzuję w debugerze wykonanie sp na linijce z blokującym insertem, idę do drugiego okienka i tamże uruchamiam ten prosty select. OK. Na razie jest prosto, myślę sobie, że dodam nolock i będzie śmigać. I tu się zdziwiłem. Zapytanie w poniższej wersji nadal się blokuje!

SELECT *
FROM
	ss_events.core.se_seats AS se with (nolock)
	INNER JOIN core.root_rows AS r with (nolock) ON r.r_root_id = se.se_row_id
	INNER JOIN core.root_sectors AS s with (nolock) ON s.s_id = r.r_sector_id
WHERE
	s.s_a_id = 12;

Postanowiłem zobaczyć, na której tabelce jest robiony przeszkadzający mi lock. Puściłem

  • SELECT * FROM ss_events.core.se_seats, poszło bez wieszania się;
  • SELECT * FROM core.root_rows - zwróciło 1400 rekordów i zablokowało się (jak widać lock na wierszu albo stronie)
  • SELECT * FROM core.root_rows with (nolock) - zwróciło wszystkie rekordy z tabelki (1412 wierszy) bez blokowania się.
  • na wszelki wypadek sprawdziłem jeszcze core.root_sectors, tam też był lock, po dodaniu with (nolock) poszło zwracając 123 wiersze.
    Zrobiłem wielkie oczy i tak sobie myślę - WTF? Dlaczego zapytanie select * from se join r join s puszczone z nolock blokuje się? Jakim prawem się to dzieje, kiedy dopuszczony jest dirty read i żaden lock nie powinien zatrzymać tego zapytania? To samo dzieje się dla puszczenia tego zapytania w transakcji dla poziomu read uncommitted (no w sumie to logiczne).

Ale to nie koniec zdziwka. Kod

SELECT *
FROM
	core.root_rows AS r with (nolock)
	INNER JOIN core.root_sectors AS s with (nolock) ON s.s_id = r.r_sector_id
WHERE
	s.s_a_id = 12;

nie blokuje się. A przecież wyłączyłem jedyną tabelkę, na której nie było locka (wiem, bo sprawdziłem SELECT * FROM ss_events.core.se_seats).

Kolejne zdziwko:

SELECT top 2489 *
FROM
	ss_events.core.se_seats AS se with (nolock)
	INNER JOIN core.root_rows AS r with (nolock) ON r.r_root_id = se.se_row_id
	INNER JOIN core.root_sectors AS s with (nolock) ON s.s_id = r.r_sector_id
WHERE
	s.s_a_id = 12;

blokuje się. Ale wykonane z top 2488 już się nie blokuje i zwraca 1 (słownie: jeden) wiersz.

Czy ktoś jest w stanie wytłumaczyć mi, co się tutaj dzieje? Z góry przepraszam za obrzydliwe nazewnictwo schematów, tabel i pól, niestety nie miałem na nie najmniejszego wpływu.

edit: doczytałem (http://sqlblog.com/blogs/linchi_shea/archive/2009/08/03/performance-impact-can-select-nolock-block-inserts.aspx), że operacje na strukturze bazy danych mogą być blokujące pomimo nolock'a, w sumie jest to dość logiczne. Jednak tutaj nie mam do czynienia ani ze zmianą struktury bazy, ani nawet z insertem.

edit2: zapytanie w poniższej postaci nie blokuje się, ale nie zwraca wszystkich danych, które są mi potrzebne, dlatego nie jest rozwiązaniem, poza chcę zrozumieć, dlaczego to tak dziwnie działało:

select * from ss_events.core.se_seats AS se with (nolock)
where se.se_row_id in (
	SELECT r.r_root_id
	FROM
		core.root_rows AS r with (nolock),
		core.root_sectors AS s with (nolock)
	WHERE
		s.s_a_id = 12 and s.s_id = r.r_sector_id 
)

Poniżej screenshot wywołania sp_who2 zawierający sp na bazach events, nie wiem czy coś wniesie, ale może nie zaszkodzi:
Capture.PNG

0

Ale jak zdiagozowałeś, że to faktycznie lock?
Co zwróci zapytanie podczas tego locka:

SELECT
db.name DBName,
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
tl.resource_type,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingTest,
tl.request_mode
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db ON db.database_id = tl.resource_database_id
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
GO
0

Pusto. Nie mam pewności, że to lock, ale co innego mogłoby blokować zapytanie? Ilość danych nie jest duża, ponadto dodałem wszystkie niezbędne indeksy, żeby mieć pewność, że to nie ilość scanów powoduje "zawieszanie się" zapytania. Ponadto tak jak pisałem, jeśli dam np. top 2000, to mam zwracany jeden wynik w krótkim czasie (koszt zapytania wysoki, ~4, ale czas wykonania poniżej sekundy).

SELECT
    trans.session_id AS [SESSION ID],
    ESes.host_name AS [HOST NAME],login_name AS [Login NAME],
    trans.transaction_id AS [TRANSACTION ID],
    tas.name AS [TRANSACTION NAME],tas.transaction_begin_time AS [TRANSACTION BEGIN TIME],
    tds.database_id AS [DATABASE ID],DBs.name AS [DATABASE NAME]
FROM sys.dm_tran_active_transactions tas
JOIN sys.dm_tran_session_transactions trans
ON (trans.transaction_id=tas.transaction_id)
LEFT OUTER JOIN sys.dm_tran_database_transactions tds
ON (tas.transaction_id = tds.transaction_id )
LEFT OUTER JOIN sys.databases AS DBs
ON tds.database_id = DBs.database_id
LEFT OUTER JOIN sys.dm_exec_sessions AS ESes
ON trans.session_id = ESes.session_id
WHERE ESes.session_id IS NOT NULL

Wynik:
Capture2.PNG

0

A to:

select cmd,* from sys.sysprocesses
where blocked > 0
0

Hmh... Też nic. Dopiero

SELECT
db.name DBName, request_type,
tl.request_session_id,
tl.resource_type,
tl.request_mode
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.DATABASES db ON db.database_id = tl.resource_database_id
where name like 'ss_e%'

zwraca

ss_events	LOCK	56	DATABASE	S
ss_events_6	LOCK	52	DATABASE	S
ss_events_6	LOCK	56	DATABASE	S
ss_events_6	LOCK	58	DATABASE	S
ss_events_6	LOCK	56	OBJECT	Sch-S
ss_events	LOCK	56	OBJECT	Sch-S
ss_events_6	LOCK	56	OBJECT	Sch-S
0

a uruchomienie procedury with recompile?

0

Obszedłem problem, zadziałało coś takiego:

WITH r (r_sector_id, r_root_id, r_id) AS
(
		SELECT r.r_sector_id, r.r_root_id, r_id FROM core.root_rows r WITH (nolock)
)
SELECT *
FROM
    ss_events.core.se_seats AS se WITH (nolock), r
    INNER JOIN core.root_sectors AS s WITH (nolock) ON s.s_id = r.r_sector_id
WHERE
    s.s_a_id = 12 and r.r_root_id = se.se_row_id;

Ale problem zostaje otwarty, bo kompletnie nie rozumiem dlaczego przepisania zapytania w ten sposób pomogło. No i dlaczego zapytanie się blokuje, kiedy odczyt jest read uncommitted na każdej używanej w zapytaniu tabeli, a jednocześnie nie ma modyfikacji struktury bazy?</del>

Dupa, wcale nie zadziałało, albo padło połączenie z bazą, albo debuger się wywrócił, dość że sp umarła i puściła locki, a ja tego nie zauważyłem. Czyli nadal jestem w lesie. Idę do domu.

0

Jeśli możesz mieszać w bazie to zduplikuj tabele se_seats np. przez creats table xxx as select * from se_seats i zobacz czy cos sie zmieni. Jak nie to z kolejnymi tabelami. Btw. Nie masz tam jakiegos triggera na insercie?

0

Tam nie ma inserta. To znaczy w debugowanej procedurze jest, ale zatrzymałem się na breakpoint'cie przed tą instrukcją. SP czeka zapauzowane, a ja na drugim połączeniu odpalam select'a, który mimo nolock'ów zawiesza się. Obciążenie procka gdzieś w okolicach 0%, I/O żadne, baza danych nudzi się, więc jak nic jakiś lock; a z drugiej strony używam nolocka, który powinien ewentualne locki ominąć. Co innego może zablokować zapytanie nie obciążając jednocześnie bazy? Dlaczego działa select top 2488, a select top 2489 już się blokuje, kiedy w wynikach jest tylko jeden wiersz? Moja wiedza skończyła się na nolock :/
Zrobię jutro kopie tabel (to localhost, więc "ooo - mogę wszystko") i zobaczę, czy cokolwiek się zmieni.

2

Mam!!! Puszczałem zapytanie w wersji nieblokującej (z top i nolock) i zdziwiło mnie, że wykonuje się czasem poniżej sekundy, a czasem i minutę. Spojrzałem po takim dłuższym czekaniu na plan zapytania, a tam widzę wykrzyknik, coś w tym stylu:
Capture3.PNG
Patrzę na hint do klocka z planu, a tam warning "the query had to wait 8 seconds for memory grant". Wklepałem to do google, znalazłem i puściłem poniższe zapytanie (wzięte z http://blog.sqlauthority.com/2009/10/09/sql-server-queries-waiting-for-memory-allocation-to-execute/, BTW bardzo polecam artykuły tego gościa):

SELECT TEXT, query_plan, requested_memory_kb,
granted_memory_kb,used_memory_kb, wait_order
FROM sys.dm_exec_query_memory_grants MG
CROSS APPLY sys.dm_exec_sql_text(sql_handle)
CROSS APPLY sys.dm_exec_query_plan(MG.plan_handle)

i widzę, że moja procedura czeka na grant 49MB pamięci... I tu palnąłem się w łeb, aż pomarszczona skóra z tyłu głowy wróciła na swoje miejsce. Jestem idiotą. Jako że mój serwer dev stoi na localhost, to przyciąłem mu ilość pamięci trochę na wyrost, bo do 256MB. Do tej pory (pół roku) nie miałem z tym problemów, aż nadszedł wczorajszy dzień... Zmieniłem limit z 256MB na 1GB i już nic się nie blokuje.

Nauka z tego taka, że zapytanie może blokować się nie tylko na transakcjach i na zmianach w strukturze bazy (Sch-M), ale także na czekaniu na alokację pamięci (i być może na milion innych wewnętrznych operacji mssql). Ciekawi mnie jeszcze tylko to, dlaczego SQL Server nie rzuca w takich przypadkach wyjątkiem (z pewnym timeoutem).

Problem rozwiązany. Wszystkim biorącym udział w tym wątku dziękuję za pomoc :-)

1 użytkowników online, w tym zalogowanych: 0, gości: 1