पदानुक्रम के साथ तालिकाओं: विदेशी कुंजियों के माध्यम से परिपत्रता को रोकने के लिए एक बाधा बनाएँ


10

मान लीजिए कि हमारे पास एक मेज है जो अपने आप में एक विदेशी कुंजी बाधा है, जैसे:

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 स्तरों से अधिक गहरा कोई भी नहीं।

पुनश्च। एमएस SQL ​​सर्वर 2008


14 मार्च 2012
को अपडेट करें कई अच्छे उत्तर थे। मैंने अब उस एक को स्वीकार कर लिया है जिसने मुझे उल्लेखित संभावना / व्यवहार्यता को समझने में मदद की है । हालांकि, कई अन्य शानदार जवाब हैं, कुछ कार्यान्वयन सुझावों के साथ भी, इसलिए यदि आप एक ही प्रश्न के साथ यहां उतरे हैं तो सभी उत्तरों पर एक नज़र डालें;)

जवाबों:


6

आप निकटता सूची मॉडल का उपयोग कर रहे हैं , जहां इस तरह की बाधा को लागू करना मुश्किल है।

आप नेस्टेड सेट मॉडल की जांच कर सकते हैं , जहां केवल सच्चे पदानुक्रमों का प्रतिनिधित्व किया जा सकता है (कोई परिपत्र पथ नहीं)। हालांकि इसमें अन्य कमियां हैं, जैसे धीमे आवेषण / अपडेट।


+1 महान लिंक, और इच्छा है कि मैं चाहता हूं कि मैं नेस्टेड सेट मॉडल के बारे में कोशिश करूं, और फिर इस उत्तर को मेरे लिए काम करने वाले के रूप में स्वीकार करें।
जीरन

मैं इस उत्तर को स्वीकार कर रहा हूं, क्योंकि यह वह था जिसने मुझे संभावना और व्यवहार्यता को समझने में मदद की , अर्थात इसने मेरे लिए प्रश्न का उत्तर दिया। हालाँकि, इस प्रश्न पर उतरने वाले किसी व्यक्ति को एक बाधा के लिए @ a1ex07 के उत्तर पर ध्यान देना चाहिए जो सरल मामलों में काम करता है, और @ JohnGietzen के महान लिंक के उत्तर के लिए HIERARCHYIDजो कि एक मूल MSSQL2008 नेस्टेड सेट मॉडल का कार्यान्वयन प्रतीत होता है।
२१:२०

7

मैंने इसे लागू करने के 2 मुख्य तरीके देखे हैं:

1, OLD रास्ता:

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

FooHierarchy कॉलम में इस तरह का मान होगा:

"|1|27|425"

जहां नंबर FooId कॉलम पर मैप करते हैं। फिर आप यह लागू करेंगे कि पदानुक्रम कॉलम "id" के साथ समाप्त होता है और बाकी का स्ट्रिंग PARH के FooHieratchy से मेल खाता है।

2, नया तरीका:

SQL Server 2008 में एक नया डेटाटाइप है जिसे HierarchyID कहा जाता है , जो आपके लिए यह सब करता है।

यह OLD तरीके के समान प्रिंसिपल पर काम करता है, लेकिन इसे SQL सर्वर द्वारा कुशलता से हैंडल किया जाता है, और आपके "पेरेंटिड" कॉलम के लिए रिप्लेसमेंट के रूप में उपयोग के लिए उपयुक्त है।

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

1
क्या आपके पास एक स्रोत या संक्षिप्त डेमो प्रदर्शित है जो HIERARCHYIDपदानुक्रम छोरों के निर्माण को रोकता है?
निक चामास

6

यह संभव है: आप CHECK बाधा से एक स्केल UDF आह्वान कर सकते हैं, और यह किसी भी लंबाई के चक्रों का पता लगा सकता है। दुर्भाग्य से, यह दृष्टिकोण बेहद धीमा और अविश्वसनीय है: आपके पास गलत सकारात्मक और गलत नकारात्मक हो सकते हैं।

इसके बजाय, मैं भौतिक मार्ग का उपयोग करूंगा।

चक्र से बचने का दूसरा तरीका एक CHECK (ID> ParentID) है, जो शायद बहुत संभव नहीं है।

फिर भी चक्र से बचने का एक और तरीका दो और कॉलम जोड़ना है, LevelInHierarchy और ParentLevelInHierarchy, है (ParentID, ParentLevelInHierarchy) (ID, LevelInHierarchy, और एक CHECK (LevelInHierarchy> ParentLevelInHierarchy) का संदर्भ लें।


CHECK बाधाओं में UDFs काम नहीं करते हैं। आप एक समय में एक पंक्ति पर चलने वाले फ़ंक्शन से अद्यतन-अद्यतन प्रस्तावित स्थिति की तालिका-स्तरीय सुसंगत तस्वीर नहीं प्राप्त कर सकते हैं। आपको एक AFTER ट्रिगर का उपयोग करना चाहिए और वापस रोल करना चाहिए या ट्रिगर का INSTEAD और अपडेट करने से इंकार करना चाहिए।
एरिक

लेकिन अब मैं बहु-पंक्ति अपडेट के बारे में अन्य उत्तर पर टिप्पणियों को देखता हूं।
एरिक

@ सही है कि, CHECK बाधाओं में UDFs काम नहीं करते।
एके

@ एलेक्स सहमत। मुझे एक बार ठोस रूप से यह साबित करने में कुछ घंटे लगे।
एरिक

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
मैं सहमत हूं कि "यह काम करने लगता है", लेकिन यह बहु-पंक्ति अपडेट के लिए विफल हो सकता है, स्नैपशॉट अलगाव के तहत विफल हो सकता है, और बहुत धीमा है।
एके

@AlexKuznetsov: मुझे एहसास है कि पुनरावर्ती क्वेरी अपेक्षाकृत धीमी है, और मैं मानता हूं कि बहु-पंक्ति अपडेट एक समस्या हो सकती है (हालांकि उन्हें अक्षम किया जा सकता है)।
a1ex07

इस सुझाव के लिए @ a1ex07 Thx। मैंने इसकी कोशिश की, और साधारण मामलों में यह वास्तव में ठीक काम करता है। यकीन नहीं अभी तक अगर बहु-पंक्ति अपडेट पर विफलता एक समस्या है (हालांकि शायद यह है)। मैं अनिश्चित हूं कि आप "वे अक्षम हो सकते हैं" से क्या मतलब है?
जीरन

मेरी समझ में, कार्य का अर्थ है कर्सर (या पंक्ति) आधारित तर्क। तो यह अपडेट को अक्षम करने के लिए समझ में आता है जो 1 से अधिक पंक्ति को संशोधित करता है (अपडेट ट्रिगर के बजाय सरल है जो एक त्रुटि उठाता है यदि सम्मिलित तालिका में 1 से अधिक पंक्ति है)।
a1ex07

यदि आप तालिका को फिर से डिज़ाइन नहीं कर सकते हैं, तो मैं एक प्रक्रिया बनाता हूं जो सभी बाधाओं की जांच करता है और रिकॉर्ड को जोड़ता / अपडेट करता है। तब मुझे यकीन है कि इस तालिका को सम्मिलित करने / अद्यतन करने के अलावा कोई नहीं कर सकता।
a1ex07

3

यहां एक और विकल्प है: एक ट्रिगर जो बहु-पंक्ति अपडेट की अनुमति देता है और कोई चक्र लागू नहीं करता है। जब तक यह मूल तत्व (मूल NULL के साथ) नहीं मिल जाता है, तब तक यह पूर्वज श्रृंखला का पता लगाकर काम करता है, इस प्रकार यह साबित होता है कि कोई चक्र नहीं है। यह 10 पीढ़ियों तक सीमित है क्योंकि बेशक एक चक्र अंतहीन है।

यह केवल संशोधित पंक्तियों के वर्तमान सेट के साथ काम करता है, इसलिए जब तक अपडेट तालिका में बड़ी संख्या में बहुत गहरी वस्तुओं को नहीं छूता है, तो प्रदर्शन बहुत खराब नहीं होना चाहिए। यह प्रत्येक तत्व के लिए श्रृंखला तक सभी तरह से जाना पड़ता है, इसलिए इसका कुछ प्रदर्शन प्रभाव होगा।

एक सही मायने में "बुद्धिमान" ट्रिगर सीधे जाँच करके यह देखने के लिए होगा कि क्या कोई आइटम खुद तक पहुंच गया है और फिर बोइंग। हालाँकि, इसके लिए प्रत्येक लूप के दौरान पहले से पाए गए नोड्स की स्थिति की जाँच करना आवश्यक है और इस प्रकार एक WHILE लूप और अधिक कोडिंग लेता है, जो मैं अभी करना चाहता था। यह वास्तव में किसी भी अधिक महंगा नहीं होना चाहिए क्योंकि सामान्य ऑपरेशन में चक्र नहीं होना चाहिए और इस मामले में यह प्रत्येक लूप के दौरान पिछले सभी नोड्स के बजाय केवल पूर्व पीढ़ी के साथ तेजी से काम करेगा।

मुझे @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;

अपडेट करें

मुझे पता चला कि कैसे एक अतिरिक्त शामिल होने से बचने के लिए वापस सम्मिलित तालिका में। अगर किसी को ग्रुप बीवाई करने के लिए एक बेहतर तरीका दिखाई देता है, तो यह पता लगाने के लिए कि एक नल नहीं है कृपया मुझे बताएं।

यदि मैं वर्तमान सत्र SNAPSHOT अलगाव स्तर में है, तो मैंने READ COMMITTED में एक स्विच भी जोड़ा। इससे विसंगतियों को रोका जा सकेगा, हालांकि दुर्भाग्य से वृद्धि अवरुद्ध हो जाएगी। यह हाथ में कार्य के लिए अपरिहार्य है।


आपको (READCOMMITTEDLOCK) संकेत के साथ उपयोग करना चाहिए। ह्यूगो कोर्नेलिस ने एक उदाहरण लिखा: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/…/
AK

धन्यवाद @Alex उन लेखों को डायनामाइट किया गया था और मुझे स्नैपशॉट अलगाव को बहुत बेहतर समझने में मदद की। मैंने अपने कोड के लिए बिना पढ़े एक सशर्त स्विच जोड़ा है।
एरिक

2

यदि आपके रिकॉर्ड 1 से अधिक स्तर पर नेस्टेड हैं, तो एक बाधा काम नहीं कर रही है (मैं मान रहा हूं कि आप का मतलब है जैसे रिकॉर्ड 1 रिकॉर्ड 2 के माता-पिता हैं, और रिकॉर्ड 3 रिकॉर्ड 1 का माता-पिता है)। ऐसा करने का एकमात्र तरीका माता-पिता कोड में या ट्रिगर के साथ होगा, लेकिन यदि आप एक बड़ी तालिका और कई स्तरों पर देख रहे हैं तो यह बहुत गहन होगा।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.