views:

1213

answers:

7

I have a situation where i need to enforce a unique constraint on a set of columns, but only for one value of a column.

so for example i have a table like Table(ID, Name, RecordStatus)

Record status can only have a value 1 or 2 (active or deleted), and i want to create a unique constraint on ID, RecordStatus only when RecordStatus = 1, since i dont care if there are multiple deleted records with the same id.

apart from writing triggers, can i do that?

i am using sql server 2005

many thanks

+1  A: 

Because, you are going to allow duplicates, a unique constraint will not work. You can create a check constraint for RecordStatus column and a stored procedure for INSERT that checks the existing active records before inserting duplicate IDs.

ichiban
+4  A: 

You could move the deleted records to a table that lacks the constraint, and perhaps use a view with UNION of the two tables to preserve the appearance of a single table.

Carl Manaster
That's actually pretty clever Carl. It's not an answer to the question per se, but it's a good solution. If the table has a lot of rows, that could also speed up looking for an active record because you could look at the active record table. It would also speed up the constraint because the unique constraint uses an index as opposed to the check constraint I wrote below which has to execute a count. I like it.
D. Patrick
+2  A: 

Add a check constraint like this. The difference is, you'll return false if Status = 1 and 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;
D. Patrick
i looked at table level check constraints but doesnt look there is any way to pass the values being inserted or updated to the function, do you know how to ?
Okay, I posted a sample script that will help you prove what I'm talking about. I tested it and it works. If you look at the two commented lines, you'll see the message I get.Nota bene, in my implementation, I merely ensure that you cannot add a second item with the same Id which is active if there is already one active one. You could modify the logic such that if there is an active one, you cannot add any item with the same id. With this pattern, the possibilities are pretty much endless.
D. Patrick
I'd prefer the same logic in a trigger. "a query in a scalar function... can create big problems if your CHECK constraint relies on a query and if more than one row is affected by any update. What happens is that the constraint gets checked once for each row before the statement completes. That means statement atomicity is broken and the function will be exposed to the database in an inconsistent state. The results are unpredicable and inaccurate." See: http://blogs.conchango.com/davidportas/archive/2007/02/19/Trouble-with-CHECK-Constraints.aspx
onedaywhen
That's only partially true onedaywhen. The database behaves consistently and predictably. The check constraint will execute after the row is added to the table and before the transaction is committed by the dbms and you can count on that. That blog was talking about a pretty unique problem where you need to execute the constraint against a set of inserts rather than just one insert at a time. ashish is asking for a constraint on one insert at a time and this constraint will work accurately, predictably, and consistently.I'm sorry if this sounded terse; I was running out of characters.
D. Patrick
This works great for inserts but doesn't seem to work for updates. E.G. Adding this after the other inserts works here when I didn't expect it to.INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2);update CheckConstraint set Recordstatus=1 where name = 'No ProblemsA'
dwidel
+3  A: 

You could use a UNIQUE constraint if you use NULL as your deleted status.

Multiple rows with NULL are permitted by a UNIQUE constraint (at least in standard SQL -- I haven't confirmed this with a test against SQL Server 2005).

Bill Karwin
Which column would you put a unique constraint on Bill? I don't think I understand your solution. Are you saying you'd create a calculated attribute that is equal to Id when RecordStatus = 2? That could work. Other than that, it won't. RecordStatus can only be 1 or 2 (i.e., not null) and even if it could be, you could only have one active record in the entire table rather than one active record per id.Please elaborate. Thanks a lot.
D. Patrick
No, I was saying you must use NULL as the RecordStatus for deleted entries. Then create a UNIQUE constraint over the *pair* of columns (ID, RecordStatus). Thus you could have multiple deleted entries per ID but only a single entry with RecordStatus = 1 per ID.
Bill Karwin
That makes sense. If null was an option, that'd work well and wouldn't get the potential performance hit of the check constraint if the table is large.
D. Patrick
+1  A: 

You can do this in a really hacky way...

Create an schemabound view on your table.

CREATE VIEW Whatever SELECT * FROM Table WHERE RecordStatus = 1

Now create a unique constraint on the view with the fields you want.

One note about schemabound views though, if you change the underlying tables you will have to recreate the view. Plenty of gotchas because of that.

Min
+1  A: 

If you can't use NULL as a RecordStatus as Bill's suggested, you could combine his idea with a function-based index. Create a function that returns NULL if the RecordStatus is not one of the values you want to consider in your constraint (and the RecordStatus otherwise) and create an index over that.

That'll have the advantage that you don't have to explicitly examine other rows in the table in your constraint, which could cause you performance issues.

I should say I don't know SQL server at all, but I have successfully used this approach in Oracle.

Hobo
good idea, but there are no function based indexed in sql serverthanks for the answer though
A: 

Solution described here. Scroll down to "Use Computed Columns to Implement Complex Business Rules"

http://www.devx.com/dbzone/Article/30786/1954

AlexKuznetsov