SQL Server - Auto-incrementation that allows UPDATE statements - sql-server

When adding an item in my database, I need it to auto-determine the value for the field DisplayOrder. Identity (auto-increment) would be an ideal solution, but I need to be able to programmatically change (UPDATE) the values of the DisplayOrder column, and Identity doesn't seem to allow that. For the moment, I use this code:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
Is it the good way to do it or is there a better/simpler way?

A solution to this issue from "Inside Microsoft SQL Server 2008: T-SQL Querying"
CREATE TABLE dbo.Sequence(
val int IDENTITY (10000, 1) /*Seed this at whatever your current max value is*/
)
GO
CREATE PROC dbo.GetSequence
#val AS int OUTPUT
AS
BEGIN TRAN
SAVE TRAN S1
INSERT INTO dbo.Sequence DEFAULT VALUES
SET #val=SCOPE_IDENTITY()
ROLLBACK TRAN S1 /*Rolls back just as far as the save point to prevent the
sequence table filling up. The id allocated won't be reused*/
COMMIT TRAN
Or another alternative from the same book that allocates ranges easier. (You would need to consider whether to call this from inside or outside your transaction - inside would block other concurrent transactions until the first one commits)
CREATE TABLE dbo.Sequence2(
val int
)
GO
INSERT INTO dbo.Sequence2 VALUES(10000);
GO
CREATE PROC dbo.GetSequence2
#val AS int OUTPUT,
#n as int =1
AS
UPDATE dbo.Sequence2
SET #val = val = val + #n;
SET #val = #val - #n + 1;

You can set your incrementing column to use the identity property. Then, in processes that need to insert values into the column you can use the SET IDENITY_INSERT command in your batch.
For inserts where you want to use the identity property, you exclude the identity column from the list of columns in your insert statement:
INSERT INTO [dbo].[MyTable] ( MyData ) VALUES ( #MyData )
When you want to insert rows where you are providing the value for the identity column, use the following:
SET IDENTITY_INSERT MyTable ON
INSERT INTO [dbo].[MyTable] ( DisplayOrder, MyData )
VALUES ( #DisplayOrder, #MyData )
SET IDENTITY_INSERT MyTable OFF
You should be able to UPDATE the column without any other steps.
You may also want to look into the DBCC CHECKIDENT command. This command will set your next identity value. If you are inserting rows where the next identity value might not be appropriate, you can use the command to set a new value.
DECLARE #DisplayOrder INT
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
DBCC CHECKIDENT (MyTable, RESEED, #DisplayOrder)

Here's the solution that I kept:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
BEGIN TRANSACTION
SET #DisplayOrder = (SELECT ISNULL(MAX(DisplayOrder), 0) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
COMMIT TRANSACTION

One thing you should do is to add commands so that your procedure's run as a transaction, otherwise two inserts running at the same time could produce two rows with the same value in DisplayOrder.
This is easy enough to achieve: add
begin transaction
at the start of your procedure, and
commit transaction
at the end.

You way works fine (with a little modification) and is simple. I would wrap it in a transaction like #David Knell said. This would result in code like:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
BEGIN TRANSACTION
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
COMMIT TRANSACTION
Wrapping your SELECT & INSERT in a transaction guarantees that your DisplayOrder values won't be duplicated by AddItem. If you are doing a lot of simultaneous adding (many per second), there may be contention on MyTable but for occasional inserts it won't be a problem.

Related

SQL Server: statement in a batch all execute as one transaction?

So I ran a SQL batch with a bunch of UPDATE statements:
UPDATE...;
UPDATE...;
UPDATE...;
Turns out the batch ran as a single transactions consequently many locks were created (our database monitoring software clearly shows that it was one big transaction) so apparently its similar to me writing BEGIN...COMMIT around the batch:
BEGIN TRANS;
UPDATE...;
UPDATE...;
UPDATE...;
COMMIT;
Is that really the case? Are batches always executed as one transaction? (I am not in 'explicit transaction' mode) and why is that, and can it be configured or is that just the behavior of SQL Server?
Would it change anything if I wrap every statement in a BEGIN..COMMIT, something like:
BEGIN TRANS;
UPDATE...;
COMMIT;
BEGIN TRANS;
UPDATE...;
COMMIT;
BEGIN TRANS;
UPDATE...;
COMMIT;
Thank You!
Assuming that you are executing your UPDATEs from a SSMS Query Window, you can refactor your code to
BEGIN TRANS;
UPDATE...;
GO
UPDATE...;
GO
UPDATE...;
GO
COMMIT;
GO WILL make each UPDATE execute prior in its own batch in the order called, but won't be committed until the final COMMIT is called.
I have tested this via the following example that you can run for yourself in SSMS.
SET NOCOUNT ON
GO
IF EXISTS ( SELECT * FROM sys.tables WHERE name = 'BatchTest' )
DROP TABLE dbo.BatchTest;
GO
CREATE TABLE dbo.BatchTest (
id INT IDENTITY (1,1) PRIMARY KEY NOT NULL
, col1 VARCHAR(50)
, col2 VARCHAR(50)
, col3 VARCHAR(50)
, col4 VARCHAR(50)
)
GO
DECLARE #i INT = 1;
WHILE ( #i <= 100 ) BEGIN
INSERT INTO dbo.BatchTest ( col1 ) VALUES ( 'col1_' + CAST ( #i AS VARCHAR(10) ) );
SET #i += 1;
END
GO
SELECT * FROM dbo.BatchTest ORDER BY id;
/*
-- test tran/commit --
-- manually select and execute the code below after the BatchTest has been created and populated with data --
SET NOCOUNT ON
GO
BEGIN TRAN;
UPDATE dbo.BatchTest SET col2 = 'col2_' + CAST ( id AS VARCHAR(10) );
GO
UPDATE dbo.BatchTest SET col3 = 'col3_' + CAST ( id AS VARCHAR(10) );
GO
UPDATE dbo.BatchTest SET col4 = 'col4_' + CAST ( id AS VARCHAR(10) );
GO
--COMMIT;
ROLLBACK;
SELECT * FROM dbo.BatchTest ORDER BY id;
*/
Please note the comments in the code about selecting and executing the tran/commit test.
The single ROLLBACK undoes all of the UPDATEs within the current transaction, regardless of the use of GOs, whereas COMMIT will commit them all. Obviously, you should do more your diligence and test against your data, but this might help with your locking issues.
Some follow-up reading in regard to SQL Server's use of GO and its intentions:
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go?view=sql-server-2017

How to logs all rows affected after delete trigger on multiple rows

I have table QuoteDetail with multiple rows with the same Quotation Number.
The records can be deleted using bulk delete 'Delete from QuoteDetail Where QuoteNo ='12345'
How can I logs all deleted rows using after delete trigger?
Below is my trigger script but only logs 1 row and the rest are discarded.
CREATE TRIGGER [dbo].[trgcsms_QuoteDetail_DeleteLogs] ON [CSMS].[dbo].[QuoteDetail]
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #QuoteNo varchar(50)
DECLARE #SeqNo int
DECLARE #ItemCode varchar(50)
DECLARE #Qty float
DECLARE #NetPrice float
DECLARE #TotAmt float
SELECT #QuoteNo = DELETED.[QuoteNo]
,#SeqNo = DELETED.[SeqNo]
,#ItemCode = DELETED.[ItemCode]
,#Qty = DELETED.[Qty]
,#TotAmt = DELETED.[TotAmt]
FROM DELETED
INSERT INTO [dbo].[QuoteDetailDeleteLogs]
(
[QuoteNo]
,[SeqNo]
,[ItemCode]
,[Qty]
,[TotAmt]
)
VALUES
(
#QuoteNo
,#SeqNo
,#ItemCode
,#Qty
,#NetPrice
,#TotAmt
)
END
You almost got it righ. Just not using "#variables" and instead using tables. Because after all, you want multiple rows, right? That's also known as "a table".
Also worth noting - I took the liberty of upgrading your trigger to include UPDATE as well, because an update is a DELETE joined with an immediate INSERT afterwards. So I imagine if you update a line and QuoteNo (quote number) changed from '12345' to '54321' you would want that in your audit trail (your "QuoteDetailsLog").
[updated 30Aug2018 ]
CREATE TRIGGER [dbo].[trgcsms_QuoteDetail_DeleteLogs] ON [CSMS].[dbo].
[QuoteDetail]
AFTER DELETE, UPDATE
AS
BEGIN
INSERT INTO [dbo].[QuoteDetailDeleteLogs]
(
[QuoteNo]
,[SeqNo]
,[ItemCode]
,[Qty]
,[TotAmt]
)
SELECT [QuoteNo]
,[SeqNo]
,[ItemCode]
,[Qty]
,[TotAmt]
FROM DELETED
END
keeping it short and simple

Identity key counter increment by one although it is in TRY Catch and Transaction is roll-backed ? SSMS 2008

Identity counter increment by one although it is in TRY Catch and Transaction is roll-backed ? SSMS 2008 is there any way i can stop it +1 or rollback it too.
In order to understand why this happened, Let's execute below sample code first-
USE tempdb
CREATE TABLE dbo.Sales
(ID INT IDENTITY(1,1), Address VARCHAR(200))
GO
BEGIN TRANSACTION
INSERT DBO.Sales
( Address )
VALUES ( 'Dwarka, Delhi' );
ROLLBACK TRANSACTION
Now, Execution plan for above query is-
The second last operator from right Compute Scalar is computing value for [Expr1003]=getidentity((629577281),(2),NULL) which is IDENTITY value for ID column. So this clearly indicates that IDENTITY values are fetched & Incremented prior to Insertion (INSERT Operator). So its by nature that even transaction rollback at later stage once created IDENTITY value is there.
Now, in order to reseed the IDENTITY value to Maximum Identity Value present in table + 1, you need sysadmin permission to execute below DBCC command -
DBCC CHECKIDENT
(
table_name
[, { NORESEED | { RESEED [, new_reseed_value ] } } ]
)
[ WITH NO_INFOMSGS ]
So the final query should include below piece of code prior to rollback statement:-
-- Code to check max ID value, and verify it again IDENTITY SEED
DECLARE #MaxValue INT = (SELECT ISNULL(MAX(ID),1) FROM dbo.Sales)
IF #MaxValue IS NOT NULL AND #MaxValue <> IDENT_CURRENT('dbo.Sales')
DBCC CHECKIDENT ( 'dbo.Sales', RESEED, #MaxValue )
--ROLLBACK TRANSACTION
So it is recommended to leave it on SQL Server.
You are right and the following code inserts record with [Col01] equal to 2:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT IDENTITY(1,1)
,[Col02] TINYINT
);
GO
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO [dbo].[DataSource] ([Col02])
VALUES (1);
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
INSERT INTO [dbo].[DataSource] ([Col02])
VALUES (1);
SELECT *
FROM [dbo].[DataSource]
This is by design (as you can see in the documentation:
Consecutive values after server restart or other failures –SQL Server
might cache identity values for performance reasons and some of the
assigned values can be lost during a database failure or server
restart. This can result in gaps in the identity value upon insert. If
gaps are not acceptable then the application should use its own
mechanism to generate key values. Using a sequence generator with the
NOCACHE option can limit the gaps to transactions that are never
committed.
I try using NOCACHE sequence but it does not work on SQL Server 2012:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT
,[Col02] TINYINT
);
CREATE SEQUENCE [dbo].[MyIndentyty]
START WITH 1
INCREMENT BY 1
NO CACHE;
GO
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT NEXT VALUE FOR [dbo].[MyIndentyty], 1
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT NEXT VALUE FOR [dbo].[MyIndentyty], 1
SELECT *
FROM [dbo].[DataSource]
DROP TABLE [dbo].[DataSource];
DROP SEQUENCE [dbo].[MyIndentyty];
You can use MAX to solve this:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT
,[Col02] TINYINT
);
BEGIN TRY
BEGIN TRANSACTION;
DECLARE #Value SMALLINT = (SELECT MAX([Col01]) FROM [dbo].[DataSource]);
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT #Value, 1
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
DECLARE #Value SMALLINT = ISNULL((SELECT MAX([Col01]) FROM [dbo].[DataSource]), 1);
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT #Value, 1
SELECT *
FROM [dbo].[DataSource]
DROP TABLE [dbo].[DataSource];
But you must pay attentions to your isolation level for potential issues:
If you want to insert many rows at the same time, do the following:
get the current max value
create table where to store the rows (that are going to be inserted) generating ranking (you can use identity column, you can use ranking function) and adding the max value to it
insert the rows

Create Transaction and Trigger on a field

Please am new in vb.net and sql server, I have created a two tables in database called Service and Trans.
Create Table Service(
ServiceID int,
ServiceName varchar(30),
ServiceStartValue int
);
Create Table Trans(
EntryTS datetime,
EntryCounter int,
ServedTS datetime,
ServedCounter int,
Skipped int
);
I am trying to create a 'transaction and trigger' that will check and update ServedCounter based on the values in EntryCounter upon ServiceID which the update statement must not allow the ServedCounter > EntryCounter.
I don't quite understand the full requirement, but here's how you can prevent a update (or insert) from happening with a trigger.
CREATE TRIGGER Trans_upd_trg ON Trans AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Don't allow the update
IF EXISTS(SELECT 1 FROM inserted WHERE ServedCounter > EntryCounter)
RAISERROR ('ServedCounter > EntryCounter', 16, 1 );
END
GO
Within the context of the trigger you have two logical tables, INSERTED, and DELETED.
These tables contain the old and new values. (deleted is empty for a insert operation)
Hope that helps.
Use a Instead Of Trigger
CREATE TRIGGER Trans_upd_trg
ON Trans
Instead OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter)
AND EXISTS (SELECT *
FROM deleted)
UPDATE A
SET EntryTS = I.EntryTS,
EntryCounter = I.EntryCounter,
ServedTS = I.ServedTS,
ServedCounter = I.ServedCounter,
Skipped = I.Skipped
FROM Trans A
JOIN inserted I
ON A.EntryTS = I.EntryTS
AND A.ServedTS = I.ServedTS
WHERE i.ServedCounter > i.EntryCounter
ELSE
INSERT INTO Trans
SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter
END
GO

Why is the natural ID generation in this SQL Stored Proc creating duplicates?

I am incrementing the alphanumeric value by 1 for the productid using stored procedure. My procedure incrementing the values up to 10 records, once its reaching to 10th say for PRD0010...no more its incrementing... however, the problem is it is repeating
the same values PRD0010.. for each SP call.
What could be the cause of this?
create table tblProduct
(
id varchar(15)
)
insert into tblProduct(id)values('PRD00')
create procedure spInsertInProduct
AS
Begin
DECLARE #PId VARCHAR(15)
DECLARE #NId INT
DECLARE #COUNTER INT
SET #PId = 'PRD00'
SET #COUNTER = 0
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
--here increse the vlaue to numeric id by 1
SET #NId = #NId + 1
--GENERATE ACTUAL APHANUMERIC ID HERE
SET #PId = #PId + cast(#NId AS VARCHAR)
INSERT INTO tblProduct(id)values (#PId)
END
Change
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
To
SELECT TOP 1
#NId = cast(substring(id, 4, len(id)) as int)
FROM tblProduct order by LEN(id) DESC, ID DESC
You have to remember that
PRD009
is always greater than
PRD0010
or
PRD001
All in all, I think your approach is incorrect.
Your values will be
PRD00
PRD001
...
PRD009
PRD0010
PRD0011
...
PRD0099
PRD00100
This will make sorting a complete nightmare.
In addition to astander's analysis, you also have a concurrency issue.
The simple fix would be to add this at the beginning of your proc:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
And add a COMMIT at the end. Otherwise, two callers of this stored proc will get the same MAX/TOP 1 value from your table, and insert the same value.
Also, you can and should prevent these duplicates from existing by adding a key to your table, for this column. If you already have a PRIMARY KEY on this table, you can add an additional key using a UNIQUE constraint. This will prevent duplicates occurring in the future, no matter what programming errors occur. E.g.
ALTER TABLE tblProduct ADD CONSTRAINT UQ_Product_ID UNIQUE (ID)

Resources