Hầu hết các cơ sở dữ liệu nên sử dụng khóa ngoại để thực thi tính toàn vẹn tham chiếu (RI) bất cứ khi nào có thể. Tuy nhiên, quyết định này còn nhiều điều hơn là quyết định sử dụng các ràng buộc FK và tạo chúng. Có một số cân nhắc cần giải quyết để đảm bảo cơ sở dữ liệu của bạn hoạt động trơn tru nhất có thể.
Bài viết này đề cập đến một trong những cân nhắc không nhận được nhiều sự công khai:Để giảm thiểu chặn , bạn nên suy nghĩ cẩn thận về các chỉ mục được sử dụng để thực thi tính duy nhất ở phía mẹ của các mối quan hệ khóa ngoại đó.
Điều này áp dụng cho dù bạn đang sử dụng khóa đọc cam kết hoặc dựa trên phiên bản đọc cách ly ảnh chụp nhanh đã cam kết (RCSI). Cả hai đều có thể gặp sự cố bị chặn khi các mối quan hệ khóa ngoại được kiểm tra bởi công cụ SQL Server.
Trong chế độ cách ly ảnh chụp nhanh (SI), có một cảnh báo bổ sung. Cùng một vấn đề thiết yếu có thể dẫn đến thất bại giao dịch không mong muốn (và được cho là phi logic) do xung đột cập nhật rõ ràng.
Bài báo này gồm hai phần. Phần đầu tiên xem xét việc chặn khóa ngoại trong khóa cách ly đã đọc cam kết và đọc ảnh chụp nhanh đã cam kết. Phần thứ hai bao gồm các xung đột cập nhật liên quan trong cách cô lập ảnh chụp nhanh.
1. Chặn kiểm tra khóa nước ngoài
Trước tiên, hãy xem thiết kế chỉ mục có thể ảnh hưởng như thế nào khi quá trình chặn xảy ra do kiểm tra khóa ngoại.
Bản trình diễn sau sẽ được chạy trong đọc cam kết sự cô lập. Đối với SQL Server, mặc định là khóa đã cam kết đọc; Cơ sở dữ liệu Azure SQL sử dụng RCSI làm mặc định. Hãy thoải mái chọn bất kỳ cái nào bạn thích hoặc chạy các tập lệnh một lần cho mỗi cài đặt để tự mình xác minh rằng hành vi là giống nhau.
- Sử dụng khóa đã đọc cam kếtALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT OFF; - Hoặc sử dụng đã cam kết đọc lập phiên bản hàngTạo hai bảng được kết nối bằng mối quan hệ khóa ngoài:
CREATE TABLE dbo.Parent (ParentID integer NOT NULL, ParentNaturalKey varchar (10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey)); TẠO BẢNG dbo.Child (số nguyên ChildID NOT NULL, ChildNaturalKey varchar (10) NOT NULL, số nguyên ChildValue KHÔNG NULL, số nguyên ParentID NULL, CONSTRAINT [PK dbo.Child ChildID] KHÓA CHÍNH (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] DUY NHẤT (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] TÀI LIỆU THAM KHẢO TỪ KHÓA NGOẠI LỆ (ParentID) dbo.Parent (ParentID));Thêm một hàng vào bảng mẹ:
THIẾT LẬP MỨC ĐỘ CỰC KỲ GIAO DỊCH ĐÃ ĐỌC ĐƯỢC CAM KẾT; DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =100; INSERT dbo.Parent (ParentID, ParentNaturalKey, ParentValue) GIÁ TRỊ (@ParentID, @ParentNaturalKey, @ParentValue);Trên kết nối thứ hai , cập nhật thuộc tính bảng mẹ không phải khóa
ParentValue
bên trong một giao dịch, nhưng không cam kết nó chỉ được nêu ra:DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =200; BẮT ĐẦU GIAO DỊCH; CẬP NHẬT dbo.Parent SET ParentValue =@ParentValue WHERE ParentID =@ParentID;Vui lòng viết vị từ cập nhật bằng khóa tự nhiên nếu bạn thích, nó không tạo ra bất kỳ sự khác biệt nào cho mục đích hiện tại của chúng tôi.
Quay lại kết nối đầu tiên , cố gắng thêm một bản ghi con:
DECLARE @ChildID integer =101, @ChildNaturalKey varchar (10) ='CNK1', @ChildValue integer =999, @ParentID integer =1; CHÈN GIÁ TRỊ CỦA Dbo.Child (ChildID, ChildNaturalKey, ChildValue, ParentID) (@ChildID, @ChildNaturalKey, @ChildValue, @ParentID);Câu lệnh chèn này sẽ chặn , cho dù bạn chọn khóa hay lập phiên bản đọc cam kết cô lập cho thử nghiệm này.
Giải thích
Kế hoạch thực thi cho phần chèn bản ghi con là:
Sau khi chèn hàng mới vào bảng con, kế hoạch thực thi sẽ kiểm tra ràng buộc khóa ngoại. Việc kiểm tra sẽ bị bỏ qua nếu id cha đã chèn là null (đạt được thông qua một vị từ ‘chuyển qua’ ở phép nối bán bên trái). Trong trường hợp hiện tại, id cha đã thêm không phải là null, vì vậy, kiểm tra khóa ngoại là đã thực hiện.
SQL Server xác minh ràng buộc khóa ngoại bằng cách tìm kiếm một hàng phù hợp trong bảng mẹ. Công cụ không thể sử dụng lập phiên bản hàng để làm điều này - phải đảm bảo dữ liệu mà nó đang kiểm tra là dữ liệu được cam kết mới nhất , không phải một số phiên bản cũ. Công cụ đảm bảo điều này bằng cách thêm
READCOMMITTEDLOCK
nội bộ gợi ý bảng để kiểm tra khóa ngoại trên bảng mẹ.Kết quả cuối cùng là SQL Server cố gắng có được một khóa được chia sẻ trên hàng tương ứng trong bảng mẹ, hàng này chặn vì phiên khác giữ khóa chế độ độc quyền không tương thích do bản cập nhật chưa được cam kết.
Để rõ ràng hơn, gợi ý khóa bên trong chỉ áp dụng cho việc kiểm tra khóa ngoại. Phần còn lại của kế hoạch vẫn sử dụng RCSI, nếu bạn đã chọn việc triển khai mức cô lập đã cam kết đã đọc.
Tránh bị chặn
Cam kết hoặc khôi phục giao dịch đang mở trong phiên thứ hai, sau đó đặt lại môi trường thử nghiệm:
DROP TABLE NẾU TỒN TẠI dbo.Child, dbo.Parent;Tạo lại các bảng kiểm tra, nhưng lần này thay vì chấp nhận các giá trị mặc định, chúng tôi chọn đặt khóa chính là không bao gồm và ràng buộc duy nhất được nhóm lại:
CREATE TABLE dbo.Parent (ParentID integer NOT NULL, ParentNaturalKey varchar (10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] KHÓA CHÍNH KHÔNG ĐƯỢC ĐIỀU CHỈNH (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] ĐƯỢC ĐIỀU CHỈNH DUY NHẤT (ParentNaturalKey)); CREATE TABLE dbo.Child (ChildID integer NOT NULL, ChildNaturalKey varchar (10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] KHÓA CHÍNH KHÔNG ĐƯỢC ĐIỀU CHỈNH (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey ] DUY NHẤT ĐƯỢC ĐIỀU CHỈNH (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] TÀI LIỆU THAM KHẢO TỪ KHÓA NGOẠI LỆ (ParentID) dbo.Parent (ParentID));Thêm một hàng vào bảng mẹ như trước:
THIẾT LẬP MỨC ĐỘ CỰC KỲ GIAO DỊCH ĐÃ ĐỌC ĐƯỢC CAM KẾT; DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =100; INSERT dbo.Parent (ParentID, ParentNaturalKey, ParentValue) GIÁ TRỊ (@ParentID, @ParentNaturalKey, @ParentValue);Trong phiên thứ hai , chạy bản cập nhật mà không cần thực hiện lại. Tôi đang sử dụng khóa tự nhiên lần này chỉ để đa dạng - điều đó không quan trọng đối với kết quả. Sử dụng lại khóa thay thế nếu bạn muốn.
DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =200; CẬP NHẬT GIAO DỊCH BẮT ĐẦU dbo.Parent SET ParentValue =@ParentValue WHERE ParentNaturalKey =@ParentNaturalKey;Bây giờ hãy chạy lại phần chèn con vào phiên đầu tiên :
DECLARE @ChildID integer =101, @ChildNaturalKey varchar (10) ='CNK1', @ChildValue integer =999, @ParentID integer =1; CHÈN GIÁ TRỊ CỦA Dbo.Child (ChildID, ChildNaturalKey, ChildValue, ParentID) (@ChildID, @ChildNaturalKey, @ChildValue, @ParentID);Lần này chèn con không chặn . Điều này đúng cho dù bạn đang chạy trong chế độ cô lập đã đọc cam kết dựa trên khóa hoặc dựa trên phiên bản. Đó không phải là lỗi đánh máy hoặc lỗi:RCSI không có gì khác biệt ở đây.
Giải thích
Kế hoạch thực thi cho phần chèn bản ghi con lần này hơi khác một chút:
Mọi thứ vẫn giống như trước đây (bao gồm cả
READCOMMITTEDLOCK
ẩn gợi ý) ngoại trừ kiểm tra khóa ngoại hiện sử dụng nonclustered chỉ mục duy nhất thực thi khóa chính của bảng cha. Trong thử nghiệm đầu tiên, chỉ mục này đã được nhóm lại.Vậy tại sao chúng tôi không bị chặn lần này?
Bản cập nhật bảng mẹ chưa được cam kết trong phiên thứ hai có khóa riêng trên chỉ mục được phân nhóm hàng vì bảng cơ sở đang được sửa đổi. Thay đổi đối với
ParentValue
cột không ảnh hưởng đến khóa chính không phân biệt trênParentID
, do đó hàng của chỉ mục không phân tán không bị khóa .Do đó, việc kiểm tra khóa ngoại có thể nhận được khóa chia sẻ cần thiết trên chỉ mục khóa chính không phân biệt mà không tranh cãi và việc chèn bảng con thành công ngay lập tức .
Khi tài nguyên chính được nhóm lại, việc kiểm tra khóa ngoại cần một khóa chia sẻ trên cùng một tài nguyên (hàng chỉ mục được phân cụm) đã được khóa riêng bởi câu lệnh cập nhật.
Hành vi có thể đáng ngạc nhiên, nhưng nó không phải là lỗi . Việc cung cấp cho khóa ngoại kiểm tra phương pháp truy cập được tối ưu hóa của riêng nó sẽ tránh được sự tranh cãi về khóa không cần thiết về mặt logic. Không cần chặn tra cứu khóa ngoại vì
ParentID
thuộc tính không bị ảnh hưởng bởi bản cập nhật đồng thời.2. Xung đột cập nhật có thể tránh được
Nếu bạn chạy các bài kiểm tra trước đó ở cấp độ Cách ly Ảnh chụp nhanh (SI), kết quả sẽ giống nhau. Chèn hàng con khối khi khóa được tham chiếu được thực thi bởi chỉ mục được phân nhóm và không chặn khi thực thi khóa sử dụng một không bao gồm chỉ mục duy nhất.
Tuy nhiên, có một sự khác biệt tiềm năng quan trọng khi sử dụng SI. Trong cách ly được cam kết đọc (khóa hoặc RCSI), chèn hàng con cuối cùng cũng thành công sau khi cập nhật trong phiên thứ hai cam kết hoặc quay trở lại. Sử dụng SI, có rủi ro giao dịch hủy bỏ do xung đột cập nhật rõ ràng.
Điều này khó chứng minh hơn một chút vì giao dịch chụp nhanh không bắt đầu bằng
BEGIN TRANSACTION
câu lệnh - nó bắt đầu với quyền truy cập dữ liệu người dùng đầu tiên sau thời điểm đó.Tập lệnh sau đây thiết lập trình diễn SI, với một bảng giả bổ sung chỉ được sử dụng để đảm bảo giao dịch chụp nhanh đã thực sự bắt đầu. Nó sử dụng biến thể thử nghiệm trong đó khóa chính được tham chiếu được thực thi bằng cách sử dụng một nhóm duy nhất chỉ mục (mặc định):
ALTER DATABASE HIỆN TẠI SET ALLOW_SNAPSHOT_ISOLATION ON; GODROP TABLE IF TỒN TẠI dbo.Dummy, dbo.Child, dbo.Parent; GOCREATE TABLE dbo.Dummy (x integer NULL); TẠO BẢNG dbo.Parent (ParentID integer NOT NULL, ParentNaturalKey varchar (10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey) UNIQUE (ParentNaturalKey) ); TẠO BẢNG dbo.Child (số nguyên ChildID NOT NULL, ChildNaturalKey varchar (10) NOT NULL, số nguyên ChildValue KHÔNG NULL, số nguyên ParentID NULL, CONSTRAINT [PK dbo.Child ChildID] KHÓA CHÍNH (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] DUY NHẤT (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] TÀI LIỆU THAM KHẢO TỪ KHÓA NGOẠI LỆ (ParentID) dbo.Parent (ParentID));Chèn hàng mẹ:
DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =100; INSERT dbo.Parent (ParentID, ParentNaturalKey, ParentValue) GIÁ TRỊ (@ParentID, @ParentNaturalKey, @ParentValue);Vẫn trong phiên đầu tiên , bắt đầu giao dịch chụp nhanh:
- Phiên 1 GIAO DỊCH CẤP ĐỘ GIAO DỊCH SET SNAPSHOT; BẮT ĐẦU GIAO DỊCH; - Đảm bảo giao dịch chụp nhanh được bắt đầuSELECT COUNT_BIG (*) FROM dbo.Dummy AS D;Trong phiên thứ hai (chạy ở bất kỳ mức cô lập nào):
- Session 2DECLARE @ParentID integer =1, @ParentNaturalKey varchar (10) ='PNK1', @ParentValue integer =200; BẮT ĐẦU GIAO DỊCH; CẬP NHẬT dbo.Parent SET ParentValue =@ParentValue WHERE ParentID =@ParentID;Đang cố chèn hàng con trong khối phiên đầu tiên như mong đợi:
- Session 1DECLARE @ChildID integer =101, @ChildNaturalKey varchar (10) ='CNK1', @ChildValue integer =999, @ParentID integer =1; CHÈN GIÁ TRỊ CỦA Dbo.Child (ChildID, ChildNaturalKey, ChildValue, ParentID) (@ChildID, @ChildNaturalKey, @ChildValue, @ParentID);Sự khác biệt xảy ra khi chúng tôi kết thúc giao dịch trong phiên thứ hai. Nếu chúng tôi quay trở lại , chèn hàng con của phiên đầu tiên hoàn tất thành công .
Nếu thay vào đó chúng tôi cam kết giao dịch mở:
- Phiên 2COMMIT GIAO DỊCH;Phiên đầu tiên báo cáo một xung đột cập nhật và quay trở lại:
Giải thích
Xung đột cập nhật này xảy ra mặc dù thực tế là khóa ngoại đang được xác thực không được thay đổi bởi bản cập nhật của phiên thứ hai.
Lý do về cơ bản giống như trong tập hợp các bài kiểm tra đầu tiên. Khi chỉ mục được nhóm được sử dụng để thực thi khóa được tham chiếu, giao dịch ảnh chụp nhanh gặp một hàng đã được sửa đổi kể từ khi nó bắt đầu. Điều này không được phép trong chế độ cô lập ảnh chụp nhanh.
Khi khóa được thực thi bằng cách sử dụng chỉ mục không phân tán , giao dịch ảnh chụp nhanh chỉ nhìn thấy hàng chỉ mục không hợp nhất không được sửa đổi, do đó không có chặn và không phát hiện thấy "xung đột cập nhật".
Có nhiều trường hợp khác trong đó tính năng cô lập ảnh chụp nhanh có thể báo cáo xung đột cập nhật không mong muốn hoặc các lỗi khác. Xem bài viết trước của tôi để biết các ví dụ.
Kết luận
Có nhiều cân nhắc cần tính đến khi chọn chỉ mục được phân nhóm cho bảng lưu trữ hàng. Các vấn đề được mô tả ở đây chỉ là một yếu tố khác để đánh giá.
Điều này đặc biệt đúng nếu bạn đang sử dụng tính năng cô lập ảnh chụp nhanh. Không ai thích giao dịch bị hủy bỏ , đặc biệt là một điều được cho là phi logic. Nếu bạn đang sử dụng RCSI, thì chặn khi đọc để xác thực khóa ngoại có thể không mong muốn và có thể dẫn đến bế tắc.
Mặc định cho một
PRIMARY KEY
hạn chế là tạo chỉ mục hỗ trợ của nó dưới dạng nhóm , trừ khi một chỉ mục hoặc ràng buộc khác trong định nghĩa bảng rõ ràng về việc được phân cụm thay thế. Đó là một thói quen tốt để rõ ràng về ý định thiết kế của bạn, vì vậy tôi khuyến khích bạn viếtCLUSTERED
hoặcNONCLUSTERED
mọi lúc.Các chỉ mục trùng lặp?
Có thể đôi khi bạn nghiêm túc xem xét, vì những lý do chính đáng, có một chỉ mục nhóm và chỉ mục không hợp nhất có cùng (các) khóa .
Mục đích có thể là cung cấp quyền truy cập đọc tối ưu cho các truy vấn của người dùng thông qua clustered lập chỉ mục (tránh tra cứu khóa), đồng thời bật xác thực chặn tối thiểu (và xung đột cập nhật) cho khóa ngoại thông qua nonclustered nhỏ gọn chỉ mục như được hiển thị ở đây.
Điều này có thể đạt được, nhưng có một số khó khăn cần chú ý:
Cung cấp nhiều hơn một chỉ mục mục tiêu phù hợp, SQL Server không cung cấp cách để đảm bảo chỉ mục nào sẽ được sử dụng để thực thi khóa ngoại.
Dan Guzman đã ghi lại những quan sát của mình trong Bí mật về ràng buộc chỉ mục khóa nước ngoài, nhưng những quan sát này có thể chưa đầy đủ và trong mọi trường hợp là không có tài liệu và vì vậy có thể thay đổi .
Bạn có thể giải quyết vấn đề này bằng cách đảm bảo chỉ có một mục tiêu chỉ mục tại thời điểm khóa ngoại được tạo, nhưng nó làm phức tạp mọi thứ và dẫn đến các vấn đề trong tương lai nếu ràng buộc khóa ngoại bị loại bỏ và được tạo lại.
Nếu bạn sử dụng cú pháp khóa ngoại viết tắt, SQL Server sẽ chỉ ràng buộc ràng buộc với khóa chính , cho dù nó là không hợp nhất hay theo nhóm.
Đoạn mã sau thể hiện sự khác biệt sau:
TẠO BẢNG dbo.Parent (ParentID integer NOT NULL UNIQUE CLUSTERED); - Cú pháp viết tắt (không tường minh) - Không thành công với lỗi 1773CREATE TABLE dbo.Child (ChildID integer NOT NULL PRIMARY KEY NONCLUSTERED, ParentID integer NOT NULL REFERENCES dbo.Parent); - Cú pháp rõ ràng thành côngCREATE TABLE dbo.Child (Số nguyên ChildID KHÔNG NULL CHÍNH CHỦNG KHÔNG ĐƯỢC ĐIỀU CHỈNH, Số nguyên ParentID KHÔNG ĐỦ TÀI LIỆU THAM KHẢO dbo.Parent (ParentID));Mọi người đã quen với việc bỏ qua phần lớn xung đột đọc-ghi theo RCSI và SI. Hy vọng rằng bài viết này đã cung cấp cho bạn thêm điều gì đó để suy nghĩ khi triển khai thiết kế vật lý cho các bảng có liên quan đến khóa ngoại.