What is the correct way of using ##IDENTITY - sql-server

Question regarding the ##IDENTITY, I have 4 different tables:
Customer [Id]
Person [Id, fname, lname]
Account [Cd, owner, balance]
Transaction [Id, account, type]
Customer Id has a feature of identity increment 1.
My goal is to create a new person for the database, so that
Customer.Id = Person.Id = Account.owner = Transaction.ID
I have tried the following below, however I get this error:
Cannot insert null value into column owner
How do I correct the mistakes to make it work?
BEGIN TRAN
BEGIN TRY
INSERT INTO bank.customer DEFAULT VALUES
INSERT INTO bank.person (id, fname, lname)
VALUES (##IDENTITY, 'Mike', 'Phelps')
INSERT INTO bank.account (cd, owner, balance)
VALUES (2, ##IDENTITY, 0)
INSERT INTO bank.transaction (id, account, type)
VALUES (##IDENTITY, (SELECT cd FROM pankki.tili,'P')
END TRY
BEGIN CATCH
ROLLBACK
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_STATE() AS ErrorState,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage;
END CATCH

I suspect what you want is this:
BEGIN TRY
BEGIN TRAN;
DECLARE #ID int; --bigint, decimal?
INSERT INTO bank.customer DEFAULT VALUES;
SET #ID = SCOPE_IDENTITY(); --Assumes customer has a column with an IDENTITY
INSERT INTO bank.person (id,fname,lname)
VALUES(#ID,'Mike','Phelps');
INSERT INTO bank.account (cd,owner,balance)
VALUES(2,#ID,0);
INSERT INTO bank.transaction(id,account,type)
SELECT #ID,
cd,
'P'
FROM pankki.tili; --I assume, therefore, that pankki.tili only ever has 1 row
COMMIT; --YOu were missing this
END TRY
BEGIN CATCH
ROLLBACK;
THROW; --Don't SELECT the error details, THROW it.
END CATCH

From the Microsoft document:
After an INSERT, SELECT INTO, or bulk copy statement is completed, ##IDENTITY contains the last identity value that is generated by the statement. If the statement did not affect any tables with identity columns, ##IDENTITY returns NULL.
I take it PERSON does not have an identity column, so when you insert into it, ##identity becomes NULL.
If you want to user the ##identity from the insert for the other tables, use it to set the value of a variable.
declare #PersistentID int;
INSERT INTO bank.customer DEFAULT VALUES
set #PersistentID = ##IDENTITY -- or scope_identity() is safer
INSERT INTO bank.person (id,fname,lname)
VALUES( #PersistentID ,'Mike','Phelps')

First, you need to understand the difference between this two commands:
##identity returns the last inserted identity value in ANY table in the current session, regardless of scope.
IDENT_CURRENT('table_or_view') returns the last inserted identity value for a GIVEN table.
So, for your case, you need to use the second one.
And your script would be something like this:
BEGIN TRAN
BEGIN TRY
INSERT INTO bank.customer DEFAULT VALUES
SET #customerID = (SELECT IDENT_CURRENT('bank.customer'))
INSERT INTO bank.person (id,fname,lname)
VALUES( #customerID,'Mike','Phelps')
INSERT INTO bank.account (cd,owner,balance)
VALUES(2,#customerID,0)
INSERT INTO bank.transaction(id,account,type)
VALUES(#customerID,(SELECT cd FROM pankki.tili,'P')
END TRY
This way you can guarantee that the same ID in inserted in the four tables.
If you are using ##identity this value is changing with every new insert.

Related

AdventureWorks 2012 SQL Error when running a special procedure

I am creating a special procedure that applies a inputted discount to an inputted applied quantity but I keep receiving an error where the PK can not be null. Why is it not auto incrementing the row?
GO
CREATE PROCEDURE Sales.uspExcesInvSale
#DiscountPct smallmoney,
#ProductInventory int
AS
SET IDENTITY_INSERT Sales.SpecialOffer ON
INSERT INTO Sales.SpecialOffer (SpecialOfferID, Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty)
VALUES ((SELECT MAX(SpecialOfferID)+1 FROM SpecialOffer), 'New Sale', #DiscountPct, 'Excess Inventory', 'Direct', DATEADD(DAY,5,GETDATE()), DATEADD(DAY,7,GETDATE()), #ProductInventory);
INSERT INTO Sales.SpecialOfferProduct (ProductID)
SELECT ProductID
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)
SET IDENTITY_INSERT Sales.SpecialOffer OFF;
GO
BEGIN TRY
EXEC Sales.uspExcesInvSale .5, 1800;
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber
,ERROR_SEVERITY() AS ErrorSeverity
,ERROR_STATE() AS ErrorState
,ERROR_PROCEDURE() AS ErrorProcedure
,ERROR_LINE() AS ErrorLine
,ERROR_MESSAGE() AS ErrorMessage;
END CATCH;
GO
Error received is
Error Number 515 Severity 16 Error State 2 Error Procedure Sales.uspExcesInvSale Error Line 9 Error Message Cannot insert the value NULL into column 'SpecialOfferID', table 'AdventureWorks2012.Sales.SpecialOfferProduct'; column does not allow nulls. INSERT fails.
The error is in here - you're not inserting a value for for SpecialOfferID, which is required for this table.
INSERT INTO Sales.SpecialOfferProduct (ProductID) -- These should have
SELECT ProductID -- SpecialOfferID as well
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)
I believe the PKs for the two tables are
SpecialOffer: single column called SpecialOfferID, int, IDENTITY (e.g., is an auto-increment)
SpecialOfferProduct: two int columns, of which only one is an IDENTITY. The other is SpecialOfferID
When inserting into SpecialOfferProduct, you therefore need to explicitly specify the SpecialOfferID as it's not automatically entered. The other part of the PK (SalesOrderDetailID I believe) is an IDENTITY and you can just let it do its work.
In contrast - given that the PK of SpecialOffer is an IDENTITY column, I suggest letting it do its work. Remove the IDENTITY_INSERT SET statements, and then don't include a value into the insert statement. It will automatically create the next ID value.
I'm guessing you want to get the relevant SpecialOfferID from the first insert (e.g., from SpecialOffer), and then use that in the second insert (e.g., into SpecialOfferProduct).
You can get the 'last inserted ID' using SCOPE_IDENTITY().
With those changes, here's an example set of code (sadly untested as I don't have the db set up).
NOTE - I am also not 100% sure what you're doing with these inserts (particularly the second) - you will need to check that it's doing what you want. This answer is related to the error re not able to do the insert.
DECLARE #NewID int
INSERT INTO Sales.SpecialOffer (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty)
VALUES ('New Sale', #DiscountPct, 'Excess Inventory', 'Direct', DATEADD(DAY,5,GETDATE()), DATEADD(DAY,7,GETDATE()), #ProductInventory);
SET #NewID = SCOPE_IDENTITY()
INSERT INTO Sales.SpecialOfferProduct (SpecialOfferID, ProductID)
SELECT #NewID, ProductID
FROM Production.ProductInventory
GROUP BY ProductID
HAVING SUM(Quantity) > (#ProductInventory)

SQL trigger on update or delete

I have to have one single trigger that fires on either the UPDATE OR DELETE operations. I have the trigger working fine for when one certain column is updated. However, I need different logic for when a DELETE operation was fired. How would I have both logic inside of one trigger? Here is what I have so far:
ALTER TRIGGER [dbo].[Audit_Emp_Trigger]
ON [dbo].[EMPLOYEE]
AFTER UPDATE, DELETE
AS
BEGIN
--Only execute the trigger if the Dno field was updated or deleted
IF UPDATE(Dno)
BEGIN
--If the Audit_Emp_Record table does not exist already, we need to create it
IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL
BEGIN
--Table does not exist in database, so create table
CREATE TABLE Audit_Emp_Record
(
date_of_change smalldatetime,
old_Lname varchar (50),
new_Lname varchar (50),
old_ssn int,
new_ssn int,
old_dno int,
new_dno int
);
--Once table is created, insert the values of the update operation into the table
INSERT INTO Audit_Emp_Record(date_of_change, old_Lname, new_Lname, old_ssn, new_ssn, old_dno, new_dno) SELECT GETDATE(), D.Lname, I.Lname, D.Ssn, I.Ssn, D.Dno, I.Dno FROM inserted I JOIN deleted D ON I.Ssn = D.Ssn
END
ELSE
BEGIN
--The table already exists, so simply insert the new values of the update operation into the table
INSERT INTO Audit_Emp_Record(date_of_change, old_Lname, new_Lname, old_ssn, new_ssn, old_dno, new_dno) SELECT GETDATE(), D.Lname, I.Lname, D.Ssn, I.Ssn, D.Dno, I.Dno FROM inserted I JOIN deleted D ON I.Ssn = D.Ssn
END
END
END
You can test for the type of operation by seeing which of the magic-/pseudo-tables -- INSERTED and DELETED have data in them. I prefer to use something like the following:
DECLARE #Operation CHAR(1);
IF (EXISTS(SELECT * FROM inserted))
BEGIN
IF (EXISTS(SELECT * FROM deleted))
BEGIN
-- rows in both has to be an UPDATE
SET #Operation = 'U';
END;
ELSE
BEGIN
-- no rows in "deleted" has to be an INSERT
SET #Operation = 'I';
END;
END;
ELSE
BEGIN
-- no rows in "inserted" has to be a DELETE
SET #Operation = 'D';
END;
You can then use the #Operation variable in an IF statement to do one or the other of those operations.
Something like:
IF (#Operation = 'U')
BEGIN
--Only execute the trigger if the Dno field was updated or deleted
IF UPDATE(Dno)
BEGIN
{your current code here}
END;
END;
ELSE
BEGIN
{what to do if the operation is a DELETE goes here}
END;
Technically you don't need the ELSE condition that sets #Operation = 'I';, but if you are going to copy/paste this code into various triggers or keep around as a template then no harm in it handling all three conditions.
Also, just as a side-note, you don't need the ELSE condition of the IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL statement, nor the INSERT INTO Audit_Emp_Record that is just after the CREATE TABLE but before the END. Just do the CREATE TABLE if it doesn't exist and then do the INSERT outside of that test. Meaning:
IF UPDATE(Dno)
BEGIN
--If the Audit_Emp_Record table does not exist already, we need to create it
IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL
BEGIN
--Table does not exist in database, so create table
CREATE TABLE Audit_Emp_Record
...
END
INSERT INTO Audit_Emp_Record(...)
END

Create trigger for "before insert"

I want to make a trigger for one table that will be used before INSERT.
I want to check if two columns are NULL and if they are NULL raise an error otherwise INSERT the data.
So far I have this:
CREATE TRIGGER INS_TABLE_1
ON mytable
FOR INSERT
AS
BEGIN
IF (column1 IS NULL OR column2 IS NULL)
BEGIN
RAISERROR ('You are not allowed to Add These Data.', 10, 11)
END
ELSE
INSERT INTO mytable (column1,column2,column3)
END
Can you please help me?
Use instead trigger and inserted table like below and have a try.
CREATE TRIGGER INS_TABLE_1
ON mytable
INSTEAD OF INSERT
AS
BEGIN
DECLARE #fn varchar(50),#ln varchar(50)
SELECT #fn=column1 ,#ln=column12 from inserted
IF (#fn IS NULL OR #ln IS NULL)
BEGIN
RAISERROR ('You are not allowed to Add These Data.', 10, 11)
END
ELSE
INSERT INTO mytable (column1 ,column2) values (#fn,#ln)
END
The inserted table stores copies of the affected rows during INSERT and UPDATEstatements. Instead of trigger replaces the current INSERT by the trigger definition.

Trigger store information duplicate

I am using Trigger & this trigger is fire on the table when i am fill the information by UI Form. First time it is inserting the record proper in the table.
Again update it the information then it is store two records in the tables.
Mean Two Rows are inserted in the table against update. What I am doing wrong in Trigger.
My Trigger is given below:
ALTER TRIGGER [dbo].[trPkgDPRBidSubmissionDetails]
ON [dbo].[tblPkgDPRBidSubmissionDetails]
AFTER insert, update, delete
AS
BEGIN
DECLARE #Action as varchar(50);
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET #Action = 'Insert'; -- Set Action to Insert by default.
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #Action =
CASE
WHEN EXISTS(SELECT * FROM INSERTED) THEN 'UPDATE'
ELSE 'DELETE'
END
END
if( #Action = 'DELETE')
begin
insert into tblPkgDPRBidSubmissionDetailsAT(iUserId,cAction, dtAction,iActionOwner,iPackage,dtInitialDesignReport,dtDPR, dtBidDocuments,dtExternalReview1,dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2, dtApprovalFromExternalAgency,dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal, dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived)
select iUserId,#Action, getdate(), 1,iPackage,dtInitialDesignReport, dtDPR, dtBidDocuments,dtExternalReview1, dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2,dtApprovalFromExternalAgency, dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal,dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived
from deleted;
end
else
begin
insert into tblPkgDPRBidSubmissionDetailsAT(iUserId,cAction, dtAction,iActionOwner,iPackage,dtInitialDesignReport,dtDPR, dtBidDocuments,dtExternalReview1,dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2, dtApprovalFromExternalAgency,dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal, dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2, dtBidOpeningMeetingExtension3,iNumberOfBidsSold,iNumberOfBidsReceived)
select iUserId,#Action, getdate(), 1,iPackage,dtInitialDesignReport, dtDPR, dtBidDocuments,dtExternalReview1, dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2,dtApprovalFromExternalAgency, dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal,dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived
from inserted;
end
-- Insert statements for trigger here
-- action owner field to be udpated
END
The code inside your ELSE block handles both INSERT and UPDATE actions, so you end up with another row inserted into tblPkgDPRBidSubmissionDetailsAT table in both cases.
I think this is what you need:
if( #Action = 'DELETE')
begin
insert into tblPkgDPRBidSubmissionDetailsAT(iUserId,cAction, dtAction,iActionOwner,iPackage,dtInitialDesignReport,dtDPR, dtBidDocuments,dtExternalReview1,dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2, dtApprovalFromExternalAgency,dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal, dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived)
select iUserId,#Action, getdate(), 1,iPackage,dtInitialDesignReport, dtDPR, dtBidDocuments,dtExternalReview1, dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2,dtApprovalFromExternalAgency, dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal,dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived
from deleted;
end
else
begin
if #Action = 'INSERT'
insert into tblPkgDPRBidSubmissionDetailsAT(iUserId,cAction, dtAction,iActionOwner,iPackage,dtInitialDesignReport,dtDPR, dtBidDocuments,dtExternalReview1,dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2, dtApprovalFromExternalAgency,dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal, dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2, dtBidOpeningMeetingExtension3,iNumberOfBidsSold,iNumberOfBidsReceived)
select iUserId,#Action, getdate(), 1,iPackage,dtInitialDesignReport, dtDPR, dtBidDocuments,dtExternalReview1, dtComplianceToPWD1,dtComplianceToExtAgency1,dtExternalReview2,dtComplianceToPWD2,dtComplianceToExtAgency2,dtApprovalFromExternalAgency, dtIFB,dtUpToIssue,dtPreMeeting,dtBidOpeningMeetingOriginal,dtBidOpeningMeetingExtension1,dtBidOpeningMeetingExtension2,dtBidOpeningMeetingExtension3, iNumberOfBidsSold,iNumberOfBidsReceived
from inserted;
end

Select / Insert version of an Upsert: is there a design pattern for high concurrency?

I want to do the SELECT / INSERT version of an UPSERT. Below is a template of the existing code:
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE)
The query will be called from many concurrent sessions. My performance tests show that it will consistently throw primary key violations under a specific load.
Is there a high-concurrency method for this query that will allow it to maintain performance while still avoiding the insertion of data that already exists?
You can use LOCKs to make things SERIALIZABLE but this reduces concurrency. Why not try the common condition first ("mostly insert or mostly select") followed by safe handling of "remedial" action? That is, the "JFDI" pattern...
Mostly INSERTs expected (ball park 70-80%+):
Just try to insert. If it fails, the row has already been created. No need to worry about concurrency because the TRY/CATCH deals with duplicates for you.
BEGIN TRY
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE -- only error was a dupe insert so must already have a row to select
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
Mostly SELECTs:
Similar, but try to get data first. No data = INSERT needed. Again, if 2 concurrent calls try to INSERT because they both found the row missing the TRY/CATCH handles.
BEGIN TRY
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
IF ##ROWCOUNT = 0
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
The 2nd one appear to repeat itself, but it's highly concurrent. Locks would achieve the same but at the expense of concurrency...
Edit:
Why not to use MERGE...
If you use the OUTPUT clause it will only return what is updated. So you need a dummy UPDATE to generate the INSERTED table for the OUTPUT clause. If you have to do dummy updates with many calls (as implied by OP) that is a lot of log writes just to be able to use MERGE.
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
-- be sure to have a non-clustered unique index on RowValue and RowID as your clustered index.
IF EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
ELSE BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
As always, gbn's answer is correct and ultimately lead me to where I needed to be. However, I found a particular edge case that wasn't covered by his approach. That being a 2601 error which identifies a Unique Index Violation.
To compensate for this, I've modified his code as follow
...
declare #errornumber int = ERROR_NUMBER()
if #errornumber <> 2627 and #errornumber <> 2601
...
Hopefully this helps someone!

Resources