Chiến lược chung mà công cụ cơ sở dữ liệu SQL Server sử dụng để giữ một chế độ xem được lập chỉ mục được đồng bộ hóa với các bảng cơ sở của nó - mà tôi đã mô tả chi tiết hơn trong bài đăng cuối cùng của mình - là thực hiện bảo trì gia tăng của chế độ xem bất cứ khi nào thao tác thay đổi dữ liệu xảy ra đối với một trong các bảng được tham chiếu trong chế độ xem. Theo nghĩa rộng, ý tưởng là:
- Thu thập thông tin về các thay đổi của bảng cơ sở
- Áp dụng các phép chiếu, bộ lọc và phép nối được xác định trong chế độ xem
- Tổng hợp các thay đổi cho mỗi khóa nhóm chế độ xem được lập chỉ mục
- Quyết định xem mỗi thay đổi sẽ dẫn đến chèn, cập nhật hoặc xóa đối với chế độ xem
- Tính toán các giá trị để thay đổi, thêm hoặc xóa trong chế độ xem
- Áp dụng các thay đổi về chế độ xem
Hoặc, ngắn gọn hơn nữa (mặc dù có nguy cơ bị đơn giản hóa hoàn toàn):
- Tính toán các hiệu ứng xem gia tăng của các sửa đổi dữ liệu ban đầu;
- Áp dụng những thay đổi đó cho chế độ xem
Đây thường là một chiến lược hiệu quả hơn nhiều so với việc xây dựng lại toàn bộ chế độ xem sau mỗi lần thay đổi dữ liệu cơ bản (tùy chọn an toàn nhưng chậm), nhưng nó dựa vào logic cập nhật gia tăng là chính xác cho mọi thay đổi dữ liệu có thể hình dung, dựa trên mọi định nghĩa chế độ xem được lập chỉ mục có thể có.
Như tiêu đề cho thấy, bài viết này liên quan đến một trường hợp thú vị trong đó logic cập nhật tăng dần bị hỏng, dẫn đến chế độ xem được lập chỉ mục bị hỏng không còn khớp với dữ liệu cơ bản. Trước khi tự tìm ra lỗi, chúng tôi cần nhanh chóng xem xét các tổng hợp vô hướng và vectơ.
Tổng hợp vô hướng và vectơ
Trong trường hợp bạn không quen thuộc với thuật ngữ này, có hai loại tổng hợp. Tổng hợp được liên kết với mệnh đề GROUP BY (ngay cả khi nhóm theo danh sách trống) được gọi là tổng hợp vectơ . Tổng hợp không có mệnh đề GROUP BY được gọi là tổng hợp vô hướng .
Trong khi tổng hợp vectơ được đảm bảo tạo ra một hàng đầu ra duy nhất cho mỗi nhóm có trong tập dữ liệu, thì tổng hợp vô hướng có một chút khác biệt. Tổng hợp vô hướng luôn luôn tạo ra một hàng đầu ra duy nhất, ngay cả khi bộ đầu vào trống.
Ví dụ về tổng hợp vectơ
Ví dụ AdventureWorks sau đây tính toán hai tổng hợp vectơ (một tổng và một số) trên một tập hợp đầu vào trống:
-- There are no TransactionHistory records for ProductID 848 -- Vector aggregate produces no output rows SELECT COUNT_BIG(*) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848 GROUP BY TH.ProductID; SELECT SUM(TH.Quantity) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848 GROUP BY TH.ProductID;
Các truy vấn này tạo ra kết quả sau (không có hàng):
Kết quả giống nhau, nếu chúng ta thay thế mệnh đề GROUP BY bằng một tập hợp trống (yêu cầu SQL Server 2008 trở lên):
-- Equivalent vector aggregate queries with -- an empty GROUP BY column list -- (SQL Server 2008 and later required) -- Still no output rows SELECT COUNT_BIG(*) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848 GROUP BY (); SELECT SUM(TH.Quantity) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848 GROUP BY ();
Các kế hoạch thực hiện cũng giống hệt nhau trong cả hai trường hợp. Đây là kế hoạch thực thi cho truy vấn đếm:
Không có hàng nào được nhập vào Tổng hợp luồng và không có hàng nào ra ngoài. Kế hoạch thực thi tổng có dạng như sau:
Một lần nữa, không có hàng nào vào tổng thể và không có hàng nào ra ngoài. Tất cả những thứ đơn giản tốt cho đến nay.
Tổng hợp vô hướng
Bây giờ, hãy xem điều gì sẽ xảy ra nếu chúng ta xóa hoàn toàn mệnh đề GROUP BY khỏi các truy vấn:
-- Scalar aggregate (no GROUP BY clause) -- Returns a single output row from an empty input SELECT COUNT_BIG(*) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848; SELECT SUM(TH.Quantity) FROM Production.TransactionHistory AS TH WHERE TH.ProductID = 848;
Thay vì kết quả trống, tổng hợp COUNT tạo ra số 0 và SUM trả về giá trị NULL:
Kế hoạch thực thi đếm xác nhận rằng không có hàng đầu vào nào tạo ra một hàng đầu ra từ Tổng hợp luồng:
Kế hoạch thực hiện tổng thậm chí còn thú vị hơn:
Thuộc tính Tổng hợp luồng hiển thị tổng số đang được tính ngoài tổng mà chúng tôi yêu cầu:
Toán tử Vô hướng tính toán mới được sử dụng để trả về NULL nếu số hàng mà Tổng hợp luồng nhận được bằng 0, nếu không, nó trả về tổng dữ liệu gặp phải:
Điều này có vẻ hơi kỳ lạ, nhưng đây là cách nó hoạt động:
- Một tổng hợp vectơ của không hàng trả về không hàng;
- Tổng hợp vô hướng luôn tạo ra chính xác một hàng đầu ra, ngay cả đối với đầu vào trống;
- Số vô hướng của các hàng không bằng 0; và
- Tổng vô hướng của các hàng không là NULL (không phải bằng 0).
Điểm quan trọng cho mục đích hiện tại của chúng tôi là tổng hợp vô hướng luôn tạo ra một hàng đầu ra duy nhất, ngay cả khi nó có nghĩa là tạo ra một hàng từ con số không. Ngoài ra, tổng vô hướng của các hàng không là NULL, không phải bằng không.
Nhân tiện, những hành vi này đều "đúng". Mọi thứ diễn ra theo cách của chúng vì Chuẩn SQL ban đầu không xác định hành vi của các tổng hợp vô hướng, để nó phụ thuộc vào việc triển khai. SQL Server duy trì triển khai ban đầu của nó vì lý do tương thích ngược. Tổng hợp vectơ luôn có các hành vi được xác định rõ ràng.
Lượt xem được lập chỉ mục và Tổng hợp vectơ
Bây giờ, hãy xem xét một chế độ xem được lập chỉ mục đơn giản kết hợp một số tổng hợp (vectơ):
CREATE TABLE dbo.T1 ( GroupID integer NOT NULL, Value integer NOT NULL ); GO INSERT dbo.T1 (GroupID, Value) VALUES (1, 1), (1, 2), (2, 3), (2, 4), (2, 5), (3, 6); GO CREATE VIEW dbo.IV WITH SCHEMABINDING AS SELECT T1.GroupID, GroupSum = SUM(T1.Value), RowsInGroup = COUNT_BIG(*) FROM dbo.T1 AS T1 GROUP BY T1.GroupID; GO CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.IV (GroupID);
Các truy vấn sau đây hiển thị nội dung của bảng cơ sở, kết quả của việc truy vấn chế độ xem được lập chỉ mục và kết quả của việc chạy truy vấn chế độ xem trên bảng bên dưới chế độ xem:
-- Sample data SELECT * FROM dbo.T1 AS T1; -- Indexed view contents SELECT * FROM dbo.IV AS IV WITH (NOEXPAND); -- Underlying view query results SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS);
Kết quả là:
Như mong đợi, chế độ xem được lập chỉ mục và truy vấn cơ bản trả về kết quả chính xác như nhau. Các kết quả sẽ tiếp tục được đồng bộ hóa sau bất kỳ và tất cả các thay đổi có thể có đối với bảng cơ sở T1. Để tự nhắc nhở chúng ta về cách tất cả điều này hoạt động, hãy xem xét trường hợp đơn giản là thêm một hàng mới vào bảng cơ sở:
INSERT dbo.T1 (GroupID, Value) VALUES (4, 100);
Kế hoạch thực thi cho phần chèn này chứa tất cả logic cần thiết để giữ cho chế độ xem được lập chỉ mục được đồng bộ hóa:
Các hoạt động chính trong kế hoạch là:
- Tổng hợp Luồng tính toán các thay đổi cho mỗi khóa chế độ xem được lập chỉ mục
- Tham gia bên ngoài vào chế độ xem liên kết tóm tắt thay đổi với hàng chế độ xem mục tiêu, nếu có
- Phạm vi tính toán quyết định xem mỗi thay đổi sẽ yêu cầu chèn, cập nhật hoặc xóa đối với chế độ xem và tính toán các giá trị cần thiết.
- Toán tử cập nhật chế độ xem thực hiện vật lý mỗi thay đổi đối với chỉ mục nhóm chế độ xem.
Có một số khác biệt về kế hoạch đối với các hoạt động thay đổi khác nhau so với bảng cơ sở (ví dụ:cập nhật và xóa), nhưng ý tưởng rộng rãi đằng sau việc giữ đồng bộ hóa chế độ xem vẫn giống nhau:tổng hợp các thay đổi cho mỗi khóa chế độ xem, tìm hàng chế độ xem nếu nó tồn tại, sau đó thực hiện kết hợp các thao tác chèn, cập nhật và xóa trên chỉ mục chế độ xem nếu cần.
Bất kể bạn thực hiện thay đổi gì đối với bảng cơ sở trong ví dụ này, chế độ xem được lập chỉ mục sẽ vẫn được đồng bộ hóa chính xác - các truy vấn NOEXPAND và EXPAND VIEWS ở trên sẽ luôn trả về cùng một tập hợp kết quả. Đây là cách mọi thứ luôn hoạt động.
Lượt xem được lập chỉ mục và Tổng hợp vô hướng
Bây giờ hãy thử ví dụ này, trong đó chế độ xem được lập chỉ mục sử dụng tổng hợp vô hướng (không có mệnh đề GROUP BY trong chế độ xem):
DROP VIEW dbo.IV; DROP TABLE dbo.T1; GO CREATE TABLE dbo.T1 ( GroupID integer NOT NULL, Value integer NOT NULL ); GO INSERT dbo.T1 (GroupID, Value) VALUES (1, 1), (1, 2), (2, 3), (2, 4), (2, 5), (3, 6); GO CREATE VIEW dbo.IV WITH SCHEMABINDING AS SELECT TotalSum = SUM(T1.Value), NumRows = COUNT_BIG(*) FROM dbo.T1 AS T1; GO CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.IV (NumRows);
Đây là một chế độ xem được lập chỉ mục hoàn toàn hợp pháp; không gặp lỗi khi tạo nó. Tuy nhiên, có một manh mối cho thấy chúng ta có thể đang làm điều gì đó hơi kỳ lạ:khi đến lúc hiện thực hóa chế độ xem bằng cách tạo chỉ mục nhóm duy nhất được yêu cầu, không có cột rõ ràng nào để chọn làm khóa. Thông thường, tất nhiên, chúng tôi sẽ chọn nhóm các cột từ mệnh đề GROUP BY của chế độ xem.
Tập lệnh trên chọn cột NumRows một cách tùy ý. Lựa chọn đó không quan trọng. Hãy thoải mái tạo chỉ mục nhóm duy nhất theo cách bạn chọn. Chế độ xem sẽ luôn chứa chính xác một hàng bởi vì các tập hợp vô hướng, do đó không có cơ hội vi phạm khóa duy nhất. Theo nghĩa đó, lựa chọn khóa chỉ mục chế độ xem là thừa, nhưng vẫn bắt buộc.
Sử dụng lại các truy vấn kiểm tra từ ví dụ trước, chúng ta có thể thấy rằng chế độ xem được lập chỉ mục hoạt động chính xác:
SELECT * FROM dbo.T1 AS T1; SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS); SELECT * FROM dbo.IV AS IV WITH (NOEXPAND);
Chèn một hàng mới vào bảng cơ sở (như chúng ta đã làm với chế độ xem được lập chỉ mục tổng hợp vectơ) cũng tiếp tục hoạt động chính xác:
INSERT dbo.T1 (GroupID, Value) VALUES (4, 100);
Kế hoạch thực hiện tương tự, nhưng không hoàn toàn giống nhau:
Sự khác biệt chính là:
- Tính toán Vô hướng mới này có cùng những lý do giống như khi chúng tôi so sánh kết quả tổng hợp vectơ và vô hướng trước đó:nó đảm bảo trả về tổng NULL (thay vì 0) nếu tổng hợp hoạt động trên một tập hợp trống. Đây là hành vi bắt buộc đối với tổng vô hướng của không có hàng.
- Tham gia bên ngoài được thấy trước đây đã được thay thế bằng liên kết bên trong. Sẽ luôn có chính xác một hàng trong chế độ xem được lập chỉ mục (do tổng hợp vô hướng) vì vậy không có vấn đề gì về việc cần một phép nối bên ngoài để kiểm tra xem một hàng trong chế độ xem có khớp hay không. Một hàng hiện diện trong dạng xem luôn đại diện cho toàn bộ tập dữ liệu. Phép nối bên trong này không có vị từ, vì vậy về mặt kỹ thuật, nó là phép nối chéo (với một bảng có một hàng được đảm bảo).
- Các toán tử Sắp xếp và Thu gọn có mặt vì các lý do kỹ thuật được đề cập trong bài viết trước của tôi về bảo trì chế độ xem được lập chỉ mục. Chúng không ảnh hưởng đến hoạt động chính xác của việc duy trì chế độ xem được lập chỉ mục tại đây.
Trên thực tế, nhiều loại hoạt động thay đổi dữ liệu khác nhau có thể được thực hiện thành công đối với bảng cơ sở T1 trong ví dụ này; các hiệu ứng sẽ được phản ánh chính xác trong chế độ xem được lập chỉ mục. Tất cả các thao tác thay đổi sau đây đối với bảng cơ sở đều có thể được thực hiện trong khi vẫn giữ cho chế độ xem được lập chỉ mục chính xác:
- Xóa các hàng hiện có
- Cập nhật các hàng hiện có
- Chèn các hàng mới
Đây có vẻ như là một danh sách toàn diện, nhưng không phải vậy.
Lỗi được tiết lộ
Vấn đề khá phức tạp và liên quan (như bạn mong đợi) đến các hành vi khác nhau của tổng hợp vectơ và tổng hợp vô hướng. Điểm mấu chốt là một tập hợp vô hướng sẽ luôn tạo ra một hàng đầu ra, ngay cả khi nó không nhận được hàng nào trên đầu vào của nó và tổng vô hướng của một tập hợp rỗng là NULL, không phải bằng không.
Để gây ra sự cố, tất cả những gì chúng tôi cần làm là chèn hoặc xóa không có hàng nào trong bảng cơ sở.
Tuyên bố đó không quá điên rồ như thoạt nghe.
Vấn đề là một truy vấn chèn hoặc xóa ảnh hưởng đến không có hàng nào trong bảng cơ sở sẽ vẫn cập nhật chế độ xem, bởi vì Tổng hợp dòng vô hướng trong phần duy trì chế độ xem được lập chỉ mục của kế hoạch truy vấn sẽ tạo ra một hàng đầu ra ngay cả khi nó được trình bày mà không có đầu vào. Phạm vi tính toán theo sau Tổng hợp luồng cũng sẽ tạo ra tổng NULL khi số hàng bằng không.
Tập lệnh sau minh họa lỗi đang hoạt động:
-- So we can undo BEGIN TRANSACTION; -- Show the starting state SELECT * FROM dbo.T1 AS T1; SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS); SELECT * FROM dbo.IV AS IV WITH (NOEXPAND); -- A table variable intended to hold new base table rows DECLARE @NewRows AS table (GroupID integer NOT NULL, Value integer NOT NULL); -- Insert to the base table (no rows in the table variable!) INSERT dbo.T1 SELECT NR.GroupID,NR.Value FROM @NewRows AS NR; -- Show the final state SELECT * FROM dbo.T1 AS T1; SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS); SELECT * FROM dbo.IV AS IV WITH (NOEXPAND); -- Undo the damage ROLLBACK TRANSACTION;
Đầu ra của tập lệnh đó được hiển thị bên dưới:
Trạng thái cuối cùng của cột Tổng số của chế độ xem được lập chỉ mục không khớp với truy vấn chế độ xem bên dưới hoặc dữ liệu bảng cơ sở. Tổng NULL đã làm hỏng chế độ xem, có thể được xác nhận bằng cách chạy DBCC CHECKTABLE (trên chế độ xem được lập chỉ mục).
Kế hoạch thực thi chịu trách nhiệm về tham nhũng được trình bày dưới đây:
Phóng to hiển thị đầu vào không có hàng cho Tổng hợp luồng và đầu ra một hàng:
Nếu bạn muốn thử tập lệnh tham nhũng ở trên bằng cách xóa thay vì chèn, đây là một ví dụ:
-- No rows match this predicate DELETE dbo.T1 WHERE Value BETWEEN 10 AND 50;
Việc xóa không ảnh hưởng đến các hàng trong bảng cơ sở, nhưng vẫn thay đổi cột tổng của chế độ xem được lập chỉ mục thành NULL.
Tổng quát hóa lỗi
Bạn có thể đưa ra bất kỳ số lượng chèn và xóa truy vấn bảng cơ sở nào ảnh hưởng đến không có hàng nào và gây ra hỏng chế độ xem được lập chỉ mục này. Tuy nhiên, vấn đề cơ bản tương tự cũng áp dụng cho một loại vấn đề rộng hơn là chỉ chèn và xóa không ảnh hưởng đến hàng cơ sở của bảng.
Ví dụ:có thể tạo ra cùng một lỗi bằng cách sử dụng một bộ chèn does thêm hàng vào bảng cơ sở. Thành phần thiết yếu là không có hàng nào được thêm vào sẽ đủ điều kiện cho chế độ xem . Điều này sẽ dẫn đến đầu vào trống cho Tổng hợp luồng và đầu ra hàng NULL gây ra lỗi từ Vô hướng tính toán sau.
Một cách để đạt được điều này là bao gồm mệnh đề WHERE trong dạng xem từ chối một số hàng của bảng cơ sở:
ALTER VIEW dbo.IV WITH SCHEMABINDING AS SELECT TotalSum = SUM(T1.Value), NumRows = COUNT_BIG(*) FROM dbo.T1 AS T1 WHERE -- New! T1.GroupID BETWEEN 1 AND 3; GO CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.IV (NumRows);
Với hạn chế mới đối với ID nhóm được bao gồm trong chế độ xem, phần chèn sau sẽ thêm các hàng vào bảng cơ sở, nhưng vẫn làm hỏng chế độ xem được lập chỉ mục sẽ có tổng NULL:
-- So we can undo BEGIN TRANSACTION; -- Show the starting state SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS); SELECT * FROM dbo.IV AS IV WITH (NOEXPAND); -- The added row does not qualify for the view INSERT dbo.T1 (GroupID, Value) VALUES (4, 100); -- Show the final state SELECT * FROM dbo.IV AS IV OPTION (EXPAND VIEWS); SELECT * FROM dbo.IV AS IV WITH (NOEXPAND); -- Undo the damage ROLLBACK TRANSACTION;
Kết quả hiển thị lỗi chỉ mục quen thuộc hiện nay:
Một hiệu ứng tương tự có thể được tạo ra bằng cách sử dụng dạng xem có chứa một hoặc nhiều phép nối bên trong. Miễn là các hàng được thêm vào bảng cơ sở bị từ chối (ví dụ như không tham gia được), Tổng hợp luồng sẽ không nhận được hàng nào, Phân hướng tính toán sẽ tạo ra tổng NULL và chế độ xem được lập chỉ mục có thể bị hỏng.
Lời kết
Sự cố này không xảy ra đối với các truy vấn cập nhật (ít nhất là theo như tôi có thể nói) nhưng điều này có vẻ do ngẫu nhiên hơn là do thiết kế - Tổng hợp luồng có vấn đề vẫn xuất hiện trong các kế hoạch cập nhật có khả năng dễ bị tấn công, nhưng Phân hướng tính toán tạo ra tổng NULL không được thêm vào (hoặc có thể được tối ưu hóa đi). Vui lòng cho tôi biết nếu bạn quản lý để tạo lại lỗi bằng cách sử dụng truy vấn cập nhật.
Cho đến khi lỗi này được sửa (hoặc, có lẽ, các tổng hợp vô hướng không được phép trong các chế độ xem được lập chỉ mục), hãy hết sức cẩn thận về việc sử dụng các tổng hợp trong một chế độ xem được lập chỉ mục mà không có mệnh đề GROUP BY.
Bài viết này được thúc đẩy bởi một mục Kết nối do Vladimir Moldovanenko gửi, người đủ tốt để để lại nhận xét về một bài đăng blog cũ của tôi (liên quan đến việc hỏng chế độ xem được lập chỉ mục khác do tuyên bố MERGE gây ra). Vladimir đang sử dụng tổng hợp vô hướng trong chế độ xem được lập chỉ mục vì lý do hợp lý, vì vậy đừng quá vội vàng đánh giá lỗi này là một trường hợp khó mà bạn sẽ không bao giờ gặp phải trong môi trường sản xuất! Tôi cảm ơn Vladimir vì đã thông báo cho tôi về mục Kết nối của anh ấy.