Việc thêm một chỉ mục đã lọc có thể có những tác dụng phụ đáng ngạc nhiên đối với các truy vấn hiện có, ngay cả khi có vẻ như chỉ mục được lọc mới hoàn toàn không liên quan. Bài đăng này xem xét một ví dụ ảnh hưởng đến các câu lệnh DELETE dẫn đến hiệu suất kém và tăng nguy cơ bế tắc.
Môi trường thử nghiệm
Bảng sau sẽ được sử dụng trong suốt bài đăng này:
CREATE TABLE dbo.Data ( RowID integer IDENTITY NOT NULL, SomeValue integer NOT NULL, StartDate date NOT NULL, CurrentFlag bit NOT NULL, Padding char(50) NOT NULL DEFAULT REPLICATE('ABCDE', 10), CONSTRAINT PK_Data_RowID PRIMARY KEY CLUSTERED (RowID) );
Câu lệnh tiếp theo này tạo ra 499.999 hàng dữ liệu mẫu:
INSERT dbo.Data WITH (TABLOCKX) (SomeValue, StartDate, CurrentFlag) SELECT CONVERT(integer, RAND(n) * 1e6) % 1000, DATEADD(DAY, (N.n - 1) % 31, '20140101'), CONVERT(bit, 0) FROM dbo.Numbers AS N WHERE N.n >= 1 AND N.n < 500000;
Điều đó sử dụng bảng Numbers làm nguồn các số nguyên liên tiếp từ 1 đến 499,999. Trong trường hợp bạn không có một trong các mã đó trong môi trường thử nghiệm của mình, thì mã sau có thể được sử dụng để tạo hiệu quả một mã chứa các số nguyên từ 1 đến 1.000.000:
WITH N1 AS (SELECT N1.n FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (n)), N2 AS (SELECT L.n FROM N1 AS L CROSS JOIN N1 AS R), N3 AS (SELECT L.n FROM N2 AS L CROSS JOIN N2 AS R), N4 AS (SELECT L.n FROM N3 AS L CROSS JOIN N2 AS R), N AS (SELECT ROW_NUMBER() OVER (ORDER BY n) AS n FROM N4) SELECT -- Destination column type integer NOT NULL ISNULL(CONVERT(integer, N.n), 0) AS n INTO dbo.Numbers FROM N OPTION (MAXDOP 1); ALTER TABLE dbo.Numbers ADD CONSTRAINT PK_Numbers_n PRIMARY KEY (n) WITH (SORT_IN_TEMPDB = ON, MAXDOP = 1);
Cơ sở của các bài kiểm tra sau này sẽ là xóa các hàng khỏi bảng kiểm tra cho một Ngày bắt đầu cụ thể. Để làm cho quá trình xác định các hàng để xóa hiệu quả hơn, hãy thêm chỉ mục không phân biệt này:
CREATE NONCLUSTERED INDEX IX_Data_StartDate ON dbo.Data (StartDate);
Dữ liệu mẫu
Sau khi hoàn thành các bước đó, mẫu sẽ giống như sau:
SELECT TOP (100) D.RowID, D.SomeValue, D.StartDate, D.CurrentFlag, D.Padding FROM dbo.Data AS D ORDER BY D.RowID;
Dữ liệu cột SomeValue có thể hơi khác do tạo ngẫu nhiên giả, nhưng sự khác biệt này không quan trọng. Nhìn chung, dữ liệu mẫu chứa 16.129 hàng cho mỗi 31 ngày StartDate vào tháng 1 năm 2014:
SELECT D.StartDate, NumRows = COUNT_BIG(*) FROM dbo.Data AS D GROUP BY D.StartDate ORDER BY D.StartDate;
Bước cuối cùng chúng ta cần thực hiện để làm cho dữ liệu trở nên thực tế, là đặt cột CurrentFlag thành true cho RowID cao nhất cho mỗi StartDate. Tập lệnh sau hoàn thành nhiệm vụ này:
WITH LastRowPerDay AS ( SELECT D.CurrentFlag FROM dbo.Data AS D WHERE D.RowID = ( SELECT MAX(D2.RowID) FROM dbo.Data AS D2 WHERE D2.StartDate = D.StartDate ) ) UPDATE LastRowPerDay SET CurrentFlag = 1;
Kế hoạch thực thi cho bản cập nhật này có kết hợp Phân đoạn-Đầu để xác định vị trí RowID cao nhất mỗi ngày một cách hiệu quả:
Lưu ý cách kế hoạch thực thi có chút tương đồng với dạng viết của truy vấn. Đây là một ví dụ tuyệt vời về cách trình tối ưu hóa hoạt động từ đặc tả SQL logic, thay vì triển khai SQL trực tiếp. Trong trường hợp bạn đang thắc mắc, thì cần có Bảng Hứng thú trong kế hoạch đó để Bảo vệ Halloween.
Xóa một ngày dữ liệu
Được rồi, vậy là đã hoàn thành các bước sơ bộ, nhiệm vụ trước mắt là xóa các hàng cho một Ngày bắt đầu cụ thể. Đây là loại truy vấn bạn có thể chạy thường xuyên vào ngày sớm nhất trong bảng, nơi dữ liệu đã hết thời hạn sử dụng.
Lấy ngày 1 tháng 1 năm 2014 làm ví dụ của chúng tôi, truy vấn xóa thử nghiệm rất đơn giản:
DELETE dbo.Data WHERE StartDate = '20140101';
Kế hoạch thực hiện cũng khá đơn giản, mặc dù đáng xem xét một chút chi tiết:
Phân tích kế hoạch
Index Seek ở ngoài cùng bên phải sử dụng chỉ mục không phân biệt để tìm các hàng cho giá trị StartDate được chỉ định. Nó chỉ trả về các giá trị RowID mà nó tìm thấy, như chú giải công cụ của toán tử xác nhận:
Nếu bạn đang tự hỏi làm thế nào chỉ mục StartDate quản lý để trả về RowID, hãy nhớ rằng RowID là chỉ mục nhóm duy nhất cho bảng, vì vậy nó tự động được đưa vào chỉ mục không có trong StartDate.
Toán tử tiếp theo trong kế hoạch là Xóa chỉ mục theo cụm. Điều này sử dụng giá trị RowID được tìm thấy bởi Index Seek để xác định các hàng cần loại bỏ.
Toán tử cuối cùng trong kế hoạch là Xóa chỉ mục. Thao tác này sẽ xóa các hàng khỏi chỉ mục không được phân bổ IX_Data_StartDate
có liên quan đến RowID bị xóa bởi Xóa chỉ mục theo cụm. Để xác định vị trí các hàng này trong chỉ mục không phân biệt, bộ xử lý truy vấn cần Ngày bắt đầu (khóa cho chỉ mục không phân biệt).
Hãy nhớ rằng Tìm kiếm chỉ mục ban đầu không trả về Ngày bắt đầu, chỉ trả về RowID. Vậy làm cách nào để bộ xử lý truy vấn lấy StartDate để xóa chỉ mục? Trong trường hợp cụ thể này, trình tối ưu hóa có thể nhận thấy rằng giá trị StartDate là một hằng số và đã tối ưu hóa nó đi, nhưng đây không phải là điều đã xảy ra. Câu trả lời là toán tử Xóa chỉ mục theo cụm đọc giá trị StartDate cho hàng hiện tại và thêm nó vào luồng. So sánh Danh sách đầu ra của Xóa chỉ mục theo cụm được hiển thị bên dưới, với Danh sách đầu ra của Tìm kiếm chỉ mục ngay bên trên:
Có vẻ ngạc nhiên khi thấy toán tử Delete đọc dữ liệu, nhưng đây là cách nó hoạt động. Bộ xử lý truy vấn biết nó sẽ phải xác định vị trí hàng trong chỉ mục nhóm để xóa nó, vì vậy nó cũng có thể trì hoãn việc đọc các cột cần thiết để duy trì các chỉ mục không phân nhóm cho đến thời điểm đó, nếu có thể.
Thêm chỉ mục đã lọc
Bây giờ, hãy tưởng tượng ai đó có một truy vấn quan trọng chống lại bảng này đang hoạt động không tốt. DBA hữu ích thực hiện phân tích và thêm chỉ mục được lọc sau:
CREATE NONCLUSTERED INDEX FIX_Data_SomeValue_CurrentFlag ON dbo.Data (SomeValue) INCLUDE (CurrentFlag) WHERE CurrentFlag = 1;
Chỉ mục được lọc mới có tác dụng mong muốn đối với truy vấn có vấn đề và mọi người đều hài lòng. Lưu ý rằng chỉ mục mới hoàn toàn không tham chiếu đến cột Ngày bắt đầu, vì vậy chúng tôi không mong đợi nó ảnh hưởng đến truy vấn xóa trong ngày của chúng tôi.
Xóa một ngày với chỉ mục được lọc tại chỗ
Chúng tôi có thể kiểm tra kỳ vọng đó bằng cách xóa dữ liệu lần thứ hai:
DELETE dbo.Data WHERE StartDate = '20140102';
Đột nhiên, kế hoạch thực thi đã thay đổi thành Quét chỉ mục theo cụm song song:
Lưu ý rằng không có toán tử Xóa chỉ mục riêng cho chỉ mục được lọc mới. Trình tối ưu hóa đã chọn duy trì chỉ mục này bên trong toán tử Xóa chỉ mục theo cụm. Điều này được đánh dấu trong SQL Sentry Plan Explorer như được hiển thị ở trên ("+1 chỉ mục không phân cụm") với đầy đủ chi tiết trong chú giải công cụ:
Nếu bảng lớn (nghĩ là kho dữ liệu) thì sự thay đổi đối với quá trình quét song song có thể rất quan trọng. Điều gì đã xảy ra với Tìm kiếm chỉ mục tốt đẹp trên StartDate, và tại sao một chỉ mục được lọc hoàn toàn không liên quan lại thay đổi mọi thứ đáng kể như vậy?
Tìm sự cố
Manh mối đầu tiên đến từ việc xem xét các thuộc tính của Quét chỉ mục theo cụm:
Cũng như việc tìm kiếm các giá trị RowID cho toán tử Xóa chỉ mục cụm để xóa, toán tử này hiện đang đọc các giá trị CurrentFlag. Sự cần thiết của cột này là không rõ ràng, nhưng ít nhất nó cũng bắt đầu giải thích quyết định quét:cột CurrentFlag không phải là một phần của chỉ mục không có trong StartDate của chúng tôi.
Chúng tôi có thể xác nhận điều này bằng cách viết lại truy vấn xóa để buộc sử dụng chỉ mục không có trong StartDate:
DELETE D FROM dbo.Data AS D WITH (INDEX(IX_Data_StartDate)) WHERE StartDate = '20140103';
Kế hoạch thực thi gần với dạng ban đầu hơn, nhưng hiện nó có tính năng Tra cứu chính:
Thuộc tính Key Lookup xác nhận toán tử này đang truy xuất các giá trị CurrentFlag:
Bạn cũng có thể nhận thấy các hình tam giác cảnh báo trong hai kế hoạch cuối cùng. Đây là các cảnh báo chỉ mục bị thiếu:
Đây là xác nhận thêm rằng SQL Server muốn xem cột CurrentFlag được bao gồm trong chỉ mục không phân bổ. Lý do cho sự thay đổi đối với Quét chỉ mục theo cụm song song hiện đã rõ ràng:bộ xử lý truy vấn quyết định rằng việc quét bảng sẽ rẻ hơn so với việc thực hiện Tra cứu khóa.
Có, nhưng tại sao?
Tất cả đều rất kỳ lạ. Trong kế hoạch thực thi ban đầu, SQL Server có thể đọc dữ liệu cột bổ sung cần thiết để duy trì các chỉ mục không phân tán tại toán tử Xóa chỉ mục theo cụm. Giá trị cột CurrentFlag là cần thiết để duy trì chỉ mục đã lọc, vậy tại sao SQL Server không xử lý nó theo cách tương tự?
Câu trả lời ngắn gọn là nó có thể, nhưng chỉ Nếu chỉ mục được lọc được duy trì trong một toán tử Xóa chỉ mục riêng biệt. Chúng tôi có thể buộc điều này cho truy vấn hiện tại bằng cách sử dụng cờ theo dõi không có tài liệu 8790. Nếu không có cờ này, trình tối ưu hóa sẽ chọn duy trì từng chỉ mục trong một toán tử riêng biệt hay như một phần của hoạt động bảng cơ sở.
-- Forced wide update plan DELETE dbo.Data WHERE StartDate = '20140105' OPTION (QUERYTRACEON 8790);
Kế hoạch thực thi là quay lại tìm kiếm chỉ mục không có trong StartDate:
Index Seek chỉ trả về các giá trị RowID (không có thẻ CurrentFlag):
Và chỉ mục theo cụm Xóa lần đọc các cột cần thiết để duy trì các chỉ mục không phân tán, bao gồm cả CurrentFlag:
Dữ liệu này được ghi một cách háo hức vào một ống đệm bảng, dữ liệu này được phát lại cho mỗi chỉ mục cần duy trì. Cũng lưu ý toán tử Bộ lọc rõ ràng trước toán tử Xóa chỉ mục cho chỉ mục được lọc.
Một mẫu khác cần chú ý
Sự cố này không phải lúc nào cũng dẫn đến việc quét bảng thay vì tìm kiếm chỉ mục. Để xem ví dụ về điều này, hãy thêm một chỉ mục khác vào bảng thử nghiệm:
CREATE NONCLUSTERED INDEX IX_Data_SomeValue_CurrentFlag ON dbo.Data (SomeValue, CurrentFlag);
Lưu ý rằng chỉ mục này không được lọc và không liên quan đến cột Ngày bắt đầu. Bây giờ, hãy thử lại truy vấn xóa trong ngày:
DELETE dbo.Data WHERE StartDate = '20140104';
Trình tối ưu hóa hiện đưa ra con quái vật này:
Kế hoạch truy vấn này có yếu tố bất ngờ cao, nhưng nguyên nhân gốc rễ là như nhau. Cột CurrentFlag vẫn cần thiết, nhưng bây giờ trình tối ưu hóa chọn chiến lược giao điểm chỉ mục để lấy nó thay vì quét bảng. Việc sử dụng cờ theo dõi buộc một kế hoạch duy trì cho mỗi chỉ mục và sự tỉnh táo một lần nữa được khôi phục (sự khác biệt duy nhất là phát lại thêm ống chỉ để duy trì chỉ mục mới):
Chỉ các chỉ mục được lọc mới gây ra điều này
Sự cố này chỉ xảy ra nếu trình tối ưu hóa chọn duy trì một chỉ mục đã lọc trong toán tử Xóa chỉ mục theo cụm. Các chỉ mục không được lọc không bị ảnh hưởng, như ví dụ sau cho thấy. Bước đầu tiên là bỏ chỉ mục đã lọc:
DROP INDEX FIX_Data_SomeValue_CurrentFlag ON dbo.Data;
Bây giờ chúng ta cần viết truy vấn theo cách thuyết phục trình tối ưu hóa duy trì tất cả các chỉ mục trong Xóa chỉ mục theo cụm. Lựa chọn của tôi cho việc này là sử dụng một biến và một gợi ý để giảm kỳ vọng về số lượng hàng của trình tối ưu hóa:
-- All qualifying rows will be deleted DECLARE @Rows bigint = 9223372036854775807; -- Optimize the plan for deleting 100 rows DELETE TOP (@Rows) FROM dbo.Data OUTPUT Deleted.RowID, Deleted.SomeValue, Deleted.StartDate, Deleted.CurrentFlag WHERE StartDate = '20140106' OPTION (OPTIMIZE FOR (@Rows = 100));
Kế hoạch thực hiện là:
Cả hai chỉ mục không phân nhóm đều được duy trì bởi Xóa chỉ mục được phân nhóm:
Index Seek chỉ trả về RowID:
Các cột cần thiết cho việc duy trì chỉ mục được truy xuất nội bộ bởi toán tử xóa; những chi tiết này không được hiển thị trong đầu ra kế hoạch hiển thị (vì vậy danh sách đầu ra của toán tử xóa sẽ trống). Tôi đã thêm một OUTPUT
mệnh đề cho truy vấn để hiển thị Xóa chỉ mục theo cụm một lần nữa trả lại dữ liệu mà nó không nhận được trên đầu vào của nó:
Lời kết
Đây là một hạn chế khó giải quyết. Một mặt, chúng tôi thường không muốn sử dụng cờ theo dõi không có giấy tờ trong hệ thống sản xuất.
'Cách khắc phục' tự nhiên là thêm các cột cần thiết để duy trì chỉ mục đã lọc vào tất cả chỉ mục không phân tán có thể được sử dụng để xác định các hàng cần xóa. Đây không phải là một đề xuất quá hấp dẫn, theo một số quan điểm. Một giải pháp thay thế khác là hoàn toàn không sử dụng các chỉ mục đã lọc, nhưng điều đó cũng khó lý tưởng.
Cảm giác của tôi là trình tối ưu hóa truy vấn nên tự động xem xét một giải pháp thay thế duy trì trên mỗi chỉ mục cho các chỉ mục được lọc, nhưng lý do của nó dường như chưa hoàn thiện trong lĩnh vực này ngay bây giờ (và dựa trên kinh nghiệm học đơn giản hơn là định giá cho mỗi chỉ mục / mỗi hàng một cách hợp lý lựa chọn thay thế).
Để đưa ra một số con số xung quanh tuyên bố đó, kế hoạch quét chỉ mục nhóm song song được trình tối ưu hóa chọn có ở 5.5 đơn vị trong các bài kiểm tra của tôi. Cùng một truy vấn với cờ theo dõi ước tính chi phí là 1,4 các đơn vị. Với chỉ mục thứ ba tại chỗ, kế hoạch giao điểm chỉ mục song song do trình tối ưu hóa chọn có chi phí ước tính là 4,9 , trong khi kế hoạch cờ theo dõi có ở 2.7 đơn vị (tất cả các thử nghiệm trên SQL Server 2014 RTM CU1 bản dựng 12.0.2342 theo mô hình ước tính số lượng 120 và với cờ theo dõi 4199 được bật).
Tôi coi đây là hành vi cần được cải thiện. Bạn có thể bỏ phiếu đồng ý hoặc không đồng ý với tôi về mục Kết nối này.