Câu hỏi này có liên quan đến chủ đề diễn đàn này .
Chạy SQL Server 2008 Developer Edition trên máy trạm của tôi và cụm máy ảo hai nút Phiên bản doanh nghiệp nơi tôi đề cập đến "cụm alpha".
Thời gian cần thiết để xóa các hàng với cột varbinary (max) có liên quan trực tiếp đến độ dài của dữ liệu trong cột đó. Điều đó thoạt nghe có vẻ trực quan, nhưng sau khi điều tra, nó xung đột với sự hiểu biết của tôi về cách SQL Server thực sự xóa các hàng nói chung và xử lý loại dữ liệu này.
Vấn đề bắt nguồn từ vấn đề thời gian chờ xóa (> 30 giây) mà chúng ta đang thấy trong ứng dụng web .NET của mình, nhưng tôi đã đơn giản hóa nó để phục vụ cho cuộc thảo luận này.
Khi một bản ghi bị xóa, SQL Server đánh dấu nó là một con ma sẽ được dọn sạch bởi Nhiệm vụ dọn dẹp ma sau đó sau khi giao dịch được thực hiện (xem blog của Paul Randal ). Trong một thử nghiệm xóa ba hàng với dữ liệu 16 KB, 4 MB và 50 MB trong một cột phương sai (tối đa), tôi thấy điều này xảy ra trên trang với phần dữ liệu liên tiếp, cũng như trong giao dịch đăng nhập.
Điều có vẻ kỳ lạ đối với tôi là các khóa X được đặt trên tất cả các trang dữ liệu LOB trong quá trình xóa và các trang được phân bổ lại trong PFS. Tôi thấy điều này trong nhật ký giao dịch, cũng như với sp_lock
và kết quả của dm_db_index_operational_stats
DMV ( page_lock_count
).
Điều này tạo ra một nút cổ chai I / O trên máy trạm của tôi và cụm alpha của chúng tôi nếu các trang đó chưa có trong bộ đệm bộ đệm. Trong thực tế, page_io_latch_wait_in_ms
từ cùng một DMV thực tế là toàn bộ thời gian xóa và page_io_latch_wait_count
tương ứng với số lượng trang bị khóa. Đối với tệp 50 MB trên máy trạm của tôi, tệp này chuyển thành hơn 3 giây khi bắt đầu với bộ đệm bộ đệm trống ( checkpoint
/ dbcc dropcleanbuffers
) và tôi không nghi ngờ gì nữa, nó sẽ dài hơn cho phân mảnh nặng và dưới tải.
Tôi đã cố gắng đảm bảo rằng nó không chỉ phân bổ không gian trong bộ đệm chiếm thời gian đó. Tôi đã đọc trong 2 GB dữ liệu từ các hàng khác trước khi thực hiện xóa thay vì checkpoint
phương thức, phần lớn được phân bổ cho quy trình SQL Server. Không chắc đó có phải là một thử nghiệm hợp lệ hay không, vì tôi không biết SQL Server xáo trộn dữ liệu xung quanh như thế nào. Tôi cho rằng nó sẽ luôn đẩy ra cái cũ có lợi cho cái mới.
Hơn nữa, nó thậm chí không sửa đổi các trang. Điều này tôi có thể thấy với dm_os_buffer_descriptors
. Các trang được sạch sau khi xóa, trong khi số lượng trang được sửa đổi ít hơn 20 cho cả ba lần xóa nhỏ, vừa và lớn. Tôi cũng đã so sánh đầu ra của DBCC PAGE
một mẫu các trang được tra cứu và không có thay đổi nào (chỉ có ALLOCATED
bit bị xóa khỏi PFS). Nó chỉ giải quyết chúng.
Để chứng minh thêm rằng việc tra cứu / xử lý trang đang gây ra sự cố, tôi đã thử kiểm tra tương tự bằng cách sử dụng cột filestream thay vì vanilla varbinary (max). Việc xóa là thời gian không đổi, bất kể kích thước LOB.
Vì vậy, đầu tiên câu hỏi học tập của tôi:
- Tại sao SQL Server cần tra cứu tất cả các trang dữ liệu LOB để X khóa chúng? Có phải đó chỉ là một chi tiết về cách các khóa được thể hiện trong bộ nhớ (được lưu trữ cùng với trang nào đó)? Điều này làm cho tác động I / O phụ thuộc mạnh mẽ vào kích thước dữ liệu nếu không được lưu trữ hoàn toàn.
- Tại sao X khóa tất cả, chỉ để giải quyết chúng? Không đủ để chỉ khóa lá chỉ mục với phần liên tiếp, vì việc phân bổ không cần phải sửa đổi các trang? Có cách nào khác để lấy dữ liệu LOB mà khóa bảo vệ không?
- Tại sao lại sắp xếp lại các trang ở phía trước, cho rằng đã có một nhiệm vụ nền dành riêng cho loại công việc này?
Và có lẽ quan trọng hơn, câu hỏi thực tế của tôi:
- Có cách nào để làm cho xóa hoạt động khác nhau? Mục tiêu của tôi là xóa thời gian liên tục bất kể kích thước, tương tự như filestream, trong đó bất kỳ việc dọn dẹp nào xảy ra trong nền sau khi thực tế. Đây có phải là một điều cấu hình? Tôi đang lưu trữ những thứ kỳ lạ?
Dưới đây là cách tái tạo thử nghiệm được mô tả (được thực hiện thông qua cửa sổ truy vấn SSMS):
CREATE TABLE [T] (
[ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Data] [varbinary](max) NULL
)
DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier
SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration
INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))
-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN
-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID
-- Do this after test
ROLLBACK
Dưới đây là một số kết quả từ việc lược tả các xóa trên máy trạm của tôi:
| Loại cột | Xóa kích thước | Thời lượng (ms) | Đọc | Viết | CPU | -------------------------------------------------- ------------------ | Đa dạng | 16 KB | 40 | 13 | 2 | 0 | | Đa dạng | 4 MB | 952 | 2318 | 2 | 0 | | Đa dạng | 50 MB | 2976 | 28594 | 1 | 62 | -------------------------------------------------- ------------------ | Tập tin | 16 KB | 1 | 12 | 1 | 0 | | Tập tin | 4 MB | 0 | 9 | 0 | 0 | | Tập tin | 50 MB | 1 | 9 | 0 | 0 |
Thay vào đó, chúng ta không nhất thiết phải sử dụng filestream vì:
- Phân phối kích thước dữ liệu của chúng tôi không đảm bảo nó.
- Trong thực tế, chúng tôi thêm dữ liệu theo nhiều phần và filestream không hỗ trợ cập nhật một phần. Chúng tôi sẽ cần phải thiết kế xung quanh này.
Cập nhật 1
Đã kiểm tra một lý thuyết rằng dữ liệu đang được ghi vào nhật ký giao dịch như là một phần của việc xóa, và điều này dường như không phải là trường hợp. Tôi đang thử nghiệm cho điều này không chính xác? Xem bên dưới.
SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001
BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID
SELECT
SUM(
DATALENGTH([RowLog Contents 0]) +
DATALENGTH([RowLog Contents 1]) +
DATALENGTH([RowLog Contents 3]) +
DATALENGTH([RowLog Contents 4])
) [RowLog Contents Total],
SUM(
DATALENGTH([Log Record])
) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'
Đối với một tệp có kích thước trên 5 MB, điều này được trả về 1651 | 171860
.
Hơn nữa, tôi hy vọng các trang sẽ bị bẩn nếu dữ liệu được ghi vào nhật ký. Chỉ các thỏa thuận dường như được ghi lại, phù hợp với những gì bẩn sau khi xóa.
Cập nhật 2
Tôi đã nhận được phản hồi từ Paul Randal. Ông khẳng định thực tế là nó phải đọc tất cả các trang để duyệt qua cây và tìm trang nào để phân bổ, và tuyên bố rằng không có cách nào khác để tìm trang nào. Đây là một nửa câu trả lời cho 1 & 2 (mặc dù không giải thích được sự cần thiết của khóa đối với dữ liệu ngoài hàng, nhưng đó là khoai tây nhỏ).
Câu hỏi 3 vẫn đang mở: Tại sao phải sắp xếp lại các trang trước nếu đã có một tác vụ nền để dọn dẹp để xóa?
Và tất nhiên, tất cả các câu hỏi quan trọng: Có cách nào để giảm thiểu trực tiếp (tức là không khắc phục được) hành vi xóa phụ thuộc kích thước này không? Tôi nghĩ đây sẽ là một vấn đề phổ biến hơn, trừ khi chúng ta thực sự là những người duy nhất lưu trữ và xóa các hàng 50 MB trong SQL Server? Có ai khác ngoài đó làm việc xung quanh điều này với một số hình thức của một công việc thu gom rác?