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

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)
;

Related

SQL Server trigger only on changes to update a calculated column

For a time scheduling project, I have two tables: tbl_timeslots which holds available times with slotid as the primary key and totalmembers which counts the number of appointments made for this slot, and tbl_appointments with primary key apptid which holds the actual appointments, with slotid as a foreign key linking to the slot information.
I need to automatically update the totalmembers column any time an appointment is created/deleted/changed. The trigger I wrote (shown below) does not update the correct number of appointments in the tbl_timeslots column totalmembers.
CREATE TABLE tbl_timeslots
(
slotid int ,
fromdate datetime ,
todate datetime ,
totalmembers int
)
INSERT tbl_timeslots (slotid, fromdate, todate, totalmembers)
VALUES (1, '2016-01-01 10:00:00', '2016-01-01 11:00:00', 0)
INSERT tbl_timeslots (slotid, fromdate, todate, totalmembers)
VALUES (2, '2016-01-01 11:00:00', '2016-01-01 12:00:00', 0)
CREATE TABLE tbl_appointments
(
apptid int ,
slotid int ,
firstname varchar(10) ,
lastname varchar(10)
)
INSERT tbl_appointments (apptid, slotid, firstname, lastname)
VALUES (1, 1, 'Mark', 'Twain')
INSERT tbl_appointments (apptid, slotid, firstname, lastname)
VALUES (2, 1, 'Thomas', 'Jefferson')
INSERT tbl_appointments (apptid, slotid, firstname, lastname)
VALUES (3, 2, 'Donald', 'Duck')
CREATE TRIGGER [dbo].[tr_totalmembers]
ON [dbo].[TBL_appointments]
AFTER UPDATE, INSERT, DELETE
AS
BEGIN
UPDATE tbl_timeslots
SET totalmembers = (SELECT COUNT(1)
FROM tbl_appointments a
WHERE tbl_timeslots.slotid = a.slotid)
FROM inserted i
INNER JOIN deleted d ON i.apptid = d.apptid
WHERE
d.slotid <> i.slotid
AND (tbl_timeslots.slotid = i.slotid OR tbl_timeslots.slotid = d.slotid)
END
I just modified the trigger a bit and it worked for me :
CREATE TRIGGER [dbo].[tr_totalmembers]
ON [dbo].[TBL_appointments]
AFTER UPDATE, INSERT, DELETE
AS
BEGIN
UPDATE tbl_timeslots
SET totalmembers = a.cnt
from
(
SELECT slotid,COUNT(1) as cnt
FROM tbl_appointments
group by slotid
) a
WHERE
tbl_timeslots.slotid=a.slotid
END
Or in case if there is a specific problem please mention so that we could look into it .
A better way to compute totalmembers is to create and use an indexed view:
CREATE VIEW dbo.vw_timeslots_with_totalmembers
WITH SCHEMA_BINDING
AS
SELECT a.slotid, COUNT_BIG(*) AS totalmembers
FROM dbo.tbl_appointments a
GROUP BY a.slotid
GO
CREATE UNIQUE CLUSTERED INDEX IUC_vw_timeslots_with_totalmembers_slotid
ON dbo.vw_timeslots_with_totalmembers (slotid)
GO
DECLARE #slotid INT = 123
SELECT totalmembers
FROM dbo.vw_timeslots_with_totalmembers WITH(NOEXPAND) -- This table hint is needed in order to force usage of indexed view
WHERE slotid = #slotid
GO
Note: please read following notes regarding the proper configuration of SETtings (see section Required SET Options for Indexed Views): https://msdn.microsoft.com/en-us/library/ms191432.aspx .
Note #2: if last SELECT statement returns 0 rows this means that current slot doesn't have appointments (I assume that current slot is valid).

Multiple Row Param String to Single Stored Procedure

I have a stored procedure that mimics the MYSQL 'UPSERT' command. ie. insert if new / update existing if record exists.
I wish to keep the number of calls to SQL Server to an absolute minimum ie. 1
So can I pass an param string to a stored procedure (SP_MAIN) and in this stored procedure then call my 'UPSERT' stored procedure for every unique table row that is passed as a param to SP_MAIN...?
If so, can anyone illustrate with a simple example please..?
Thank you in advance.
You can use the merge statements. See a sample below: The table to be updated is dbo.Table. We use Table Valued Parameter to update/insert the data. The merge statement is within a stored procedure
CREATE TABLE dbo.[Table]
(
PrimaryKey INT IDENTITY (1, 1) NOT NULL
,Column1 INT NOT NULL
,Column2 INT NOT NULL
)
GO
CREATE TYPE dbo.[TableTVP] AS TABLE (
PrimaryKey INT NULL
,Column1 INT NULL
,Column2 INT NULL
)
GO
CREATE PROCEDURE dbo.CRUD_Table
#TableTVP dbo.TableTVP READONLY
AS
SET NOCOUNT ON
DECLARE #OutPut TABLE (Action VARCHAR(10) NULL,EntityKey INT NULL)
MERGE dbo.[Table] AS TARGET
USING (SELECT
PrimaryKey
,Column1
,Column2
,BINARY_CHECKSUM (Column1, Column2) as DataCheckSum
FROM
#TableTVP) AS SOURCE ON SOURCE.PrimaryKey = TARGET.PrimaryKey
WHEN MATCHED AND SOURCE.DataCheckSum <> BINARY_CHECKSUM (TARGET.Column1, TARGET.Column2) THEN
UPDATE SET
Column1 = SOURCE.Column1
,Column2 = SOURCE.Column2
WHEN NOT MATCHED THEN
INSERT (
Column1
,Column2
)
VALUES (
SOURCE.Column1
,SOURCE.Column2
)
OUTPUT $action as [Action]
,CASE WHEN $action IN ('INSERT', 'UPDATE') THEN Inserted.PrimaryKey ELSE Deleted.PrimaryKey END as [EntityKey] INTO #OutPut;
SELECT Action,EntityKey FROM #OutPut
GO

Update and Insert When Condition is Matched in TSQL-Merge

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.

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.

SQL Server Merge statement

I am doing merge statement in my stored procedure. I need to count the rows during updates and inserts. If i use a common variable to get the updated rows (for both update and insert) how i can differ, this is the count which i got from update and this is the count which i got from insert. Please give me a better way
You can create a table variable to hold the action type then OUTPUT the pseudo $action column to it.
Example
/*Table to use as Merge Target*/
DECLARE #A TABLE (
[id] [int] NOT NULL PRIMARY KEY CLUSTERED,
[C] [varchar](200) NOT NULL)
/*Insert some initial data to be updated*/
INSERT INTO #A
SELECT 1, 'A' UNION ALL SELECT 2, 'B'
/*Table to hold actions*/
DECLARE #Actions TABLE(act CHAR(6))
/*Do the Merge*/
MERGE #A AS target
USING (VALUES (1, '#a'),( 2, '#b'),(3, 'C'),(4, 'D'),(5, 'E')) AS source (id, C)
ON (target.id = source.id)
WHEN MATCHED THEN
UPDATE SET C = source.C
WHEN NOT MATCHED THEN
INSERT (id, C)
VALUES (source.id, source.C)
OUTPUT $action INTO #Actions;
/*Check the result*/
SELECT act, COUNT(*) AS Cnt
FROM #Actions
GROUP BY act
Returns
act Cnt
------ -----------
INSERT 3
UPDATE 2

Resources