Update and Insert When Condition is Matched in TSQL-Merge - sql-server

I have been trying to Write a Stored Procedure where i can perform UpSert using Merge with the Following Condition
If Record is Present then change EndDate of Target to Yesterday's day i.e., Present Day - 1
If Record is not Present then Insert New Record
Here is the Table tblEmployee i used in SP
CREATE TABLE tblEmployee
(
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](10) NOT NULL,
[StartDate] [date] NOT NULL,
[EndDate] [date] NOT NULL
)
Here is my SP which Takes UDTT as Input parameter
CREATE PROCEDURE [dbo].[usp_UpsertEmployees]
#typeEmployee typeEmployee READONLY -- It has same column like tblEmployye except EmployeeID
AS
BEGIN
SET NOCOUNT ON;
MERGE INTO tblEmployee AS TARGET
USING #typeEmployee AS SOURCE
ON TARGET.Name = SOURCE.Name
WHEN MATCHED and TARGET.StartDate < SOURCE.StartDate
THEN
--First Update Existing Record EndDate to Previous Date as shown below
UPDATE
set TARGET.EndDate = DATEADD(day, -1, convert(date, SOURCE.StartDate))
-- Now Insert New Record
--INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate);
WHEN NOT MATCHED by TARGET
THEN
INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate);
SET NOCOUNT OFF;
END
How can i perform both Updating Existing Record and Adding New Record When Column is matched
Can Please someone Explain me the Execution Flow of Merge in TSQL i.e.,
WHEN MATCHED --Will this Execute Everytime
WHEN NOT MATCHED by TARGET -- Will this Execute Everytime
WHEN NOT MATCHED by SOURCE -- Will this Execute Everytime
Will all above 3 condition get executed everytime in Merge or only Matching condition is executed Everytime
Thanks in Advance

This isn't what MERGE is meant to do (update and insert in same clause). To accomplish this, you can use the OUTPUT clause to get all the updated records only. The MERGE/OUTPUT combo is very picky. Your OUTPUT updates are really the TARGET records that got updated, so you have to start the TARGET records in a temp/table variable. Then you match those back against the SOURCE to do the INSERT. You won't be allowed to join the output results directly back to source or even use as a correlated subquery within the WHERE.
Setup some sample data
The code below just sets up some sample data.
-- Setup sample data
DECLARE #typeEmployee TABLE (
[Name] [varchar](10) NOT NULL,
[StartDate] [date] NOT NULL,
[EndDate] [date] NOT NULL
)
DECLARE #tblEmployee TABLE (
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](10) NOT NULL,
[StartDate] [date] NOT NULL,
[EndDate] [date] NOT NULL
)
INSERT #tblEmployee VALUES ('Emp A', '1/1/2016', '2/1/2016')
INSERT #typeEmployee VALUES ('Emp A', '1/5/2016', '2/2/2016'), ('Emp B', '3/1/2016', '4/1/2016')
Updates to Stored Procedure
You can use OUTPUT at the end of a MERGE to have it return the modified records of the target records, and by including $action, you will also get whether it was an insert, update, or delete.
However, the result set from MERGE / OUTPUT cannot be directly joined against the SOURCE table so you can do your INSERT since you only get the TARGET records back. You can't use the results of the OUTPUT within correlated sub-query from the SOURCE table either. Easiest thing is to use a temp table or table variable to capture the output.
-- Logic to do upsert
DECLARE #Updates TABLE (
[Name] [varchar](10) NOT NULL,
[StartDate] [date] NOT NULL,
[EndDate] [date] NOT NULL
)
INSERT #Updates
SELECT
Name,
StartDate,
EndDate
FROM (
MERGE INTO #tblEmployee AS TARGET
USING #typeEmployee AS SOURCE
ON TARGET.Name = SOURCE.Name
WHEN MATCHED AND TARGET.StartDate < SOURCE.StartDate
THEN
--First Update Existing Record EndDate to Previous Date as shown below
UPDATE SET
EndDate = DATEADD(DAY, -1, CONVERT(DATE, SOURCE.StartDate))
WHEN NOT MATCHED BY TARGET -- OR MATCHED AND TARGET.StartDate >= SOURCE.StartDate -- Handle this case?
THEN
INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate)
OUTPUT $action, INSERTED.Name, INSERTED.StartDate, INSERTED.EndDate
-- Use the MERGE to return all changed records of target table
) AllChanges (ActionType, Name, StartDate, EndDate)
WHERE AllChanges.ActionType = 'UPDATE' -- Only get records that were updated
Now that you've captured the output of the MERGE and filtered to only get updated TARGET records, you can then do your outstanding INSERT by filtering only the SOURCE records that were part of the MERGE update.
INSERT #tblEmployee
SELECT
SOURCE.Name,
SOURCE.StartDate,
SOURCE.EndDate
FROM #typeEmployee SOURCE
WHERE EXISTS (
SELECT *
FROM #Updates Updates
WHERE Updates.Name = SOURCE.Name
-- Other join conditions to ensure 1:1 match against SOURCE (start date?)
)
Ouput
This is the output of the sample records after the change. Your intended TARGET changes were made.
-- Show output
SELECT * FROM #tblEmployee

Following the idea from the accepted answer, this works as well in Sql server 2008 r2:
create table Test1 (
Id int, FromDate date, ThruDate date, Value int
)
insert into dbo.Test1
(Id, FromDate, ThruDate, [Value])
select
t.Id, t.FromDate, T.ThruDate, t.Value * 100
from (
MERGE dbo.Test1 AS Target
USING (
select 1 as Id, '2000-01-01' as FromDate, '2000-12-31' as ThruDate, 2 as Value
) AS Source
ON ( target.id = source.Id
)
WHEN MATCHED
THEN
UPDATE SET Target.[Id] = Source.[Id]
, Target.[FromDate] = Source.[FromDate]
, Target.[ThruDate] = Source.[ThruDate]
, Target.[Value] = Source.[Value]
WHEN NOT MATCHED BY TARGET
THEN
INSERT ([Id]
, [FromDate]
, [ThruDate]
, [Value])
VALUES (Source.[Id]
, Source.[FromDate]
, Source.[ThruDate]
, Source.[Value])
OUTPUT $ACTION as Act, Inserted.*
) t
where t.Act = 'Update'
You can play with different values.

Related

T-SQL : merge insert old and new value then make changes

It's my first time working with T-SQL Merge and I have been trying to write a Stored Procedure using with the following condition:
If record is present then insert into Changes table the Id, old value (before update) and the new value and after that update the old value from Affected
if record is not present then insert into Changes the Id, empty for old and the new value and after that insert new record in Affected
if record didn't match then insert into Changes the Id, old value (value before deletion) and empty as new value and after that delete the record
Here are the tables I used
CREATE TABLE Affected
(
[Id] int IDENTITY(1,1) NOT NULL,
[ZoneId] int NOT NULL,
[Name] varchar(100) NOT NULL,
[Value] varchar(100)
)
CREATE TABLE Changes
(
[AffectedId] int NOT NULL,
[OldValue] varchar(100),
[NewValue] varchar(100)
)
And here is my stored procedure which takes two zone ids as input parameters
CREATE PROCEDURE spAffectChanges
#ZoneId1 int,
#ZoneId2 int
AS
IF(#ZoneId1 < 10)
BEGIN
;WITH fromQ AS
(
SELECT DISTINCT Name, Value
FROM Affected
WHERE ZoneId = #ZoneId1 AND Name NOT IN ('aaa', 'bbb', 'ccc')
), toQ AS
(
SELECT DISTINCT Name, Value
FROM Affected
WHERE ZoneId = #ZoneId2 AND Name NOT IN ('aaa', 'bbb', 'ccc')
)
MERGE toQ
USING fromQ ON (toQ.Name = fromQ.Name)
WHEN MATCHED AND fromQ.Value<>toQ.Value THEN
----first insert
INSERT INTO Changes (AffectedId, OldValue, NewValue)
VALUES(toQ.Id, toQ.Value, fromQ.Value)
----then update
UPDATE SET toQ.Value=fromQ.Value
WHEN NOT MATCHED BY toQ THEN
----first insert
INSERT INTO Changes (AffectedId, OldValue, NewValue)
VALUES(toQ.Id, '', fromQ.Value)
----second insert
INSERT (Name, Zone, Value)
VALUES (fromQ.Name, #ZoneId1, fromQ.Value)
WHEN NOT MATCHED BY fromQ THEN
----first insert
INSERT INTO Changes (AffectedId, OldValue, NewValue)
VALUES(toQ.Id, toQ.Value, '')
----then delete
DELETE
END
Is there a way to perform both insert to Changes and Update/Insert/Delete for Affected?
Thanks in advance
You can use the OUTPUT clause to do this.
Notes:
MERGE requires a semi-colon terminator. ;WITH is silly, ; is not a beginning-ator, it is meant to be placed at the end of every command
The options for WHEN NOT MATCHED are BY SOURCE and BY TARGET (this is the default)
I don't quite understand how you have a DISTINCT on the target, I don't believe it is allowed, nor how it makes sense to do so.
WITH fromQ AS
(
SELECT DISTINCT Name, Value
FROM Affected
WHERE ZoneId = #ZoneId1 AND Name NOT IN ('aaa', 'bbb', 'ccc')
), toQ AS
(
SELECT Name, Value
FROM Affected
WHERE ZoneId = #ZoneId2 AND Name NOT IN ('aaa', 'bbb', 'ccc')
)
MERGE toQ
USING fromQ ON (toQ.Name = fromQ.Name)
WHEN MATCHED AND fromQ.Value <> toQ.Value THEN
UPDATE
SET toQ.Value = fromQ.Value
WHEN NOT MATCHED BY TARGET THEN
INSERT (Name, Zone, Value)
VALUES (fromQ.Name, #ZoneId1, fromQ.Value)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT inserted.Id, ISNULL(deleted.Value, ''), ISNULL(inserted.Value, '')
INTO Changes (AffectedId, OldValue, NewValue)
;

How to get a column derived from joining on inserted value?

For the following example I set shipping method to 'UPS'
CREATE TABLE [dbo].[Customer] (CustomerID int primary key, ShipMethodRef INT)
INSERT INTO [dbo].[Customer] VALUES (5497, 20);
CREATE TABLE [dbo].ShipMethod(ShipMethodID int PRIMARY KEY, Name varchar(10));
INSERT INTO [dbo].ShipMethod VALUES (20, 'Fedex'), (21, 'UPS')
UPDATE [dbo].[Customer]
set ShipMethodRef = CASE WHEN EXISTS (SELECT ShipMethodID from [dbo].[ShipMethod]
WHERE [dbo].[ShipMethod].Name = 'UPS')
THEN (SELECT ShipMethodID from [dbo].[ShipMethod]
WHERE [dbo].[ShipMethod].Name = 'UPS')
ELSE curTable.ShipMethodRef END
OUTPUT ShipMethod.Name as ShipMethodName
FROM [dbo].[Customer] curTable
JOIN [dbo].ShipMethod ShipMethod ON curTable.ShipMethodRef = ShipMethod.ShipMethodID
WHERE CustomerID=5497;
The OUTPUT clause returns Fedex - How can I change it to reflect the post insert state that the customer's shipping method is 'UPS' (as their shipping method Id is now 21)?
I don't think this can be done with a single statement except in the way Martin showed in his comment, but you can get the output from inserted into a table variable or a temporary table and then select from that joined to the translation tables.
Here's how I would do that (note the update statement is simplified):
DECLARE #UpdatedIds AS TABLE (ShipMethodID int);
UPDATE [dbo].[Customer]
SET ShipMethodRef = COALESCE((
SELECT ShipMethodID
FROM [dbo].[ShipMethod]
WHERE [dbo].[ShipMethod].Name = 'UPS'
), ShipMethodRef)
OUTPUT inserted.ShipMethodRef INTO #UpdatedIds
FROM [dbo].[Customer]
WHERE CustomerID=5497;
SELECT SM.ShipMethodID, SM.Name
FROM [dbo].ShipMethod AS SM
JOIN #UpdatedIds AS Updated
ON SM.ShipMethodID = Updated.ShipMethodID

Update, if not exists Insert using XML input parameter in SQL Server

CREATE TABLE [dbo].[TelecommunicationsNumber]
(
[ID] [int] NOT NULL,
[ContactTypeID] [int] NOT NULL,
[CountryID] [int] NOT NULL
)
Here is my sample XML input to the above mentioned table.
DECLARE #TelecommunicationsNumberList XML = '<TelecommunicationsNumber><ContactTypeID>2</ContactTypeID><CountryID>1</CountryID></TelecommunicationsNumber><TelecommunicationsNumber><ContactTypeID>4</ContactTypeID><CountryID>1</CountryID></TelecommunicationsNumber>'
I figured out the UPDATE SQL query as below.
UPDATE TelecommunicationsNumber
SET ContactTypeID = n.ContactTypeID,
CountryID = n.CountryID
FROM (SELECT
T.C.value('(ContactTypeID)[1]', 'INT') AS ContactTypeID,
T.C.value('(CountryID)[1]', 'INT') AS CountryID
FROM
#TelecommunicationsNumberList.nodes('/TelecommunicationsNumber') AS T (C)) AS n
WHERE
TelecommunicationsNumber.ContactTypeID = n.ContactTypeID
How can I insert a new record if the input XML and the TelecommunicationsNumber table does exists the same ContactTypeID and update if exists.
In order to do that first I have to fetch the rows in order to check weather the same ContactTypeID exists or not.
QUESTION: I am unable to figure out the SELECT query. How can I integrate both the insert and update queries by writing the SELECT query.
I use the below query to INSERT the records.
INSERT INTO TelecommunicationsNumber (ContactTypeID,CountryID)
SELECT
Entries.value('(ContactTypeID)[1]', 'INT') AS 'ContactTypeID',
Entries.value('(CountryID)[1]', 'nvarchar(256)') AS 'CountryID'
FROM
#TelecommunicationsNumberList.nodes('/TelecommunicationsNumber') AS TelecommunicationsNumberEntries (Entries)
I managed to resolve the issue using MERGE command.
;
WITH TelecommunicationsNumber
AS (SELECT
ParamValues.x1.value('ContactTypeID[1]', 'int') AS ContactTypeID,
ParamValues.x1.value('CountryID[1]', 'int') AS CountryID
FROM #TelecommunicationsNumberList.nodes('/TelecommunicationsNumber') AS ParamValues (x1))
MERGE INTO dbo.TelecommunicationsNumber AS old
USING TelecommunicationsNumber AS new
ON (new.ContactTypeID = old.ContactTypeID)
WHEN MATCHED THEN UPDATE SET
old.CountryID = new.CountryID
WHEN NOT MATCHED THEN
INSERT (ContactTypeID, CountryID)
VALUES (new.ContactTypeID, new.CountryID);

How to make a Trigger on Master-Detail

I Have the following scenario:
CREATE TABLE dbo.Orders
(
OrderID int IDENTITY (1,1) NOT NULL
, OrderVersion int DEFAULT(1)
, Customer varchar(30)
, ScheduleDate date
, PaymentOption int
);
CREATE TABLE dbo.OrdersItems
(
OrderItemsID int IDENTITY (1,1) NOT NULL
, OrderID int
, Product varchar(100)
, Qty int
, value decimal(18,2)
);
CREATE TABLE dbo.logOrders
(
OrderID int NOT NULL
, OrderVersion int DEFAULT(1)
, Customer varchar(30)
, ScheduleDate date
, PaymentOption int
);
CREATE TABLE dbo.logOrdersItems
(
OrderItemsID int NOT NULL
, OrderID int
, Product varchar(100)
, Qty int
, value decimal(18,2)
);
-- Insert values into the table.
INSERT INTO dbo.Orders (Customer , ScheduleDate, PaymentOption)
VALUES ('John', 2016-09-01, 1);
INSERT INTO dbo.OrdersItems( OrderId, Product, Qty, Value)
VALUES (1, 'Foo', 20, 35.658),
(1, 'Bla', 50, 100)
(1, 'XYZ', 10, 3589)
First Statement
UPDATE Orders set ScheduleDate = 2016-10-05 WHERE OrderId = 1
Second Statement
Delete From OrdersItems WHERE OrderItemsID = 2
UPDATE OrdersItems set Qty = 5 WHERE OrderItemsID = 1
Third Statement
Update Orders set PaymentOption = 2 WHERE OrderId = 1
Update OrdersItems set Value = 1050 WHERE OrderItemsID = 3
I am trying to figure out how to make a trigger that after each one of the Statements Sample above Insert on the log Tables the data before the changing. And setting the OrderVersion to OrderVersion + 1 on table Orders.
So on the log Tables I will have all versions after the later one.
Is it possible to make a single trigger to monitor both tables and execute getting the original data before the UPDATE, DELETE , INSERT statement to get the original data and INSERT on the logTables ?
Here comes a sample to explain better what result I want.
This is the Initial Data on table Orders and OrdersItems
If I make an Update on Orders ( any column ) or Make an Update,Insert,Delete on OrdersItems I need to Insert on respectively logTables the data on the image.
And with this I'll have on logOrders and logItems the original data and on the Orders and Items the altered data.
I Hope I could explain better what I mean.
You will need two triggers. The trigger for the Orders table handles Orders table update/delete. The trigger for the OrdersItems table does the same for OrdersItems. The triggers look like this:
For the Orders table:
CREATE TRIGGER dbo.Orders_trigger
ON dbo.Orders
AFTER DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.logOrders
SELECT * FROM DELETED;
INSERT INTO dbo.logOrdersItems
SELECT oi.* FROM OrdersItems oi
WHERE oi.OrderID IN (SELECT OrderId FROM DELETED);
END
GO
For OrdersItems:
CREATE TRIGGER dbo.OrdersItems_trigger
ON dbo.OrdersItems
AFTER DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Inerst the changed/deleted OrdersItems into the log
INSERT INTO dbo.logOrdersItems
SELECT * FROM DELETED
--Inserts the unchanged sibling OrdersItems records into the log
INSERT INTO dbo.logOrdersItems
SELECT oi.* FROM OrdersItems oi
WHERE oi.OrderId IN (SELECT DISTINCT OrderId FROM DELETED)
AND oi.OrderItemsID NOT IN (SELECT DISTINCT OrderItemsID FROM DELETED);
INSERT INTO dbo.logOrders
SELECT o.* FROM Orders o
WHERE o.OrderID IN (SELECT DISTINCT OrderId FROM DELETED);
END
GO
The Orders Trigger is fairly straightforward. Use the virtual DELETED table to insert the original version of the records into the log. Then join to the child OrdersItems records and insert them into the log as well. The way this is written, it will work even if you update or delete multiple Order records at a time.
The OrdersItems Trigger is a bit more complicated. You need to log the pre-chage version of the OrdersItems and Orders Records. But you also want (I think) to log the unchanged "sibling" OrdersItems records as well so that you have a complete picture of the records.
I know this is just your sample data, but you will want to add some kind of a timestamp to the records in the log tables. Otherwise you just end up with a bunch of duplicate rows and you cannot tell which is which. At the beginning of the trigger you can create a variable to hold the update datetime and then append that to your INSERT statement for the logs.
DECLARE #UpdateDateTime DATETIME;
SET #UpdateDateTime = GETUTCDATE();

Insert from single table into multiple tables, invalid column name error

I am trying to do the following but getting an "Invalid Column Name {column}" error. Can someone please help me see the error of my ways? We recently split a transaction table into 2 tables, one containing the often updated report column names and the other containing the unchanging transactions. This leave me trying to change what was a simple insert into 1 table to a complex insert into 2 tables with unique columns. I attempted to do that like so:
INSERT INTO dbo.ReportColumns
(
FullName
,Type
,Classification
)
OUTPUT INSERTED.Date, INSERTED.Amount, INSERTED.Id INTO dbo.Transactions
SELECT
[Date]
,Amount
,FullName
,Type
,Classification
FROM {multiple tables}
The "INSERTED.Date, INSERTED.Amount" are the source of the errors, with or without the "INSERTED." in front.
-----------------UPDATE------------------
Aaron was correct and it was impossible to manage with an insert but I was able to vastly improve the functionality of the insert and add some other business rules with the Merge functionality. My final solution resembles the following:
DECLARE #TransactionsTemp TABLE
(
[Date] DATE NOT NULL,
Amount MONEY NOT NULL,
ReportColumnsId INT NOT NULL
)
MERGE INTO dbo.ReportColumns AS Trgt
USING ( SELECT
{FK}
,[Date]
,Amount
,FullName
,Type
,Classification
FROM {multiple tables}) AS Src
ON Src.{FK} = Trgt.{FK}
WHEN MATCHED THEN
UPDATE SET
Trgt.FullName = Src.FullName,
Trgt.Type= Src.Type,
Trgt.Classification = Src.Classification
WHEN NOT MATCHED BY TARGET THEN
INSERT
(
FullName,
Type,
Classification
)
VALUES
(
Src.FullName,
Src.Type,
Src.Classification
)
OUTPUT Src.[Date], Src.Amount, INSERTED.Id INTO #TransactionsTemp;
MERGE INTO dbo.FinancialReport AS Trgt
USING (SELECT
[Date] ,
Amount ,
ReportColumnsId
FROM #TransactionsTemp) AS Src
ON Src.[Date] = Trgt.[Date] AND Src.ReportColumnsId = Trgt.ReportColumnsId
WHEN NOT MATCHED BY TARGET And Src.Amount <> 0 THEN
INSERT
(
[Date],
Amount,
ReportColumnsId
)
VALUES
(
Src.[Date],
Src.Amount,
Src.ReportColumnsId
)
WHEN MATCHED And Src.Amount <> 0 THEN
UPDATE SET Trgt.Amount = Src.Amount
WHEN MATCHED And Src.Amount = 0 THEN
DELETE;
Hope that helps someone else in the future. :)
Output clause will return values you are inserting into a table, you need multiple inserts, you can try something like following
declare #staging table (datecolumn date, amount decimal(18,2),
fullname varchar(50), type varchar(10),
Classification varchar(255));
INSERT INTO #staging
SELECT
[Date]
,Amount
,FullName
,Type
,Classification
FROM {multiple tables}
Declare #temp table (id int, fullname varchar(50), type varchar(10));
INSERT INTO dbo.ReportColumns
(
FullName
,Type
,Classification
)
OUTPUT INSERTED.id, INSERTED.fullname, INSERTED.type INTO #temp
SELECT
FullName
,Type
,Classification
FROM #stage
INSERT into dbo.transacrions (id, date, amount)
select t.id, s.datecolumn, s.amount from #temp t
inner join #stage s on t.fullname = s.fullname and t.type = s.type
I am fairly certain you will need to have two inserts (or create a view and use an instead of insert trigger). You can only use the OUTPUT clause to send variables or actual inserted values ti another table. You can't use it to split up a select into two destination tables during an insert.
If you provide more information (like how the table has been split up and how the rows are related) we can probably provide a more specific answer.

Resources