How can this SQL check constraint for time ranges fail, when table is empty? - sql

I have implemented a time range validation, as a check constraint, using a function in SQL, using this guide, almost to the letter.
Creating the function first:
create function dbo.ValidateStatusPeriodInfoTimeRange
(
#btf_id VARCHAR(32),
#start_time BIGINT,
#end_time BIGINT
)
returns bit
as
begin
declare #Valid bit = 1;
if exists( select *
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time )
set #Valid = 0;
return #Valid;
end
And then the constraint, using the function:
alter table dbo.StatusPeriodInfoOccurrence with nocheck add constraint
CK_StatusPeriodInfoOccurrence_ValidateTimeRange
check (dbo.ValidateStatusPeriodInfoTimeRange(btf_id, start_time, end_time) = 1);
When I try to insert an element into a completely empty table, I get:
The INSERT statement conflicted with the CHECK constraint
"CK_StatusPeriodInfoOccurrence_ValidateTimeRange". The conflict occurred in
database "D600600TD01_BSM_Surveillance", table "dbo.StatusPeriodInfoOccurrence".
I tried to figure out if I did something wrong in the function itself, and created this query to check it's return value:
DECLARE #ReturnValue INT
EXEC #ReturnValue = ValidateStatusPeriodInfoTimeRange
#btf_id = 'a596933eff9143bceda5fc5d269827cd',
#start_time = 2432432,
#end_time = 432432423
SELECT #ReturnValue
But this returns 1, as it should.
I am at a loss on how to continue debugging this. All parts seem to work, but the whole does not. Any ideas on how the insert statement can conflict with the check constraint?
Edit: Here is my insert statement for completion:
INSERT INTO StatusPeriodInfoOccurrence (btf_id, start_time, end_time) VALUES ('a596933eff9143bceda5fc5d269827cd',2432432,432432423);
There is an additional primary key comlumn with identity auto increment.

CHECK constraints happen after the row is inserted, so in its current form, the constraint fails because the very row that was inserted matches the constraint. In order for this to work as a constraint (not a trigger) there must be a way to distinguish the row we're checking from all other rows. MichaƂ's answer shows how to do this without relying on an IDENTITY, but if you do have that explicitly excluding the row may be simpler:
create function dbo.ValidateStatusPeriodInfoTimeRange
(
#id INT,
#btf_id VARCHAR(32),
#start_time BIGINT,
#end_time BIGINT
)
returns bit
as
begin
declare #Valid bit = 1;
if exists( select *
from dbo.StatusPeriodInfoOccurrence o
where o.id <> #id AND o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time )
set #Valid = 0;
return #Valid;
end;
with the constraint defined as
check (dbo.ValidateStatusPeriodInfoTimeRange(id, btf_id, start_time, end_time) = 1)
Regardless of the approach, indexes on (btf_id, start_time) and (btf_id, end_time) are a good idea to keep this scalable, otherwise a full table scan is necessary on every insert.

As was mentioned in comments, constraint is checked after the record is inserted into a table, then the transaction is commited or rolled back, depending on result of a check, which in your example will always fails, as query:
select *
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time
will return always at least one row (the one being inserted).
So, knowing that, you should check if the query returns more than one record, so the condition in if statement should become:
if (select count(*)
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time ) > 1
This solution works fine (tested on my DB).

Related

Insert data based on non existence and different criteria's [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I'm trying to add a row to my table only on two conditions but when inserting it retrieves error and I cannot figure it out
Create PROC [dbo].[setvisitorqueue]
#pid bigint = null , #vid int = NULL ,#regdate nvarchar(50) =NULL
AS
declare #queNum int =null
set #queNum = (select max([ticketNo]) + 1 from [dbo].[queue] where [ticketdate]= GetDate())
if( #queNum is null) begin set #queNum=1 end
Declare #Tktt int = null
set #Tktt = (select count(queue.ticketid) from queue where (queue.pid = #pid )and (queue.ticketdate = GetDate()) and (queue.vid = #vid and queue.checked = 0))
if (#Tktt is null )
begin insert into queue (vid , pid , ticketNo , ticketdate ) Values (#Vid,#pid,#queNum,#regdate ) end
Its not working for me.
Can you try it simple way like this?
CREATE PROC [dbo].[setvisitorqueue]
#pid BIGINT = null,
#vid INT = NULL,
#regdate NVARCHAR(50) = NULL
AS
IF (
SELECT COUNT(ticketid)
FROM [dbo].[queue]
WHERE checked = 0 and pid = #pid and vid = #vid and ticketdate = GetDate()
) = 0
INSERT INTO [dbo].[queue](vid pid, ticketdate, ticketNo )
SELECT #Vid, #pid, #regdate, ticketNo = IsNull(MAX([ticketNo]),0) + 1
FROM [dbo].[queue]
WHERE [ticketdate]= GetDate();
RETURN;
GO
In this code I've done following:
Improved readability by Caps, intend, spaces, etc.
Eliminated variables - you do not need them in that code You do not
need to calculate a "TicketNo" in the beginning if it won't be used.
So, it will be calculated if needed within IF statement.
You do not need to use BEGIN-END on every transaction, single
request IS a transaction
Not sure what your error was, but your procedure won't do anything just because when you do "COUNT" it returns a number. That means your "#Tktt" variable would never be NULL.
I guess your intention is to run the Insert statement when it is no records found and compared "COUNT" query to "0" value.
Here is your SP with all the issues I spotted corrected with comments, and with best practices added. As noted by the other answer you can probably simplify things. I have just aimed to correct existing issues.
-- NOTES: Keep your casing and layout consistent
-- Always terminate statements with a semi-colon
-- Don't add un-necessary brackets, they just clutter the code
-- You also have a concurrency issue:
-- if this proc is called twice at the same time you could issue the same ticket number twice
create proc [dbo].[setvisitorqueue]
(
#pid bigint = null,
#vid int = null,
-- Only every use a datetime/date datatype to store a datatime/date. Datetime2 is the current standard. Change precision to suit.
#regdate datetime2(0) = null
)
as
begin
-- Always start your SP with
set nocount, xact_abort on;
declare #queNum int = null;
set #queNum = (
select max([ticketNo]) + 1
from dbo.[queue]
-- This seems very unlikely to happen? It has to match down to the fraction of a second.
where [ticketdate] = getdate()
);
if #queNum is null begin
set #queNum = 1;
end;
declare #Tktt int = null;
-- #Tktt will *never* be null after this, it may be zero though.
set #Tktt = (
select count(*)
from dbo.[queue]
where pid = #pid
-- This seems very unlikely to happen? It has to match down to the fraction of a second.
and ticketdate = getdate()
and vid = #vid and checked = 0
);
-- Handle 0 or null just in case
-- if #Tktt is null -- THIS IS WHAT PREVENTED YOUR INSERT
if coalesce(#Tktt,0) = 0
begin
insert into dbo.[queue] (vid, pid, ticketNo, ticketdate)
values (#Vid, #pid, #queNum, #regdate);
end;
-- Always return the status of the SP, 0 means OK
return 0;
end;

Is it safe to save previous value of a column as a variable in update?

Think of a simple update stored procedure like this:
CREATE PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
Now, to return a result based on previous value of ModifiedOn, I changed it like this:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT #ModifiedOn
ELSE
SELECT -1
Is it safe to fill #PreviousModifiedOn variable, with previous value of ModifiedOn, in SET part? Or is it possible that ModifiedOn value changes before it is saved into variable?
UPDATE
Same query using OUTPUT:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn AS TABLE (ModifiedOn datetime)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
OUTPUT
Deleted.[ModifiedOn] INTO #PreviousModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF EXISTS (SELECT * FROM #PreviousModifiedOn WHERE [ModifiedOn] <= #GeneratedOn)
SELECT #ModifiedOn
ELSE
SELECT -1
It seems that OUTPUT is the correct way to solve the problem, but because of the variable table, I think it has more performance cost.
So my question is... Why using OUTPUT is better than my solution? Is there anything wrong with my solution? Which one is better in terms of performance and speed?
I believe that this is safe. Although variable assignment is a proprietary extension, the rest of the SET clause follows the SQL Standard here - all assignments are computed as if they occur in parallel. That is, all expressions on the right of assignments are computed based on pre-update values for all columns.
This is e.g. why UPDATE Table SET A=B, B=A will swap the contents of two columns, not set them both equal to whatever B was previously.
The one thing to be wary of here, for me, would be that the UPDATE may have performed no assignments (due to the WHERE clause) and so still be NULL, or may have performed multiple assignments; In the latter case, your variable will be set to one of the previous values but it is not guaranteed which row's value it will have retained.
It is not required, since MS SQL Server 2005 you can use OUTPUT for this kind of scenarios.
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
--Declare a table variable for storing the info from Output
DECLARE #ModifiedOnTable AS TABLE
(
ModifiedOn DATETIME
)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
OUTPUT DELETED.ModifiedOn INTO #ModifiedOnTable
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT ModifiedOn FROM #ModifiedOnTable
ELSE SELECT -1

check constraint not working as per the condition defined in sql server

I have a table aa(id int, sdate date, edate date, constraint chk check(sdate<= enddate). For a particular id I have to check for overlapping dates. That is I do not want any one to insert data of a perticular id which has overlapping dates. So i need to check the below conditions -
if #id = id and (#sdate >= edate or #edate <= sdate) then allow insert
if #id = id and (#sdate < edate or #edate > sdate) then do not allow insert
if #id <> id then allow inserts
I have encapsulated the above logic in a function and used that function in check constraint. Function is working fine but check constraint is not allowing me to enter any records. I do not know why - my function and constraint are mentioned below :
alter function fn_aa(#id int,#sdate date,#edate date)
returns int
as
begin
declare #i int
if exists (select * from aa where id = #id and (#sdate >= edate or #edate <= sdate)) or not exists(select * from aa where id = #id)
begin
set #i = 1
end
if exists(select * from aa where id = #id and (#sdate < edate or #edate < sdate))
begin
set #i = 0
end
return #i
end
go
alter table aa
add constraint aa_ck check(dbo.fn_aa(id,sdate,edate) = 1)
Now when I try to insert any value in the table aa I get the following error -
"Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "aa_ck". The conflict occurred in database "tempdb", table "dbo.aa".
The statement has been terminated."
Function is returning value 1 but constraint is not allowing to insert data. Can some one help me here. I am trying for last 2 hours but cannot understand what am i doing wrong?
-
I think your logic is wrong. Two rows overlap
alter function fn_aa(#id int,#sdate date,#edate date)
returns int
as
begin
if exists (select *
from aa
where id = #id and
#sdate < edate and #edate > sdate
)
begin
return 0;
end;
return 1;
end;
Your version would return 1 when either of these conditions is true: #sdate >= edate or #edate <= sdate. However, checking for an overlap depends on both end points.

SQL Server 2005 - writing an insert and update trigger for validation

I'm pretty bad at SQL, so I need someone to check my trigger query and tell me if it solves the problem and how acceptable it is. The requirements are a bit convoluted, so please bear with me.
Suppose I have a table declared like this:
CREATE TABLE Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime
);
For this table, the following constraints must be observed:
At least one of the number-date pairs should be filled (both the number and the date field not null).
If both number1 and date1 are not null, a record is uniquely identified by this pair. There cannot be two records with the same number1 and date1 if both fields are not null.
If either number1 or date1 is null, a record is uniquely identified by the number2-date2 pair.
Yes, there is a problem of poor normalization, but I cannot do anything about that.
As far as I know, I cannot write unique indexes on the number-date pairs that check whether some of the values are null in SQL Server 2005. Thus, I tried validating the constraints with a trigger.
One last requirement - the trigger should have no inserts of its own, only validation checks. Here's what I came up with:
CREATE TRIGGER validate_requisite_uniqueness
ON [Documents]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #NUMBER1 NVARCHAR (32)
DECLARE #DATE1 DATETIME
DECLARE #NUMBER2 NVARCHAR (32)
DECLARE #DATE2 DATETIME
DECLARE #DATETEXT VARCHAR(10)
DECLARE inserted_cursor CURSOR FAST_FORWARD FOR SELECT number1, date1, number2, date2 FROM Inserted
IF NOT EXISTS (SELECT * FROM INSERTED)
RETURN
OPEN inserted_cursor
FETCH NEXT FROM inserted_cursor into #NUMBER1, #DATE1, #NUMBER2, #DATE2
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#NUMBER1 IS NULL OR #DATE1 IS NULL)
BEGIN
IF (#NUMBER2 IS NULL OR #DATE2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number-date pair should be filled.', 10, 1)## Heading ##
END
END
IF (#NUMBER1 IS NOT NULL AND #DATE1 IS NOT NULL)
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number1 = #NUMBER1 AND date1 = #DATE1) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE1, 104)
RAISERROR ('A document with the number1 ''%s'' and date1 ''%s'' already exists.', 10, 1, #NUMBER1, #DATETEXT)
END
END
ELSE IF (#NUMBER2 IS NOT NULL AND #DATE2 IS NOT NULL) /*the DATE2 check is redundant*/
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number2 = #NUMBER2 AND date2 = #DATE2) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE2, 104)
RAISERROR ('A document with the number2 ''%s'' and date2 ''%s'' already exists.', 10, 1, #NUMBER2, #DATETEXT)
END
END
FETCH NEXT FROM inserted_cursor
END
CLOSE inserted_cursor
DEALLOCATE inserted_cursor
END
Please tell me how well-written and efficient this solution is.
A couple of questions I can come up with:
Will this trigger validate correctly against existing rows and newly inserted/updated rows in case of bulk modifications? It should, because the modifications are already applied to the table in the scope of this transaction, right?
Are the constraint violations handled correctly? Meaning, was I right to use the rollback transaction and raiserror pair?
Is the "IF NOT EXISTS (SELECT * FROM INSERTED) RETURN" statement used correctly?
Is the use of COUNT to check the constraints acceptable, or should I use some other way of checking the uniqueness of number-date pairs?
Can this solution be optimized in terms of execution speed? Should I add non-unique indexes on both number-date pairs?
Thanks.
EDIT:
A solution using a check constraint and an indexed view, based on Damien_The_Unbeliever's answer:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
CASE WHEN (number1 is not null and date1 is not null)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END as first_pair_filled,
CASE WHEN (number1 is not null and date1 is not null)
THEN number1
ELSE number2
END as number,
CASE WHEN (number1 is not null and date1 is not null)
THEN date1
ELSE date2
END as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(first_pair_filled,number,[date])
go
I would avoid the trigger, and use a check constraint and an indexed view:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
COALESCE(number1,number2) as number,
COALESCE(date1,date2) as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(number,[date])
go
Which has the advantage that, although there is some "trigger-like" behaviour because of the indexed view, it's well-tested code that's already been deeply integrated into SQL Server.
I would use this logic instead (I didn't type it all as it takes ages), and definitely use SELECT 1 FROM ... in the IF EXISTS() statement as it helps performance. Also remove the cursors like marc_s said.
CREATE TRIGGER trg_validate_requisite_uniqueness
ON dbo.[Documents]
AFTER INSERT, UPDATE
AS
DECLARE #Number1 NVARCHAR(100) = (SELECT TOP 1 number1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date1 DATETIME = (SELECT TOP 1 date1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Number2 NVARCHAR(100) = (SELECT TOP 1 number2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date2 DATETIME = (SELECT TOP 1 date2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #DateText NVARCHAR(100)
IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Number1 IS NULL AND I.number2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Date1 IS NULL AND I.Date2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second date pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
GROUP BY D.number1, D.date1 HAVING COUNT(*) >1
)
BEGIN
ROLLBACK TRANSACTION
SET #DateText = (SELECT CONVERT(VARCHAR(10), #Date1, 104))
RAISERROR ('Cannot have duplicate values', 10, 1, #Number1, #DateText )
END

Lock entire table stored procedure

Guys I have a stored procedure that inserts a new value in the table, only when the last inserted value is different.
CREATE PROCEDURE [dbo].[PutData]
#date datetime,
#value float
AS
IF NOT EXISTS(SELECT * FROM Sensor1 WHERE SensorTime <= #date AND SensorTime = (SELECT MAX(SensorTime) FROM Sensor1) AND SensorValue = #value)
INSERT INTO Sensor1 (SensorTime, SensorValue) VALUES (#date, #value)
RETURN 0
Now, since I'm doing this at a high frequency (say every 10ms), the IF NOT EXISTS (SELECT) statement is often getting old data, and because of this I'm getting duplicate data. Would it be possible to lock the entire table during the stored procedure, to make sure the SELECT statement always receives the latest data?
According to the poster's comments to the question, c# code receives a value from a sensor. The code is supposed to insert the value only if it is different from the previous value.
Rather than solving this in the database, why not have the code store the last value inserted and only invoke the procedure if the new value is different? Then the procedure will not need to check whether the value exists in the database; it can simply insert. This will be much more efficient.
You could write it like this :
CREATE PROCEDURE [dbo].[PutData]
#date datetime,
#value float
AS
BEGIN TRANSACTION
INSERT INTO Sensor1 (SensorTime, SensorValue)
SELECT SensorTime = #date,
SensorValue = #value
WHERE NOT EXISTS(SELECT *
FROM Sensor1 WITH (UPDLOCK, HOLDLOCK)
WHERE SensorValue = #value
AND SensorTime <= #date
AND SensorTime = (SELECT MAX(SensorTime) FROM Sensor1) )
COMMIT TRANSACTION
RETURN 0
Thinking a bit about it, you could probably write it like this too:
CREATE PROCEDURE [dbo].[PutData]
#date datetime,
#value float
AS
BEGIN TRANSACTION
INSERT INTO Sensor1 (SensorTime, SensorValue)
SELECT SensorTime = #date,
SensorValue = #value
FROM (SELECT TOP 1 SensorValue, SensorTime
FROM Sensor1 WITH (UPDLOCK, HOLDLOCK)
ORDER BY SensorTime DESC) last_value
WHERE last_value.SensorTime <= #date
AND last_value.SensorValue <> #value
COMMIT TRANSACTION
RETURN 0
Assuming you have a unique index (PK?) on SensorTime this should be quite fast actually.