Các bảng có phân cấp: tạo một ràng buộc để ngăn chặn tính tuần hoàn thông qua các khóa ngoại


10

Giả sử chúng ta có một bảng có ràng buộc khóa ngoại đối với chính nó, như sau:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

INSERT INTO Foo (FooId, ParentFooId) 
VALUES (1, NULL), (2, 1), (3, 2)

UPDATE Foo SET ParentFooId = 3 WHERE FooId = 1

Bảng này sẽ có các hồ sơ sau:

FooId  ParentFooId
-----  -----------
1      3
2      1
3      2

Có những trường hợp kiểu thiết kế này có thể có ý nghĩa (ví dụ như mối quan hệ "nhân viên và sếp-nhân viên" điển hình), và trong mọi trường hợp: Tôi đang ở trong tình huống tôi có điều này trong lược đồ của mình.

Kiểu thiết kế này không may cho phép tính tuần hoàn trong các bản ghi dữ liệu, như trong ví dụ trên.

Câu hỏi của tôi là:

  1. thể viết một ràng buộc kiểm tra điều này? và
  2. khả thi để viết một ràng buộc kiểm tra điều này? (nếu chỉ cần đến một độ sâu nhất định)

Đối với phần (2) của câu hỏi này có thể có liên quan để đề cập rằng tôi chỉ mong đợi hàng trăm hoặc có thể trong một số trường hợp hàng ngàn hồ sơ trong bảng của tôi, thông thường không được lồng sâu hơn khoảng 5 đến 10 cấp.

Tái bút Máy chủ MS SQL 2008


Cập nhật ngày 14 tháng 3 năm 2012
Có một số câu trả lời tốt. Bây giờ tôi đã chấp nhận một trong đó giúp tôi hiểu khả năng / tính khả thi được đề cập. Tuy nhiên, có một số câu trả lời tuyệt vời khác, một số có đề xuất thực hiện, vì vậy nếu bạn đến đây với cùng một câu hỏi, hãy xem tất cả các câu trả lời;)

Câu trả lời:


6

Bạn đang sử dụng mô hình Danh sách điều chỉnh , nơi khó thực thi một ràng buộc như vậy.

Bạn có thể kiểm tra mô hình Nested Set , trong đó chỉ có thể phân cấp thứ bậc thực sự (không có đường dẫn tròn). Điều này có những nhược điểm khác, như Chèn / Cập nhật chậm.


+1 liên kết tuyệt vời, và tôi ước tôi có thể đi và thử Mô hình tập hợp lồng nhau, và sau đó chấp nhận câu trả lời này là câu trả lời phù hợp với tôi.
Jeroen

Tôi chấp nhận câu trả lời này, bởi vì nó là một trong đó đã giúp tôi hiểu được những khả năngfeasability , tức là nó đã trả lời các câu hỏi cho tôi. Tuy nhiên, bất kỳ ai đáp ứng câu hỏi này nên xem câu trả lời của @ a1ex07 cho một ràng buộc hoạt động trong các trường hợp đơn giản và câu trả lời của @ JohnGietzen cho các liên kết tuyệt vời HIERARCHYIDmà dường như là triển khai MSSQL2008 của mô hình tập hợp lồng nhau.
Jeroen

7

Tôi đã thấy 2 cách chính để thực thi điều này:

1, cách OLD:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FooHierarchy VARCHAR(256),
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

Cột FooHVELy ​​sẽ chứa một giá trị như thế này:

"|1|27|425"

Trong đó các số ánh xạ tới cột FooId. Sau đó, bạn sẽ thực thi rằng cột Phân cấp kết thúc bằng "| id" và phần còn lại của chuỗi khớp với FooHieratchy của PARENT.

2, cách MỚI:

SQL Server 2008 có một kiểu dữ liệu mới được gọi là HVELyID , thực hiện tất cả những điều này cho bạn.

Nó hoạt động trên cùng một nguyên tắc như cách OLD, nhưng được SQL Server xử lý hiệu quả và phù hợp để sử dụng làm THAY THẾ cho cột "ParentID" của bạn.

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     FooHierarchy HIERARCHYID )

1
Bạn có một nguồn hoặc bản demo ngắn gọn chứng minh rằng HIERARCHYIDngăn chặn việc tạo các vòng lặp phân cấp không?
Nick Chammas

6

Đó là một cách có thể: bạn có thể gọi UDF vô hướng từ ràng buộc KIỂM TRA của bạn và loại này có thể phát hiện các chu kỳ có độ dài bất kỳ. Thật không may, cách tiếp cận này cực kỳ chậm và không đáng tin cậy: bạn có thể có dương tính giả và âm tính giả.

Thay vào đó, tôi sẽ sử dụng đường dẫn cụ thể hóa.

Một cách khác để tránh chu kỳ là có KIỂM TRA (ID> ParentID), điều này có lẽ cũng không khả thi.

Tuy nhiên, một cách khác để tránh chu kỳ là thêm hai cột nữa, LevelInHVELy ​​và ParentLevelInHVELy, có (ParentID, ParentLevelInHVELy) tham khảo (ID, LevelInHVELy) và có CHECK (LevelInHVELy> ParentLevelInHVELy).


Các UDF trong các ràng buộc KIỂM TRA KHÔNG hoạt động. Bạn không thể có được một hình ảnh nhất quán ở cấp độ bảng về trạng thái đề xuất sau cập nhật từ một chức năng chạy trên một hàng tại một thời điểm. Bạn phải sử dụng trình kích hoạt SAU và quay lại hoặc trình kích hoạt INSTEAD OF và từ chối cập nhật.
ErikE

Nhưng bây giờ tôi thấy các ý kiến ​​về câu trả lời khác về cập nhật nhiều hàng.
ErikE

@ErikE đúng vậy, UDF trong các ràng buộc KIỂM TRA KHÔNG hoạt động.
AK

@Alex Đồng ý. Tôi mất vài giờ để kiên quyết chứng minh điều này một lần.
ErikE

4

Tôi tin rằng nó có thể:

create function test_foo (@id bigint) returns bit
as
begin
declare @retval bit;

with t1 as (select @id as FooId, 0 as lvl  
union all 
 select f.FooId , t1.lvl+1 from t1 
 inner join Foo f ON (f.ParentFooId = t1.FooId)
 where lvl<11) -- you said that max nested level 10, so if there is any circular   
-- dependency, we don't need to go deeper than 11 levels to detect it

 select @retval =
 CASE(COUNT(*)) 
 WHEN 0 THEN 0 -- for records that don't have children
 WHEN 1 THEN 0 -- if a record has children
  ELSE 1 -- recursion detected
 END
 from t1
 where t1.FooId = @id ;

return @retval; 
end;
GO
alter table Foo add constraint CHK_REC1 CHECK (dbo.test_foo(ParentFooId) = 0)

Tôi có thể đã bỏ lỡ điều gì đó (xin lỗi, tôi không thể kiểm tra nó một cách xuyên suốt), nhưng nó dường như hoạt động.


1
Tôi đồng ý rằng "nó có vẻ hoạt động", nhưng nó có thể thất bại đối với các cập nhật nhiều hàng, thất bại trong cách ly ảnh chụp nhanh và rất chậm.
AK

@AlexKuznetsov: Tôi nhận thấy rằng truy vấn đệ quy tương đối chậm và tôi đồng ý rằng các cập nhật nhiều hàng có thể là một vấn đề (mặc dù chúng có thể bị vô hiệu hóa).
a1ex07

@ a1ex07 Thx cho đề xuất này. Tôi đã thử nó, và trong những trường hợp đơn giản, nó dường như hoạt động tốt. Không chắc chắn nếu thất bại trên các bản cập nhật nhiều hàng là một vấn đề (mặc dù có lẽ nó là). Tôi không chắc chắn mặc dù ý của bạn là "họ có thể bị vô hiệu hóa"?
Jeroen

Theo hiểu biết của tôi, tác vụ ngụ ý logic dựa trên con trỏ (hoặc hàng). Vì vậy, sẽ vô hiệu hóa các cập nhật sửa đổi nhiều hơn 1 hàng (đơn giản thay vì kích hoạt cập nhật gây ra lỗi nếu bảng được chèn có nhiều hơn 1 hàng).
a1ex07

Nếu bạn không thể thiết kế lại bảng, tôi sẽ tạo một quy trình kiểm tra tất cả các ràng buộc và thêm / cập nhật bản ghi. Sau đó, tôi sẽ đảm bảo không ai ngoại trừ sp này có thể chèn / cập nhật bảng này.
a1ex07

3

Đây là một tùy chọn khác: một kích hoạt cho phép cập nhật nhiều hàng và thực thi không có chu kỳ. Nó hoạt động bằng cách duyệt qua chuỗi tổ tiên cho đến khi tìm thấy phần tử gốc (với NULL gốc), do đó chứng minh không có chu kỳ. Nó được giới hạn trong 10 thế hệ vì tất nhiên một chu kỳ là vô tận.

Nó chỉ hoạt động với tập hợp các hàng đã sửa đổi hiện tại, miễn là các bản cập nhật không chạm vào một số lượng lớn các mục rất sâu trong bảng, hiệu suất không quá tệ. Nó phải đi hết chuỗi cho từng yếu tố, vì vậy nó sẽ có một số tác động hiệu suất.

Một trình kích hoạt thực sự "thông minh" sẽ tìm kiếm các chu kỳ trực tiếp bằng cách kiểm tra xem liệu một vật phẩm có tự chạm tới không và sau đó được bảo lãnh. Tuy nhiên, điều này đòi hỏi phải kiểm tra trạng thái của tất cả các nút được tìm thấy trước đó trong mỗi vòng lặp và do đó cần một vòng lặp WHILE và mã hóa nhiều hơn tôi muốn làm ngay bây giờ. Điều này không thực sự tốn kém hơn bởi vì hoạt động bình thường sẽ không có chu kỳ và trong trường hợp này, nó sẽ hoạt động nhanh hơn chỉ với thế hệ trước thay vì tất cả các nút trước đó trong mỗi vòng lặp.

Tôi thích đầu vào từ @AlexKuznetsov hoặc bất kỳ ai khác về cách điều này sẽ diễn ra trong sự cô lập ảnh chụp nhanh. Tôi nghi ngờ nó sẽ không tốt lắm, nhưng muốn hiểu nó tốt hơn.

CREATE TRIGGER TR_Foo_PreventCycles_IU ON Foo FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;

IF EXISTS (
   SELECT *
   FROM sys.dm_exec_session
   WHERE session_id = @@SPID
   AND transaction_isolation_level = 5
)
BEGIN;
  SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
END;
DECLARE
   @CycledFooId bigint,
   @Message varchar(8000);

WITH Cycles AS (
   SELECT
      FooId SourceFooId,
      ParentFooId AncestorFooId,
      1 Generation
   FROM Inserted
   UNION ALL
   SELECT
      C.SourceFooId,
      F.ParentFooId,
      C.Generation + 1
   FROM
      Cycles C
      INNER JOIN dbo.Foo F
         ON C.AncestorFooId = F.FooId
   WHERE
      C.Generation <= 10
)
SELECT TOP 1 @CycledFooId = SourceFooId
FROM Cycles C
GROUP BY SourceFooId
HAVING Count(*) = Count(AncestorFooId); -- Doesn't have a NULL AncestorFooId in any row

IF @@RowCount > 0 BEGIN
   SET @Message = CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATE' ELSE 'INSERT' END + ' statement violated TRIGGER ''TR_Foo_PreventCycles_IU'' on table "dbo.Foo". A Foo cannot be its own ancestor. Example value is FooId ' + QuoteName(@CycledFooId, '"') + ' with ParentFooId ' + Quotename((SELECT ParentFooId FROM Inserted WHERE FooID = @CycledFooId), '"');
   RAISERROR(@Message, 16, 1);
   ROLLBACK TRAN;   
END;

Cập nhật

Tôi đã tìm ra cách để tránh tham gia thêm vào bảng Đã chèn. Nếu bất cứ ai thấy một cách tốt hơn để thực hiện NHÓM THEO để phát hiện những người không chứa NULL, vui lòng cho tôi biết.

Tôi cũng đã thêm một công tắc để ĐỌC CAM KẾT nếu phiên hiện tại ở cấp độ ISAATION SNAPSHOT. Điều này sẽ ngăn chặn sự không nhất quán, mặc dù không may sẽ gây ra sự gia tăng chặn. Đó là loại không thể tránh khỏi cho nhiệm vụ trong tầm tay.


Bạn nên sử dụng gợi ý VỚI (READCOMMITTEDLOCK). Hugo Kornelis đã viết một ví dụ: sqlblog.com/bloss/hugo_kornelis/archive/2006/09/15/ chủ đề
AK

Cảm ơn @Alex những bài viết đó là thuốc nổ và giúp tôi hiểu cách ly ảnh chụp tốt hơn rất nhiều. Tôi đã thêm một công tắc có điều kiện để đọc không được cam kết với mã của tôi.
ErikE

2

Nếu các bản ghi của bạn được lồng nhiều hơn 1 cấp, một ràng buộc sẽ không hoạt động (tôi giả sử rằng bạn có nghĩa là bản ghi 1 là cha của bản ghi 2 và bản ghi 3 là cha của bản ghi 1). Cách duy nhất để làm điều này là trong mã cha hoặc kích hoạt, nhưng nếu bạn đang nhìn vào một bảng lớn và nhiều cấp độ thì điều này sẽ khá chuyên sâu.

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.