Date range overlapping check constraint - sql-server

I've a simple table in sql server 2005 with 3 columns: DateStart, DateEnd and Value. I tried to set a "table check constraint" to avoid inserting overlapping records. For instance if in such table there is a record with DateStart = 2012-01-01 (first January) and DateEnd 2012-01-15 (15th January) than Check constraint must avoid inserting a record with DateStart=2012-01-10 (no care DateEnd), a record with DateEnd=2012-01-10 (no care DateStart) or a record with DateStart 2011-12-10 and DateEnd 2012-02-01.
I defined a UDF in such way:
CREATE FUNCTION [dbo].[ufn_checkOverlappingDateRange]
(
#DateStart AS DATETIME
,#DateEnd AS DATETIME
)
RETURNS BIT
AS
BEGIN
DECLARE #retval BIT
/* date range at least one day */
IF (DATEDIFF(day,#DateStart,#DateEnd) < 1)
BEGIN
SET #retval=0
END
ELSE
BEGIN
IF EXISTS
(
SELECT
*
FROM [dbo].[myTable]
WHERE
((DateStart <= #DateStart) AND (DateEnd > #DateStart))
OR
((#DateStart <= DateStart) AND (#DateEnd > DateStart))
)
BEGIN
SET #retval=0
END
ELSE
BEGIN
SET #retval=1
END
END
RETURN #retval
END
Then thought check could be this:
ALTER TABLE [dbo].[myTable] WITH CHECK ADD CONSTRAINT [CK_OverlappingDateRange] CHECK ([dbo].[ufn_checkOverlappingDateRange]([DateStart],[DateEnd])<>(0))
But even with [myTable] empty EXISTS Operator returns true when i insert first record. Where i'm wrog ? Is it possible to set a constraint like this ?
BTW I consider DateStart includes in range and DateEnd excludes from range.

The CHECK is being executed after the row has been inserted, so the range overlaps with itself.
You'll need to amend your WHERE to include something like: #MyTableId <> MyTableId.
BTW, your WHERE expression can be simplified.
Ranges don't overlap if:
end of the one range is before the start of the other
or start of the one range is after the end of the other.
Which could be written in SQL like:
WHERE #DateEnd < DateStart OR DateEnd < #DateStart
Negate that to get the ranges that do overlap...
WHERE NOT (#DateEnd < DateStart OR DateEnd < #DateStart)
...which according to De Morgan's laws is the same as...
WHERE NOT (#DateEnd < DateStart) AND NOT (DateEnd < #DateStart)
...which is the same as:
WHERE #DateEnd >= DateStart AND DateEnd >= #DateStart
So your final WHERE should be:
WHERE
#MyTableId <> MyTableId
AND #DateEnd >= DateStart
AND DateEnd >= #DateStart
[SQL Fiddle]
NOTE: to allow ranges to "touch", use <= in the starting expression, which would produce > in the final expression.

CREATE TABLE [dbo].[TEMPLATE] (
[ID] BIGINT IDENTITY (1, 1) NOT NULL,
[DATE_START] DATETIME NOT NULL,
[DATE_END] DATETIME NOT NULL,
[TEMPLATE_NAME] VARCHAR (50) NOT NULL,
CONSTRAINT [PK_TEMPLATE] PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [FK_current_start_and_end_dates_in_sequence] CHECK ([DATE_START]<=[DATE_END])
);
go
CREATE FUNCTION [dbo].[Check_Period]
(
#start DateTime,
#end DateTime
)
RETURNS INT
AS
BEGIN
declare #result INT = 0 ;
set #result = (Select count(*) from [dbo].[TEMPLATE] F where F.DATE_START <= #start and F.DATE_END >= #start );
set #result = #result +
(
Select count(*) from [dbo].[TEMPLATE] F where F.DATE_START <= #end and F.DATE_END >= #end
)
RETURN #result
END
go
ALTER TABLE [dbo].[TEMPLATE]
ADD CONSTRAINT [FK_overlap_period_t]
CHECK ([dbo].[Check_Period]([DATE_START],[DATE_END])=(2));
go
Insert Into [dbo].[TEMPLATE] (DATE_START, DATE_END, TEMPLATE_NAME) values ('2020-01-01','2020-12-31', 'Test1');
-- (1 row(s) affected)
Insert Into [dbo].[TEMPLATE] (DATE_START, DATE_END, TEMPLATE_NAME) values ('2021-01-01','2022-12-31', 'Test2');
-- (1 row(s) affected)
Insert Into [dbo].[TEMPLATE] (DATE_START, DATE_END, TEMPLATE_NAME) values ('2020-01-01','2020-12-31', 'Test3');
-- The INSERT statement conflicted with the CHECK constraint "FK_overlap_period_t".

I'd just like to add onto the Answer of Branko Dimitrijevic the case where the DateEnd is null since I currently have such a scenario.
This can occur when you're keeping punch in logs and the user is still logged in.
WHERE
#MyTableId <> MyTableId
AND #DateEnd >= DateStart
AND DateEnd >= #DateStart
OR #DateEnd >= DateStart
AND DateEnd is null
OR #DateStart >= DateStart
AND DateEnd is null
I don't know how well this query is performance wise, I'm sure there are ways to optimize it.

Related

I'm getting an error message but it exec fine?

IF EXISTS (SELECT name
FROM sys.tables
WHERE name = 'Nums')
BEGIN
DROP TABLE dbo.Nums;
END
CREATE TABLE dbo.Nums
(
number INT NOT NULL,
CONSTRAINT PK_Nums PRIMARY KEY CLUSTERED(number ASC),
code Char(9),
date DATETIME
) ON [PRIMARY]
INSERT INTO Nums (number, code, date)
VALUES (0, 485658235, '2000/01/01')
DECLARE #number int, #code Char(9), #date datetime
SET #number = (SELECT MAX (Number) FROM nums)
SET #date = (SELECT Date FROM Nums)
SET #code = (SELECT code FROM Nums)
WHILE #number < 100000
BEGIN
INSERT INTO Nums--(number, code, date)
VALUES (#number, #code, #date)
SET #number = #number + 1
SET #date = DATEADD(DAY, 5, #date)
SET #code = LEFT(CAST(CAST(CEILING(RAND()* 10000000000) AS bigint) AS varchar),9)
END
SELECT * FROM Nums
This is the error I get:
Msg 2627, Level 14, State 1, Line 41
Violation of PRIMARY KEY constraint 'PK_Nums'. Cannot insert duplicate key in object 'dbo.Nums'. The duplicate key value is (0)
You need to add 1 to the number before the loop starts.
The error Violation of PRIMARY KEY constraint is not normally batch-aborting, therefore execution will continue with the next statement.
If you want to stop that, either force the batch to abort and rollback: SET XACT_ABORT ON (probably a good idea), or use TRY/CATCH.
I assume you also realize your procedure can be done in one statement, by joining a tally table or function (for example these by Itzik Ben-Gan), something like this:
INSERT #Nums (number, code, date)
SELECT
t.Num,
LEFT(CAST(CAST(CEILING(RAND()* 10000000000) AS bigint) AS varchar),9),
'2000/01/01'
FROM fnTallyTable (0, 99999) t

How to check if a month and year lies between 2 dates

I have a 2 columns in a table startdate and enddate and I need to create a function get all ID which lies between the date data passed in function.
my function input parameters are
#Year int,
#Month int = null,
#Quarter int = null
now if month is null I need to check only with date which is easy but if month is provided how to check if it lies between startdate and enddate or else if #Quarter is provided I need to check if 3 months of the year collides with startdate and enddate .
What I have written upto now is
CREATE FUNCTION GetAssociatesEmpID(
#Year int,
#Month int = null,
#Quarter int = null
)
RETURNS TABLE
AS BEGIN
IF #Month IS NOT NULL -- Monthly Statistics
BEGIN
END
ELSE IF #Quarter IS NOT NULL -- Quarterly Statistics
BEGIN
END
ELSE -- Yearly Statistics
BEGIN
return SELECT ID FROM Table WHRER #Year>=YEAR(startdate) AND #Year<=YEAR(enddate)
END
END
Kindly help me with condition with month and Quarter
Quarter has 4 possible inuts range between 1-4
and its month range is between #Quarter*3-3 and #Quarter*3
Start by creating two local DateTime variables. (Or DateTime2, or whatever data type your table's start and end date columns are using.) Maybe call them #WhereStartDate and #WhereEndDate.
Use some IF statements to populate your new #WherexxxDate variables. For example, if Month is provided, something like:
DECLARE #Year int = 2016;
DECLARE #Month int = 3;
DECLARE #WhereStartDate datetime;
DECLARE #WhereEndDate datetime;
SET #WhereStartDate = CONVERT( datetime, CAST(#Year as char(4)) + '/' + CAST(#Month as varchar(2)) + '/01');
SET #WhereEndDate = DATEADD( day, -1, DATEADD( month, 1, #WhereStartDate ));
SELECT #WhereStartDate, #WhereEndDate;
Once you have actual date/time variables, you can write your query appropriately...
SELECT ...
...
WHERE startDate >= #WhereStartDate
AND enddate <= #WhereEndDate
This has the added benefit of being sargable. The way that you have written your query is non-sargable. (In short, non-sargable queries will not make use of indexes properly and will have poor performance. If the table is large, the resulting table scans could take a very long time.)
Not where I can test, but this should be close...
WHERE
(#Year >= YEAR(startdate))
AND
(#Year <= YEAR(startdate))
AND
(
( (#Month IS NOT NULL) AND (#Month >= MONTH(startdate)) AND (#Month <= MONTH(startdate)) )
OR
( (#Month IS NULL) AND (#Quarter >= DATEPART(QUARTER, startdate)) AND (#Quarter <= DATEPART(QUARTER, startdate)) )
)

Build datetime in SQL Server 2012

I need to build a datetime in a select statement based on another 2 columns (datetime).
I cannot seem to get the conversion correct. Can you spot what I am doing wrong?
It seems to me that DatePart it omits the "0" part of the day
Below script should create all the data necessary
IF EXISTS (SELECT * FROM sys.databases WHERE name='TestDB')
BEGIN
ALTER DATABASE TestDB
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE TestDB
END
CREATE DATABASE TestDB
GO
IF OBJECT_ID(N'[dbo].[TestTable]','U') IS NOT NULL
DROP TABLE [dbo].[TestTable]
GO
CREATE TABLE [dbo].[TestTable]
(
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[DateSample1] datetime NOT NULL,
[DateSample2] datetime NOT NULL
)
GO
INSERT dbo.TestTable (DateSample1, DateSample2)
VALUES('2006-10-06 00:00:00.000', '2007-01-17 00:00:00.000')
/*
In your select statement you should return another column "DateSample3"
and this should be year from DateSample1 and month and day from dateSample2
*/
--my try1
SELECT
tt.DateSample1, tt.DateSample2,
DateSample3 = CAST(DATEPART(YYYY, tt.DateSample1) AS CHAR(4))
+ CAST(DATEPART(MM, tt.DateSample2) AS CHAR(2))
+ CAST(DATEPART(dd, tt.DateSample2) AS CHAR(2))
,WantedResultForDateSample3='2006-01-17 00:00:00.000'
FROM
dbo.TestTable tt
--mytry2 THROWS AN ERROR
--Conversion failed when converting date and/or time from character string
/*
SELECT
tt.DateSample1, tt.DateSample2,
DateSample3 = CONVERT(DATETIME,CAST(DATEPART(YYYY, tt.DateSample1) AS CHAR(4))
+ CAST(DATEPART(MM, tt.DateSample2) AS CHAR(2))
+ CAST(DATEPART(dd, tt.DateSample2) AS CHAR(2)),120),
WantedResult='2006-01-17 00:00:00.000'
FROM
dbo.TestTable tt
*/
You can use DATETIMEFROMPARTS
DATETIMEFROMPARTS(YEAR(tt.DateSample1),
MONTH(tt.DateSample2),
DAY(tt.DateSample2),
0,0,0,0)
Which is a lot cleaner than constructing a string IMO.
Whichever method you use you might have to deal with impossible dates with this requirement. One approach is below
SELECT CASE WHEN month=2
AND day = 29
AND (yr % 4 != 0 OR (yr % 100 = 0 AND yr % 400 != 0))
THEN NULL
ELSE
DATETIMEFROMPARTS(yr,
month,
day,
0,0,0,0)
END
FROM [dbo].[TestTable] tt
CROSS APPLY (VALUES (YEAR(tt.DateSample1),
MONTH(tt.DateSample2),
DAY(tt.DateSample2))) V(yr, month, day)
SQL Fiddle

Create Unique Constraint based on Start Date and End Date

I created a unique constraint as below which is working fine. But I want to create a constraint where the productNumber between the two dates should be unique
ALTER TABLE [dbo].[Product] ADD CONSTRAINT U_Product UNIQUE ([ProductNumber],[StartDate],[EndDate])
Right now, it is taking the exact value in columns, but I want it between two dates. How can I do this ?
create function dbo.IsProductUnique (#ProductNumber int, #StartDate date, #EndDate date) returns bit as
begin
if exists (
select *
from Product
where ProductNumber = #ProductNumber
and StartDate < #EndDate
and EndDate > #StartDate) return 0
else
return 1 -- Unique
end
go
alter table Product add constraint CK_Product_Unique check (dbo.IsProductUnique(ProductNumber, StartDate, EndDate) = 1)
go

Check Constraint not firing correctly

I'm pretty sure I am doing something wrong, as this is my first check constraint, but I can't understand why it's not working. I need to check that a date range doesn't overlap.
ALTER FUNCTION fn_DateOverlaps (#StartDate DATE, #EndDate DATE, #ProjectID INT)
RETURNS BIT
AS
BEGIN
DECLARE #Ret BIT
SET #Ret = 1
IF NOT EXISTS(
SELECT * FROM project_sprint
WHERE ((#StartDate >= StartDate AND #EndDate <= EndDate)
OR (#StartDate <= StartDate AND #EndDate >= EndDate))
AND ProjectId = #ProjectId
)
BEGIN
SET #Ret = 0
END
RETURN #Ret
END
GO
I then apply this to my table:
ALTER TABLE Project_Sprint WITH CHECK ADD CONSTRAINT ck_DateOverlaps CHECK (dbo.fn_DateOverlaps([StartDate], [EndDate], [ProjectId])=1)
GO
When I test the function, I get a good result:
SELECT dbo.fn_DateOverlaps('2013-06-10', '2013-06-13', 1)
But then when I apply the same date range and project ID to my table, it allows the insert. It should fail it.
What am I doing wrong?
If you change SELECT * FROM project_sprint to SELECT * FROM dbo.project_sprint the function will correctly evaluate, but you won't get the desired behavior, since the value you are inserting or editing will lead to an unwanted find.
To prevent this you will have to add an additional ID field to except the row you want to edit/insert for the check.
Create Table Project_Sprint(ID int IDENTITY(1,1) NOT NULL,ProjectID int,StartDate DateTime,EndDate DateTime)
go
Alter FUNCTION fn_DateOverlaps (#ID int,#StartDate DATE, #EndDate DATE, #ProjectID INT)
RETURNS BIT
AS
BEGIN
DECLARE #Ret BIT
SET #Ret = 0
IF NOT EXISTS(
SELECT * FROM dbo.project_sprint
WHERE
#ID<>ID AND
((#StartDate >= StartDate AND #EndDate <= EndDate)
OR (#StartDate <= StartDate AND #EndDate >= EndDate))
AND ProjectId = #ProjectId
)
BEGIN
SET #Ret = 1
END
RETURN #Ret
END
GO
ALTER TABLE Project_Sprint ADD CONSTRAINT ck_DateOverlaps CHECK (dbo.fn_DateOverlaps([ID],[StartDate], [EndDate], [ProjectId])=1)

Resources