T-SQL If Statement issue - c#

I have 2 tables: dbo.Videos and dbo.Checkouts.
The dbo.Videos table contains a list of videos while the dbo.Checkouts table is keeping track of the videos that have been checked out.
The goal in my TSQL command is to insert a new row in the dbo.Checkouts table including VideoId, UserId, CheckoutDate.
Once this is successful I then want to update the dbo.Videos and decrement the TotalCopies column value based on the VideoID selected only if the value is greater than 0.
If less than 0 I want to throw an exception.
The VideoID in both tables are linked by foreign key.
However, the IF statement I have included in my statement below throws an error.
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (32, 'b0281f0d-8398-4a27-ba92-828bfaa9f90e', CURRENT_TIMESTAMP)
IF (SELECT TotalCopies FROM dbo.Videos WHERE VideoId = 32) > 0
UPDATE dbo.Videos SET
TotalCopies = TotalCopies - 1
WHERE VideoID = 32

You've got it backwards.
Instead of adding a record to Checkouts and then test if you have the video in Videos, You need to first check you have a copy you can check out.
It's like when you go to buy something from any shop - first you get the product off the shelf, and only then you pay for it.
If the product isn't on the shelf, there's no need for you to pay.
first version
You need at least three steps to do it right:
First, you check if you have a copy to check out.
If not, you don't do anything, just return a message that there are no free copies to checkout.
If there is a copy, you need to update the Video table (TotalCopies -= 1)
And last - you need to insert the record to checkouts.
The most important thing here is that if any of these steps fails, all of them fails - For instance, if for some reason you failed to insert the row to checkouts, you must revert the update you did on the Video table, since you can't complete the process.
This is the first reason why you need to wrap the entire process in a transaction.
The second reason you need a transaction is to avoid a race condition between the test if there are copies to checkout and the update of the video table. You can read more about it on Dan Guzman's blog post about Conditional INSERT/UPDATE Race Condition.
So, having said all that, let's show some code:
CREATE PROCEDURE VideoCheckout
(
#VideoId int,
#UserId uniqueIdentifier,
#Success bit OUTPUT
)
AS
SET XACT_ABORT ON
SET #Success = 0
BEGIN TRANSACTION
BEGIN TRY
DECLARE #NumberOfCopies int
SET #NumberOfCopies = ISNULL(
(
SELECT TotalCopies
FROM dbo.Videos WITH (UPDLOCK, HOLDLOCK)
WHERE VideoId = #VideoId
)
, 0)
IF #NumberOfCopies > 0
BEGIN
UPDATE dbo.Videos
SET TotalCopies = TotalCopies - 1
WHERE VideoId = #VideoId;
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (#VideoId, #UserId, CURRENT_TIMESTAMP)
SET #Success = 1
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
update - the using ##rowcount version:
SQL Server's ##Rowcount global variable returns the number of rows effected (generally. there are some exceptions that are documented in the link) - using that you can unify the test part with the update part - having SQL Server report back if the update effected any rows on not. This enables you to write simpler SQL and will probably have better performace.
CREATE PROCEDURE VideoCheckout
(
#VideoId int,
#UserId uniqueIdentifier,
#Success bit OUTPUT
)
AS
SET XACT_ABORT ON
SET #Success = 0
BEGIN TRANSACTION
BEGIN TRY
UPDATE dbo.Videos
SET TotalCopies = TotalCopies - 1
WHERE VideoId = #VideoId
AND TotalCopies > 0;
IF ##ROWCOUNT > 0
BEGIN
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (#VideoId, #UserId, CURRENT_TIMESTAMP)
SET #Success = 1
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO

Related

T-SQL insert into sometimes fails to add to a table at random

So I have two tables, Records (Input ID = Primary Key) and TaskNotes (Input Id, TaskNote : No primary key).
There used to be a single stored procedure which would add to the record table, get the primary id that was generated, then add that ID to the TaskNotes table, along with the task notes text.
Recently, there was an issue where the sproc would run seemingly half way, with the record being added, but the task notes entry not being run.
I since split out into an AddRecord stored procedure and an AddTaskNotes stored procedure, which are being called from a C# application.
This works as similarly as before, however, at random the AddTaskNotes still wont be run.
I think the issue is a locking of the TaskNotes table.
Has anyone experienced this before and could let me know how it was resolved?
The current rate is about 1 failed tasknotes for every 400 record entries.
This is the AddRecord statement;
INSERT INTO Time.Records
( TeamID ,
UserID ,
TimeIN ,
TimeOUT
)
VALUES ( #TeamID , #UserID , #TimeIN , #TimeOUT );
return SCOPE_IDENTITY();
This is the AddTaskNotes statement;
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
INSERT INTO Time.TaskNotes ( InputID, TaskNotes )
VALUES ( #InputID, #TaskNotes );
END

SQL Server Trigger - Prevent row update if column IsLocked BIT is true

SQL Server 2016 accessed by ASP.NET 4.6.2 MVC web application
I have table "Building" and a Building can have multiple "Components". For example, Building1 has Component1 and Component2 ... etc
It was requested of me to be able to lock a building. Lock means that a component can no longer be modified (CREATE/UPDATE/DELETE). Well, as you can imagine, this is a huge application and a component can be modified in 100+ places. No one can even answer the question, "Where all do I need to lock?".
My thought is to lock everywhere I can think of and then as a safety net create a SQL Trigger that prevents all modifications if the column on the Component table "IsLocked BIT" is true. Currently, the only way I know if a component is locked is if the IsLocked column equals true.
So, I say all of that for this. How do I create a SQL Server Trigger that prevents a row of data from being modified if the row being modified has column IsLocked = 1?
Edit 1
In my opinion, this is not a duplicate. Using Instead of Delete or Instead of... will not work for me. If I do the instead of ... then inside of that I will need to provide commit logic. I don't want to provide commit logic. I just want to run a check prior to insert, update, delete.
Edit 2 - Instead of Update/Delete is best choice
If instead of... is my best choice than can someone rewrite what I have using the instead of update/delete? I don't know how to do it. Please keep in mind that requests will be coming from a web app. I won't know if they are updating one column or the entire entity or what they will passing in. I know that the way I have it written that it will catch any insert/update/delete and prevent it if locked. If there is a better way then please write it and explain why it is better.
Here is the solution I came up with:
ALTER TRIGGER [dbo].[PreventLockedModification]
ON [dbo].[Component]
FOR INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
--DETERMINE INSERT(I) UPDATE(U) OR DELETE(D)
----------------------------------------------------------------------------------------------------
DECLARE #action as char(1);
SET #action = 'I'; -- Set Action to Insert by default.
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #action =
CASE
WHEN EXISTS(SELECT * FROM INSERTED) THEN 'U' -- Set Action to Updated.
ELSE 'D' -- Set Action to Deleted.
END
END
----------------------------------------------------------------------------------------------------
DECLARE #ErrorMsg nvarchar(100) = 'This row is locked and cannot be updated';
DECLARE #IsLocked bit;
DECLARE #BuildingId bigint;
DECLARE #UnitId bigint;
DECLARE #IsComplete_Building bit;
DECLARE #IsComplete_Unit bit;
----------------------------------------------------------------------------------------------------
IF #action = 'U' or #action = 'D'
BEGIN
SELECT #IsLocked = IsLocked FROM deleted;
IF #IsLocked = 1
BEGIN
RAISERROR (#ErrorMsg, 16, 1);
ROLLBACK TRANSACTION
END
END
----------------------------------------------------------------------------------------------------
ELSE IF #action = 'I'
BEGIN
SELECT #BuildingId = BuildingId FROM inserted;
SELECT #UnitId = UnitId FROM inserted;
SELECT #IsComplete_Building = IsComplete FROM Building WHERE BuildingId = #BuildingId
SELECT #IsComplete_Unit = IsComplete FROM Unit WHERE UnitId = #UnitId
IF #IsComplete_Building = 1 or #IsComplete_Unit = 1
BEGIN
RAISERROR (#ErrorMsg, 16, 1);
ROLLBACK TRANSACTION
END
END
----------------------------------------------------------------------------------------------------
END

How to insert Huge dummy data to Sql server

Currently development team is done their application, and as a tester needs to insert 1000000 records into the 20 tables, for performance testing.
I gone through the tables and there is relationship between all the tables actually.
To insert that much dummy data into the tables, I need to understand the application completely in very short span so that I don't have the dummy data also by this time.
In SQL server is there any way to insert this much data insertion possibility.
please share the approaches.
Currently I am planning with the possibilities to create dummy data in excel, but here I am not sure the relationships between the tables.
Found in Google that SQL profiler will provide the order of execution, but waiting for the access to analyze this.
One more thing I found in Google is red-gate tool can be used.
Is there any script or any other solution to perform this tasks in simple way.
I am very sorry if this is a common question, I am working first time in SQL real time scenario. but I have the knowledge on SQL.
Why You don't generate those records in SQL Server. Here is a script to generate table with 1000000 rows:
DECLARE #values TABLE (DataValue int, RandValue INT)
;WITH mycte AS
(
SELECT 1 DataValue
UNION all
SELECT DataValue + 1
FROM mycte
WHERE DataValue + 1 <= 1000000
)
INSERT INTO #values(DataValue,RandValue)
SELECT
DataValue,
convert(int, convert (varbinary(4), NEWID(), 1)) AS RandValue
FROM mycte m
OPTION (MAXRECURSION 0)
SELECT
v.DataValue,
v.RandValue,
(SELECT TOP 1 [User_ID] FROM tblUsers ORDER BY NEWID())
FROM #values v
In table #values You will have some random int value(column RandValue) which can be used to generate values for other columns. Also You have example of getting random foreign key.
Below is a simple procedure I wrote to insert millions of dummy records into the table, I know its not the most efficient one but serves the purpose for a million records it takes around 5 minutes. You need to pass the no of records you need to generate while executing the procedure.
IF EXISTS (SELECT 1 FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[DUMMY_INSERT]') AND type in (N'P', N'PC'))
BEGIN
DROP PROCEDURE DUMMY_INSERT
END
GO
CREATE PROCEDURE DUMMY_INSERT (
#noOfRecords INT
)
AS
BEGIN
DECLARE #count int
SET #count = 1;
WHILE (#count < #noOfRecords)
BEGIN
INSERT INTO [dbo].[LogTable] ([UserId],[UserName],[Priority],[CmdName],[Message],[Success],[StartTime],[EndTime],[RemoteAddress],[TId])
VALUES(1,'user_'+CAST(#count AS VARCHAR(256)),1,'dummy command','dummy message.',0,convert(varchar(50),dateadd(D,Round(RAND() * 1000,1),getdate()),121),convert(varchar(50),dateadd(D,Round(RAND() * 1000,1),getdate()),121),'160.200.45.1',1);
SET #count = #count + 1;
END
END
you can use the cursor for repeat data:
for example this simple code:
Declare #SYMBOL nchar(255), --sample V
#SY_ID int --sample V
Declare R2 Cursor
For SELECT [ColumnsName]
FROM [TableName]
For Read Only;
Open R2
Fetch Next From R2 INTO #SYMBOL,#SY_ID
While (##FETCH_STATUS <>-1 )
Begin
Insert INTO [TableName] ([ColumnsName])
Values (#SYMBOL,#SY_ID)
Fetch Next From R2 INTO #SYMBOL,#SY_ID
End
Close R2
Deallocate R2
/*wait a ... moment*/
SELECT COUNT(*) --check result
FROM [TableName]

Multiple inserts in a single stored procedure?

TableName: Information
Stored procedure that inserts data into the above table.
CREATE PROCEDURE sp_insert_information
(
#profileID as int,
#profileName as varchar(8)
#profileDescription as varchar(100)
)
AS
BEGIN
INSERT INTO information(profileid, profilename, profiledescription)
VALUES (#profileID, #profileName, #profileDescription);
END
I call this procedure from .NET, is there a way to do multiple inserts if I pass profileID's as a comma separated parameter? (can I use split function?)
I can either loop through the profileID's and send 1 by 1 to procedure, however my data is going to be the same except different profileID.
Table data (with 3 columns):
1 profileUnavailable User Error
2 profileUnavailable User Error
3 profileUnavailable User Error
4 profileUnavailable User Error
5 profileUnavailable User Error
Any other approaches that I can try to do this in a single shot?
You have a couple options:
SqlBulkInsert - You can create a dataset that you can dump to the table. This is useful for many inserts. This will bypass the procedure altogether.
Table Valued Parameters - You can use a table value parameter as a parameter of the stored procedure, again manipulating data using a dataset.
The CSV Parameter with string split IS an option, but I would recommend one of the above over it.
Nope. That sproc does one insert at a time as it is written presently. You have to invoke it separately.
You might also consider wrapping that up into a transaction so if one fails, all of them won't be committed.
My favourite technique up to some years ago was to have an arsenal of splitting functions, that could split a delimited list of homogeneous values (e.g. all integers, all booleans, all datetimes, etc.) into a table variable. Here is an example of such a function.
CREATE FUNCTION [dbo].[fn_SplitInt](#text varchar(8000),
#delimiter varchar(20) = '|')
RETURNS #Values TABLE
(
pos int IDENTITY PRIMARY KEY,
val INT
)
AS
BEGIN
DECLARE #index int
SET #index = -1
-- while the list is not over...
WHILE (LEN(#text) > 0)
BEGIN
-- search the next delimiter
SET #index = CHARINDEX(#delimiter , #text)
IF (#index = 0) -- if no more delimiters (hence this field is the last one)
BEGIN
IF (LEN(#text) > 0) -- and if this last field is not empty
INSERT INTO #Values VALUES (CAST (#text AS INT)) -- then insert it
ELSE -- otherwise, if this last field is empty
INSERT INTO #Values VALUES (NULL) -- then insert NULL
BREAK -- in both cases exit, since it was the last field
END
ELSE -- otherwise, if there is another delimiter
BEGIN
IF #index>1 -- and this field is not empty
INSERT INTO #Values VALUES (CAST(LEFT(#text, #index - 1) AS INT)) -- then insert it
ELSE -- otherwise, if this last field is empty
INSERT INTO #Values VALUES (NULL) -- then insert NULL
SET #text = RIGHT(#text, (LEN(#text) - #index)) -- in both cases move forward the read pointer,
-- since the list was not over
END
END
RETURN
END
When you have a set of functions like these, then your problem has a solution as simple as this one:
CREATE PROCEDURE sp_insert_information
(
#profileID as varchar(2000),
#profileName as varchar(8)
#profileDescription as varchar(100)
)
AS
BEGIN
DECLARE #T TABLE (Id int)
INSERT INTO #T (Id)
SELECT val FROM dbo.fn_SplitInt(#profileID)
INSERT INTO information(profileid, profilename,profiledescription)
SELECT Id, #profileName, #profileDescription
FROM #T
END
But today it might be quicker to execute, and even require less coding, to generate an XML representation of the data to insert, then pass the XML to the stored procedure and have it INSERT INTO table SELECT FROM xml, if you know what I mean.
WHILE len(#ProfileId) > 0
BEGIN
DECLARE #comm int= charindex(',',#ProfileId)
IF #comm = 0 set #comm = len(#ProfileId)+1
DECLARE #Profile varchar(1000) = substring(#ProfileId, 1, #comm-1)
INSERT INTO Information(ProfileId,ProfileName,ProfileDescription)
VALUES (#ProfileId,#ProfileName,#ProfileDescription)
SET #ProfileId= substring(#ProfileId, #comm+1, len(#ProfileId))
END

Concurrent access to database - preventing two users from obtaining the same value

I have a table with sequential numbers (think invoice numbers or student IDs).
At some point, the user needs to request the previous number (in order to calculate the next number). Once the user knows the current number, they need to generate the next number and add it to the table.
My worry is that two users will be able to erroneously generate two identical numbers due to concurrent access.
I've heard of stored procedures, and I know that that might be one solution. Is there a best-practice here, to avoid concurrency issues?
Edit: Here's what I have so far:
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_GetNextOrderNumber]
AS
BEGIN
BEGIN TRAN
DECLARE #recentYear INT
DECLARE #recentMonth INT
DECLARE #recentSequenceNum INT
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- get the most recent numbers
SELECT #recentYear = Year, #recentMonth = Month, #recentSequenceNum = OrderSequenceNumber
FROM dbo.OrderNumbers
WITH (XLOCK)
WHERE Id = (SELECT MAX(Id) FROM dbo.OrderNumbers)
// increment the numbers
IF (YEAR(getDate()) > IsNull(#recentYear,0))
BEGIN
SET #recentYear = YEAR(getDate());
SET #recentMonth = MONTH(getDate());
SET #recentSequenceNum = 0;
END
ELSE
BEGIN
IF (MONTH(getDate()) > IsNull(#recentMonth,0))
BEGIN
SET #recentMonth = MONTH(getDate());
SET #recentSequenceNum = 0;
END
ELSE
SET #recentSequenceNum = #recentSequenceNum + 1;
END
-- insert the new numbers as a new record
INSERT INTO dbo.OrderNumbers(Year, Month, OrderSequenceNumber)
VALUES (#recentYear, #recentMonth, #recentSequenceNum)
COMMIT TRAN
END
This seems to work, and gives me the values I want. So far, I have not yet added any locking to prevent concurrent access.
Edit 2: Added WITH(XLOCK) to lock the table until the transaction completes. I'm not going for performance here. As long as I don't get duplicate entries added, and deadlocks don't happen, this should work.
you know that SQL Server does that for you, right? You can you a identity column if you need sequential number or a calculated column if you need to calculate the new value based on another one.
But, if that doesn't solve your problem, or if you need to do a complicated calculation to generate your new number that cant be done in a simple insert, I suggest writing a stored procedure that locks the table, gets the last value, generate the new one, inserts it and then unlocks the table.
Read this link to learn about transaction isolation level
just make sure to keep the "locking" period as small as possible
Here is a sample Counter implementation. Basic idea is to use insert trigger to update numbers of lets say, invoices. First step is to create a table to hold a value of last assigned number:
create table [Counter]
(
LastNumber int
)
and initialize it with single row:
insert into [Counter] values(0)
Sample invoice table:
create table invoices
(
InvoiceID int identity primary key,
Number varchar(8),
InvoiceDate datetime
)
Stored procedure LastNumber first updates Counter row and then retrieves the value. As the value is an int, it is simply returned as procedure return value; otherwise an output column would be required. Procedure takes as a parameter number of next numbers to fetch; output is last number.
create proc LastNumber (#NumberOfNextNumbers int = 1)
as
begin
declare #LastNumber int
update [Counter]
set LastNumber = LastNumber + #NumberOfNextNumbers -- Holds update lock
select #LastNumber = LastNumber
from [Counter]
return #LastNumber
end
Trigger on Invoice table gets number of simultaneously inserted invoices, asks next n numbers from stored procedure and updates invoices with that numbers.
create trigger InvoiceNumberTrigger on Invoices
after insert
as
set NoCount ON
declare #InvoiceID int
declare #LastNumber int
declare #RowsAffected int
select #RowsAffected = count(*)
from Inserted
exec #LastNumber = dbo.LastNumber #RowsAffected
update Invoices
-- Year/month parts of number are missing
set Number = right ('000' + ltrim(str(#LastNumber - rowNumber)), 3)
from Invoices
inner join
( select InvoiceID,
row_number () over (order by InvoiceID desc) - 1 rowNumber
from Inserted
) insertedRows
on Invoices.InvoiceID = InsertedRows.InvoiceID
In case of a rollback there will be no gaps left. Counter table could be easily expanded with keys for different sequences; in this case, a date valid-until might be nice because you might prepare this table beforehand and let LastNumber worry about selecting the counter for current year/month.
Example of usage:
insert into invoices (invoiceDate) values(GETDATE())
As number column's value is autogenerated, one should re-read it. I believe that EF has provisions for that.
The way that we handle this in SQL Server is by using the UPDLOCK table hint within a single transaction.
For example:
INSERT
INTO MyTable (
MyNumber ,
MyField1 )
SELECT IsNull(MAX(MyNumber), 0) + 1 ,
"Test"
FROM MyTable WITH (UPDLOCK)
It's not pretty, but since we were provided the database design and cannot change it due to legacy applications accessing the database, this was the best solution that we could come up with.

Categories