I came across an merge statement in the Database at work today and looked into it.
I learned that it is an easy way to check if an entry is in the target or the source table or if it is in both.
And if I use when matched and ... I can check it on different columns of the tables.
But if I now have multiple of these conditions do they then all triger together?
For example if I merge two Person tables how would the following code behave?
MERGE SourcePersons AS sp
USING TargetPerson AS tp
ON (sp.Id = tp.Id)
WHEN NOT MATCHED BY TARGET
THEN INSERT (Id, Firstname, Lastname) VALUES (sp.Id, sp.Firstname, sp.Lastname)
WHEN MATCHED AND (sp.Firstname <> tp.Firstname)
THEN UPDATE SET Firstname = sp.Firstname
WHEN MATCHED AND (sp.Lastname <> tp.Lastname)
THEN UPDATE SET Lastname = sp.Lastname
WHEN NOT MATCHED BY SOURCE
/*No idea what to write here*/
;
In the above I have two updates for the firstname and lastname respectively, if now a person changes both names do both of them trigger or does only firstname trigger as that is the first one?
You can combine both Firstname and Lastname update when Matched and ignore NOT MATCHED BY SOURCE
MERGE SourcePersons AS sp
USING TargetPerson AS tp ON (sp.Id = tp.Id)
WHEN NOT MATCHED BY TARGET
THEN INSERT (Id, Firstname, Lastname) VALUES (sp.Id, sp.Firstname, sp.Lastname)
WHEN MATCHED AND ((sp.Firstname <> tp.Firstname) OR (sp.Lastname <> tp.Lastname))
THEN UPDATE SET Firstname = sp.Firstname, Lastname = sp.Lastname ;
OR
You can have both the WHEN MATCHED clauses. According to Document, we can have two WHEN MATCHED clauses MERGE
MERGE SourcePersons AS sp
USING TargetPerson AS tp ON (sp.Id = tp.Id)
WHEN NOT MATCHED BY TARGET
THEN INSERT (Id, Firstname, Lastname) VALUES (sp.Id, sp.Firstname, sp.Lastname)
WHEN MATCHED AND (sp.Firstname <> tp.Firstname)
THEN UPDATE SET Firstname = sp.Firstname
WHEN MATCHED AND (sp.Lastname <> tp.Lastname)
THEN UPDATE SET Lastname = sp.Lastname ;
Related
I have an excel file with thousand of rows which I need to use to delete/update/insert some tables.
The excel provides the following data: provider_id, country_name, locale, property1, property2.
The tables which need to be updated are:
provider_country with columns : provider_country_id, provider_id, country_id, property1, property2 and
provider_country_language with columns : provider_country_language_id, provider_country_id, language_id.
I can also use table country with columns (for joins): country_id, country_name.
And table language with columns: language_id, locale, country_id.
The fields which need to be updated are country_id, language_id, property1,property2 (from provider_country and provider_country_language)
I have created a temporary table with all the data from the excel:
CREATE TABLE #TempProviderCountryLanguage(
[provider_id] int NULL,
[country_name] nvarchar(50) NULL,
[locale] nvarchar(10) NULL,
[property1] int NULL,
[property2] decimal(5,2) NULL
)
INSERT INTO #TempProviderCountryLanguage VALUES
(1,N'Provider1',N'Brazil',N'en-br',4,NULL)
INSERT INTO #TempProviderCountryLanguage VALUES
(1,N'Provider1',N'Brazil',N'pt-br',4,NULL)
INSERT INTO #TempProviderCountryLanguage VALUES
(1,N'Provider1',N'Denmark',N'da-dk',4,12.21)
INSERT INTO #TempProviderCountryLanguage VALUES
(2,N'Provider2',N'Denmark',N'da-dk',5,14.21)
......
MERGE [provider_country] AS TARGET
USING (
SELECT tb.provider_id
,c.country_id
,l.language_id
,tb.property1
,tb.property2
FROM #TempProviderCountryLanguage tb
INNER JOIN country c ON c.country_name = tb.country_name
INNER JOIN language l ON l.locale = l.locale
) AS SOURCE
ON (
TARGET.provider_id = SOURCE.provider_id AND
TARGET.country_id = SOURCE.country_id
)
WHEN MATCHED
THEN
UPDATE
SET TARGET.country_id = SOURCE.country_id,
TARGET.property1 = SOURCE.property1,
TARGET.property2 = SOURCE.property2
WHEN NOT MATCHED BY TARGET
THEN
INSERT (
provider_id
,country_id
,property1
,property2
)
VALUES (
SOURCE.provider_id
,SOURCE.country_id
,SOURCE.property1
,SOURCE.property2
)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
For the provider_country_language I plan to make another merge.
I am trying to update the tables using merge but I have a problem because I cannot make a unique select here (somehow I would need the language_id as well):
ON (
TARGET.provider_id = SOURCE.provider_id AND
TARGET.country_id = SOURCE.country_id
)
And the error is :
The MERGE statement attempted to UPDATE or DELETE the same row more
than once. This happens when a target row matches more than one source
row. A MERGE statement cannot UPDATE/DELETE the same row of the target
table multiple times. Refine the ON clause to ensure a target row
matches at most one source row, or use the GROUP BY clause to group
the source rows.
How can I make this work and make sure all the tables are updated correctly?(not necessarily using Merge)
And from performance point of view what would be the best approach? (only the INSERT INTO will be performed thousand of times...)
I have such sql statement:
MERGE pvl.testTable AS T
USING temp.testTable AS S
ON (T.Id = S.ID)
WHEN NOT MATCHED BY TARGET THEN
INSERT (first,
second,
third,
fourth) VALUES (s.first,
s.second,
s.third,
s.fourth)
WHEN MATCHED
THEN
UPDATE
SET
T.first = S.first,
T.second = S.second,
T.third = S.third,
T.fourth = S.fourth
WHEN NOT MATCHED BY SOURCE
THEN
DELETE;
Also I know that I must use ON CONFLICT, but how i can deal with WHEN NOT MATCHED BY TARGET and WHEN NOT MATCHED BY SOURCE?
You could do it in two steps: 1. Upsert and 2. Delete
-- Perform upsert and return all inserted/updated ids
WITH upsert(id) AS
(
INSERT INTO target_table (first, second, third, fourth)
SELECT first, second, third, fourth FROM source_table
ON CONFLICT (id) DO UPDATE SET
first = excluded.first,
second = excluded.second,
third = excluded.third,
fourth = excluded.fourth
RETURNING id
)
-- Delete any records in target that aren't in source table
DELETE FROM target_table
WHERE id NOT IN (SELECT id FROM upsert);
I have written a stored procedure in SQL Server. Basically it tries to handle SCD2. Three things am trying to do:
When not matched by target, insert the record
When not matched by source, inactivate the active record(update)
When matched, update the active record flag to N
These 3 scenarios are being properly handled.
My problem is when matched I need to insert a new record as well in source. I have some article suggesting to use $output clause and do insert on top of MERGE; I tried it but I get this error:
Msg 356, Level 16, State 1, Procedure myscd2, Line 17
The target table 'dbo.dimproducts' of the INSERT statement cannot be on either side of a (primary key, foreign key) relationship when the FROM clause contains a nested INSERT, UPDATE, DELETE, or MERGE statement. Found reference constraint 'FK_FactSalesOrders_DimProducts'.
Please see my code below and let me know, what can I do to resolve this issue:
begin
insert into dbo.dimproducts(ProductID, ProductName, StandardListPrice, ProductSubCategoryID, ProductSubCategoryName, ProductCategoryID, ProductCategoryName, active_flag)
select
productid, name, ListPrice,
productsubcategoryid, ProductSubCategoryName,
ProductCategoryID, ProductCategoryName, Active_Flag
from
(merge dbo.dimproducts as tgt
using AdventureWorks_Basics.dbo.Products as src on (tgt.productid = src.productid)
when matched and src.name != tgt.productname and tgt.active_flag = 'Y'
then
update set tgt.active_flag = 'N'
when not matched by target
then
insert(ProductID, ProductName, StandardListPrice, ProductSubCategoryID, ProductSubCategoryName, ProductCategoryID, ProductCategoryName, active_flag)
values (src.productid, src.name, src.listprice, 100, 'ABC', 101, coalesce(src.productline, 'XYZ'), 'Y')
when not matched by source
then
update set tgt.active_flag = 'N'
OUTPUT $action AS Action, src.productid, src.name, src.listprice, 100 as productsubcategoryid, 'ABC' as ProductSubCategoryName, 101 as ProductCategoryID, coalesce(src.productline, 'XYZ') as ProductCategoryName, 'Y' as Active_Flag) AS MergeOutput
where
MergeOutput.Action = 'UPDATE';
There is a limitation on the OUTPUT clause that the target cannot have a FOREIGN KEY constraint, or be referenced by a FOREIGN KEY constraint. Hence the error you are getting.
However you can do the following, insert data into temp table from output clause and then select from temp table to insert into your target table.
Warning I would still advise you to avoid using MERGE statement and read the article for the reasons.
CREATE TABLE #dimproducts (
Act VARCHAR(10)
,ProductID INT
,ProductName VARCHAR(100)
,StandardListPrice INT
,ProductSubCategoryID INT
,ProductSubCategoryName VARCHAR(100)
,ProductCategoryID INT
,ProductCategoryName VARCHAR(100)
,active_flag VARCHAR(1));
merge dbo.dimproducts as tgt
using AdventureWorks_Basics.dbo.Products as src on (tgt.productid = src.productid)
when matched and src.name != tgt.productname and tgt.active_flag = 'Y'then
update set tgt.active_flag = 'N'
when not matched by target then
insert(ProductID,ProductName,StandardListPrice,ProductSubCategoryID,ProductSubCategoryName,ProductCategoryID,ProductCategoryName,active_flag)
values(src.productid,src.name,src.listprice,100,'ABC',101,coalesce(src.productline,'XYZ'),'Y')
when not matched by source then
update set tgt.active_flag = 'N'
OUTPUT $action AS Action, src.productid,src.name,src.listprice,100 as productsubcategoryid,'ABC' as ProductSubCategoryName
,101 as ProductCategoryID,coalesce(src.productline,'XYZ') as ProductCategoryName,'Y' as Active_Flag
INTO #dimproducts (Act,ProductID,ProductName,StandardListPrice,ProductSubCategoryID,ProductSubCategoryName,ProductCategoryID,ProductCategoryName,active_flag);
INSERT INTO dbo.dimproducts (productid,name,ListPrice,productsubcategoryid,ProductSubCategoryName,ProductCategoryID,ProductCategoryName,Active_Flag)
select productid,name,ListPrice,productsubcategoryid,ProductSubCategoryName,ProductCategoryID,ProductCategoryName,Active_Flag
FROM #dimproducts
WHERE Act = 'UPDATE';
http://www.sqlservercentral.com/articles/MERGE/73805/
Slowly changing dimensions using T-SQL MERGE
By Adam Aspin, 2011/06/20
See section: Type 2 SCD - Keep a history of previous attributes as separate records, indicate the dates they were valid, and flag the currently valid record
its basically M.Ali's answer with a ton more information as well as info on SCD 1, 3, and 4:
I currently have a stored procedure that compares my target table (Ticket_Report) to my data source table (New_Tickets).
I am using a MERGE INTO statement to compare these two. When it finds a match between the two tables, it updates the current row in the target table with the corresponding info from the source table. If it dosent find a match, it inserts that data from the source table into the target table.
MERGE INTO Ticket_REPORT T1
USING #New_Tickets T2
ON T1.TICKET_NO=T2.TICKET_NO
WHEN MATCHED THEN
UPDATE SET
T1.TICKET_NO = T2.TICKET_NO,
T1.ASSIGNED_GROUP = T2.ASSIGNED_GROUP,
T1.ASSIGNEE = T2.ASSIGNEE,
T1.FNAME = T2.FNAME,
T1.LNAME = T2.LNAME
WHEN NOT MATCHED THEN
INSERT VALUES(
T2.TICKET_NO,
T2.ASSIGNED_GROUP,
T2.ASSIGNEE,
T2.FNAME,
T2.LNAME
);
I need to change this, so that when match is found on the Ticket Number, instead up just updating it, I need to A.)replace the current row in the Target table by deleting it, then B.)inserting the corresponding Row from the source table.
I currently have
MERGE INTO Ticket_REPORT T1
USING #New_Tickets T2
ON T1.Ticket_NO=T2.Ticket_NO
WHEN MATCHED THEN DELETE
//Now I need to replace what I deleted with the row from the source table
Which will delete the row from the Target Table. Now I want to Insert the corresponding Row from the Source Table. I am having trouble trying to do multiple things inside the WHEN MATCHED clause. Does anyone know how I can accomplish this?
*Side note: When matched, I could Insert the row from the Source, but then I how would I delete the original?
Strictly solution For your boss
First put the matched records into one temp table
Next use Merge query to delete the matched records from target table and Insert the unmatched records
Finally insert the records from temp table to target table
Try something like this
select * into #temp
from #New_Tickets T2
where exists(select 1
from Ticket_REPORT T1
where T1.Ticket_NO=T2.Ticket_NO)
MERGE INTO Ticket_REPORT T1
USING #New_Tickets T2
ON T1.TICKET_NO=T2.TICKET_NO
WHEN MATCHED THEN DELETE
WHEN NOT MATCHED THEN
INSERT VALUES(
T2.TICKET_NO,
T2.ASSIGNED_GROUP,
T2.ASSIGNEE,
T2.FNAME,
T2.LNAME
);
insert into Ticket_REPORT (col1,col2,..)
select col1,col2,..
from #temp
Note :
What has to be done is delete the matched records from target table and insert all then records from source table to target table
One approach is to subquery your MERGE statement. You can filter the output based on the action (INSERT, UPDATE or DELETE). The filtered records can then be INSERTED, UPDATED or DELETED.
This is a common technique for loading slowly changing dimensions into a data warehouse.
My example uses the follow temp tables:
SETUP
/* Create and populate sample tables.
*/
CREATE TABLE #NewTicket
(
Id INT
)
;
CREATE TABLE #TicketReport
(
Id INT
)
;
INSERT INTO #NewTicket
(
Id
)
VALUES
(1),
(2),
(3)
;
INSERT INTO #TicketReport
(
Id
)
VALUES
(3),
(4),
(5)
;
In the example 1, 2 and 3 appear in new ticket. 3, 4 and 5 appear in ticket report. 3 is deleted by the MERGE statement and INSERTED by the outer query.
EXAMPLE
/* Filter the results from the sub query
* for deleted records.
* These are then appended in the main
* outer query.
*/
INSERT INTO #TicketReport
(
Id
)
SELECT
Id
FROM
(
/* MERGE statments can be used as a sub query.
* You'll need the OUTPUT clause for this to work.
* The column $action describes what happened to each record.
*/
MERGE
#TicketReport AS t
USING #NewTicket AS s ON s.Id = t.Id
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED BY TARGET THEN
INSERT
(
Id
)
VALUES
(
Id
)
OUTPUT
$action,
s.*
) AS r
WHERE
[$Action] = 'DELETE'
;
/* View the final result.
*/
SELECT
*
FROM
#TicketReport
ORDER BY
Id
;
I have a stored procedure that receives a TVP as input. Now, I need to check the received data for a particular ID in a primary key column. If it exists, then I just need to update the table using those new values for other column (sent via TVP). Else, do an insert.
How to do it?
CREATE PROCEDURE ABC
#tvp MyTable READONLY
AS
IF EXISTS (SELECT 1 FROM MYTAB WHERE ID= #tvp.ID)
DO update
ELSE
Create
Just wondering the if exists loop I did is correct. I reckon its wrong as it will only check for first value and then update. What about other values? How should I loop through this?
Looping/CURSOR is the weapon of last resort, always search for solution that is SET based, not ROW based.
You should use MERGE which is designed for this type of operation:
MERGE table_name AS TGT
USING (SELECT * FROM #tvp) AS SRC
ON TGT.id = SRC.ID
WHEN MATCHED THEN
UPDATE SET col = SRC.col
WHEN NOT MATCHED THEN
INSERT (col_name, col_name2, ...)
VALUES (SRC.col_name1, SRC.col_name2, ...)
If you don't like MERGE use INSERT/UPDATE:
UPDATE table_name
SET col = tv.col,
...
FROM table_name AS tab
JOIN #tvp AS tv
ON tab.id = tv.id
INSERT INTO table_name(col1, col2, ...)
SELECT tv.col1, tv.col2, ...
FROM table_name AS tab
RIGHT JOIN #tvp AS tv
ON tab.id = tv.id
WHERE tab.id IS NULL