I'm creating a table that'll have a single bit not null column IsDefault. I need to write a constraint that'll make sure there'll be only one default value per UserId (field in the same table).
I can't use unique constraint on this because it is possible to have many non-default values.
What is the best approach to do this using MS SQL Server 2008?
Thanks.
The easiest way I see is a check constraint with a UDF (User Defined function).
Look at here, for example.
http://sqljourney.wordpress.com/2010/06/25/check-constraint-with-user-defined-function-in-sql-server/
Untested example
CREATE FUNCTION dbo.CheckDefaultUnicity(#UserId int)
RETURNS int
AS
BEGIN
DECLARE #retval int
SELECT #retval = COUNT(*) FROM <your table> where UserId = #UserId and <columnwithDefault> = 1-- or whatever is your default value
RETURN #retval
END;
GO
and alter your table
ALTER TABLE <yourTable>
ADD CONSTRAINT Ck_UniqueDefaultForUser
CHECK (dbo.CheckDefaultUnicity(UserId) <2)
Another relatively simple option is to use a CLUSTERED INDEXED VIEW. The gist of this is to
Select all UserID's from your table where IsDefault=1 in a view.
Add a unique index on UserID
Clustered indexed view
CREATE VIEW dbo.VIEW_Users_IsDefault WITH SCHEMABINDING AS
SELECT UserID, IsDefault
FROM dbo.Users
WHERE IsDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_USERS_ISDEFAULT
ON dbo.VIEW_Users_IsDefault (UserID)
GO
Test script
BEGIN TRAN
CREATE TABLE dbo.Users (UserID INT, IsDefault BIT)
GO
CREATE VIEW dbo.VIEW_Users_IsDefault WITH SCHEMABINDING AS
SELECT UserID, IsDefault
FROM dbo.Users
WHERE IsDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_USERS_ISDEFAULT ON dbo.VIEW_Users_IsDefault (UserID)
GO
INSERT INTO dbo.Users VALUES (1, 0)
INSERT INTO dbo.Users VALUES (1, 1)
INSERT INTO dbo.Users VALUES (1, 1) -- Fails because of clustered index
ROLLBACK TRAN
A Check Constraint would definitely work, however it's not good a design choice in my opinion. The reason being that your UDF for the constraint would be something like
SELECT #Count = COUNT(UserId)
FROM User
WHERE IsDefault = 1
GROUP BY UserId
HAVING COUNT(UserId) > 1
IF #Count > 0
....'FAIL
As this touches 2 columns it would thus need to be a Table level constraint and the more records you have the slower a Insert/Update/Delete will take.
A better option would be to only allow access to that table via Stored Procedure, so before an insert/update you could run a very quick
IF EXISTS(SELECT UserId FROM User where UserId = #UserId and IsDefault = 1)
before your inserts/updates/deletes
I can however appreciate that you may be using an ORM and might not want to have Stored Procs in your system so you could change the design of your table to the below. This assumes that
tblUser: UserId, FirstName, Suraname, etc
tblUserDefault: UserId (Unique Constraint)
I'm not sure what IsDefault represents in your system so I'm assuming in the above that Users are either default or not. Anybody you can use that as a reference. It allows you to enforce the constraint without using USP's or horrid tablewide check constraints (or triggers) and would be mappable in any decent ORM
What about CHECK Constraints. See here: http://msdn.microsoft.com/en-us/library/ms188258(v=sql.105).aspx
ALTER TABLE yourtable
ADD CONSTRAINT IsDefaultChecked CHECK (IsDefault = T );
While I think the trigger and constraint solutions are better, if you control insert/update via stored procedure a much simpler approach would be to just update the conflicting rows first (assuming the new default always wins):
ALTER PROCEDURE dbo.UserWhateverTable_<action>
#UserID INT,
#CardID INT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.UserWhatever
SET IsDefault = 0
WHERE UserID = #UserID
AND CardID = #CardID
AND IsDefault = 1;
-- insert or update here
END
GO
In fact it might not be a bad idea to do this (so the business logic is 100% clear in your DML procedures) in addition to guarding it with a trigger or a constraint (to catch cases where updates are made outside of your procedures).
You might want to use a trigger in this case. When the user is changing their default, the trigger could automatically flip the current default for the current user to false.
Basically, use an AFTER insert/update trigger to set the IsDefault column to 0 for any user in the insert/update where the IsDefault value is being set to 1.
CREATE TRIGGER dbo.tr_default
ON dbo.MyTable
AFTER INSERT, UPDATE
AS
if(exists(select * from inserted where IsDefault = 1)
begin
update dbo.MyTable
set IsDefault = 0
from inserted i
join dbo.MyTable t on i.userid = t.userid
where i.IsDefault = 1
and i.TheValue != t.TheValue
end
I don't see any problem with any of the answers so far suggesting CHECK CONSTRAINTS and TRIGGERS however, it seems like a bit of a backwards solution to me.
I can only assume UserID in your table is a foreign key to a User table, so why not add a column to your User table to store the default CardID, rather than marking one as default? This makes it impossible for a user to have multiple default CardIDs without costly triggers/constraints. If you make the column non nullable then it is also impossible for a user not to have a default CardID if you so wish.
Related
I need to define a table which basically will contain an Id for a user, and a second column which will list names of tables to which the user has access. I can't think of anyway to define any relationships here in case the original table names change. All the logic will be at the application level. However, I would like to be able to define some sort of constraints. How can I do this? Also, I am open to advice regarding any other way to do this.
I am really confused. Doesn't the grant command do exactly what you want? This assumes that the operations you want are database operations.
If you have a more customized set of operations, then you can keep track of table name changes via DDL triggers.
Here's a detailed code example of how to achieve this using RLS
USE tempdb;
GO
CREATE TABLE dbo.OKTable
(
OKTableID int IDENTITY(1,1) NOT NULL
CONSTRAINT PK_dbo_OKTable PRIMARY KEY,
SecuredInfo varchar(100)
);
GO
INSERT dbo.OKTable (SecuredInfo)
VALUES ('Very'), ('Secret'), ('Stuff');
GO
CREATE TABLE dbo.NotOKTable
(
NotOKTableID int IDENTITY(1,1) NOT NULL
CONSTRAINT PK_dbo_NotOKTable PRIMARY KEY,
SecuredInfo varchar(100)
);
GO
INSERT dbo.NotOKTable (SecuredInfo)
VALUES ('Other'), ('Important'), ('Things');
GO
CREATE SCHEMA [Security] AUTHORIZATION dbo;
GO
CREATE TABLE [Security].PermittedTableUsers
(
PermittedTableUsers int IDENTITY(1,1) NOT NULL
CONSTRAINT PK_Security_PermittedTableUsers
PRIMARY KEY,
UserName sysname,
SchemaName sysname,
TableName sysname
);
GO
INSERT [Security].PermittedTableUsers (UserName, SchemaName, TableName)
VALUES (N'dbo', N'dbo', 'OKTable');
GO
ALTER FUNCTION [Security].CheckUserAccess
(
#SchemaName AS sysname,
#TableName AS sysname
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS CheckUserAccessOutcome
WHERE EXISTS (SELECT 1 FROM [Security].PermittedTableUsers AS ptu
WHERE ptu.UserName = USER_NAME()
AND ptu.SchemaName = #SchemaName
AND ptu.TableName = #TableName);
GO
CREATE SECURITY POLICY OKTableAccessFilter
ADD FILTER PREDICATE [Security].CheckUserAccess (N'dbo', N'OKTable')
ON dbo.OKTable
WITH (STATE = ON);
GO
CREATE SECURITY POLICY NotOKTableAccessFilter
ADD FILTER PREDICATE [Security].CheckUserAccess (N'dbo', N'NotOKTable')
ON dbo.NotOKTable
WITH (STATE = ON);
GO
SELECT * FROM dbo.OKTable;
SELECT * FROM dbo.NotOKTable;
GO
It's described more fully in this link:
https://blog.greglow.com/2019/10/10/sql-how-to-control-access-to-sql-server-tables-by-entries-in-another-table/
Since users are NOT SQL Server Logins, therefore, I guess you can use the DDL trigger to monitor table rename where you can change the table name in your custom security table. But I don't know if you can throw exception within this trigger to prevent table rename (simulating some type of constraint). Also it would be better if you store the table name in each line rather saving comma separated table names in 1 field.
If you can utilize SQL Logins then the Gordan's solution is also applicable, but sometimes you cannot create SQL Logins, in-case if you have different application databases along with hundreds of thousands of users.
I'm trying to replicate a foreign key across two servers using trigger. I know using trigger across 2 servers is not the best practice but my company on gives me read-only access to their database which I need to relate to my application.
I have DB1 which is my local database and it is attached to DB2 using linked server. I want trigger to check if a specific ID from a DB2_table on DB2 exists before executing an INSERT on DB1_table where the ID from DB2_table will act as a foreign key.
CREATE TRIGGER trigger_DB1_Table_Insert
#ID
BEFORE INSERT ON DB1_Table
AS
BEGIN
if exists(Select ID from DB2_Table where ID = #ID)
--execute insert
END
GO
I would actually recommend using a check constraint instead of a trigger. Check Constraints are designed to enforce data integrity and are a semantically better option. The example below creates some working tables, then creates a function which will check if the record exists in the other table.
The check constraint then uses the function and returns an error if the value doesn't exist.
CREATE TABLE dbo.OtherTable(
id INT
)
CREATE TABLE dbo.a(
id INT IDENTITY(1,1)
,OtherTableId INT
)
GO
INSERT INTO OtherTable (id) VALUES (1), (2), (3)
GO
-- Function will check if the ID exists in the other table
CREATE FUNCTION dbo.CheckOtherTableId(
#OtherTableId INT
)
RETURNS BIT
AS BEGIN
RETURN
(
SELECT
CASE
WHEN EXISTS
(
SELECT 1
FROM OtherTable
WHERE id = #OtherTableId
)
THEN 1
ELSE 0
END
)
END
GO
-- Add check constraint
ALTER TABLE a WITH CHECK
ADD CONSTRAINT [CK_OtherTable] CHECK (1=dbo.CheckOtherTableId(OtherTableId))
GO
-- Test should work
INSERT INTO a (OtherTableId) values (1)
Go
-- Test should fail
INSERT INTO a (OtherTableId) values (8)
GO
I need to be able to monitor a Table and react very time a record is inserted. This table has no ITN IDENTITY field, only a UNIQUEIDENTIFIER as its primary key. Without any alteration of existing inputs, SPs, etc. I need to be able to find the last inserted ID from within a trigger. This is what I have (obviously does not work):
CREATE TRIGGER TR_UserInserted
ON Users
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
EXEC UserInserted (SELECT User_Id FROM INSERTED);
END
GO
Here I am trying to get the User_Id from the last inserted record in the Users table and run it through the UserInserted SP. Thank you for the help, I am stumped.
HLGEM Made a great point - even on a bulk insert, I only need the last record inserted - I know this is a strange request.
You need to change your trigger to fire INSTEAD OF INSERT. A uniqueidentifier variable must be generated using the NEWID() function. In the INSERT statement in the trigger body, the columns must be provided in order. Assuming a table defined this way:
CREATE TABLE Users (
First int,
User_Id uniqueidentifier PRIMARY KEY,
Third int,
Fourth int)
Then the trigger is:
CREATE TRIGGER TR_UserInserted ON Users
INSTEAD OF INSERT AS
BEGIN
DECLARE #newid uniqueidentifier = NEWID()
INSERT INTO Users
SELECT
First,
#newid,
Third,
Fourth
FROM inserted
EXECUTE UserInserted(#newid)
-- you can actually provide all the columns to UserInserted
END
For this to work properly, make sure that the table does not have a default for the primary key as NEWID().
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).
RecordStatus 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 don't 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.
Behold, the filtered index. From the documentation (emphasis mine):
A filtered index is an optimized nonclustered index especially suited to cover queries that select from a well-defined subset of data. It uses a filter predicate to index a portion of rows in the table. A well-designed filtered index can improve query performance as well as reduce index maintenance and storage costs compared with full-table indexes.
And here's an example combining a unique index with a filter predicate:
create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;
This essentially enforces uniqueness of ID when RecordStatus is 1.
Following the creation of that index, a uniqueness violation will raise an arror:
Msg 2601, Level 14, State 1, Line 13
Cannot insert duplicate key row in object 'dbo.MyTable' with unique index 'MyIndex'. The duplicate key value is (9999).
Note: the filtered index was introduced in SQL Server 2008. For earlier versions of SQL Server, please see this answer.
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;
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.
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.
For those still searching for a solution, I came accross a nice answer, to a similar question and I think this can be still useful for many. While moving deleted records to another table may be a better solution, for those who don't want to move the record can use the idea in the linked answer which is as follows.
Set deleted=0 when the record is available/active.
Set deleted=<row_id or some other unique value> when marking the row
as deleted.
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.
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.
How could I set a constraint on a table so that only one of the records has its isDefault bit field set to 1?
The constraint is not table scope, but one default per set of rows, specified by a FormID.
Use a unique filtered index
On SQL Server 2008 or higher you can simply use a unique filtered index
CREATE UNIQUE INDEX IX_TableName_FormID_isDefault
ON TableName(FormID)
WHERE isDefault = 1
Where the table is
CREATE TABLE TableName(
FormID INT NOT NULL,
isDefault BIT NOT NULL
)
For example if you try to insert many rows with the same FormID and isDefault set to 1 you will have this error:
Cannot insert duplicate key row in object 'dbo.TableName' with unique
index 'IX_TableName_FormID_isDefault'. The duplicate key value is (1).
Source: http://technet.microsoft.com/en-us/library/cc280372.aspx
Here's a modification of Damien_The_Unbeliever's solution that allows one default per FormID.
CREATE VIEW form_defaults
AS
SELECT FormID
FROM whatever
WHERE isDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix_form_defaults on form_defaults (FormID)
GO
But the serious relational folks will tell you this information should just be in another table.
CREATE TABLE form
FormID int NOT NULL PRIMARY KEY
DefaultWhateverID int FOREIGN KEY REFERENCES Whatever(ID)
From a normalization perspective, this would be an inefficient way of storing a single fact.
I would opt to hold this information at a higher level, by storing (in a different table) a foreign key to the identifier of the row which is considered to be the default.
CREATE TABLE [dbo].[Foo](
[Id] [int] NOT NULL,
CONSTRAINT [PK_Foo] PRIMARY KEY CLUSTERED
(
[Id] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[DefaultSettings](
[DefaultFoo] [int] NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[DefaultSettings] WITH CHECK ADD CONSTRAINT [FK_DefaultSettings_Foo] FOREIGN KEY([DefaultFoo])
REFERENCES [dbo].[Foo] ([Id])
GO
ALTER TABLE [dbo].[DefaultSettings] CHECK CONSTRAINT [FK_DefaultSettings_Foo]
GO
You could use an insert/update trigger.
Within the trigger after an insert or update, if the count of rows with isDefault = 1 is more than 1, then rollback the transaction.
CREATE VIEW vOnlyOneDefault
AS
SELECT 1 as Lock
FROM <underlying table>
WHERE Default = 1
GO
CREATE UNIQUE CLUSTERED INDEX IX_vOnlyOneDefault on vOnlyOneDefault (Lock)
GO
You'll need to have the right ANSI settings turned on for this.
I don't know about SQLServer.But if it supports Function-Based Indexes like in Oracle, I hope this can be translated, if not, sorry.
You can do an index like this on suposed that default value is 1234, the column is DEFAULT_COLUMN and ID_COLUMN is the primary key:
CREATE
UNIQUE
INDEX only_one_default
ON my_table
( DECODE(DEFAULT_COLUMN, 1234, -1, ID_COLUMN) )
This DDL creates an unique index indexing -1 if the value of DEFAULT_COLUMN is 1234 and ID_COLUMN in any other case. Then, if two columns have DEFAULT_COLUMN value, it raises an exception.
The question implies to me that you have a primary table that has some child records and one of those child records will be the default record. Using address and a separate default table here is an example of how to make that happen using third normal form. Of course I don't know if it's valuable to answer something that is so old but it struck my fancy.
--drop table dev.defaultAddress;
--drop table dev.addresses;
--drop table dev.people;
CREATE TABLE [dev].[people](
[Id] [int] identity primary key,
name char(20)
)
GO
CREATE TABLE [dev].[Addresses](
id int identity primary key,
peopleId int foreign key references dev.people(id),
address varchar(100)
) ON [PRIMARY]
GO
CREATE TABLE [dev].[defaultAddress](
id int identity primary key,
peopleId int foreign key references dev.people(id),
addressesId int foreign key references dev.addresses(id))
go
create unique index defaultAddress on dev.defaultAddress (peopleId)
go
create unique index idx_addr_id_person on dev.addresses(peopleid,id);
go
ALTER TABLE dev.defaultAddress
ADD CONSTRAINT FK_Def_People_Address
FOREIGN KEY(peopleID, addressesID)
REFERENCES dev.Addresses(peopleId, id)
go
insert into dev.people (name)
select 'Bill' union
select 'John' union
select 'Harry'
insert into dev.Addresses (peopleid, address)
select 1, '123 someplace' union
select 1,'work place' union
select 2,'home address' union
select 3,'some address'
insert into dev.defaultaddress (peopleId, addressesid)
select 1,1 union
select 2,3
-- so two home addresses are default now
-- try adding another default address to Bill and you get an error
select * from dev.people
join dev.addresses on people.id = addresses.peopleid
left join dev.defaultAddress on defaultAddress.peopleid = people.id and defaultaddress.addressesid = addresses.id
insert into dev.defaultaddress (peopleId, addressesId)
select 1,2
GO
You could do it through an instead of trigger, or if you want it as a constraint create a constraint that references a function that checks for a row that has the default set to 1
EDIT oops, needs to be <=
Create table mytable(id1 int, defaultX bit not null default(0))
go
create Function dbo.fx_DefaultExists()
returns int as
Begin
Declare #Ret int
Set #ret = 0
Select #ret = count(1) from mytable
Where defaultX = 1
Return #ret
End
GO
Alter table mytable add
CONSTRAINT [CHK_DEFAULT_SET] CHECK
(([dbo].fx_DefaultExists()<=(1)))
GO
Insert into mytable (id1, defaultX) values (1,1)
Insert into mytable (id1, defaultX) values (2,1)
This is a fairly complex process that cannot be handled through a simple constraint.
We do this through a trigger. However before you write the trigger you need to be able to answer several things:
do we want to fail the insert if a default exists, change it to 0 instead of 1 or change the existing default to 0 and leave this one as 1?
what do we want to do if the default record is deleted and other non default records are still there? Do we make one the default, if so how do we determine which one?
You will also need to be very, very careful to make the trigger handle multiple row processing. For instance a client might decide that all of the records of a particular type should be the default. You wouldn't change a million records one at a time, so this trigger needs to be able to handle that. It also needs to handle that without looping or the use of a cursor (you really don't want the type of transaction discussed above to take hours locking up the table the whole time).
You also need a very extensive tesing scenario for this trigger before it goes live. You need to test:
adding a record with no default and it is the first record for that customer
adding a record with a default and it is the first record for that customer
adding a record with no default and it is the not the first record for that customer
adding a record with a default and it is the not the first record for that customer
Updating a record to have the default when no other record has it (assuming you don't require one record to always be set as the deafault)
Updating a record to remove the default
Deleting the record with the deafult
Deleting a record without the default
Performing a mass insert with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record inserts
Performing a mass update with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record updates
Performing a mass delete with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record deletes
#Andy Jones gave an answer above closest to mine, but bearing in mind the Rule of Three, I placed the logic directly in the stored proc that updates this table. This was my simple solution. If I need to update the table from elsewhere, I will move the logic to a trigger. The one default rule applies to each set of records specified by a FormID and a ConfigID:
ALTER proc [dbo].[cpForm_UpdateLinkedReport]
#reportLinkId int,
#defaultYN bit,
#linkName nvarchar(150)
as
if #defaultYN = 1
begin
declare #formId int, #configId int
select #formId = FormID, #configId = ConfigID from csReportLink where ReportLinkID = #reportLinkId
update csReportLink set DefaultYN = 0 where isnull(ConfigID, #configId) = #configId and FormID = #formId
end
update
csReportLink
set
DefaultYN = #defaultYN,
LinkName = #linkName
where
ReportLinkID = #reportLinkId