using MERGE for incremental insert - sql

I have a scenario where one column of the target table needs to be auto incremented . I do not have identity enabled on this column. So i need to pick the last number and add 1 to it , each time an insert is done.
http://sqlfiddle.com/#!6/61eb4/5
A similar scenario is given in the fiddle link. I do not want the productid of ProductChanges table to be inserted. Instead, i need the last id to be picked and i need it to be incremented and inserted for each new row

Code to get this working
DECLARE #intctr int
SELECT #intctr = MAX(productid)+1 from products
DECLARE #strQry varchar(200)
SET #strQry =
'CREATE SEQUENCE dbo.seq_key_prd
START WITH ' +convert( varchar(12),#intctr) +' INCREMENT BY 1 ;'
print #strQry
exec( #strQry)
alter table Products
add default next value for seq_key_prd
for ProductId;
GO
--Merge statement for data sync
MERGE Products USING ProductChanges ON (Products.Productid = ProductChanges.Productid)
WHEN MATCHED AND Products.VendorlD =0 THEN DELETE
WHEN NOT MATCHED by target then insert (productid,Productname,VendorlD)
values(default,productname,VendorlD)
WHEN MATCHED THEN UPDATE SET
Products.ProductName = ProductChanges.ProductName ,
Products.VendorlD = ProductChanges.VendorlD;

1)create sequence and set to target table.
example
CREATE SEQUENCE table_seq
MINVALUE 1
START WITH 1
INCREMENT BY 1
CACHE 20;
2)create trigger for that sequence,to set the table
CREATE OR REPLACE TRIGGER my_trigger
BEFORE INSERT
ON myTable
FOR EACH ROW
WHEN (new.id is null)
DECLARE
v_id qname.qname_id%TYPE;
BEGIN
SELECT table_seq.nextval INTO v_id FROM DUAL;
:new.qname_id := v_id;
END my_trigger;

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

SQL Server update value in another table if value in (inserted) first table is greater

I have the following tables: (simplified for clarity)
AppWorkHeader with columns: (ProjectID (FK), ProgPercent, + 40 others not relevant)
AppProjects with columns: (ProjectID (PK), ProgPercent + 9 others not relevant)
I am trying to create a trigger that after insert, the ProgPercent value from the AppWorkHeader table updates the ProgPercent value in the AppProjects table only if it's greater than the existing value. I can get it to work with single insertions with the following:
-- Only works on single row insert
CREATE TRIGGER [dbo].[AppWorkHeader_project_trigger]
ON
[dbo].[AppWorkHeader]
AFTER INSERT AS
IF ##ROWCOUNT = 0
RETURN
SET NOCOUNT ON
-- Get the current project completion percentage from the AppProjects table
DECLARE #inserted_projectID int = (SELECT i.ProjectID FROM inserted i)
DECLARE #current_percent decimal(5,4) = (SELECT ProgPercent FROM AppProjects WHERE ProjectID = #inserted_projectID)
UPDATE AppProjects
SET ProgPercent = inserted.ProgPercent
FROM inserted
WHERE AppProjects.ProjectID = inserted.ProjectID AND inserted.ProgPercent > #current_percent
I can get multiple insertions to work with the following code. However, the greater-than part of my where clause seems to be ignored. Multiple insertions with the same ProjectID are updated to lower values.
-- Multiple row insert
CREATE TRIGGER [dbo].[AppWorkHeader_project_trigger]
ON
[dbo].[AppWorkHeader]
AFTER INSERT AS
IF ##ROWCOUNT = 0
RETURN
SET NOCOUNT ON
UPDATE AppProjects
SET ProgPercent = inserted.ProgPercent
FROM inserted
WHERE AppProjects.ProjectID = inserted.ProjectID AND inserted.ProgPercent > (SELECT ProgPercent FROM AppProjects WHERE ProjectID = (SELECT inserted.ProjectID))
I can't figure out how to get the existing value in the AppProjects table without using a variable, and I can't seem to get multiple insertions to work if I use a variable. Where am I going wrong?
For each inserted ProjectID find the maximum ProgPercent (if there are several rows inserted with the same ProjectID).
Then join table with new values to the table with old values.
CREATE TRIGGER [dbo].[AppWorkHeader_project_trigger]
ON [dbo].[AppWorkHeader]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
WITH
CTE_InsertedProjects
AS
(
SELECT
inserted.ProjectID
,MAX(inserted.ProgPercent) AS MaxProgPercent
FROM inserted
GROUP BY inserted.ProjectID
)
,CTE_AllProjects
AS
(
SELECT
AppProjects.ProjectID
,AppProjects.ProgPercent AS OldProgPercent
,CTE_InsertedProjects.MaxProgPercent AS NewProgPercent
FROM
AppProjects
INNER JOIN CTE_InsertedProjects ON CTE_InsertedProjects.ProjectID = AppProjects.ProjectID
WHERE
CTE_InsertedProjects.MaxProgPercent > AppProjects.ProgPercent
)
UPDATE CTE_AllProjects
SET OldProgPercent = NewProgPercent;
END
While Tring to Update Use JOIN To match the column ProjectID from AppProjects on AppWorkHeader ..
Update AP
set AP.ProgPercent=AWH.ProgPercent
FROM AppProjects AP Left JOIN AppWorkHeader AWH on AP.ProjectID=AWH.ProjectID
Where AP.ProgPercent < AWH.ProgPercent
This will only Update those rows where ProgPercent in AppProjects is Lesser than ProgPercent in AppWorkHeader

Generating the Next Id when Id is non-AutoNumber

I have a table called Employee. The EmpId column serves as the primary key. In my scenario, I cannot make it AutoNumber.
What would be the best way of generating the the next EmpId for the new row that I want to insert in the table?
I am using SQL Server 2008 with C#.
Here is the code that i am currently getting, but to enter Id's in key value pair tables or link tables (m*n relations)
Create PROCEDURE [dbo].[mSP_GetNEXTID]
#NEXTID int out,
#TABLENAME varchar(100),
#UPDATE CHAR(1) = NULL
AS
BEGIN
DECLARE #QUERY VARCHAR(500)
BEGIN
IF EXISTS (SELECT LASTID FROM LASTIDS WHERE TABLENAME = #TABLENAME and active=1)
BEGIN
SELECT #NEXTID = LASTID FROM LASTIDS WHERE TABLENAME = #TABLENAME and active=1
IF(#UPDATE IS NULL OR #UPDATE = '')
BEGIN
UPDATE LASTIDS
SET LASTID = LASTID + 1
WHERE TABLENAME = #TABLENAME
and active=1
END
END
ELSE
BEGIN
SET #NEXTID = 1
INSERT INTO LASTIDS(LASTID,TABLENAME, ACTIVE)
VALUES(#NEXTID+1,#TABLENAME, 1)
END
END
END
Using MAX(id) + 1 is a bad idea both performance and concurrency wise.
Instead you should resort to sequences which were design specifically for this kind of problem.
CREATE SEQUENCE EmpIdSeq AS bigint
START WITH 1
INCREMENT BY 1;
And to generate the next id use:
SELECT NEXT VALUE FOR EmpIdSeq;
You can use the generated value in a insert statement:
INSERT Emp (EmpId, X, Y)
VALUES (NEXT VALUE FOR EmpIdSeq, 'x', 'y');
And even use it as default for your column:
CREATE TABLE Emp
(
EmpId bigint PRIMARY KEY CLUSTERED
DEFAULT (NEXT VALUE FOR EmpIdSeq),
X nvarchar(255) NULL,
Y nvarchar(255) NULL
);
Update: The above solution is only applicable to SQL Server 2012+. For older versions you can simulate the sequence behavior using dummy tables with identity fields:
CREATE TABLE EmpIdSeq (
SeqID bigint IDENTITY PRIMARY KEY CLUSTERED
);
And procedures that emulates NEXT VALUE:
CREATE PROCEDURE GetNewSeqVal_Emp
#NewSeqVal bigint OUTPUT
AS
BEGIN
SET NOCOUNT ON
INSERT EmpIdSeq DEFAULT VALUES
SET #NewSeqVal = scope_identity()
DELETE FROM EmpIdSeq WITH (READPAST)
END;
Usage exemple:
DECLARE #NewSeqVal bigint
EXEC GetNewSeqVal_Emp #NewSeqVal OUTPUT
The performance overhead of deleting the last inserted element will be minimal; still, as pointed out by the original author, you can optionally remove the delete statement and schedule a maintenance job to delete the table contents off-hour (trading space for performance).
Adapted from SQL Server Customer Advisory Team Blog.
Working SQL Fiddle
The above
select max(empid) + 1 from employee
is the way to get the next number, but if there are multiple user inserting into the database, then context switching might cause two users to get the same value for empid and then add 1 to each and then end up with repeat ids. If you do have multiple users, you may have to lock the table while inserting. This is not the best practice and that is why the auto increment exists for database tables.
I hope this works for you. Considering that your ID field is an integer
INSERT INTO Table WITH (TABLOCK)
(SELECT CASE WHEN MAX(ID) IS NULL
THEN 1 ELSE MAX(ID)+1 END FROM Table), VALUE_1, VALUE_2....
Try following query
INSERT INTO Table VALUES
((SELECT isnull(MAX(ID),0)+1 FROM Table), VALUE_1, VALUE_2....)
you have to check isnull in on max values otherwise it will return null in final result when table contain no rows .

Incremental count column based on another column contents

I need to populate a column with the running count based on another column contents. The table is like this:
count seq_num
1 123-456-789
1 123-456-780
1 123-456-990
2 123-456-789
2 123-456-990
So, as the seq_num column changes, the counter resets to '1' and as the column repeats, the counter increments by 1.
This is using SQL2000, and the seq_num field is varchar.
Any ideas?
If you're inserting, you can use a subquery:
insert into
table (count, seq_num)
values
((select count(*)+1 from table where seq_num = #seq)
,#seq)
Otherwise, you'll need to have a date on there or some way of telling it how to determine what was first:
update table
set count =
(select count(*)+1 from table t2
where t2.seq_num = table.seq_num
and t2.insertdate < table.insertdate)
if you need to be able to continue updating this in the future, you might try this. It's a few steps but would fix it AND set it up for future use. (probably need to check my syntax - I mess with ORacle more now, so I may have mixed up some things - but the logic should work.)
first, create a table to contain the current counter level per sequence:
Create newTable (counter int, sequence varchar)
then, fill it with data like this:
insert into newTable
(select distinct 0 as Counter, sequence
from table)
This will put each sequence number in the table one time and the counter for each will be set at 0.
Then, create an update proc with TWO update statements and a bit of extra logic:
Create procedere counterUpdater(#sequence varchar) as
Declare l_counter as int;
select l_counter = counter
from newTable
where sequence = #sequence
--assuming you have a primary key in the table.
Declare #id int;
Select top 1 #id = id from table
where sequence = #sequence
and counter is null;
--update the table needing edits.
update table
set counter = l_counter + 1
where id = #id
--update the new table so you can keep track of which
--counter you are on
update newTable
set counter = l_counter + 1
where id = #id
Then run a proc to execute this proc for each record in your table.
Now you should have a "newTable" filled with the currently used counter for each record in the table. Set up your insert proc so that anytime a new record is created, if it is a sequence not already in the newTable, you add it with a count of 1 and you put a count of 1 in the main table. If the sequence DOES already exist, use the logic above (increment the count already in use the "newTable" and place that count as the counter value in the newTable and the mainTable.
Basically, this method decided to use memory in place of querying the existing table. It will become most beneficial if you have a large table with lots of repeated sequence numbers. If your sequence numbers only happen two or three times, you probably want to do a query instead when you update and then later insert:
First, to update:
--find out the counter value
Declare l_counter int
select l_counter = max(counter)
from table where sequence = #sequence
update table
set counter = l_counter + 1
where id = (select top 1 id from table where sequence = #sequence
and counter is null)
then run that for each record.
Then, when inserting new records:
Declare l_counter int
select l_counter = max(counter)
from table where sequence = #sequence
IsNull(l_counter, 0)
Insert into table
(counter, sequence) values (l_counter + 1, #sequence)
Again, I'm positive I've mixed-and-matched my syntaxes here, but the concepts should work. OF course, it's a "one at a time" approach instead of set based, so it might be a little inefficient, but it will work.

Update duplicate varchars to be unique in SQL database

I need to change a database to add a unique constraint on a table column, but the VARCHAR data in it is not unique.
How can I update those duplicate records so that each value is unique by adding a sequential number at the end of the existing data?
e.g. I would like to change 'name' to 'name1', 'name2', 'name3'
Here are 2 examples with using the MS SQL SERVER flavor of sql.
Setup Example:
create table test (id int identity primary key, val varchar(20) )
--id is a pk for the cursor so it can update using "where current of"
-- name a is not duplicated
-- name b is duplicated 3 times
-- name c is duplicated 2 times
insert test values('name a')
insert test values('name b')
insert test values('name c')
insert test values('name b')
insert test values('name b')
insert test values('name c')
Sql 2005\2008: ( Computed Table Expression )
begin tran; -- Computed table expressions require the statement prior to end with ;
with cte(val,row) as (
select val, row_number() over (partition by val order by val) row
--partiton is important. it resets the row_number on a new val
from test
where val in ( -- only return values that are duplicated
select val
from test
group by val
having count(val)>1
)
)
update cte set val = val + ltrim(str(row))
--ltrim(str(row)) = converting the int to a string and removing the padding from the str command.
select * from test
rollback
Sql 2000: (Cursor example)
begin tran
declare #row int, #last varchar(20), #current varchar(20)
set #last = ''
declare dupes cursor
for
select val
from test
where val in ( -- only return values that are duplicated
select val
from test
group by val
having count(val)>1
)
order by val
for update of val
open dupes
fetch next from dupes into #current
while ##fetch_status = 0
begin
--new set of dupes, like the partition by in the 2005 example
if #last != #current
set #row = 1
update test
--#last is being set during the update statement
set val = val + ltrim(str(#row)), #last = val
where current of dupes
set #row = #row + 1
fetch next from dupes into #current
end
close dupes
deallocate dupes
select * from test
rollback
I rolled back each of the updates because my script file contains both examples. This allowed me to test the functionality without resetting the rows on the table.
Open a cursor on the table, ordered by that column. Keep a previous value variable, initialized to null, and an index variable initialized to 0. If the current value = the previous value, increment the index and append the index to the field value. if the current value <> the previous value, reset the index to 0 and keep the field value as is. Set the previous value variable = the current value. Move on to the next row and repeat.
You could add another column to it... like
update mytable set mycolumn = concat(mycolumn, id)
where id in (<select duplicate records>);
replace id with whatever column makes mycolumn unique
What database are you using?
In Oracle there is a:
NOVALIDATE Validates changes but does not validate data previously existing in the table
Example:
ALTER TABLE <table_name> ENABLE NOVALIDATE UNIQUE;
If you are not using Oracle then check the SQL reference for your respective database.