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

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

Related

Update statement, update value is conditionally evaluated

I have the below pseudo code written that I want to implement in T-SQL. I need this code included in an existing stored procedure, I was trying to achieve the below with function call passing in a temp table as a parameter, is it possible to pass a temp table as a function parameter. Please let me know if there is a better approach to this.
Table: #Temp_Table has a column RefId which refers to #TempReadUpdateValue.Id. There are rules to identify if the #TempReadUpdateValue.Id can be applied to #Temp_Table.RefId.
Rule 1: the data qualifies in the DateRange
Rule 2: the #TempReadUpdateValue.Id is available if (Allowed - Used) > 0.
Allowed is fixed value and used will increment as its assigned.
I want to achieve the above with an UPDATE statement on Temp_Table, the challenge that I face is #Temp_Table.RefId = #TempReadUpdateValue.Id, need to increment
#TempReadUpdateValue.Used = #TempReadUpdateValue.Used + #Temp_Table.Units
every next row in #Temp_Table need to re-evaluate rules #1 and #2 for RefId assignment.
Update statement:
DECLARE #OLD INT = 0; -- THIS CAN ALSO BE SET TO 1, basically passed in as param to the stored procedure.
CREATE TABLE #TempReadUpdateValue
(
Id INT,
From_Date DateTime,
Thru_Date DateTime,
Allowed int,
Used int
)
CREATE TABLE #Temp_Table
(
Pk_ID INT,
DOS DateTime,
Units Int,
Ref_Id int
)
UPDATE #Temp_Table
SET Ref_Id = CASE
WHEN #OLD = 0 THEN 121
ELSE NewImplementation(DOS, Units, #TempReadUpdateValue)
END
CREATE OR ALTER FUNCTION NewImplementation
(#DOS DATETIME, #Units INT, #TempReadUpdateValue)
RETURNS INT
AS
BEGIN
DECLARE #Id INT
DECLARE #Allowed INT
DECLARE #Used INT
SELECT
#Id = Id,
#Allowed = Allowed,
#Used = Used
FROM
#TempReadUpdateValue
DECLARE #ReturnValue INT = 0
IF (#Id > 0) AND (#Allowed - #Used ) > 0
BEGIN
#ReturnValue = #Id;
UPDATE #TempReadUpdateValue
SET Used = (Used + #Units);
END
RETURN #ReturnValue
END

New to SQL - Why is my Insert into trying to insert NULL into primary key?

What I want to do is insert a range of dates into multiple rows for customerID=1. I have and insert for dbo.Customer(Dates), specifying my that I want to insert a record into the Dates column for my Customer table, right? I am getting error:
Cannot insert the value NULL into column 'CustomerId', table 'dbo.Customers'
Sorry if I am way off track here. I have looked at similar threads to find out what I am missing, but I'm not piecing this together. I am thinking it wants to overwrite the existing customer ID as NULL, but I am unsure why exactly since I'm specifying dbo.Customer(Dates) and not the existing customerID for that record.
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
while #date <= #date_End
begin
insert into dbo.Customer(Dates) select #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
end
select * from dbo.Customer
The primary key is customerId, but you are not inserting a value.
My guess is that you declared it as a primary key with something like this:
customerId int primary key,
You want it to be an identity column, so the database assigns a value:
customerId int identity(1, 1) primary key
Then, you don't need to assign a value into the column when you insert a new row -- the database does it for you.
Your Customer table has a column named CustomerId and which column is NOT Nullable so you have to provide that column value as well. If your column type is Int try the bellow code:
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
DECLARE #cusId INT
SET #cusId = 1
while #date <= #date_End
begin
insert into dbo.Customer(CustomerId, Dates) select #cusId, #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
SET #cusId = #cusId + 1;
end
select * from dbo.Customer
thank you for the feedback. I think I'm scrapping this and going to go with creating a separate table to JOIN. Not sure why I didn't start doing that before

Update existing value in database using Stored Procedure

I want to update my table contentment, which has the following attributes:
employeeid, questionid, date, score, comment
The words in bold are the primary key of this table. My stored procedure needs to update a questionid for a employeeid. Only the questionid needs to be changed. The employeeid, date, score and comment need to stay the same.
I have the following:
create procedure [dbo].[spUpdateContentment]
(
#employeeid int,
#questionid int,
#date date
)
as
begin
update contentment
set questionid= #questionid
where employeeid= #employeeid and questionid= #questionid and date = #date
else
RAISERROR(#ErrMsg,16,1)
end
But this is not working, it does nothing. I think this is because my stored procedure does not exactly know where he needs to update it, and which questionid he needs to update. But I'm not sure. I use SQL Server
You are not changing any values, therefore it does nothing. You may need the following:
create procedure [dbo].[spUpdateContentment]
(
#employeeid int,
#new_questionid int,
#old_questionid int,
#date date
)
as
begin
update contentment
set questionid= #new_questionid
where employeeid= #employeeid and questionid= #old_questionid and date = #date
else
RAISERROR(#ErrMsg,16,1)
end
I think you want:
update contentment
set questionid = #questionid
where employeeid = #employeeid and date = #date;
The condition and questionid = #questionid is going to prevent the row being found. If it is found, the update won't be changing any values.
It is possible that the logic requires two #questionids, one for the old value and one for the new value.

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.

SQL update numeric column from decimal datatype

I am trying to update a column of type numeric(5,2) from decimal(10,2) but the column is remaining null and I have no error messages.
DECLARE #newDurationAsDec decimal(10,2)
DECLARE #newDurationAsNum numeric(5,2)
--#newDurationAsDec is set by some logic from another table and holds the correct value e.g 2.00
set #newDurationAsNum = CAST(#newDurationAsDec AS numeric(5,2))
--selecting #newDurationAsNum contains the correct value e.g. 2.00
UPDATE table
SET Duration = #newDurationAsNum
WHERE ID = #ID AND
Duration IS NULL AND
OtherColumn = 'T'
No errors are retruned and the column is not updated. Changing the update to a select returns the correct row. can someone point out my mistake?
Thanks in advance.
Works fine for me below. Check there are no triggers that might be interfering with things.
If you are on at least SQL Server 2005 you can use the OUTPUT clause to see the row(s) updated as illustrated below.
CREATE TABLE #T
(
ID INT PRIMARY KEY,
Duration NUMERIC(5,2),
OtherColumn CHAR(1)
)
INSERT INTO #T VALUES (1,NULL,'T')
DECLARE #ID INT = 1
DECLARE #newDurationAsDec DECIMAL(10,2) = 2
DECLARE #newDurationAsNum NUMERIC(5,2)
SET #newDurationAsNum = CAST(#newDurationAsDec AS NUMERIC(5,2))
SELECT #newDurationAsNum
UPDATE #T
SET Duration = #newDurationAsNum
OUTPUT inserted.*, deleted.*
WHERE ID = #ID AND
Duration IS NULL AND
OtherColumn = 'T'
SELECT * FROM #T
DROP TABLE #T
Run a select to see what you will be updating. I embed my updates statements as shown below so it is easy to move back and forth from the select to the update just by commenting and uncommenting lines of code
--UPDATE t
--SET Duration = #newDurationAsNum
Select *, #newDurationAsNum
FROM table t
WHERE ID = #ID AND
Duration IS NULL AND
OtherColumn = 'T'