SQL stored procedure inserting duplicate OrderNumber - sql

I searched the Internet for days, no effort, maybe I cant ask in a right way.
I have a sql table like this:
create table Items
(
Id int identity(1,1),
OrderNumber varchar(7),
ItemName varchar(255),
Count int
)
Then I have a stored procedure inserting items, on demand creating new OrderNumber:
create procedure spx_insertItems
#insertNewOrderNr bit,
#orderNumber varchar(7),
#itemName varchar(255),
#count int
as
begin
set nocount on;
if (#insertNewOrderNr = 1)
begin
declare #newNr = (select dbo.fun_getNewOrderNr())
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
select #newNr
end
else
begin
INSERT INTO Items (OrderNumber, ItemName, Count) values (#orderNumber, #itemName, #count)
select scope_identity()
end
end
Finally there is a user defined function returning new OrderNumber:
create function dbo.fun_getNewOrderNr
()
return varchar(7)
as
begin
/* this func works well */
declare #output varchar(7)
declare #currentMaxNr varchar(7)
set #currentMaxNr = (isnull((select max(OrderNumber) from Items), 'some_default_value_here')
/* lets assume the #currentMaxNr is '01/2014', here comes logic that increments to #newValue='02/2014' and sets to #output, so: */
set #output = #newValue
return #output
end
Into Items can be inserted items that do as well that do not belong to any OrderNumber.
Whether an Item should become new OrderNumber, the procedure is called with #insertNewOrderNr=1, returns the new order number, that can be used to insert next items with that OrderNumber while #insertNewOrderNr=0.
Occasionally there happens that there come simultaneously 2 requests to #insertNewOrderNr and THERE IS THE PROBLEM - Items, that should correspond with different OrderNumbers get the same OrderNumber.
I tried to use transaction with no success.
The table structure cant be modified by me.
What would be the right way to ensure, that there won't be used the same newOrderNumber when simultaneous requests to the procedure come?
I am stuck here for a long time till now. Please, help.

You will have that problem as long as you use MAX(OrderNumber).
You might consider using sequences:
Create sequence
CREATE SEQUENCE dbo.OrderNumbers
AS int
START WITH 1
INCREMENT BY 1
NO CACHE;
GO
CREATE SEQUENCE dbo.OrderNumberYear
AS int
START WITH 2014
INCREMENT BY 1
NO CACHE;
SELECT NEXT VALUE FOR dbo.OrderNumberYear; --Important, run this ONE time after creation, this years value must be returned one initial time to work correctly.
Insert code
DECLARE #orderNumberYear INT = (SELECT CONVERT(INT, current_value) FROM sys.sequences WHERE name = 'OrderNumberYear');
IF(#orderNumberYear < YEAR(GETDATE()))
BEGIN
SELECT #orderNumberYear = NEXT VALUE FOR dbo.OrderNumberYear;
ALTER SEQUENCE dbo.OrderNumbers RESTART WITH 1 ;
END
IF(#orderNumberYear != YEAR(GETDATE()))
RAISERROR(N'Order year sequence is out of sync.', 16, 1);
DECLARE #newNr VARCHAR(15) = CONCAT(FORMAT(NEXT VALUE FOR dbo.OrderNumbers, '00/', 'en-US'), #orderNumberYear);
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
SELECT #newNr

The duplicity still occured, not so often, but did.
The trick I finally used to get around didn't find itself inside SQL. Since the DB is always used by ONLY one web app that is used by several users, this is the solution:
in all (about 5) places in my VB.NET code I surrounded the myCommand.ExecuteScalar() with SyncLock (read more) statement that DID the trick :)

Related

Generate a unique column sequence value based on a query handling concurrency

I have a requirement to automatically generate a column's value based on another query's result. Because this column value must be unique, I need to take into consideration concurrent requests. This query needs to generate a unique value for a support ticket generator.
The template for the unique value is CustomerName-Month-Year-SupportTicketForThisMonthCount.
So the script should automatically generate:
AcmeCo-10-2019-1
AcmeCo-10-2019-2
AcmeCo-10-2019-3
and so on as support tickets are created. How can ensure that AcmeCo-10-2019-1 is not generated twice if two support tickets are created at the same time for AcmeCo?
insert into SupportTickets (name)
select concat_ws('-', #CustomerName, #Month, #Year, COUNT())
from SupportTickets
where customerName = #CustomerName
and CreatedDate between #MonthStart and #MonthEnd;
One possibility:
Create a counter table:
create table Counter (
Id int identify(1,1),
Name varchar(64)
Count1 int
)
Name is a unique identifier for the sequence, and in your case name would be CustomerName-Month-Year i.e. you would end up with a row in this table for every Customer/Year/Month combination.
Then write a stored procedure similar to the following to allocate a new sequence number:
create procedure [dbo].[Counter_Next]
(
#Name varchar(64)
, #Value int out -- Value to be used
)
as
begin
set nocount, xact_abort on;
declare #Temp int;
begin tran;
-- Ensure we have an exclusive lock before changing variables
select top 1 1 from dbo.Counter with (tablockx);
set #Value = null; -- if a value is passed in it stuffs us up, so null it
-- Attempt an update and assignment in a single statement
update dbo.[Counter] set
#Value = Count1 = Count1 + 1
where [Name] = #Name;
if ##rowcount = 0 begin
set #Value = 10001; -- Some starting value
-- Create a new record if none exists
insert into dbo.[Counter] ([Name], Count1)
select #Name, #Value;
end;
commit tran;
return 0;
end;
You could look into using a TIME type instead of COUNT() to create unique values. That way it is much less likely to have duplicates. Hope that helps

Stored procedure with AS MERGE not returning anything?

EDIT: Sequential invoice numbering is the law in multiple countries.
EDIT: Poor variable naming on my part suggested I wanted to use my generated Id as a key. This is not the case. Should have stuck with 'invoiceNumber'.
I have the exact same question as posed here https://stackoverflow.com/a/24196374/1980516
However, since the proposed solution threw a syntax error, I've adapted it to use a cursor.
First, there is the stored procedure that generates a new Nr, for a given Business+Year combination:
CREATE PROCEDURE PROC_NextInvoiceNumber #businessId INT, #year INT, #Nr NVARCHAR(MAX) OUTPUT
AS MERGE INTO InvoiceNextNumbers ini
USING (VALUES (#businessId, #year)) Incoming(BusinessId, Year)
ON Incoming.BusinessId = ini.BusinessId AND Incoming.Year = ini.Year
WHEN MATCHED THEN UPDATE SET ini.Nr = ini.Nr + 1
WHEN NOT MATCHED BY TARGET THEN INSERT (BusinessId, Year, Nr)
VALUES(#businessId, #year, 1)
OUTPUT INSERTED.Nr;
Then, using that stored procedure, I've created an INSTEAD OF INSERT trigger:
CREATE TRIGGER TRIG_GenerateInvoiceNumber ON Invoices INSTEAD OF INSERT
AS
BEGIN
DECLARE #BusinessId INT
DECLARE #InvoiceId INT
DECLARE #BillingDate DATETIME2(7)
-- Cursors are expensive, but I don't see any other way to call the stored procedure per row
-- Mitigating factor: Mostly, we're only inserting one Invoice at a time
DECLARE InsertCursor CURSOR FAST_FORWARD FOR
SELECT BusinessId, Id, BillingDate FROM INSERTED
OPEN InsertCursor
FETCH NEXT FROM InsertCursor
INTO #BusinessId, #InvoiceId, #BillingDate
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #year INT
SET #year = year(#BillingDate)
DECLARE #Number NVARCHAR(MAX)
EXEC PROC_NextInvoiceNumber #BusinessId, #year, #Number OUTPUT
-- SET #Number = 'this works'
INSERT INTO Invoices (BusinessId, Id, BillingDate, Number)
VALUES (#BusinessId, #InvoiceId, #BillingDate, #Number)
FETCH NEXT FROM InsertCursor
INTO #BusinessId, #InvoiceId, #BillingDate
END
CLOSE InsertCursor
DEALLOCATE InsertCursor
END
If I uncomment SET #Number = 'this works', then in my database that exact string ('this works') is successfully set in Invoice.Number.
Somehow, my OUTPUT parameter is not set and I can't figure out why not.. Can someone shed a light on this?
EDIT update in response to comments (thank you):
I have a composite key (BusinessId, Id) for Invoice. The desired end result is a unique Invoice Identifier Number of the form '20180001' that is a continuous sequence of numbers within the businessId. So business 1 has invoice Numbers 20180001, 20180002, 20180003 and business 2 also has invoice numbers 20180001, 20180002, 20180003. (But different composite primary keys)
I don't want that cursor either, but I saw no other way within the framework as suggested by the question I refer to up above.
Manual call of PROC_NextInvoiceNumber with existing business id and year returns NULL.
If I try to set Id in PROC_NextInvoiceNumber, I get A MERGE statement must be terminated by a semi-colon (;). if I set it inside the MERGE or The multi-part identifier "INSERTED.Nr" could not be bound. if I set outside the MERGE.
Your OUTPUT parameter is never set. You are using the OUTPUT clause of the MERGE statement to create a result set. This is unrelated to assigning a value to a parameter.
MERGE INTO..
USING ... ON ...
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED BY TARGET THEN INSERT ...
OUTPUT INSERTED.Nr; /* <-- HERE this is the OUTPUT *clause* */
Change the code to actually assign something to #Nr:
SET #Nr = ...
The typical way is to use the OUTPUT clause to store the desired value into a table variable and then assign the value to the desired output *variable:
DECLARE #t TABLE (Nr NVARCHAR(MAX));
MERGE INTO..
USING ... ON ...
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED BY TARGET THEN INSERT ...
OUTPUT INSERTED.Nr INTO #t;
SELECT #Nr = Nr FROM #t;

SQL while loop with select query

I'm trying to accomplish the following thing:
I receive a DocumentID. Find all the records in a table that match the specific DocumentID, for example I have 10 records matching and every record is with different DocumentAttachmentID.
I update all the records with the new data.
The problem comes that I need to insert some of the information from these ten records + other information received to a new table, which is History table, i.e. I need to insert ten new records there.
I've succeeded to this with Cursor, but it looks like that the Cursor isn't really good, because of the performance.
Is there a way to loop throught the 10 records that I selected from this table and for every record to take some information, add some additional info and then insert this in the other table ?
EDIT:
I tried to do this withoud looping(thanks you all for the answers)
I will try it tomorrow, do you think this is gonna work ?:
With the first Update, I update all documentAttachments,
The second block is INSERT TO, which should insert all document attachments in the other table with some extra columns.
UPDATE [sDocumentManagement].[tDocumentAttachments]
SET DeletedBy = #ChangedBy,
DeletedOn = #CurrentDateTime,
IsDeleted = 1,
WHERE DocumentID = #DocumentID;
INSERT INTO [sDocumentManagement].[tDocumentHistory] ( DocumentAttachmentID, DocumentID, ActivityCodeID, ChangedOn, ChangedBy, AdditionalInformation )
SELECT DocumentAttachmentID,
#DocumentID, [sCore].[GetActivityCodeIDByName] ( 'DeletedDocument' ),
#CurrentDateTime,
#ChangedBy,
#AdditionalInformation
FROM [sDocumentManagement].[tDocumentAttachments]
WHERE DocumentID = #DocumentID;
for looping without a cursor I quite often use the following technique:
DECLARE #items TABLE(id INT, val INT);
DECLARE #id INT;
DECLARE #val INT;
WHILE EXISTS(SELECT * FROM #items) BEGIN
SELECT TOP(1) #id = id, #val = val FROM #items;
DELETE FROM #items WHERE (id = #id);
--do what is needed with the values here.
SELECT #id, #val;
END
this treats the #items table as a queue pulling the rows off one at a time till it is empty.

Keep the last 'x' results in a database

I would like to ask if there is a quick way to keep the last 'x' inserted rows in a database.
For example, i have an application through which users can search for items and I want to keep the last 10 searches that each user has made. I do not want to keep all his searches into the database as this will increase db_size. Is there a quick way of keeping only the latest 10 searches on my db or shall i check every time:
A) how many searches has been stored on database so far
B) if (searches = 10) delete last row
C) insert new row
I think that this way will have an impact on performance as it will need 3 different accesses on the database: check, delete and insert
I don't think an easy/quick way to do this. Based on your conditions i created the below stored procedure.
I considered SearchContent which is going to store the data.
CREATE TABLE SearchContent (
Id INT IDENTITY (1, 1),
UserId VARCHAR (8),
SearchedOn DATETIME,
Keyword VARCHAR (40)
)
In the stored procedure passing the UserId, Keyword and do the calculation. The procedure will be,
CREATE PROCEDURE [dbo].[pub_SaveSearchDetails]
(
#UserId VARCHAR (8),
#Keyword VARCHAR (40)
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Count AS INT = 0;
-- A) how many searches has been stored on database so far
SELECT #Count = COUNT(Keyword) FROM SearchContent WHERE UserId = #UserId
IF #Count >= 10
BEGIN
-- B) if (searches = 10) delete last row
DELETE FROM SearchContent WHERE Id IN ( SELECT TOP 1 Id FROM SearchContent WHERE UserId = #UserId ORDER BY SearchedOn ASC)
END
-- C) insert new row
INSERT INTO SearchContent (UserId, SearchedOn, Keyword)
VALUES (#UserId, GETDATE(), #Keyword)
END
Sample execution: EXEC pub_SaveSearchDetails 'A001', 'angularjs'

How to ensure my SQL varchar field is unique

I'm sure this is really simple but I've been up through the night and am now getting stuck.
I have a piece of functionality that clones a record in a database however I need to ensure the new name field is unique in the database.
eg, the first record is
[ProjectName] [ResourceCount]
'My Project' 8
Then when I click the clone I want
'My Project Cloned', 8
But then if I hit the button again it should notice that the cloned name exists and rather spit out
'My Project Cloned 2', 8
Is that making sense?
I can do it with temp tables and cursors but there has to be a much nicer way to do this?
Using SQL Server 2008 R2
The solution needs to be entirely T-SQL based though, this occurs in a single stored procedure
So from my understanding of your problem, here's how I would approach it:
My table:
CREATE TABLE [dbo].[deal]
(
[dealName] varchar(100),
[resourceCount] int
)
Then create a unique index on the dealName column:
CREATE UNIQUE NONCLUSTERED INDEX [UQ_DealName] ON [dbo].[deal]
(
[dealName] ASC
)
Once you have the unique index, you can then just handle any exceptions such as a unique constraint violation (error 2601) directly in T-SQL using try/catch
SET NOCOUNT ON;
DECLARE #dealName VARCHAR(100) = 'deal'
DECLARE #resourceCount INT = 8
DECLARE #count INT
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO dbo.deal (dealName,resourceCount)
VALUES (#dealName, #resourceCount)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##ERROR = 2601
BEGIN
ROLLBACK TRANSACTION
SET #count = (SELECT COUNT(dealName) FROM dbo.deal WHERE resourceCount = #resourceCount)
SET #resourceCount = (SELECT resourceCount FROM dbo.deal WHERE dealName = #dealName)
SET #dealName = #dealName + ' Cloned ' + CAST(#count AS VARCHAR(100))
BEGIN TRANSACTION
INSERT INTO dbo.deal (dealName,resourceCount)
VALUES (#dealName,#resourceCount)
COMMIT TRANSACTION
END
END CATCH
SELECT * FROM dbo.deal
You can easily put this code into a procedure, all it does is try and insert a deal name with the resource count, if the unique constraint is violated, it enters the catch block, appends the information that you want onto the deal name after finding the resource count of the original deal and then inserts these values.
It's not bulletproof by any means, but I find this technique really useful, not just for enforcing uniqueness, but you can use a similar way of handling exception numbers to deal with deadlocking, primary key violations and loads of other errors, all in T-SQL.
Ensuring the value is unique is easy: Create a unique constraint. If a unique value is inserted MSSQL will throw an exception and you can recover in your application.
Creating a unique name based on a counter (Proj1, Proj2, etc.) is a bit more involved.
Note, this is best mitigated in the web layer, where you can perform an existence check and inform the user prior to attempting the insert that the project name "is already in use." And, if this isnt an option, there are far simpler methods of ensuring uniqueness than enumerating a count as you've described. Appending a datetime or guid would make things relatively easy and would greatly (if not completely) avoid race conditions.
If you absolutely must implement in t-sql as requested then incorporating a counter column somewhere (ie, my "sequence" table below) should help minimize race conditions. I suspect even with the below example you might see some contention under high frequency calls.
--setup
/*
--your existing table
create table dbo.Project
(
[ProjectName] varchar(100) primary key,
[ResourceCount] int
);
--a new table to transactionally constrain the increment
create table dbo.ProjectNameSequence (ProjectName varchar(100) primary key, Seq int);
--cleanup
--drop table dbo.ProjectNameSequence
--drop table dbo.Project
*/
declare #ProjectName varchar(100), #ResourceCount int;
set #ProjectName = 'Test Project XX';
set #ResourceCount = 9;
merge dbo.ProjectNameSequence [d]
using (values(#ProjectName)) [s] (ProjectName) on
d.ProjectName = s.ProjectName
when matched then update set Seq += 1
when not matched then insert values(#ProjectName, 1)
output #ProjectName + case inserted.Seq when 1 then '' else cast(inserted.Seq as varchar) end,
#ResourceCount
into dbo.Project;
select * from dbo.Project
I resolve this using an IF EXISTS inside a WHILE loop..
Personally I can't see what's wrong with this method but will obviously take any comments into account
DECLARE #NameInvalid varchar(100)
DECLARE #DealName varchar(100)
DECLARE #Count int
SET #Count = 1
SET #NameInvalid = 'true'
SELECT #DealName = DealName FROM Deal WHERE DealId = #DealId
--Ensure we get a unique deal name
WHILE( #NameInvalid = 'true')
BEGIN
IF NOT EXISTS(SELECT DealName FROM Deal where DealName = #DealName + ' Cloned ' + cast(#Count as varchar(10)))
BEGIN
INSERT INTO Deal
(DealName)
SELECT #DealName + ' Cloned ' + cast(#Count as varchar(10))
FROM Deal
WHERE DealID = #DealId
SET #NewDealId = ##IDENTITY
SET #NameInvalid = 'false'
END
ELSE
BEGIN
SET #NameInvalid = 'true'
SET #Count = #Count + 1
END
END