sql copy of identity column as a string - sql

Im creating a procedure that simply creates a copy of a row but changes some values.
In the newly created record (which is a copy of an existing record) I need one of the fields to be a string representation of the recordID (an auto-generated IDENTITY column) of the newly created record.
CREATE PROC sp #RecordID int
AS
BEGIN
INSERT INTO TheTable([RecordName], ...)
SELECT CAST(#RecordID as nvarchar(500)), ...
FROM TheTable
WHERE [RecordID] = #RecordID
END
my code is wrong because it sets the RecordName to the RecordID of the record being copied FROM. I need the RecordName to be the same as the RecordID of the record being created.
Thanks for the help

I don't think that what you describe is possible — the identity value isn't available until the statement completes — but there's a simple workaround:
CREATE PROC sp #RecordID int
AS
BEGIN
INSERT INTO TheTable([RecordName], ...)
SELECT 'temporary dummy value', ...
FROM TheTable
WHERE [RecordID] = #RecordID
;
UPDATE TheTable
SET [RecordName] = CAST(##IDENTITY as nvarchar(500))
WHERE [RecordID] = ##IDENTITY
;
END
(See the documentation for ##IDENTITY.)

Sounds like you'll need to do this in 2 steps - insert the new data into the table, then update the record with the new recordID
For example (assuming Sql Server),
declare #myNewID int
INSERT INTO TheTable([RecordName], ...)
SELECT CAST(#RecordID as nvarchar(500)), ...
FROM TheTable
WHERE [RecordID] = #RecordID
set #myNewID = SCOPE_IDENTITY()
UPDATE TheTable
set [RecordName] = CAST(#myNewID as nvarchar(500))
WHERE [RecordID] = #myNewID

Related

Enumerate the multiple rows in a multi-update Trigger

I have something like the table below:
CREATE TABLE updates (
id INT PRIMARY KEY IDENTITY (1, 1),
name VARCHAR (50) NOT NULL,
updated DATETIME
);
And I'm updating it like so:
INSERT INTO updates (name, updated)
VALUES
('fred', '2020-11-11),
('fred', '2020-11-11'),
...
('bert', '2020-11-11');
I need to write an after update Trigger and enumerate all the name(s) that were added and add each one to another table but can't work out how enumerate each one.
EDIT: - thanks to those who pointed me in the right direction, I know very little SQL.
What I need to do is something like this
foreach name in inserted
look it up in another table and
retrieve a count of the updates a 'name' has done
add 1 to the count
and update it back into the other table
I can't get to my laptop at the moment, but presumably I can do something like:
BEGIN
SET #count = (SELECT UCount from OTHERTAB WHERE name = ins.name)
SET #count = #count + 1
UPDATE OTHERTAB SET UCount = #count WHERE name = ins.name
SELECT ins.name
FROM inserted ins;
END
and that would work for each name in the update?
Obviously I'll have to read up on set based SQL processing.
Thanks all for the help and pointers.
Based on your edits you would do something like the following... set based is a mindset, so you don't need to compute the count in advance (in fact you can't). It's not clear whether you are counting in the same table or another table - but I'm sure you can work it out.
Points:
Use the Inserted table to determine what rows to update
Use a sub-query to calculate the new value if its a second table, taking into account the possibility of null
If you are really using the same table, then this should work
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE(UCount,0) + 1
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
If however you are using a second table then this should work:
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE((SELECT UCount+1 from OTHERTAB T2 WHERE T2.[name] = OTHERTAB.[name]),0)
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
Using inserted and set-based approach(no need for loop):
CREATE TRIGGER trg
ON updates
AFTER INSERT
AS
BEGIN
INSERT INTO tab2(name)
SELECT name
FROM inserted;
END

In SQL Server 2008 R2, is there a way to create a custom auto increment identity field without using IDENTITY(1,1)?

I would like to be able to pull the custom key value from a table, but would also like it to perform like SQL Server's IDENTITY(1,1) column on inserts.
The custom key is for another application and will need to be used by different functions so the value will need to be pulled from a table and available for other areas.
Here are some if my attempts:
Tried a trigger on the table works well on single inserts, failed on using SQL insert (forgetting the fact that a triggers are not per row but by batch)
ALTER TRIGGER [sales].[trg_NextInvoiceDocNo]
ON [sales].[Invoice]
AFTER INSERT
AS
BEGIN
DECLARE #ResultVar VARCHAR(25)
DECLARE #Key VARCHAR(25)
EXEC [dbo].[usp_GetNextKeyCounterChar]
#tcForTbl = 'docNbr', #tcForGrp = 'docNbr', #NewKey = #ResultVar OUTPUT
UPDATE sales.InvoiceRET
SET DocNbr = #ResultVar
FROM sales.InvoiceRET
JOIN inserted ON inserted.id = sales.InvoiceRET.id;
END;
Thought about a scalar function, but functions cannot exec stored procedures or update statements in order to set the new key value in the lookup table.
Thanks
You can use ROW_NUMBER() depending on the type of concurrency you are dealing with. Here is some sample data and a demo you can run locally.
-- Sample table
USE tempdb
GO
IF OBJECT_ID('dbo.sometable','U') IS NOT NULL DROP TABLE dbo.sometable;
GO
CREATE TABLE dbo.sometable
(
SomeId INT NULL,
Col1 INT NOT NULL
);
GO
-- Stored Proc to insert data
CREATE PROC dbo.InsertProc #output BIT AS
BEGIN -- Your proc starts here
INSERT dbo.sometable(Col1)
SELECT datasource.[value]
FROM (VALUES(CHECKSUM(NEWID())%100)) AS datasource([value]) -- simulating data from somewhere
CROSS APPLY (VALUES(1),(1),(1)) AS x(x);
WITH
id(MaxId) AS (SELECT ISNULL(MAX(t.SomeId),0) FROM dbo.sometable AS t),
xx AS
(
SELECT s.SomeId, RN = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))+id.MaxId, s.Col1, id.MaxId
FROM id AS id
CROSS JOIN dbo.sometable AS s
WHERE s.SomeId IS NULL
)
UPDATE xx SET xx.SomeId = xx.RN;
IF #output = 1
SELECT t.* FROM dbo.sometable AS t;
END
GO
Each time I run: EXEC dbo.InsertProc 1; it returns 3 more rows with the correct ID col. Each time I execute it, it adds more rows and auto-increments as needed.
SomeId Col1
-------- ------
1 62
2 73
3 -17

Output Always 1

I have a stored procedure and am using a Merge Statement to Insert and Update. This aspect is working as I require.
However, the output when inserting the record is always 1 and I cannot see why? I would be grateful if someone could review this procedure and let me know what I could be doing wrong,.
CREATE PROCEDURE [dbo].[FileAdd]
#FileId int,
#FileData varbinary(max),
#ContentType Varchar(100),
#OperatorId int
AS
BEGIN
--In Memory Table to
DECLARE #MergeOutput TABLE
(
Id INT
);
--Merge needs a table to Merge with so using a CTE
WITH CTE AS (
SELECT #FileId as FileId)
--Merge
MERGE INTO [dbo].[Files] as T
USING CTE AS S
ON T.FileId = S.FileId
WHEN NOT MATCHED THEN
INSERT (
FileData,
ContentType,
OperatorIdCreated,
OperatorIdUpdated
)
VALUES(
#FileData,
#ContentType,
#OperatorId,
#OperatorId
)
WHEN MATCHED THEN
UPDATE SET
FileData = #FileData,
ContentType= #ContentType,
OperatorIdUpdated = #OperatorId,
Updated = GetDate()
OUTPUT
INSERTED.FileId
INTO #MergeOutput;
SELECT * FROM #MergeOutput;
END
GO
The reason you are getting 1 is because that is what is being UPDATED or INSERTED. When it's the UPDATED value, then it is the value are passing into #FileID.
With the OUTPUT clause:
INSERTED Is a column prefix that specifies the value added by the
insert or update operation.
Thus, what ever value is UPDATED (which is #FileID) or INSERTED (which will be whatever your FileID table logic is) this will be returned in your code. If you are always getting 1, then you must me always updating the column for FileID = 1.
Changing your bottom to inserted.* would show you this, as it would OUTPUT the updated row.
Check the demo here.

SQL Server : Merge across multiple tables with foreign key

Here's what I am trying to do: basically send XML to SQL Server to update/insert (Merge) my data as a "save" function in my code.
I have managed to successfully do this if I send one "item" in the XML using the following XML:
<root>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
<M_iItemId>0</M_iItemId>
<M_iItemTypeId>1</M_iItemTypeId>
<M_sItemValue>German</M_sItemValue>
<M_iRaceId>1</M_iRaceId>
<M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
</root>
in this stored procedure:
ALTER PROCEDURE [dbo].[spFormula1_Save]
#Formula1Xml xml--Formula1 as xml
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF DATALENGTH(#Formula1Xml) = 0
RETURN 0
BEGIN TRANSACTION
BEGIN TRY
DECLARE #hDoc INT
EXEC sp_xml_preparedocument #hDoc OUTPUT, #Formula1Xml
-------------------
--Formula1 Table
-------------------
DECLARE #Formula1Id bigint = 0;
MERGE INTO Formula1 AS tab
USING
OPENXML (#hDoc, '/root/Formula1', 2)
WITH (
M_iFormula1Id bigint,
M_bDataInUse bit,
M_bActive bit
) AS [xml]
ON (tab.Formula1Id = [xml].[M_iFormula1Id])
WHEN MATCHED THEN UPDATE SET tab.DataInUse = [xml].M_bDataInUse,
tab.Active = [xml].M_bActive,
#Formula1Id = [xml].M_iFormula1Id
WHEN NOT MATCHED THEN INSERT (DataInUse,
Active)
VALUES([xml].M_bDataInUse,
[xml].M_bActive
);
IF(#Formula1Id = 0)--then we haven''t updated so get inserted rowid
BEGIN
SET #Formula1Id = SCOPE_IDENTITY();--get the inserted identity
END
-------------------
--Formula1Item Table
-------------------
MERGE INTO Formula1Item AS tab
USING
OPENXML (#hDoc, '/root/Formula1/M_lstItem', 2)
WITH (
M_iItemId bigint,
M_iItemTypeId bit,
M_sItemValue varchar(1000),
M_iRaceId int,
M_iDriverId int
) AS [xml]
ON (tab.ItemId = [xml].M_iItemId)
WHEN MATCHED THEN UPDATE SET tab.ItemTypeId = [xml].M_iItemTypeId,
tab.ItemValue = [xml].M_sItemValue,
tab.RaceId = [xml].M_iRaceId,
tab.DriverId = [xml].M_iDriverId
WHEN NOT MATCHED THEN INSERT (Formula1Id,
ItemTypeId,
ItemValue,
RaceId,
DriverId)
VALUES(#Formula1Id,
[xml].M_iItemTypeId,
[xml].M_sItemValue,
[xml].M_iRaceId,
[xml].M_iDriverId
);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
When I have multiple records in the XML the #Formula1Id gets set to the last one inserted in the first merge statement so all the Child data in the XML gets merged using this id, meaning all child data belongs to one parent!
<root>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
<M_iItemId>0</M_iItemId>
<M_iItemTypeId>1</M_iItemTypeId>
<M_sItemValue>German</M_sItemValue>
<M_iRaceId>1</M_iRaceId>
<M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
<M_iItemId>0</M_iItemId>
<M_iItemTypeId>1</M_iItemTypeId>
<M_sItemValue>French</M_sItemValue>
<M_iRaceId>2</M_iRaceId>
<M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
</root>
Is there any way to perform this keeping the foreign key relationship correct.
Perhaps the Merge statement is the wrong way to go but it seems like the best way to handle a lot of inserts/updates at once.
Maybe you could suggest an alternative method - the main criteria is performance as there could be thousands of items to "save" - I have tried to look at SqlBulkCopy but this doesn't seem to handle foreign key relationships very well either... I know I could save to one table at a time but then I lose the ROLLBACK functionality should one part of the "save" go wrong!
Any help/suggestions are greatly appreciated. Thanks in advance.
Try using following solution (it's not tested; I assumed that you can have many "Formula1" elements; you should carefully read my notes):
ALTER PROCEDURE [dbo].[spFormula1_Save]
#Formula1Xml xml--Formula1 as xml
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT, XACT_ABORT ON;
IF DATALENGTH(#Formula1Xml) = 0
RETURN 0
------------------------
--Xml shredding
------------------------
-- I prefer using the new XML methods (nodes, value, exist) instead of sp_xml_preparedocument + OPENXML
-- because you may get memory leaks if we don't use sp_xml_removedocument
DECLARE #Formula1_Table TABLE
(
M_iFormula1Id bigint,
Rnk bigint primary key, -- It's used to unique identify the old and the new rows
M_bDataInUse bit,
M_bActive bit
);
INSERT #Formula1_Table (M_iFormula1Id, Rnk, M_bDataInUse, M_bActive)
SELECT x.XmlCol.value('(M_iFormula1Id)[1]', 'BIGINT') AS M_iFormula1Id,
ROW_NUMBER() OVER(ORDER BY x.XmlCol) AS Rnk, -- It's used to unique identify the old and the new rows
x.XmlCol.value('(M_bDataInUse)[1]', 'BIT') AS M_bDataInUse,
x.XmlCol.value('(M_bActive)[1]', 'BIT') AS M_bActive
FROM #Formula1Xml.nodes('/root/Formula1') x(XmlCol);
DECLARE #Formula1_M_lstItem_Table TABLE
(
M_iFormula1Id bigint,
Rnk bigint, -- It's used to unique identify new "Formula1" rows (those rows having M_iFormula1Id=0)
M_iItemId bigint,
M_iItemTypeId bit,
M_sItemValue varchar(1000),
M_iRaceId int,
M_iDriverId int
);
INSERT #Formula1_M_lstItem_Table
(
M_iFormula1Id,
Rnk,
M_iItemId,
M_iItemTypeId,
M_sItemValue,
M_iRaceId,
M_iDriverId
)
SELECT /*x.XmlCol.value('(M_iFormula1Id)[1]', 'BIGINT')*/
-- At this moment we insert only nulls
NULL AS M_iFormula1Id,
DENSE_RANK() OVER(ORDER BY x.XmlCol) AS Rnk, -- It's used to unique identify new and old "Formula1" rows
y.XmlCol.value('(M_iItemId)[1]', 'BIGINT') AS M_iItemId,
y.XmlCol.value('(M_iItemTypeId)[1]', 'BIT') AS M_iItemTypeId,
y.XmlCol.value('(M_sItemValue)[1]', 'VARCHAR(1000)') AS M_sItemValue,
y.XmlCol.value('(M_iRaceId)[1]', 'INT') AS M_iRaceId,
y.XmlCol.value('(M_iDriverId)[1]', 'INT') AS M_iDriverId
FROM #Formula1Xml.nodes('/root/Formula1') x(XmlCol)
CROSS APPLY x.XmlCol.nodes('M_lstItem') y(XmlCol);
------------------------
--End of Xml shredding
------------------------
BEGIN TRANSACTION
BEGIN TRY
-------------------
--Formula1 Table
-------------------
DECLARE #Merged_Rows TABLE
(
Merge_Action nvarchar(10) not null,
Rnk bigint not null,
M_iFormula1Id bigint -- The old id's and the new inserted id's.
);
DECLARE #Formula1Id bigint = 0;
MERGE INTO Formula1 WITH(HOLDLOCK) AS tab -- To prevent race condition. http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
USING #Formula1_Table AS [xml]
ON (tab.Formula1Id = [xml].[M_iFormula1Id])
WHEN MATCHED THEN UPDATE SET tab.DataInUse = [xml].M_bDataInUse,
tab.Active = [xml].M_bActive
-- We no more need this line because of OUTPUT clause
-- #Formula1Id = [xml].M_iFormula1Id
WHEN NOT MATCHED THEN INSERT (DataInUse,
Active)
VALUES([xml].M_bDataInUse,
[xml].M_bActive
)
-- This OUTPUT clause will insert into #Merged_Rows the Rnk and the new M_iFormula1Id for every /root/Formula1 element
-- http://msdn.microsoft.com/en-us/library/ms177564.aspx
OUTPUT $action, [xml].Rnk, inserted.M_iFormula1Id INTO #Merged_Rows (Merge_Action, Rnk, M_iFormula1Id);
-- This is replaced by previous OUTPUT clause
/*
IF(#Formula1Id = 0)--then we haven''t updated so get inserted rowid
BEGIN
SET #Formula1Id = SCOPE_IDENTITY();--get the inserted identity
END
*/
-- At this moment we replace all previously inserted NULLs with the real (old and new) id's
UPDATE x
SET M_iFormula1Id = y.M_iFormula1Id
FROM #Formula1_M_lstItem_Table x
JOIN #Merged_Rows y ON x.Rnk = y.Rnk;
-------------------
--Formula1Item Table
-------------------
MERGE INTO Formula1Item AS tab
USING #Formula1_M_lstItem_Table AS [xml]
ON (tab.ItemId = [xml].M_iItemId)
-- Maybe you should need also this join predicate (tab.M_iFormula1Id = [xml].M_iFormula1Id)
WHEN MATCHED THEN UPDATE SET tab.ItemTypeId = [xml].M_iItemTypeId,
tab.ItemValue = [xml].M_sItemValue,
tab.RaceId = [xml].M_iRaceId,
tab.DriverId = [xml].M_iDriverId
WHEN NOT MATCHED THEN INSERT (Formula1Id,
ItemTypeId,
ItemValue,
RaceId,
DriverId)
VALUES([xml].M_iFormula1Id,
[xml].M_iItemTypeId,
[xml].M_sItemValue,
[xml].M_iRaceId,
[xml].M_iDriverId
);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
-- The caller should be informed when an error / exception is catched
-- THROW
END CATCH;
END

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 .