vincolo univoco condizionale


92

Ho una situazione in cui ho bisogno di applicare un vincolo univoco su un insieme di colonne, ma solo per un valore di una colonna.

Quindi, ad esempio, ho una tabella come Table (ID, Name, RecordStatus).

RecordStatus può avere solo un valore 1 o 2 (attivo o cancellato) e voglio creare un vincolo univoco su (ID, RecordStatus) solo quando RecordStatus = 1, poiché non mi interessa se ci sono più record eliminati con lo stesso ID.

Oltre a scrivere trigger, posso farlo?

Sto usando SQL Server 2005.


1
Questo design è un dolore comune. Hai preso in considerazione la possibilità di modificare il design in modo che i record "eliminati" siano fisicamente eliminati dalla tabella e magari spostati in una tabella "archivio"?
un

1
... perché l'incapacità di scrivere un vincolo UNICO per applicare una chiave semplice dovrebbe essere considerata un "odore di codice", IMO. Se non puoi modificare il design (SQL DDL) perché molte altre tabelle fanno riferimento a questa tabella, scommetto che anche il tuo SQL DML soffre di conseguenza, ovvero devi ricordarti di aggiungere ... AND Table.RecordStatus = 1 ' alla maggior parte delle condizioni di ricerca e alle condizioni di join che coinvolgono questa tabella e che si verificano piccoli bug quando inevitabilmente viene omessa occasionalmente.
un

Risposte:


36

Aggiungi un vincolo di controllo come questo. La differenza è che restituirai false se Status = 1 e Count> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;

ho esaminato i vincoli di controllo a livello di tabella ma non sembra che ci sia un modo per passare i valori inseriti o aggiornati alla funzione, sai come?
np-hard

Ok, ho pubblicato uno script di esempio che ti aiuterà a dimostrare di cosa sto parlando. L'ho provato e funziona. Se guardi le due righe commentate, vedrai il messaggio che ricevo. Nota bene, nella mia implementazione, mi limito a garantire che non sia possibile aggiungere un secondo elemento con lo stesso ID che è attivo se ce n'è già uno attivo. È possibile modificare la logica in modo tale che, se è attiva, non è possibile aggiungere alcun elemento con lo stesso ID. Con questo modello, le possibilità sono praticamente infinite.
D. Patrick

Preferirei la stessa logica in un trigger. "una query in una funzione scalare ... può creare grossi problemi se il vincolo CHECK si basa su una query e se più di una riga è interessata da un aggiornamento. Quello che succede è che il vincolo viene controllato una volta per ogni riga prima del completamento dell'istruzione Ciò significa che l'atomicità dell'istruzione è interrotta e la funzione verrà esposta al database in uno stato incoerente. I risultati sono imprevedibili e imprecisi. " Vedi: blogs.conchango.com/davidportas/archive/2007/02/19/…
onedaywhen

Questo è solo parzialmente vero un giorno quando. Il database si comporta in modo coerente e prevedibile. Il vincolo di controllo verrà eseguito dopo che la riga è stata aggiunta alla tabella e prima che la transazione venga confermata dal dbms e su questo puoi contare. Quel blog parlava di un problema piuttosto unico in cui è necessario eseguire il vincolo rispetto a un insieme di inserti anziché un solo inserto alla volta. ashish richiede un vincolo su un inserto alla volta e questo vincolo funzionerà in modo accurato, prevedibile e coerente. Mi dispiace se questo suonava conciso; Ero a corto di personaggi.
D.Patrick

3
Funziona alla grande per gli inserti ma non sembra funzionare per gli aggiornamenti. EG Aggiungendo questo dopo gli altri inserti funziona qui quando non me lo aspettavo. INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2); update CheckConstraint set Recordstatus = 1 where name = 'No ProblemsA'
dwidel

148

Ecco, l'indice filtrato . Dalla documentazione (enfasi mia):

Un indice filtrato è un indice non cluster ottimizzato particolarmente adatto a coprire le query che selezionano da un sottoinsieme di dati ben definito. Utilizza un predicato di filtro per indicizzare una porzione di righe nella tabella. Un indice filtrato ben progettato può migliorare le prestazioni delle query e ridurre i costi di manutenzione e archiviazione dell'indice rispetto agli indici di tabella completa.

Ed ecco un esempio che combina un indice univoco con un predicato di filtro:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

Questo essenzialmente impone l'unicità di IDquando RecordStatusè 1.

In seguito alla creazione di tale indice, una violazione di unicità solleverà un arror:

Il messaggio 2601, livello 14, stato 1, riga 13
Impossibile inserire la riga chiave duplicata nell'oggetto "dbo.MyTable" con l'indice univoco "MyIndex". Il valore della chiave duplicata è (9999).

Nota: l'indice filtrato è stato introdotto in SQL Server 2008. Per le versioni precedenti di SQL Server, vedere questa risposta .


Tieni presente che SQL Server richiede ansi_paddingindici filtrati, quindi assicurati che questa opzione sia attivata eseguendo SET ANSI_PADDING ONprima di creare un indice filtrato.
naXa

10

È possibile spostare i record eliminati in una tabella priva del vincolo e magari utilizzare una vista con UNION delle due tabelle per preservare l'aspetto di una singola tabella.


2
In realtà è piuttosto intelligente, Carl. Non è una risposta alla domanda in sé, ma è una buona soluzione. Se la tabella ha molte righe, ciò potrebbe anche velocizzare la ricerca di un record attivo perché potresti guardare la tabella dei record attivi. Accelererebbe anche il vincolo perché il vincolo univoco utilizza un indice anziché il vincolo di controllo che ho scritto di seguito che deve eseguire un conteggio. Mi piace.
D. Patrick

3

Puoi farlo in un modo davvero hacky ...

Crea una vista schemabound sul tuo tavolo.

CREATE VIEW Qualunque sia SELECT * FROM Table WHERE RecordStatus = 1

Ora crea un vincolo univoco sulla vista con i campi che desideri.

Una nota sulle viste schemabound, tuttavia, se si modificano le tabelle sottostanti, sarà necessario ricreare la vista. Un sacco di trucchi per questo.


Questo è un suggerimento abbastanza buono, e non quello "hacky". Di seguito sono riportate ulteriori informazioni su questa alternativa all'indice filtrato .
Scott Whitlock

È una cattiva idea. La domanda non è questa.
FabianoLothor

Ho usato una vista schemabound una volta e non ho mai ripetuto l'errore. Possono essere un dolore reale con cui lavorare. Non è che devi ricreare la vista se modifichi la tabella sottostante: potenzialmente devi farlo per tutte le viste, almeno in SQL Server. È che non puoi cambiare la tabella senza prima rilasciare la vista, cosa che potresti non essere in grado di fare senza prima rilasciare i riferimenti ad essa. Oh, inoltre l'archiviazione potrebbe essere problematica, a causa dello spazio o del costo che aggiunge per inserire e aggiornare.
MattW

1

Poiché consentirai i duplicati, un vincolo univoco non funzionerà. È possibile creare un vincolo di controllo per la colonna RecordStatus e una procedura memorizzata per INSERT che controlla i record attivi esistenti prima di inserire ID duplicati.


1

Se non puoi usare NULL come RecordStatus come suggerito da Bill, potresti combinare la sua idea con un indice basato su funzioni. Crea una funzione che restituisca NULL se RecordStatus non è uno dei valori che vuoi considerare nel tuo vincolo (e RecordStatus altrimenti) e crea un indice su quello.

Ciò avrà il vantaggio di non dover esaminare esplicitamente altre righe nella tabella nel vincolo, il che potrebbe causare problemi di prestazioni.

Dovrei dire che non conosco affatto il server SQL, ma ho utilizzato con successo questo approccio in Oracle.


buona idea, ma non ci sono funzioni basate su indicizzazione nel server sql, grazie per la risposta però
np-hard
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.