Hiyerarşili tablolar: yabancı anahtarlar aracılığıyla daireselliği önlemek için bir kısıtlama oluşturun


10

Varsayalım, kendisine karşı yabancı bir anahtar kısıtlaması olan bir masamız var:

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

Bu tablo aşağıdaki kayıtlara sahip olacaktır:

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

Bu tür bir tasarımın mantıklı olabileceği durumlar vardır (örneğin, tipik "çalışan-ve-patron-çalışan" ilişkisi) ve her durumda: Şemamda böyle bir durumdayım.

Bu tür tasarım maalesef yukarıdaki örnekte gösterildiği gibi veri kayıtlarında daireselliğe izin verir.

O zaman sorum şu:

  1. Bunu kontrol eden bir kısıtlama yazmak mümkün mü ? ve
  2. Öyle mi mümkün olmadığını kontrol eder, bu bir kısıtlamayı yazmak için? (yalnızca belirli bir derinliğe ihtiyaç duyulursa)

Bu sorunun (2) kısmı için, normalde yaklaşık 5 ila 10 seviyeden daha derin iç içe geçmeyen, sadece yüzlerce veya belki de bazı durumlarda masamda binlerce kayıt beklediğimi belirtmek yerinde olabilir.

PS. MS SQL Server 2008


Güncelleme 14 Mart 2012
Birkaç iyi yanıt vardı. Şimdi bahsettiğim olasılığı / fizibiliteyi anlamama yardımcı olanı kabul ettim . Yine de bazı büyük cevaplar var, bazıları uygulama önerileri de var, bu yüzden aynı soru ile buraya indiğinizde tüm cevaplara bir göz atın;)

Yanıtlar:


6

Böyle bir kısıtlamanın uygulanmasının zor olduğu Bitişiklik Listesi modelini kullanıyorsunuz .

Yalnızca gerçek hiyerarşilerin temsil edilebileceği (dairesel yol yok) İç İçe Ayarlanmış modeli inceleyebilirsiniz . Bu yavaş Ekler / Güncellemeler gibi başka dezavantajları vardır.


+1 harika bağlantılar, ve darnit Keşke ben de, yuvalanmış küme modelini deneyebilir ve sonra bu cevap benim için çalışan bir cevap kabul.
Jeroen

Bu cevabı kabul ediyorum, çünkü olasılık ve fizibiliteyi anlamama yardımcı olan buydu , yani soruyu benim için yanıtladı. Ancak, bu soruya kimse iniş @ a1ex07 en bakmak gerekir cevap basit durumlarda çalışır ve en JohnGietzen @ bir kısıtlama için cevap harika bağlantılar için için HIERARCHYIDhangi iç içe küme modelinin bir yerli MSSQL2008 uygulama gibi görünüyor.
Jeroen

7

Bunu zorlamanın 2 ana yolunu gördüm:

1, ESKİ yol:

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

FooHierarchy sütunu şöyle bir değer içerir:

"|1|27|425"

Sayılar FooId sütununa eşlenir. Daha sonra Hiyerarşi sütununun "| id" ile bitmesini ve dizenin geri kalanının PARENT'in FooHieratchy'siyle eşleşmesini zorlarsınız.

2, YENİ yol:

SQL Server 2008, tüm bunları sizin için yapan HierarchyID adlı yeni bir veri tipine sahiptir .

ESKİ yolla aynı prensipte çalışır, ancak SQL Server tarafından verimli bir şekilde işlenir ve "ParentID" sütununuz için YEDEK olarak kullanım için uygundur.

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

1
HIERARCHYIDHiyerarşi döngülerinin oluşturulmasını engelleyen bir kaynak veya kısa demo var mı?
Nick Chammas

6

Bu bir tür olasıdır: Sizden SINIR kısıtlamasından bir skaler UDF çağırabilirsiniz ve bu tür herhangi bir uzunluktaki döngüleri algılayabilir. Ne yazık ki, bu yaklaşım son derece yavaş ve güvenilmezdir: yanlış pozitiflere ve yanlış negatiflere sahip olabilirsiniz.

Bunun yerine maddileştirilmiş bir yol kullanırdım.

Döngüleri önlemenin başka bir yolu da muhtemelen çok uygun olmayan bir CHECK (ID> ParentID) kullanmaktır.

Döngülerden kaçınmanın başka bir yolu da iki sütun daha eklemektir: LevelInHierarchy ve ParentLevelInHierarchy, (ParentID, ParentLevelInHierarchy) ifadesine (ID, LevelInHierarchy) ve bir CHECK'e (LevelInHierarchy> ParentLevelInHierarchy) sahiptir.


CHECK kısıtlamalarındaki UDF'ler ÇALIŞMAZ. Her seferinde bir satırda çalışan bir işlevden güncelleme sonrası önerilen durumun tablo düzeyinde tutarlı bir resmini alamazsınız. Bir AFTER tetikleyici kullanmalı ve geri almalı veya INSTEAD OF tetikleyici kullanmalı ve güncellemeyi reddetmelisiniz.
ErikE

Ama şimdi çok satırlı güncellemelerin diğer cevabındaki yorumları görüyorum.
ErikE

@ErikE doğru, CHECK kısıtlamalarındaki UDF'ler ÇALIŞMIYOR.
AK

@Alex Kabul Edildi. Bunu kesin olarak kanıtlamak için birkaç saatimi aldım.
ErikE

4

Bunun mümkün olduğuna inanıyorum:

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)

Bir şeyi özlemiş olabilirim (özür dilerim, bunu test edemiyorum), ama işe yarıyor gibi görünüyor.


1
"Çalışıyor gibi görünüyor", ancak çok satırlı güncellemeler için başarısız olabilir, anlık görüntü yalıtımı altında başarısız olabilir ve çok yavaş olabilir.
AK

@AlexKuznetsov: Yinelenen sorgunun nispeten yavaş olduğunu ve çok satırlı güncelleştirmelerin bir sorun olabileceğini kabul ediyorum (yine de devre dışı bırakılabilir).
a1ex07

@ a1ex07 Bu öneri için teşekkürler. Denedim ve basit durumlarda gerçekten iyi çalışıyor gibi görünüyor. Çok satırlı güncellemelerde başarısızlığın bir sorun olup olmadığından henüz emin değilim (muhtemelen olsa da). "Engelli olabilirler" ile ne demek istediğinden emin değilim?
Jeroen

Anladığım kadarıyla, görev imleç (veya satır) tabanlı mantık anlamına geliyor. Bu nedenle, 1'den fazla satırı değiştiren güncellemeleri devre dışı bırakmak mantıklıdır (eklenen tablo 1'den fazla satıra sahipse bir hata oluşturan güncelleme tetikleyicisi yerine basit).
a1ex07

Tabloyu yeniden tasarlayamıyorsanız, tüm kısıtlamaları kontrol eden ve kayıt ekleyen / güncelleyen bir yordam oluştururdum. O zaman bu sp dışında hiç kimsenin bu tabloyu ekleyemeyeceğinden / güncelleyemediğinden emin olacağım.
a1ex07

3

İşte başka bir seçenek: çok satırlı güncellemelere izin veren ve hiçbir döngüyü zorlamayan bir tetikleyici. Bir kök eleman (ana NULL ile) bulana kadar ata zincirini çevirerek çalışır, böylece döngü olmadığını kanıtlar. Tabii ki bir döngü sonsuz olduğu için 10 nesil ile sınırlıdır.

Yalnızca geçerli değiştirilmiş satır kümesiyle çalışır, bu nedenle güncellemeler tablodaki çok sayıda çok derin öğeye dokunmadığı sürece, performans çok kötü olmamalıdır. O olacak, böylece tüm yol, her element için zincire kadar gitmek zorunda mı bazı performans etkisini.

Gerçekten "akıllı" bir tetikleyici, bir öğenin kendisine ulaşıp ulaşmadığını kontrol ederek ve sonra kefaletle doğrudan döngüleri arayacaktır. Bununla birlikte, bu, her döngü sırasında önceden bulunan tüm düğümlerin durumunu kontrol etmeyi gerektirir ve bu nedenle bir WHILE döngüsü ve şu anda yapmak istediğimden daha fazla kodlama gerektirir. Bu gerçekten daha pahalı olmamalı çünkü normal işlem döngüleri olmayacaktı ve bu durumda her döngü sırasında önceki tüm düğümlerden ziyade sadece önceki nesil ile çalışmak daha hızlı olacaktır.

@AlexKuznetsov ya da bu anlık görüntü izolasyonu nasıl olacağını başka birinden girdi isterim. Çok iyi olmayacağından şüpheleniyorum, ancak daha iyi anlamak istiyorum.

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;

Güncelleme

Eklenen tabloya fazladan birleşmeyi nasıl önleyebileceğimi anladım. Herkes bir BOŞ içermeyenleri tespit etmek için GROUP BY yapmak için daha iyi bir yol görürse lütfen bana bildirin.

Geçerli oturum SNAPSHOT İZOLASYON seviyesindeyse OKUYU DEVRE DIŞI bir anahtar da ekledim. Bu maalesef tutarsızlıkları önleyecek olsa da maalesef engellemenin artmasına neden olacaktır. Bu, eldeki görev için kaçınılmazdır.


WITH (READCOMMITTEDLOCK) ipucunu kullanmalısınız. Hugo Kornelis bir örnek yazdı: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/…
AK

Teşekkürler @Alex bu makaleler dinamit ve anlık izolasyon çok daha iyi anlamama yardımcı oldu. Koduma bağlı olmadan okumak için koşullu bir anahtar ekledim.
ErikE

2

Kayıtlarınız 1'den fazla iç içe yerleştirilmişse, bir kısıtlama işe yaramayacaktır (örneğin, kayıt 1'in kayıt 2'nin üstü ve kayıt 3'ün kayıt 1'in üstü olduğu anlamına geldiğini varsayıyorum). Bunu yapmanın tek yolu ya ana kodda ya da bir tetikleyicide olmaktır, ancak büyük bir masaya ve çoklu seviyelere bakıyorsanız bu oldukça yoğun olacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.