sql unique constraint with time window - sql

I have a table where records have a (begin, end) time window of existence (for things like employement duration, birth and death, rent duration, ...)
begin IS NULL or end IS NULL if there is no bound.
CREATE TABLE mytable(
id int primary key,
value int, --UNIQUE at any point in time
begin datetime NULL,
end datetime NULL
);
I want column value to be unique at any point in time.
INSERT INTO mytable VALUES(1, 1, '2021-07-23', '2021-07-24'),(2, 1, '2021-07-25', NULL);
Is OK
Whereas
INSERT INTO mytable VALUES(1, 1, '2021-07-23', '2021-07-30'),(2, 1, '2021-07-25', NULL);
Is not OK, because both records have value=1 and overlapping time windows.
Is there a way to enforce such a constraint in SQL ?

You can't do this on the table, no, as there's nothing to make UNIQUE on.
What you could do, however, is use a VIEW to enforce it.
Firstly, let's create your table. I assume the columns datetime, should actually be begin and end; I recommend against these names as they are reserved keywords. As such I am calling them DateBegin and DateEnd. I am also assuming that they are date only (no time portion) values and so define them as a date not a datetime:
CREATE TABLE dbo.mytable(ID int primary key,
Value int,
[BeginDate] date NULL,
[EndEnd] date NULL);
And we'll INSERT your first 2 rows, as they are "ok":
INSERT INTO dbo.mytable (ID, Value, BeginDate, EndDate)
VALUES(1, 1, '20210723', '20210724'),
(2, 1, '20210725', NULL);
Now we need to make a VIEW, but we need one row per date. As such you'll want to create a Calendar Table. I'm not going to cover how to create one here, but there are literally 100's of articles, such as there on SQL Server Central: Bones of SQL - The Calendar Table, Calendar Tables in T-SQL.
Once you have your Calendar table, you can create the VIEW below, which JOINs the data in your table to the calendar table. We're going to make it so that the VIEW just returns the columns value and the date. WE're also going to schemabind it; this means we'll be able to add an UNIQUE INDEX to it:
CREATE VIEW dbo.MyView
WITH SCHEMABINDING
AS
SELECT MT.[Value],
CT.CalendarDate
FROM dbo.MyTable MT
JOIN dbo.CalendarTable CT ON MT.BeginDate <= CT.CalendarDate --I assume, despite your schema, MT.BeginDate can't be NULL
AND (MT.EndDate >= CT.CalendarDate OR MT.EndDate IS NULL);
Now we have a VIEW that has a row for each date, and for each value. This means we can now create our UNIQUE INDEX:
CREATE UNIQUE CLUSTERED INDEX MyIndex ON dbo.MyView ([Value], CalendarDate);
Now if we try to INSERT a row that is on the same date and value, we'll get an error:
INSERT INTO dbo.MyTable (ID, Value, BeginDate, EndDate)
VALUES(3, 1, '20210720', '20210723');
Cannot insert duplicate key row in object 'dbo.MyView' with unique index 'MyIndex'. The duplicate key value is (1, 2021-07-23).

Related

Prohibit Inserting Rows With Default Constraint

Is there any way of how to prevent inserting data in specified columns in table and use only the default (constraint) values?
E.g. I have columns:
LogInsert (DF GETDATE())
LogUser (DF ORIGINAL_LOGIN())
both defined with DEFAULT constraint. I do not want to allow users to insert into those columns, but use default values here instead when inserting new row.
This should raise an error.
INSERT INTO T1
( C1
,C2
,LogInsert
,LogUser
)
VALUES ( 'A'
,'B'
,'20160101 10:53'
,'domain\user'
);
User should be able to perform the following script without error.
INSERT INTO T1
( C1, C2 )
VALUES ( 'A', 'B' );
You could always give your users a view to work against instead of a table. You can then either choose to hide the columns completely or (as here) make them computed so that they cannot insert a value into the column, via the view:
create table dbo._T1 (
ID int IDENTITY(1,1) not null,
Inserted datetime2 constraint DF__T1_Inserted DEFAULT (SYSDATETIME()) not null,
ABC varchar(10) not null,
constraint PK__T1 PRIMARY KEY (ID)
)
go
create view dbo.T1
with schemabinding
as
select
ID,
COALESCE(Inserted,SYSDATETIME()) as Inserted,
ABC
from dbo._T1
go
insert into dbo.T1 (ABC) values ('abc')
go
insert into dbo.T1 (ABC,Inserted) values ('def',SYSDATETIME())
Results:
(1 row(s) affected)
Msg 4406, Level 16, State 1, Line 19
Update or insert of view or function 'dbo.T1' failed because it contains a derived or constant field.
All of the users queries continue to just use T1. It just happens to be a view rather than a table.
In the above, the view uses COALESCE(Inserted,SYSDATETIME()). It doesn't really matter what's used here, and it doesn't need to match e.g. the default definition. All that's important is that some computation is performed on the Inserted column so that it becomes a read-only column in the view.
You can create a Check Constraint on the table, for example the below
CREATE TABLE [dbo].[T1]
(
[C1] VARCHAR(50)
,[LogInsert] DATETIME DEFAULT GETDATE()
,[LogUser] VARCHAR(500) DEFAULT ORIGINAL_LOGIN()
)
ALTER TABLE [T1] WITH CHECK ADD CONSTRAINT [CK_T1_LogInsert] CHECK ([LogInsert] = GETDATE())
ALTER TABLE [T1] WITH CHECK ADD CONSTRAINT [CK_T1_LogUser] CHECK ([LogUser] = ORIGINAL_LOGIN())
INSERT INTO [dbo].[T1] ([C1]) VALUES ('A')
INSERT INTO [dbo].[T1] ([C1]) VALUES ('B')
INSERT INTO [dbo].[T1] ([C1]) VALUES ('C')
SELECT * FROM [dbo].[T1]
--Will Fail
INSERT INTO [dbo].[T1] ([C1],[LogInsert]) VALUES ('D','2016-11-11 00:00')
INSERT INTO [dbo].[T1] ([C1],[LogUser]) VALUES ('D','Not Your UserName')
OR
You can force the user to only insert using a stored procedure and not allow that as a parameter, this can be done with a table variable too for bulk inserts.

Foreign key to 1 of several tables

I have two tables (OrderFreshGoods and OrderUtensils) and then I have an AuditTrail table. The AuditTrail table is related to the OrderFreshGoods table but I want to change it so that an Audit must relate to either an OrderFreshGoods or OrderUtensils record. I have seen a lot of solutions where the Audit table say would have 2 foreign keys (OrderIDFresh, OrderIDUtensils and it is optional that 1 of them must be populated). Note that I do not want that solution. I want the Audit table to have 1 foreign key (OrderID) and it must relate to either OrderFreshGoods.OrderID or OrderUtensils.OrderID.
Also my two order tables have no fields in common and are used in a large number of queries around the system so I don't want a parent table for both types of order.
Can anybody help? My sql script is below, the comments should help explain my tests...
--Setup tables
create table OrderFreshGoods (OrderID int not null primary key, sellBy date, name varchar(20))
go
create table OrderUtensils (OrderID int not null primary key, requiresOver18CheckForKnives bit, colour varchar(20), title varchar(20))
go
create table AuditTrail (AuditId int not null primary key, OrderID int, timeOfEvent date, eventDescription varchar(100))
go
--Base data
insert into OrderFreshGoods values (7, DATEADD(dd, 3, getdate()), 'Organic milk')
insert into OrderUtensils values (8, 0, 'Red', 'Garlic crusher')
--Test data!!!!!!!!!!!!!!
--This should work
insert into AuditTrail values (15, 7, getdate(), 'Logging order for Organic Milk from Corkys Coffee shop.')
--This should work
insert into AuditTrail values (16, 8, getdate(), 'Logging order for a Red Garlic Crusher from Perrys Pizza Place.')
--This should not be allowed
insert into AuditTrail values (17, 9, getdate(), 'Wrongly adding an audit entry before the order, please stop me now!')
--This should not be allowed
insert into AuditTrail values (18, null, getdate(), 'Oh dear, bad code has caused the OrderId to be lost, please stop me now!')
What you want is not possible as you describe it, it goes against the very premise of Relational databases.
If you leave out the actual Foreign Key, then you can populate the AuditTrail.OrderId with whatever you want.
But you'd lose the referential integrity check, so your third insert into AuditTrail statement wouldn't fail. That could then be fixed by applying an on-insert trigger which does a reference check. But it would still not prevent Orders from being deleted, causing the pseudo-relation to go bad again.
Another and perhaps much better alternative is to add an AuditId field to both of the Order tables, and fill that as needed.
If you want to use always proper relations, then I can see next solution:
create yet another table xxxx, like:
create table xxxx(id int identity(1,1) primary key)
add foreign keys in all your auditable tables to xxxx(id)
create new records in xxxx while adding records to auditable tables (triggers can be handy) (you may include more info into xxxx)
add foreign key in audit table to xxxx(id)
This way you can insert into audit table only records, pointing to xxxx - and these are always related to actual orders (and do not vanish when some order gets deleted either).

Add table level Contraint that checks for date overlap in Sql Server 2005

I have a table in Sql Server where i have Columns : UserId, RoleId, FromDate and Todate.
I want to write a contraint that check same RoleId and UserId are not present for the same date.
Thanks In Advance....
If you don't have time portion or the time portion is the same in all records you can use UNIQUE constraint:
ALTER TABLE yourSchema.yourTable
ADD CONSTRAINT uniqueConstraint1 UNIQUE (RoleId, UserId, FromDate);
This way combination of date, RoleId and UserId can occur only once in table, other attempts to insert the same combination will fail.
Note that this will work if your date field has values for time portion that are the same in every record (for instance 0) or the data type of the field is DATE (which eliminates the time portion).
If your date field has time portion that varies among records, try one of this approaches:
A) Add computed column of definition
ALTER TABLE yourSchema.yourTable
ADD constraintCheckDate AS CAST(FromDate AS DATE)
and add UNIQUE constraint of definition
ALTER TABLE yourSchema.yourTable
ADD CONSTRAINT uniqueConstraint1 UNIQUE (RoleId, UserId, constraintCheckDate)
B) Use trigger to validate data before inserting it, data will be entered only if it doesn't already exist:
CREATE TRIGGER trig1 ON yourSchema.yourTable
INSTEAD OF INSERT
AS
BEGIN
IF NOT EXISTS
(
SELECT *
FROM yourSchema.yourTable t
JOIN inserted i ON
CAST(t.FromDate AS DATE) = CAST(i.FromDate AS DATE)
AND t.RoleId = i.RoleId
AND t.UserId = i.UserId
)
INSERT yourTable(RoleId, UserId, FromDate, ToDate)
SELECT RoleId, UserId, FromDate, ToDate
FROM inserted
ELSE
RAISERROR('Error', 16, 0)
END
GO

SQL can I have a "conditionally unique" constraint on a table?

I've had this come up a couple times in my career, and none of my local peers seems to be able to answer it. Say I have a table that has a "Description" field which is a candidate key, except that sometimes a user will stop halfway through the process. So for maybe 25% of the records this value is null, but for all that are not NULL, it must be unique.
Another example might be a table which must maintain multiple "versions" of a record, and a bit value indicates which one is the "active" one. So the "candidate key" is always populated, but there may be three versions that are identical (with 0 in the active bit) and only one that is active (1 in the active bit).
I have alternate methods to solve these problems (in the first case, enforce the rule code, either in the stored procedure or business layer, and in the second, populate an archive table with a trigger and UNION the tables when I need a history). I don't want alternatives (unless there are demonstrably better solutions), I'm just wondering if any flavor of SQL can express "conditional uniqueness" in this way. I'm using MS SQL, so if there's a way to do it in that, great. I'm mostly just academically interested in the problem.
If you are using SQL Server 2008 a Index filter would maybe your solution:
http://msdn.microsoft.com/en-us/library/ms188783.aspx
This is how I enforce a Unique Index with multiple NULL values
CREATE UNIQUE INDEX [IDX_Blah] ON [tblBlah] ([MyCol]) WHERE [MyCol] IS NOT NULL
In the case of descriptions which are not yet completed, I wouldn't have those in the same table as the finalized descriptions. The final table would then have a unique index or primary key on the description.
In the case of the active/inactive, again I might have separate tables as you did with an "archive" or "history" table, but another possible way to do it in MS SQL Server at least is through the use of an indexed view:
CREATE TABLE Test_Conditionally_Unique
(
my_id INT NOT NULL,
active BIT NOT NULL DEFAULT 0
)
GO
CREATE VIEW dbo.Test_Conditionally_Unique_View
WITH SCHEMABINDING
AS
SELECT
my_id
FROM
dbo.Test_Conditionally_Unique
WHERE
active = 1
GO
CREATE UNIQUE CLUSTERED INDEX IDX1 ON Test_Conditionally_Unique_View (my_id)
GO
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 1)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 1)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 1) -- This insert will fail
You could use this same method for the NULL/Valued descriptions as well.
Thanks for the comments, the initial version of this answer was wrong.
Here's a trick using a computed column that effectively allows a nullable unique constraint in SQL Server:
create table NullAndUnique
(
id int identity,
name varchar(50),
uniqueName as case
when name is null then cast(id as varchar(51))
else name + '_' end,
unique(uniqueName)
)
insert into NullAndUnique default values
insert into NullAndUnique default values -- Works
insert into NullAndUnique default values -- not accidentally :)
insert into NullAndUnique (name) values ('Joel')
insert into NullAndUnique (name) values ('Joel') -- Boom!
It basically uses the id when the name is null. The + '_' is to avoid cases where name might be numeric, like 1, which could collide with the id.
I'm not entirely aware of your intended use or your tables, but you could try using a one to one relationship. Split out this "sometimes" unique column into a new table, create the UNIQUE index on that column in the new table and FK back to the original table using the original tables PK. Only have a row in this new table when the "unique" data is supposed to exist.
OLD tables:
TableA
ID pk
Col1 sometimes unique
Col...
NEW tables:
TableA
ID
Col...
TableB
ID PK, FK to TableA.ID
Col1 unique index
Oracle does. A fully null key is not indexed by a Btree in index in Oracle, and Oracle uses Btree indexes to enforce unique constraints.
Assuming one wished to version ID_COLUMN based on the ACTIVE_FLAG being set to 1:
CREATE UNIQUE INDEX idx_versioning_id ON mytable
(CASE active_flag WHEN 0 THEN NULL ELSE active_flag END,
CASE active_flag WHEN 0 THEN NULL ELSE id_column END);

Calculated columns in mysql on INSERT statements

Let's say that I want to have a table that logs the date and the number of columns in some other table (or really any sort of math / string concat etc).
CREATE TABLE `log` (
`id` INTEGER NOT NULL AUTO_INCREMENT ,
`date` DATETIME NOT NULL ,
`count` INTEGER NOT NULL ,
PRIMARY KEY (`id`)
);
Is it possible to have the count column calculated for me whenever I do an insert?
e.g. do something like:
INSERT INTO log (date='foo');
and have count calculated by mysql.
Obviously I could do it myself by doing a query to get the count and inserting it, but this would be better.
Triggers are the best tool for annotating data when a table is changed by insert, update or delete.
To automatically set the date column of a new row in the log with the current date, you'd create a trigger that looked something like this:
create trigger log_date before insert on log
for each row begin
set new.date = current_date()
end;
You definitly have to declare what to insert. This should be possible by using the INSERT ... SELECT statement.
INSERT INTO log (date, count)
SELECT DATE() as date, count(id) as count
from foo;
Which should insert a new row into the log table, containing todays date and the number of rows in the foo table. (Assuming the foo table has an id column.. Use the primary key or another indexed column)
Why don't you use information_schema.TABLES?