Dit artikel is deel 4 in een serie over NULL-complexiteit. In de vorige artikelen (Deel 1, Deel 2 en Deel 3) heb ik de betekenis van de NULL als een markering voor een ontbrekende waarde behandeld, hoe NULL's zich gedragen in vergelijkingen en in andere query-elementen, en standaard NULL-afhandelingsfuncties die niet nog beschikbaar in T-SQL. Deze maand behandel ik het verschil tussen de manier waarop een unieke beperking is gedefinieerd in de ISO/IEC SQL-standaard en de manier waarop het werkt in T-SQL. Ik bied ook oplossingen op maat die u kunt implementeren als u de standaardfunctionaliteit nodig heeft.
Standaard UNIEKE beperking
SQL Server verwerkt NULL's net als niet-NULL-waarden om een unieke beperking af te dwingen. Dat wil zeggen dat aan een unieke beperking op T wordt voldaan als en alleen als er geen twee rijen R1 en R2 van T bestaan, zodat R1 en R2 dezelfde combinatie van NULL- en niet-NULL-waarden in de unieke kolommen hebben. Stel bijvoorbeeld dat u een unieke beperking voor col1 definieert, een NULL-kolom van een INT-gegevenstype. Een poging om de tabel te wijzigen op een manier die zou resulteren in meer dan één rij met een NULL in col1 zal worden afgewezen, net zoals een wijziging die zou resulteren in meer dan één rij met de waarde 1 in col1 zal worden afgewezen.
Stel dat u een samengestelde unieke beperking definieert voor de combinatie van NULLable INT-kolommen col1 en col2. Een poging om de tabel zodanig te wijzigen dat een van de volgende combinaties van (col1, col2) waarden meer dan één keer voorkomt, wordt afgewezen:(NULL, NULL), (3, NULL), (NULL, 300 ), (1, 100).
Zoals u kunt zien, behandelt de T-SQL-implementatie van de unieke beperking NULL's net als niet-NULL-waarden met het oog op het afdwingen van uniciteit.
Als u een externe sleutel wilt definiëren voor een tabel X die verwijst naar een tabel Y, moet u uniciteit afdwingen op de kolom(men) waarnaar wordt verwezen met een van de volgende opties:
- Primaire sleutel
- Unieke beperking
- Niet-gefilterde unieke index
Een primaire sleutel is niet toegestaan op NULL-kolommen. Zowel een unieke beperking (die een index onder de omslagen creëert) als een expliciet gemaakte unieke index zijn toegestaan op NULL-kolommen en dwingen hun uniciteit af in T-SQL met behulp van de bovengenoemde logica. De verwijzingstabel mag rijen hebben met een NULL in de verwijzingskolom, ongeacht of de tabel waarnaar wordt verwezen een rij heeft met een NULL in de kolom waarnaar wordt verwezen. Het idee is om een optionele relatie te ondersteunen. Sommige rijen in de tabel waarnaar wordt verwezen, kunnen rijen zijn die niet zijn gerelateerd aan rijen in de tabel waarnaar wordt verwezen. U implementeert dit door een NULL te gebruiken in de verwijzingskolom.
Om de T-SQL-implementatie van een unieke beperking te demonstreren, voert u de volgende code uit, waarmee een tabel met de naam T3 wordt gemaakt met een unieke beperking die is gedefinieerd in de NULLable INT-kolom col1, en deze vult met een paar voorbeeldrijen:
USE tempdb; GO DROP TABLE IF EXISTS dbo.T3; GO CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1)); INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(2, -1),(NULL, -1),(3, 300);
Gebruik de volgende code om de tabel te doorzoeken:
SELECT * FROM dbo.T3;
Deze query genereert de volgende uitvoer:
col1 col2 ----------- ----------- 1 100 2 -1 NULL -1 3 300
Poging om een tweede rij in te voegen met een NULL in col1:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);
Deze poging wordt afgewezen en u krijgt de volgende foutmelding:
Msg 2627, niveau 14, staat 1Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is (
De standaard unieke beperkingsdefinitie is een beetje anders dan de T-SQL-versie. Het belangrijkste verschil heeft te maken met de NULL-behandeling. Hier is de unieke beperkingsdefinitie van de standaard:
"Er wordt voldaan aan een unieke beperking op T als en alleen als er geen twee rijen R1 en R2 van T bestaan, zodat R1 en R2 dezelfde niet-NULL-waarden hebben in de unieke kolommen."
Een tabel T met een unieke beperking op col1 staat dus meerdere rijen toe met een NULL in col1, maar staat meerdere rijen met dezelfde niet-NULL-waarde in col1 niet toe.
Wat een beetje lastiger uit te leggen is, is wat er gebeurt volgens de standaard met een samengestelde unieke beperking. Stel dat u een unieke beperking hebt gedefinieerd voor (col1, col2). U kunt meerdere rijen hebben met (NULL, NULL), maar u kunt niet meerdere rijen hebben met (3, NULL), net zoals u niet meerdere rijen kunt hebben met (1, 100). Evenzo kunt u niet meerdere rijen hebben met (NULL, 300). Het punt is dat je niet meerdere rijen met dezelfde niet-NULL-waarden in de unieke kolommen mag hebben. Wat betreft een externe sleutel, u kunt een willekeurig aantal rijen in de verwijzingstabel hebben met NULL's in alle verwijzingskolommen, ongeacht wat er in de tabel waarnaar wordt verwezen, bestaat. Dergelijke rijen zijn niet gerelateerd aan rijen in de tabel waarnaar wordt verwezen (optionele relatie). Als u echter een niet-NULL-waarde in een van de verwijzende kolommen heeft, moet er een rij in de tabel waarnaar wordt verwezen met dezelfde niet-NULL-waarden in de kolommen waarnaar wordt verwezen.
Stel dat u een database hebt op een platform dat de standaard unieke beperking ondersteunt en dat u die database naar SQL Server moet migreren. U kunt problemen ondervinden met het afdwingen van unieke beperkingen in SQL Server als de unieke kolommen NULL's ondersteunen. Gegevens die in het bronsysteem als geldig werden beschouwd, kunnen in SQL Server als ongeldig worden beschouwd. In de volgende paragrafen zal ik een aantal mogelijke oplossingen in SQL Server onderzoeken.
Oplossing 1, met gefilterde index of geïndexeerde weergave
Een veelvoorkomende oplossing in T-SQL voor het afdwingen van de standaard unieke beperkingsfunctionaliteit wanneer er slechts één doelkolom is, is het gebruik van een unieke gefilterde index die alleen de rijen filtert waar de doelkolom niet NULL is. De volgende code verwijdert de bestaande unieke beperking van T3 en implementeert een dergelijke index:
ALTER TABLE dbo.T3 DROP CONSTRAINT UNQ_T3; CREATE UNIQUE NONCLUSTERED INDEX idx_col1_notnull ON dbo.T3(col1) WHERE col1 IS NOT NULL;
Aangezien de index alleen rijen filtert waarin col1 niet NULL is, wordt de eigenschap UNIQUE alleen afgedwongen op de niet-NULL col1-waarden.
Bedenk dat T3 al een rij heeft met een NULL in col1. Om deze oplossing te testen, gebruikt u de volgende code om een tweede rij toe te voegen met een NULL in col1:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);
Deze code is succesvol uitgevoerd.
Bedenk dat T3 al een rij heeft met de waarde 1 in col1. Voer de volgende code uit om te proberen een tweede rij toe te voegen met 1 in col1:
INSERT INTO dbo.T3(col1, col2) VALUES(1, 500);
Zoals verwacht mislukt deze poging met de volgende fout:
Msg 2601, Level 14, State 1Kan geen dubbele sleutelrij invoegen in object 'dbo.T3' met unieke index 'idx_col1_notnull'. De dubbele sleutelwaarde is (1).
Gebruik de volgende code om T3 op te vragen:
SELECT * FROM dbo.T3;
Deze code genereert de volgende uitvoer met twee rijen met een NULL in col1:
col1 col2 ----------- ----------- 1 100 2 -1 NULL -1 3 300 NULL 400
Deze oplossing werkt goed wanneer u uniciteit op slechts één kolom moet afdwingen en wanneer u geen referentiële integriteit hoeft af te dwingen met een externe sleutel die naar die kolom verwijst.
Het probleem met de externe sleutel is dat SQL Server een primaire sleutel of een unieke beperking of een unieke niet-gefilterde index vereist die is gedefinieerd in de kolom waarnaar wordt verwezen. Het werkt niet als er alleen een unieke gefilterde index is gedefinieerd in de kolom waarnaar wordt verwezen. Laten we proberen een tabel te maken met een externe sleutel die verwijst naar T3.col1. Gebruik eerst de volgende code om de tabel T3 te maken:
DROP TABLE IF EXISTS dbo.T3FK; GO CREATE TABLE dbo.T3FK ( id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY, col1 INT NULL, col2 INT NULL, othercol VARCHAR(10) NOT NULL );
Probeer vervolgens de volgende code uit te voeren in een poging een externe sleutel toe te voegen die wijst van T3FK.col1 naar T3.col1:
ALTER TABLE dbo.T3FK ADD CONSTRAINT FK_T3_T3FK FOREIGN KEY(col1) REFERENCES dbo.T3(col1);
Deze poging mislukt met de volgende fout:
Msg 1776, Level 16, State 0Er zijn geen primaire of kandidaat-sleutels in de tabel 'dbo.T3' waarnaar wordt verwezen die overeenkomen met de lijst met verwijzende kolommen in de refererende sleutel 'FK_T3_T3FK'.
Msg 1750, niveau 16, staat 1
Kan geen beperking of index maken. Zie eerdere fouten.
Laat op dit punt de bestaande gefilterde index vallen voor opschoning:
DROP INDEX idx_col1_notnull ON dbo.T3;
Laat de tabel T3FK niet vallen, aangezien u deze in latere voorbeelden zult gebruiken.
Het andere probleem met de gefilterde indexoplossing, ervan uitgaande dat u geen externe sleutel nodig hebt, is dat het niet werkt wanneer u de standaard unieke beperkingsfunctionaliteit op meerdere kolommen moet afdwingen, bijvoorbeeld op de combinatie (col1, col2) . Houd er rekening mee dat de standaard unieke beperking dubbele niet-NULL-combinaties van waarden in de unieke kolommen niet toestaat. Als u deze logica wilt implementeren met een gefilterde index, hoeft u alleen rijen te filteren waarin een van de unieke kolommen niet NULL is. Anders gezegd, u hoeft alleen rijen te filteren die geen NULL's hebben in alle unieke kolommen. Helaas laten gefilterde indexen alleen zeer eenvoudige uitdrukkingen toe. Ze ondersteunen geen OR, NOT of manipulatie op de kolommen. Dus geen van de volgende indexdefinities wordt momenteel ondersteund:
CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2) WHERE col1 IS NOT NULL OR col2 IS NOT NULL; CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2) WHERE NOT (col1 IS NULL AND col2 IS NULL); CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2) WHERE COALESCE(col1, col2) IS NOT NULL;
De tijdelijke oplossing in een dergelijk geval is om een geïndexeerde weergave te maken op basis van een query die col1 en col2 van T3 retourneert met een van de WHERE-clausules hierboven, met een unieke geclusterde index op (col1, col2), zoals zo:
CREATE VIEW dbo.T3CustomUnique WITH SCHEMABINDING AS SELECT col1, col2 FROM dbo.T3 WHERE col1 IS NOT NULL OR col2 IS NOT NULL; GO CREATE UNIQUE CLUSTERED INDEX idx_col1_col2 ON dbo.T3CustomUnique(col1, col2); GO
U mag meerdere rijen toevoegen met (NULL, NULL) in (col1, col2), maar u mag niet meerdere keren voorkomen van niet-NULL-combinaties van waarden in (col1, col2), zoals (3 , NULL) of (NULL, 300) of (1, 100). Toch ondersteunt deze oplossing geen externe sleutel.
Voer nu de volgende code uit om op te schonen:
DROP VIEW IF EXISTS dbo.T3CustomUnique;
Oplossing 2, met surrogaatsleutel en berekende kolom
De oplossingen met de gefilterde index en de geïndexeerde weergave zijn goed zolang u geen externe sleutel hoeft te ondersteunen. Maar wat als u referentiële integriteit wel moet afdwingen? Een optie is om de gefilterde index of geïndexeerde weergave-oplossing te blijven gebruiken om uniciteit af te dwingen, en triggers te gebruiken om referentiële integriteit af te dwingen. Deze optie is echter vrij duur.
Een andere optie is om een geheel andere oplossing te gebruiken voor het uniciteitsgedeelte dat wel een externe sleutel ondersteunt. De oplossing omvat het toevoegen van twee kolommen aan de tabel waarnaar wordt verwezen (T3 in ons geval). Eén kolom met de naam id is een surrogaatsleutel met een identiteitseigenschap. Een andere kolom met de naam vlag is een persistente berekende kolom die id retourneert wanneer col1 NULL is en 0 wanneer het niet NULL is. Vervolgens dwingt u een unieke beperking af op de combinatie van col1 en vlag. Hier is de code om de twee kolommen en de unieke beperking toe te voegen:
ALTER TABLE dbo.T3 ADD id INT NOT NULL IDENTITY, flag AS CASE WHEN col1 IS NULL THEN id ELSE 0 END PERSISTED, CONSTRAINT UNQ_T3_col1_flag UNIQUE(col1, flag);
Gebruik de volgende code om T3 op te vragen:
SELECT * FROM dbo.T3;
Deze code genereert de volgende uitvoer:
col1 col2 id flag ----------- ----------- ----------- ----------- 1 100 1 0 2 -1 2 0 NULL -1 3 3 3 300 4 0 NULL 400 5 5
Wat betreft de referentietabel (T3FK in ons geval), voeg je een berekende kolom toe met de naam vlag die altijd is ingesteld op 0, en een externe sleutel gedefinieerd op (col1, vlag) die verwijst naar de unieke kolommen van T3 (col1, vlag), zoals zo :
ALTER TABLE dbo.T3FK ADD flag AS 0 PERSISTED, CONSTRAINT FK_T3_T3FK FOREIGN KEY(col1, flag) REFERENCES dbo.T3(col1, flag);
Laten we deze oplossing testen.
Probeer de volgende rijen toe te voegen:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES (1, 100, 'A'), (2, -1, 'B'), (3, 300, 'C');
Deze rijen zijn toegevoegd, zoals het hoort, aangezien ze allemaal corresponderende rijen hebben waarnaar wordt verwezen.
Vraag de tabel T3FK:
SELECT * FROM dbo.T3FK;
U krijgt de volgende uitvoer:
id col1 col2 othercol flag ----------- ----------- ----------- ---------- ----------- 1 1 100 A 0 2 2 -1 B 0 3 3 300 C 0
Probeer een rij toe te voegen die geen overeenkomstige rij heeft in de tabel waarnaar wordt verwezen:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES (4, 400, 'D');
De poging wordt afgewezen, zoals het hoort, met de volgende fout:
Msg 547, niveau 16, staat 0De INSERT-instructie was in strijd met de FOREIGN KEY-beperking "FK_T3_T3FK". Het conflict deed zich voor in database "TSQLV5", tabel "dbo.T3".
Probeer een rij toe te voegen aan T3FK met een NULL in col1:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES (NULL, NULL, 'E');
Deze rij wordt beschouwd als niet gerelateerd aan een rij in T3FK (optionele relatie) en zou volgens de standaard moeten worden toegestaan, ongeacht of er een NULL bestaat in de tabel waarnaar wordt verwezen in col1. T-SQL ondersteunt dit scenario en de rij is succesvol toegevoegd.
Vraag de tabel T3FK:
SELECT * FROM dbo.T3FK;
Deze code genereert de volgende uitvoer:
id col1 col2 othercol flag ----------- ----------- ----------- ---------- ----------- 1 1 100 A 0 2 2 -1 B 0 3 3 300 C 0 5 NULL NULL E 0
De oplossing werkt goed wanneer u de standaard uniciteitsfunctionaliteit op een enkele kolom moet afdwingen. Maar het heeft een probleem wanneer u uniciteit op meerdere kolommen moet afdwingen. Om het probleem te demonstreren, laat u eerst de tabellen T3 en T3FK vallen:
DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;
Gebruik de volgende code om T3 opnieuw te maken met een samengestelde unieke beperking op (col1, col2, vlag):
CREATE TABLE dbo.T3 ( col1 INT NULL, col2 INT NULL, id INT NOT NULL IDENTITY, flag AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN id ELSE 0 END PERSISTED, CONSTRAINT UNQ_T3 UNIQUE(col1, col2, flag) );
Merk op dat de vlag is ingesteld op id wanneer zowel col1 als col2 NULL zijn en anders 0.
De unieke beperking zelf werkt goed.
Voer de volgende code uit om een paar rijen aan T3 toe te voegen, inclusief meerdere keren dat (NULL, NULL) in (col1, col2) voorkomt:
INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);
Deze rijen zijn toegevoegd zoals het hoort.
Probeer twee exemplaren van (1, NULL) toe te voegen in (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);
Deze poging mislukt zoals het hoort met de volgende fout:
Msg 2627, niveau 14, staat 1Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is (1,
Probeer twee exemplaren van (NULL, 100) toe te voegen in (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);
Deze poging mislukt ook zoals het hoort met de volgende fout:
Msg 2627, niveau 14, staat 1Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is (
Probeer de volgende twee rijen toe te voegen, waar geen overtreding mag plaatsvinden:
INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);
Deze rijen zijn succesvol toegevoegd.
Vraag de tabel T3 op dit punt op:
SELECT * FROM dbo.T3;
U krijgt de volgende uitvoer:
col1 col2 id flag ----------- ----------- ----------- ----------- 1 100 1 0 1 200 2 0 NULL NULL 3 3 NULL NULL 4 4 3 NULL 9 0 NULL 300 10 0
Tot nu toe zo goed.
Voer vervolgens de volgende code uit om de tabel T3FK te maken met een samengestelde externe sleutel die verwijst naar de unieke kolommen van T3:
CREATE TABLE dbo.T3FK ( id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY, col1 INT NULL, col2 INT NULL, othercol VARCHAR(10) NOT NULL, flag AS 0 PERSISTED, CONSTRAINT FK_T3_T3FK FOREIGN KEY(col1, col2, flag) REFERENCES dbo.T3(col1, col2, flag) );
Deze oplossing maakt het natuurlijk mogelijk om rijen toe te voegen aan T3FK met (NULL, NULL) in (col1, col2). Het probleem is dat het ook toestaat om rijen een NULL toe te voegen in col1 of col2, zelfs als de andere kolom niet NULL is en de tabel waarnaar wordt verwezen T3 niet zo'n toetsencombinatie heeft. Probeer bijvoorbeeld de volgende rij toe te voegen aan T3FK:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');
Deze rij is succesvol toegevoegd, ook al is er geen gerelateerde rij in T3. Volgens de norm mag deze rij niet worden toegestaan.
Terug naar de tekentafel...
Oplossing 3, met surrogaatsleutel en berekende kolom
Het probleem met de vorige oplossing (oplossing 2) doet zich voor wanneer u een samengestelde externe sleutel moet ondersteunen. Het staat rijen in de verwijzingstabel toe die een NULL hebben in lijst één verwijzende kolom, zelfs als er niet-NULL-waarden zijn in andere verwijzingskolommen, en geen gerelateerde rij in de tabel waarnaar wordt verwezen. Om dit aan te pakken, kunt u een variatie op de vorige oplossing gebruiken, die we Oplossing 3 zullen noemen.
Gebruik eerst de volgende code om de bestaande tabellen te verwijderen:
DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;
In de nieuwe oplossing in de tabel waarnaar wordt verwezen (T3 in ons geval), gebruikt u nog steeds de op identiteit gebaseerde id-surrogaatsleutelkolom. U gebruikt ook een persistente berekende kolom met de naam unqpath. Als alle unieke kolommen (col1 en col2 in ons voorbeeld) NULL zijn, stelt u unqpath in op een tekenreeksrepresentatie van id (geen scheidingstekens ). Wanneer een van de unieke kolommen niet NULL is, stelt u unqpath in op een tekenreeksrepresentatie van een gescheiden lijst van de unieke kolomwaarden met behulp van de CONCAT-functie. Deze functie vervangt een NULL door een lege string. Het is belangrijk om ervoor te zorgen dat u een scheidingsteken gebruikt dat normaal gesproken niet in de gegevens zelf kan voorkomen. Met integer col1 en col2 waarden heb je bijvoorbeeld alleen cijfers, dus elk ander scheidingsteken dan een cijfer zou werken. In mijn voorbeeld gebruik ik een punt (.). U dwingt dan een unieke beperking af op unqpath. U zult nooit een conflict hebben tussen de unqpath-waarde wanneer alle unieke kolommen NULL zijn (ingesteld op id) versus wanneer een van de unieke kolommen niet NULL is, omdat in het eerste geval unqpath geen scheidingsteken bevat, en in het laatste geval wel . Onthoud dat u Oplossing 3 zult gebruiken als u een samengesteld sleutelgeval hebt, en waarschijnlijk de voorkeur geeft aan Oplossing 2, wat eenvoudiger is, wanneer u een sleutelgeval met één kolom hebt. Als u Oplossing 3 ook wilt gebruiken met een sleutel met één kolom en niet met Oplossing 2, zorg er dan voor dat u het scheidingsteken toevoegt wanneer de unieke kolom niet NULL is, ook al is er maar één waarde bij betrokken. Op deze manier heb je geen conflict wanneer id in een rij waar col1 NULL is gelijk is aan col1 in een andere rij, aangezien de eerste geen scheidingsteken heeft en de laatste wel.
Hier is de code om T3 te maken met de bovengenoemde toevoegingen:
CREATE TABLE dbo.T3 ( col1 INT NULL, col2 INT NULL, id INT NOT NULL IDENTITY, unqpath AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN CAST(id AS VARCHAR(10)) ELSE CONCAT(CAST(col1 AS VARCHAR(11)), '.', CAST(col2 AS VARCHAR(11))) END PERSISTED, CONSTRAINT UNQ_T3 UNIQUE(unqpath) );
Laten we, voordat we een externe sleutel en de referentietabel behandelen, de unieke beperking testen. Onthoud dat het de bedoeling is dat dubbele combinaties van niet-NULL-waarden in de unieke kolommen niet worden toegestaan, maar dat het meerdere keren voorkomen van alle-NULL-waarden in de unieke kolommen moet toestaan.
Voer de volgende code uit om een paar rijen toe te voegen, inclusief twee exemplaren van (NULL, NULL) in (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);
Deze code wordt voltooid zoals het hoort.
Probeer twee exemplaren van (1, NULL) toe te voegen aan (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);
Deze code mislukt met de volgende fout zoals het hoort:
Msg 2627, niveau 14, staat 1Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is (1.).
Evenzo wordt de volgende poging ook afgewezen:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);
U krijgt de volgende foutmelding:
Msg 2627, niveau 14, staat 1Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is (.100).
Voer de volgende code uit om nog een paar rijen toe te voegen:
INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);
Deze code werkt zoals het hoort.
Vraag nu T3 op:
SELECT * FROM dbo.T3;
U krijgt de volgende uitvoer:
col1 col2 id unqpath ----------- ----------- ----------- ----------------------- 1 100 1 1.100 1 200 2 1.200 NULL NULL 3 3 NULL NULL 4 4 3 NULL 9 3. NULL 300 10 .300
Observeer de unqpath-waarden en zorg ervoor dat u de logica achter hun constructie begrijpt, en het verschil tussen een geval waarin alle unieke kolommen NULL zijn (geen scheidingsteken) en wanneer ten minste één niet NULL is (scheidingsteken bestaat).
Wat betreft de referentietabel, T3FK; u definieert ook een berekende kolom met de naam unqpath, maar in het geval dat alle verwijzende kolommen NULL zijn, stelt u de kolom in op NULL - niet op id. Als een van de verwijzingskolommen niet NULL is, maakt u dezelfde gescheiden lijst met waarden als in T3. Vervolgens definieert u een externe sleutel op T3FK.unqpath die naar T3.unqpath wijst, zoals:
CREATE TABLE dbo.T3FK ( id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY, col1 INT NULL, col2 INT NULL, othercol VARCHAR(10) NOT NULL, unqpath AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN NULL ELSE CONCAT(CAST(col1 AS VARCHAR(11)), '.', CAST(col2 AS VARCHAR(11))) END PERSISTED, CONSTRAINT FK_T3_T3FK FOREIGN KEY(unqpath) REFERENCES dbo.T3(unqpath) );
Deze externe sleutel zal rijen in T3FK weigeren waar een van de verwijzende kolommen niet NULL is, en er is geen gerelateerde rij in de tabel T3 waarnaar wordt verwezen, zoals de volgende poging laat zien:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');
Deze code genereert de volgende fout:
Msg 547, niveau 16, staat 0De INSERT-instructie was in strijd met de FOREIGN KEY-beperking "FK_T3_T3FK". Het conflict deed zich voor in database "TSQLV5", tabel "dbo.T3", kolom 'unqpath'.
Deze oplossing zal rijen in T3FK waar een van de referentiekolommen niet NULL is zolang er een gerelateerde rij in T3 bestaat, evenals rijen met NULL's in alle referentiekolommen, aangezien dergelijke rijen worden beschouwd als niet-gerelateerd aan rijen in T3. De volgende code voegt zulke geldige rijen toe aan T3FK:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES (1 , 100 , 'A'), (1 , 200 , 'B'), (3 , NULL, 'C'), (NULL, 300 , 'D'), (NULL, NULL, 'E'), (NULL, NULL, 'F');
Deze code is succesvol voltooid.
Voer de volgende code uit om T3FK op te vragen:
SELECT * FROM dbo.T3FK;
U krijgt de volgende uitvoer:
id col1 col2 othercol unqpath ----------- ----------- ----------- ---------- ----------------------- 2 1 100 A 1.100 3 1 200 B 1.200 4 3 NULL C 3. 5 NULL 300 D .300 6 NULL NULL E NULL 7 NULL NULL F NULL
Er was dus wat creativiteit voor nodig, maar nu heb je een oplossing voor de standaard unieke beperking, inclusief ondersteuning voor externe sleutels.
Conclusie
Je zou denken dat een unieke beperking een eenvoudige functie is, maar het kan een beetje lastig worden als je NULL's in de unieke kolommen moet ondersteunen. Het wordt ingewikkelder wanneer u de standaard unieke beperkingsfunctionaliteit in T-SQL moet implementeren, aangezien de twee verschillende regels gebruiken in termen van hoe ze met NULL's omgaan. In dit artikel heb ik het verschil tussen de twee uitgelegd en tijdelijke oplossingen gegeven die wel werken in T-SQL. U kunt een eenvoudige gefilterde index gebruiken wanneer u uniciteit op slechts één NULL-kolom wilt afdwingen, en u hoeft geen externe sleutel te ondersteunen die naar die kolom verwijst. Als u echter een externe sleutel of een samengestelde unieke beperking met de standaardfunctionaliteit moet ondersteunen, heeft u een complexere implementatie nodig met een surrogaatsleutel en een berekende kolom.