Xóa hiệu suất cho dữ liệu LOB trong SQL Server


16

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_lockvà kết quả của dm_db_index_operational_statsDMV ( 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_mstừ cùng một DMV thực tế là toàn bộ thời gian xóa và page_io_latch_wait_counttươ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ì checkpointphươ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 PAGEmột mẫu các trang được tra cứu và không có thay đổi nào (chỉ có ALLOCATEDbit 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:

  1. 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.
  2. 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?
  3. 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ì:

  1. Phân phối kích thước dữ liệu của chúng tôi không đảm bảo nó.
  2. 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?


Tôi ước có một giải pháp tốt hơn, nhưng chưa tìm thấy. Tôi có một tình huống ghi nhật ký khối lượng lớn các hàng có kích thước khác nhau, lên tới 1MB + và tôi có quy trình "thanh lọc" để xóa các bản ghi cũ. Vì việc xóa quá chậm, tôi phải chia nó thành hai bước - đầu tiên xóa tham chiếu giữa các bảng (rất nhanh), sau đó xóa các hàng mồ côi. Công việc xóa trung bình ~ 2,2 giây / MB để xóa dữ liệu. Vì vậy, tất nhiên tôi phải giảm bớt sự tranh chấp, vì vậy tôi có một quy trình được lưu trữ với "XÓA TOP (250)" trong một vòng lặp cho đến khi không còn hàng nào bị xóa nữa.
Bàn tính

Câu trả lời:


5

Tôi không thể nói tại sao chính xác việc xóa VARBINARY (MAX) sẽ kém hiệu quả hơn nhiều so với luồng tệp nhưng một ý tưởng bạn có thể cân nhắc nếu bạn chỉ cố gắng tránh thời gian thoát khỏi ứng dụng web của mình khi xóa các LOBS này. Bạn có thể lưu trữ các giá trị VARBINARY (MAX) trong một bảng riêng biệt (hãy gọi nó là tblLOB) được tham chiếu bởi bảng gốc (hãy gọi tblParent này).

Từ đây khi bạn xóa một bản ghi, bạn chỉ có thể xóa nó khỏi bản ghi gốc và sau đó có một quy trình thu gom rác thỉnh thoảng để đi vào và dọn sạch các bản ghi trong bảng LOB. Có thể có thêm hoạt động ổ cứng trong quá trình thu gom rác này nhưng ít nhất nó sẽ tách biệt với web front end và có thể được thực hiện trong thời gian không cao điểm.


Cảm ơn. Đó chính xác là một trong những lựa chọn của chúng tôi trên bảng. Bảng này là một hệ thống tệp và chúng tôi hiện đang trong quá trình tách dữ liệu nhị phân ra một cơ sở dữ liệu hoàn toàn tách biệt với meta phân cấp. Chúng tôi có thể làm như bạn đã nói và xóa hàng phân cấp và có một quy trình GC dọn sạch các hàng LOB mồ côi. Hoặc có dấu thời gian xóa với dữ liệu để thực hiện cùng một mục tiêu. Đây là con đường chúng ta có thể đi nếu không có câu trả lời thỏa mãn cho vấn đề.
Jeremy Rosenberg

1
Tôi sẽ thận trọng khi chỉ có một dấu thời gian để chỉ ra rằng nó đã bị xóa. Điều đó sẽ hoạt động nhưng sau đó bạn cuối cùng sẽ có rất nhiều không gian được sử dụng chiếm trong các hàng hoạt động. Bạn sẽ cần phải có một số loại quy trình gc tại một số điểm, tùy thuộc vào mức độ bị xóa, và sẽ ít ảnh hưởng hơn để xóa ít hơn một cách thường xuyên thay vì rất nhiều lần trên cơ sở.
Ian Chamberland
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.