ตารางที่มีลำดับชั้น: สร้างข้อ จำกัด เพื่อป้องกันการเวียนผ่านปุ่มต่างประเทศ


10

สมมติว่าเรามีตารางที่มีข้อ จำกัด foreign key กับตัวเองเช่น:

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

ตารางนี้จะมีบันทึกต่อไปนี้:

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

มีหลายกรณีที่การออกแบบประเภทนี้เหมาะสม (เช่นความสัมพันธ์แบบ "พนักงานและหัวหน้าพนักงาน") และในกรณีใด ๆ : ฉันอยู่ในสถานการณ์ที่ฉันมีสิ่งนี้ในสคีมาของฉัน

การออกแบบชนิดนี้โชคไม่ดีที่อนุญาตให้มีการเวียนในการบันทึกข้อมูลดังแสดงในตัวอย่างด้านบน

คำถามของฉันคือ:

  1. เป็นไปได้ไหมที่จะเขียนข้อ จำกัด ที่ตรวจสอบเรื่องนี้? และ
  2. เป็นไปได้ไหมที่จะเขียนข้อ จำกัด ที่ตรวจสอบเรื่องนี้? (หากจำเป็นต้องมีความลึกเท่านั้น)

สำหรับส่วนที่ (2) ของคำถามนี้อาจเกี่ยวข้องกับการกล่าวถึงว่าฉันคาดหวังเพียงร้อยหรือบางทีในบางกรณีหลายพันระเบียนในตารางของฉันโดยปกติไม่ซ้อนกันลึกกว่าประมาณ 5 ถึง 10 ระดับ

PS MS SQL Server 2008


อัปเดต 14 มีนาคม 2555
มีคำตอบที่ดีมากมาย ตอนนี้ฉันได้ยอมรับสิ่งที่ช่วยให้ฉันเข้าใจถึงความเป็นไปได้ / ความเป็นไปได้ที่กล่าวถึง มีคำตอบที่ยอดเยี่ยมอื่น ๆ อีกมากมายบางคำแนะนำการใช้งานเช่นกันดังนั้นหากคุณมาที่นี่ด้วยคำถามเดียวกันให้ดูที่คำตอบทั้งหมด;)

คำตอบ:


6

คุณกำลังใช้โมเดลAdjacency Listซึ่งเป็นการยากที่จะบังคับใช้ข้อ จำกัด ดังกล่าว

คุณสามารถตรวจสอบโมเดลการตั้งค่าแบบซ้อนได้ซึ่งสามารถแสดงเฉพาะลำดับชั้นที่แท้จริงได้ (ไม่มีเส้นทางแบบวงกลม) สิ่งนี้มีข้อเสียอื่น ๆ เช่นเดียวกับส่วนแทรก / อัปเดตที่ช้า


+1 ลิงก์ที่ยอดเยี่ยมและฉันหวังว่าฉันจะลองทำแบบจำลองการตั้งค่าแบบซ้อนและยอมรับคำตอบนี้เป็นคำตอบที่เหมาะกับฉัน
Jeroen

ฉันยอมรับคำตอบนี้เพราะมันเป็นคนที่ช่วยให้ฉันเข้าใจความเป็นไปได้และfeasabilityคือมันตอบคำถามสำหรับฉัน แต่ทุกคนที่เชื่อมโยงไปถึงคำถามนี้ควรมีลักษณะที่ @ a1ex07 เป็นคำตอบสำหรับข้อ จำกัด ที่ทำงานในกรณีที่เรียบง่ายและ @ JohnGietzen ของคำตอบสำหรับการเชื่อมโยงที่ดีในการHIERARCHYIDซึ่งดูเหมือนว่าจะเป็นชนพื้นเมือง MSSQL2008 การดำเนินการตามรูปแบบชุดที่ซ้อนกัน
Jeroen

7

ฉันได้เห็น 2 วิธีหลักในการบังคับใช้นี้:

1 วิธีเก่า:

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

คอลัมน์ FooHierarchy จะมีค่าดังนี้:

"|1|27|425"

ตำแหน่งที่หมายเลขจะจับคู่กับคอลัมน์ FooId จากนั้นคุณจะบังคับให้คอลัมน์ลำดับชั้นลงท้ายด้วย "| id" และส่วนที่เหลือของสตริงตรงกับ FooHieratchy ของผู้ปกครอง

2 วิธีใหม่:

SQL Server 2008 มีประเภทข้อมูลใหม่ที่เรียกว่าHierarchyIDซึ่งทำสิ่งนี้ให้คุณทั้งหมด

มันทำงานบนหลักการเดียวกันกับวิธี OLD แต่ถูกจัดการอย่างมีประสิทธิภาพโดย SQL Server และเหมาะสำหรับใช้เป็น REPLACEMENT สำหรับคอลัมน์ "ParentID" ของคุณ

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

1
คุณมีการสาธิตแหล่งที่มาหรือการสาธิตสั้น ๆ ที่HIERARCHYIDป้องกันการสร้างลำดับชั้นลูปหรือไม่?
Nick Chammas

6

เป็นไปได้มาก: คุณสามารถเรียกใช้ข้อ จำกัด UDF ของเซนต์คิตส์และเนวิสได้และคุณสามารถตรวจจับวงจรได้ทุกความยาว น่าเสียดายที่วิธีนี้ช้ามากและไม่น่าเชื่อถือ: คุณสามารถมีผลบวกปลอมและลบได้

ฉันจะใช้เส้นทางที่เป็นรูปธรรมแทน

อีกวิธีหนึ่งในการหลีกเลี่ยงรอบคือการตรวจสอบ (ID> ParentID) ซึ่งอาจไม่เป็นไปได้เช่นกัน

อีกวิธีหนึ่งในการหลีกเลี่ยงรอบคือการเพิ่มคอลัมน์อีกสองคอลัมน์คือ LevelInHierarchy และ ParentLevelInHierarchy มี (ParentID, ParentLevelInHierarchy) อ้างถึง (ID, LevelInHierarchy) และมี CHECK (LevelInHierarchy> ParentLevelInHierarchy)


UDF ในข้อ จำกัด การตรวจสอบไม่ทำงาน คุณไม่สามารถรับรูปภาพที่สอดคล้องระดับตารางของสถานะที่เสนอหลังการอัพเดตจากฟังก์ชันที่เรียกใช้ในแต่ละครั้ง คุณต้องใช้ทริกเกอร์ AFTER แล้วย้อนกลับหรือทริกเกอร์ INSTEAD OF และปฏิเสธที่จะอัปเดต
ErikE

แต่ตอนนี้ฉันเห็นความคิดเห็นในคำตอบอื่น ๆ เกี่ยวกับการอัปเดตหลายแถว
ErikE

@ErikE ถูกต้อง UDF ในข้อ จำกัด การตรวจสอบไม่ทำงาน
AK

@Alex เห็นด้วย ฉันใช้เวลาสองสามชั่วโมงเพื่อพิสูจน์สิ่งนี้ครั้งเดียว
ErikE

4

ฉันเชื่อว่าเป็นไปได้:

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)

ฉันอาจจะพลาดบางสิ่งบางอย่าง (ขออภัยฉันไม่สามารถทดสอบได้อย่างทั่วถึง) แต่ดูเหมือนว่าจะใช้งานได้


1
ฉันยอมรับว่า "ดูเหมือนว่าจะใช้งานได้" แต่อาจล้มเหลวสำหรับการอัปเดตหลายแถวล้มเหลวภายใต้การแยกสแน็ปช็อตและช้ามาก
AK

@AlexKuznetsov: ฉันรู้ว่าคิวรีแบบเรียกซ้ำค่อนข้างช้าและฉันยอมรับว่าการอัปเดตหลายแถวอาจมีปัญหา (อาจปิดใช้งานได้)
a1ex07

@ a1ex07 ขอบคุณสำหรับคำแนะนำนี้ ฉันลองและในกรณีง่าย ๆ ดูเหมือนว่าจะทำงานได้ดีแน่นอน ยังไม่แน่ใจว่าหากการอัปเดตหลายแถวล้มเหลวเป็นปัญหาหรือไม่ ฉันไม่แน่ใจว่าสิ่งที่คุณหมายถึงโดย "พวกเขาสามารถปิดการใช้งาน"?
Jeroen

ในความเข้าใจของฉันงานหมายถึงตรรกะตามเคอร์เซอร์ (หรือแถว) ดังนั้นจึงเหมาะสมที่จะปิดใช้งานการอัปเดตที่แก้ไขมากกว่า 1 แถว (ง่าย ๆ แทนที่จะเป็นทริกเกอร์การอัปเดตที่ทำให้เกิดข้อผิดพลาดหากตารางที่แทรกมีมากกว่า 1 แถว)
a1ex07

หากคุณไม่สามารถออกแบบตารางฉันจะสร้างขั้นตอนที่ตรวจสอบข้อ จำกัด ทั้งหมดและเพิ่ม / ปรับปรุงบันทึก จากนั้นฉันจะทำให้แน่ใจว่าไม่มีใครยกเว้น sp นี้สามารถแทรก / อัปเดตตารางนี้
a1ex07

3

นี่เป็นอีกตัวเลือกหนึ่ง: ทริกเกอร์ที่อนุญาตให้อัปเดตหลายแถวและบังคับใช้รอบ มันทำงานได้โดยการข้ามผ่านสายโซ่บรรพบุรุษจนกว่าจะพบองค์ประกอบราก (กับผู้ปกครองเป็นโมฆะ) ดังนั้นการพิสูจน์ว่าไม่มีรอบ มันถูก จำกัด ไว้ที่ 10 ชั่วอายุคนเนื่องจากแน่นอนว่าวัฏจักรไม่มีที่สิ้นสุด

ใช้งานได้กับชุดแถวที่แก้ไขปัจจุบันเท่านั้นตราบใดที่การอัปเดตไม่ได้สัมผัสไอเท็มที่มีความลึกมาก ๆ ในตารางประสิทธิภาพก็ไม่ควรเลวร้ายเกินไป มันไม่ต้องไปตลอดทางขึ้นห่วงโซ่สำหรับแต่ละองค์ประกอบดังนั้นก็จะมีบางอย่างที่ส่งผลกระทบต่อประสิทธิภาพการทำงาน

ทริกเกอร์ "อัจฉริยะ" อย่างแท้จริงจะมองหารอบโดยตรงโดยการตรวจสอบเพื่อดูว่ารายการถึงตัวเองแล้วประกันตัว อย่างไรก็ตามสิ่งนี้ต้องการการตรวจสอบสถานะของโหนดที่พบก่อนหน้านี้ทั้งหมดในแต่ละลูปดังนั้นจึงต้องใช้ลูปขณะที่และการเข้ารหัสมากกว่าที่ฉันต้องการตอนนี้ นี่ไม่น่าจะแพงไปกว่านี้อีกแล้วเพราะการทำงานปกติจะไม่มีวัฏจักรและในกรณีนี้มันจะทำงานได้เร็วขึ้นกับรุ่นก่อนเท่านั้นมากกว่าโหนดก่อนหน้าทั้งหมดในแต่ละรอบ

ฉันชอบที่จะป้อนข้อมูลจาก @AlexKuznetsov หรือคนอื่น ๆ ว่าสิ่งนี้จะเป็นประโยชน์อย่างไรในการแยกสแนปช็อต ฉันคิดว่ามันคงไม่ดีนัก แต่อยากจะเข้าใจมันให้ดีขึ้น

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;

ปรับปรุง

ฉันหาวิธีหลีกเลี่ยงการเข้าร่วมพิเศษกลับไปที่ตารางแทรก หากใครเห็นวิธีที่ดีกว่าในการทำ GROUP BY เพื่อตรวจหาผู้ที่ไม่มี NULL โปรดแจ้งให้เราทราบ

ฉันยังเพิ่มสวิตช์เป็น READ COMMITTED ถ้าเซสชันปัจจุบันอยู่ในระดับ SNAPSHOT ISOLATION วิธีนี้จะป้องกันความไม่สอดคล้องกัน แต่น่าเสียดายที่จะทำให้การบล็อกเพิ่มขึ้น นั่นเป็นสิ่งที่หลีกเลี่ยงไม่ได้สำหรับงานในมือ


คุณควรใช้คำแนะนำ WITH (READCOMMITTEDLOCK) Hugo Kornelis เขียนตัวอย่าง: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/…
AK

ขอบคุณ @Alex บทความเหล่านั้นเป็นไดนาไมต์และช่วยให้ฉันเข้าใจการแยกสแน็ปช็อตได้ดีขึ้นมาก ฉันได้เพิ่มสวิทช์ตามเงื่อนไขเพื่ออ่านปราศจากข้อผูกมัดในรหัสของฉัน
ErikE

2

หากบันทึกของคุณซ้อนกันมากกว่า 1 ระดับข้อ จำกัด จะไม่ทำงาน (ฉันสมมติว่าคุณหมายถึงเช่นบันทึก 1 เป็นต้นกำเนิดของระเบียน 2 และบันทึก 3 เป็นระเบียนหลัก 1) วิธีเดียวในการทำเช่นนี้อาจจะอยู่ในโค้ดหลักหรือด้วยทริกเกอร์ แต่ถ้าคุณดูที่ตารางขนาดใหญ่และหลายระดับมันจะค่อนข้างเข้มข้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.