2010-07-30 5 views
6

DBMS: MS Sql Server 2005, Standardvincolo unico all'interno di un gruppo di record in cui un certo valore è lo stesso

Mi piacerebbe fare un vincolo di tabella di avere un solo record ha un particolare valore all'interno di un sottoinsieme della tabella (dove le righe condividono un valore in una particolare colonna). È possibile?

Esempio: Ho record in myTable che hanno una chiave esterna non univoca (fk1), e una colonna di bit chiamato isPrimary a segnare che questo particolare dovrebbe essere utilizzato dal nostro app per logica speciale.

in astratto, sembra che questo:

myTable 
------------- 
pk1  (int, not null) 
name  (varchar(50), null) 
fk1  (int, not null) 
isPrimary (bit, not null) 

voglio garantire che vi sia una e una sola record con la bandiera isPrimary impostato a 1, per ogni valore unico di FK1.

dati esempio: Questo dovrebbe essere legale:

pk1  name  fk1 isPrimary 
---- ----- ----- ---------- 
1  Bill  111 1 
2  Tom  111 0 
3  Dick  222 1 
4  Harry 222 0 

Ma questo dovrebbe non essere (più di una in cui fk = 111):

pk1  name  fk1 isPrimary 
---- ----- ----- ---------- 
1  Bill  111 1 
2  Tom  111 1 
3  Dick  222 1 
4  Harry 222 0 

E nemmeno questo dovrebbe (nessuno dove fk = 222):

pk1  name  fk1 isPrimary 
---- ----- ----- ---------- 
1  Bill  111 1 
2  Tom  111 0 
3  Dick  222 0 
4  Harry 222 0 

È c'è un modo per farlo con un vincolo di tabella?

UPDATE Sono andato con la risposta di Martin Smith, per ora, anche se sarò spingendo per il refactoring di JohnFx in una prossima release, in quanto è la migliore soluzione a lungo termine. Tuttavia volevo pubblicare la mia UDF aggiornata sulla base della risposta di Raze2dust, nel caso in cui i futuri lettori decidessero che è più adatto alle loro esigenze.

CREATE FUNCTION [dbo].[OneIsPrimaryPerFK1](@fk1 INT, @dummyIsPrimary BIT) 
RETURNS INT 
AS 
BEGIN 
    DECLARE @retval INT; 
    DECLARE @primarySum INT; 
    SET @retval = 0; 
    DECLARE @TempTable TABLE (
    fk1 INT, 
    PrimarySum INT) 

INSERT INTO @TempTable 
    SELECT fk1, SUM(CAST(isPrimary AS INT)) AS PrimarySum 
    FROM FacAdmin 
    WHERE fk1 = @fk1 
    GROUP BY fk1; 

    SELECT @primarySum = PrimarySum FROM @TempTable; 
    IF(@primarySum=1) 
     BEGIN 
      SET @retval = 1 
     END 
    RETURN @retval 
END; 

Modifiche:

  1. Usato @tempTable piuttosto che

    TempTable come richiesto dalla UDF

  2. passato @ fk1 come parametro in modo che posso (in memoria v scritti su disco.) selezionare per unicità all'interno di un gruppo di valori fk1.
  3. difficile doveva passare anche isPrimary anche se non è necessario per la logica della funzione , altrimenti l'ottimizzatore SQL2005 non verrà eseguito il vincolo di controllo quando isPrimary è aggiornato.

risposta

4

L'utilizzo di UDF nei limiti di controllo può non riuscire in snapshot isolation o multirow updates.

Supponendo che tutti i valori FK1 e PK1 sono attualmente (e saranno sempre) positiva è possibile creare una colonna calcolata con la seguente definizione

CASE WHEN isPrimary = 1 THEN fk1 ELSE -pk1 END 

quindi aggiungere un vincolo unico per quella. O se tale ipotesi non si può fare allora forse

CASE WHEN isPrimary = 0 THEN 1.0/pk1 ELSE fk1 END 
+0

Sebbene la soluzione di JohnFx sia la migliore soluzione a lungo termine, questa è la soluzione per me al momento. –

6

SQL 2005 non consente di applicare una clausola Where a un indice univoco come SQL 2008.Tuttavia, esistono alcuni modi per risolvere il problema in SQL 2005:

  1. Creare una vista indicizzata che filtri per IsPrimary = 1 e aggiungere un indice univoco a tale vista.
  2. Creare un trigger in cui si garantisce che solo uno possa essere primario.
  3. Incapsula la tua logica in un proc memorizzato e costringi gli utenti a passare attraverso il proc memorizzato da inserire o aggiornare dalla tua tabella.
+2

4. Creare una funzione memorizzata che si utilizza in un vincolo di controllo – Frank

+1

Attenzione: una funzione definita dall'utente in un vincolo può funzionare, ma può anche causare seri problemi di prestazioni se sta facendo seleziona. – JohnFx

+1

La penalizzazione delle prestazioni sarebbe solo su inserimento e aggiornamento, giusto? Non riesco a immaginare un blocco dei limiti di controllo quando si selezionano solo i dati. Se è vero, sto prendendo il colpo, perché il tavolo non è scritto molto spesso. –

3

Si potrebbe provare a creare una funzione e quindi utilizzando un vincolo di controllo:

CREATE FUNCTION ChkFn() 
RETURNS INT 
AS 
BEGIN 
    DECLARE @retval INT 
    DECLARE @distinct INT 
    DECLARE @top INT 
    SET @retval = 0 
    SELECT fk1 AS ForeignKey, SUM(isPrimary) AS PrimarySum 
    INTO #TempTable 
    FROM myTable 
    GROUP BY fk1 
    SELECT @distinct = COUNT(DISTINCT(PrimarySum)) FROM #TempTable 
    SELECT @top = top PrimarySum FROM #TempTable 
    IF(@distinct=1 AND @top=1) 
    BEGIN 
    @retval = 1 
    END 
    RETURN @retval 
END; 
GO 

ALTER TABLE myTable 
ADD CONSTRAINT chkFkPk CHECK (dbo.ChekFn() = 1); 
GO 

Provatelo e fatemi sapere se ha funzionato. Non molto elegante però ..

+0

Questo è stato molto vicino e mi ha fatto andare nella giusta direzione, quindi lo contrassegnerò come accettato. Pubblicherò la funzione completata il lunedì. Primo problema: non è possibile utilizzare #TempTables in UDF, è necessario utilizzare @TempTable. L'altro sarò indirizzato nel post aggiornato. –

+1

Potrebbe non riuscire per gli aggiornamenti multipli o sotto l'isolamento dello snapshot. –

+0

ah ah dimenticati di UDF ... Nessuna idea di aggiornamenti multipli/isolamento dello snapshot. Thx Martin per l'articolo. Cercherò di capirlo. Sono un principiante :-) –

7

Ho iniziato una nuova risposta da quando ho storpiato male la prima.

Sembra che tu possa risolvere il problema ripensando il design del tuo tavolo un po 'per evitare di forzare la forza bruta ad applicare una regola aziendale.

Come ridurre la colonna IsPrimary da MyTable e aggiungere una colonna PrimaryPersonID all'altra tabella che fa riferimento a persona principale?

In questo modo la struttura stessa avrebbe applicato quella 1 e solo 1 voce nella tabella FK era primaria per ogni persona.

+2

Hai perfettamente ragione, questo dovrebbe essere il modello di dati, e vorrei poterlo implementare. Per ora, abbiamo alcune app diverse che puntano a questo e non possiamo cambiarle tutte per accedere al modello aggiornato in questo momento. Spingerò a fare questo cambiamento in futuro. –