Tại sao NOLOCK thực hiện quét với phân công biến chậm hơn?


11

Tôi đang chiến đấu chống lại NOLOCK trong môi trường hiện tại của tôi. Một lập luận tôi đã nghe là chi phí khóa làm chậm một truy vấn. Vì vậy, tôi đã nghĩ ra một thử nghiệm để xem mức phí này có thể là bao nhiêu.

Tôi phát hiện ra rằng NOLOCK thực sự làm chậm quá trình quét của tôi.

Lúc đầu, tôi rất vui mừng, nhưng bây giờ tôi chỉ bối rối. Là bài kiểm tra của tôi không hợp lệ bằng cách nào đó? NOLOCK có thực sự không cho phép quét nhanh hơn một chút không? Chuyện gì đang xảy ra ở đây vậy?

Đây là kịch bản của tôi:

USE TestDB
GO

--Create a five-million row table
DROP TABLE IF EXISTS dbo.JustAnotherTable
GO

CREATE TABLE dbo.JustAnotherTable (
ID INT IDENTITY PRIMARY KEY,
notID CHAR(5) NOT NULL )

INSERT dbo.JustAnotherTable
SELECT TOP 5000000 'datas'
FROM sys.all_objects a1
CROSS JOIN sys.all_objects a2
CROSS JOIN sys.all_objects a3

/********************************************/
-----Testing. Run each multiple times--------
/********************************************/
--How fast is a plain select? (I get about 587ms)
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID  --trash variable prevents any slowdown from returning data to SSMS
FROM dbo.JustAnotherTable
ORDER BY ID
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

----------------------------------------------
--Now how fast is it with NOLOCK? About 640ms for me
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID
FROM dbo.JustAnotherTable (NOLOCK)
ORDER BY ID --would be an allocation order scan without this, breaking the comparison
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

Những gì tôi đã thử mà không hoạt động:

  • Chạy trên các máy chủ khác nhau (cùng kết quả, máy chủ là 2016-SP1 và 2016-SP2, cả hai đều yên tĩnh)
  • Chạy trên dbfiddle.uk trên các phiên bản khác nhau (ồn ào, nhưng có thể có cùng kết quả)
  • THIẾT LẬP CẤP ĐỘ thay vì gợi ý (cùng kết quả)
  • Tắt leo thang khóa trên bàn (kết quả tương tự)
  • Kiểm tra thời gian thực hiện quét thực tế trong kế hoạch truy vấn thực tế (cùng kết quả)
  • Biên dịch lại gợi ý (cùng kết quả)
  • Chỉ đọc filegroup (cùng kết quả)

Khám phá hứa hẹn nhất đến từ việc loại bỏ biến rác và sử dụng truy vấn không có kết quả. Ban đầu điều này cho thấy NOLOCK nhanh hơn một chút, nhưng khi tôi đưa bản demo cho sếp của mình, NOLOCK đã trở lại chậm hơn.

NOLOCK làm chậm quá trình quét với phép gán biến là gì?


Nó sẽ đưa ai đó có quyền truy cập mã nguồn và một hồ sơ để đưa ra một câu trả lời dứt khoát. Nhưng NOLOCK phải thực hiện một số công việc bổ sung để đảm bảo nó không đi vào một vòng lặp vô hạn khi có dữ liệu đột biến. Và có thể tối ưu hóa bị vô hiệu hóa (còn gọi là chưa bao giờ được kiểm tra) cho các truy vấn NOLOCK.
David Browne - Microsoft

1
Không repro cho tôi trên Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith

Câu trả lời:


12

LƯU Ý: đây có thể không phải là loại câu trả lời bạn đang tìm kiếm. Nhưng có lẽ nó sẽ hữu ích cho những người trả lời tiềm năng khác khi cung cấp manh mối về nơi bắt đầu tìm kiếm

Khi tôi chạy các truy vấn này theo dấu vết ETW (sử dụng PerfView), tôi nhận được các kết quả sau:

Plain  - 608 ms  
NOLOCK - 659 ms

Vì vậy, sự khác biệt là 51ms . Điều này là khá chết với sự khác biệt của bạn (~ 50ms). Con số của tôi cao hơn một chút vì tổng thể lấy mẫu hồ sơ.

Tìm sự khác biệt

Dưới đây là so sánh song song cho thấy sự khác biệt 51ms nằm trong FetchNextRowphương thức trong sqlmin.dll:

FetchNextRow

Chọn đồng bằng ở bên trái ở mức 332 ms, trong khi phiên bản nolock ở bên phải ở 383 ( dài hơn 51ms ). Bạn cũng có thể thấy rằng hai đường dẫn mã khác nhau theo cách này:

  • Trơn SELECT

    • sqlmin!RowsetNewSS::FetchNextRow các cuộc gọi
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • Sử dụng NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow các cuộc gọi
      • sqlmin!DatasetSession::GetNextRowValuesNoLock mà gọi một trong hai
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal hoặc là
        • kernel32!TlsGetValue

Điều này cho thấy rằng có một số phân nhánh trong FetchNextRowphương thức dựa trên mức độ cô lập / gợi ý nolock.

Tại sao NOLOCKchi nhánh mất nhiều thời gian hơn?

Chi nhánh nolock thực sự dành ít thời gian gọi vào GetNextRowValuesInternal(ít hơn 25ms). Nhưng mã trực tiếp trong GetNextRowValuesNoLock(không bao gồm các phương thức mà nó gọi là AKA là cột "Exc") chạy trong 63ms - phần lớn của sự khác biệt (thời gian CPU tăng 63 - 25 = 38ms).

Vậy 13ms còn lại (tổng cộng 51ms - chiếm 38ms cho đến nay) là FetchNextRowbao nhiêu?

Giao diện công văn

Tôi nghĩ rằng điều này gây tò mò hơn bất cứ điều gì, nhưng phiên bản nolock dường như phải chịu một số chi phí gửi giao diện bằng cách gọi vào phương thức API của Windows kernel32!TlsGetValuethông qua kernel32!TlsGetValueStub- tổng cộng 17ms. Lựa chọn đơn giản dường như không đi qua giao diện, vì vậy nó không bao giờ chạm vào sơ khai và chỉ dành 6ms cho TlsGetValue(chênh lệch 11ms ). Bạn có thể thấy điều này ở trên trong ảnh chụp màn hình đầu tiên.

Tôi có lẽ nên chạy lại dấu vết này với nhiều lần lặp lại của truy vấn, tôi nghĩ rằng có một số điều nhỏ, như gián đoạn phần cứng, không được chọn bởi tốc độ mẫu 1ms của PerfView


Ngoài phương pháp đó, tôi nhận thấy một sự khác biệt nhỏ khác khiến phiên bản nolock chạy chậm hơn:

Phát hành khóa

Chi nhánh nolock dường như chạy mạnh hơn sqlmin!RowsetNewSS::ReleaseRowsphương thức mà bạn có thể thấy trong ảnh chụp màn hình này:

Phát hành ổ khóa

Lựa chọn đơn giản nằm ở trên cùng, ở mức 12ms, trong khi phiên bản nolock ở phía dưới ở mức 26ms ( dài hơn 14ms ). Bạn cũng có thể thấy trong cột "Khi" mã được thực thi thường xuyên hơn trong mẫu. Đây có thể là một chi tiết triển khai của nolock, nhưng nó dường như giới thiệu khá nhiều chi phí cho các mẫu nhỏ.


Có rất nhiều sự khác biệt nhỏ khác, nhưng đó là những khối lớn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.