Merge statement on a single record table - sql-server

I need to write a single statement to insert or update a record in a single record table
the merge statement allows me to write this:
create table t1 (n int)
-- insert into t1 (n) Values (1); -- uncomment to test the matched branch
MERGE t1 AS P
USING (SELECT 3 AS n) AS S
ON 1 = 1
WHEN MATCHED THEN
UPDATE SET n = S.n
WHEN NOT MATCHED THEN
INSERT (n)
VALUES (S.n);
select * from t1
this work, but I think that the 1=1 condition purpose is not very easy to understand.
Is there a different syntax to insert a record when the table is empty or update the record when it does already exist?

The other option would be to do it the old fashioned way.
if exists (select null from t1)
update t1 set n = 3
else
insert into t1 (n) values (3)

Replace
ON 1 = 1
with
ON S.n = P.n

Example of recent procedure I wrote to either update an existing row or insert a new row.
Table has the same structure as MembershipEmailFormat the table variable.
Found it easiest to create a table variable to be the source in the Using clause. I realize that the main purpose of Merge statements really are merging muliple rows between two tables. My use case is that I need to insert a new email address for a user or modify and existing email address.
CREATE PROCEDURE [dbo].[usp_user_merge_emailformat]
#UserID UNIQUEIDENTIFIER,
#Email varchar(256),
#UseHTML bit
AS
BEGIN
--SELECT #UserID='04EFF187-AEAC-408E-9FA8-284B31890FBD',
-- #Email='person#xxxx.com',
-- #UseHTML=0
DECLARE #temp TABLE
(
UserID UNIQUEIDENTIFIER,
Email varchar(256),
HtmlFormat bit
)
INSERT INTO #temp(UserID,Email, HtmlFormat)
Values(#UserID,#Email,#UseHTML)
SELECT * FROM #temp
MERGE dbo.MembershipEmailFormat as t
USING #temp AS s
ON (t.UserID = s.UserID and t.Email = s.Email)
WHEN MATCHED THEN UPDATE SET t.HtmlFormat = s.HtmlFormat
WHEN NOT MATCHED THEN INSERT VALUES(s.UserID,s.Email,s.HtmlFormat);
END

Related

Trigger to insert multiple record

I'm trying to create a trigger to insert all the value that I delete from a table in a "Backup" table,
Ex:
Table 1: NomePilota, ModelloVettura, NomeScuderia
BackupTable1 (Table 2): NomePilota, ModelloVettura, NomeScuderia
What I want from the trigger to do: Insert into 'Table 2' deleted values from table 1.
I tried like this:
CREATE TRIGGER Backup ON dbo.Table1 AFTER (i can only use after) DELETE AS
BEGIN
DECLARE #Pilota VARCHAR(20) = (SELECT NomePilota FROM deleted)
DECLARE #Vettura VARCHAR(50) = (SELECT ModelloVettura FROM deleted)
DECLARE #Scuderia VARCHAR(20) = (SELECT NomeScuderia FROM deleted)
INSERT INTO Table2 (NomePilota, ModelloVettura, NomeScuderia) VALUES (#Pilota, #Vettura, #Scuderia)
But it send a error:
Cannot insert multiple records in #Pilota, #Vet, #Scud
How can I fix that? Does the deleted table already have a default ID column to use like in a for? Can I use something like vectors? (like #Nome[] = SELECT * FROM Tabella, Insert into Tabella2 (Nome) VALUES #Nome[#Numero (numero is like the record number of nome]).
Why not make life simple?
INSERT INTO Table2 (NomePilota, ModelloVettura, NomeScuderia)
SELECT NomePilota, ModelloVettura, NomeScuderia FROM deleted
The insert statment can work on the results of a select -- here we leverage this to simplify the task at hand.

Creating a temporary table within a TRIGGER with GROUP BY

I need to create and use a temporary table with GROUP BY clause within a trigger, but I'm having difficulties doing so.
My attempt:
Here I'm trying to use two temporary tables which are dropped after the trigger reach an end.
First I create a #Temptable and the trigger.
CREATE TABLE #TempTable (admID smallint, diagID smallint);
CREATE TRIGGER tr_newTest
ON Adm_Diag
FOR INSERT
AS
BEGIN
...
END
Since the table inserted only contains rows for a current INSERT and UPDATE statements I'm passing several INSERT and UPDATE statements to #TempTable.
DECLARE #admID smallint
SELECT #admID = Adm_ID
FROM inserted
DECLARE #diagID smallint
SELECT #diagID=Diag_ID
FROM inserted
INSERT INTO #TempTable VALUES (#admID, #diagID)
Now with this data I want to create a temporary table that groups the rows of #TempTable:
SELECT *
INTO #TempGroupTable
FROM
(
SELECT admID, COUNT(*) as Diag
FROM #TempTable
GROUP BY admID
) t1
WHERE Diag > 2
The whole script
CREATE TABLE #TempTable (admID smallint, diagID smallint);
CREATE TRIGGER tr_newTest
ON Adm_Diag
FOR INSERT
AS
BEGIN
DECLARE #admID smallint
SELECT #admID = Adm_ID
FROM inserted
DECLARE #diagID smallint
SELECT #diagID=Diag_ID
FROM inserted
INSERT INTO #TempTable VALUES (#admID, #diagID)
-- Below I'm tring to create #TempGroupTable
SELECT *
INTO #TempGroupTable
FROM
(
SELECT admID, COUNT(*) as Diag
FROM #TempTable
GROUP BY admID
) t1
WHERE Diag > 2
END
After executing the trigger I get an error:
Msg 208, Level 16, State 0, Line 41 Invalid object name
'#TempGroupTable'.
How can I create #TempGroupTable?
Not quote sure what you are trying to do but would a global temporary tables which starts with ## work for you? So make the #TempGroupTable into ##TempGroupTable?
Why not dispense with all the overhead of temp tables and variables? Try:
CREATE TRIGGER tr_newTest
ON Adm_Diag
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO Adm_Diag (adminID, Diag)
SELECT admID, COUNT(*) as Diag
FROM inserted
GROUP BY admID
END

How to set variables in While Loop in Sql Server

I am trying to use while loop instead of CURSOR in SQL SERVER. I am trying to select TOP 1 in while and set them to the variables like below. It doesnt let me set the variables in while loop. What am I doing wrong?
WHILE (
SELECT TOP 1 #WAOR_CODE = WAOR_.WAOR_CODE
, #WAOD_INVENTORYITEMID = WAOD_.WAOD_INVENTORYITEMID
FROM #wmsorder
)
BEGIN
SELECT #WAOR_CODE
, #WAOD_INVENTORYITEMID
DELETE TOP (1) #wmsorder
END
Another option:
WHILE EXISTS(select 1 FROM #wmsorder)
BEGIN
DELETE TOP (1)
FROM #wmsorder
END
However, deleting all records from a table one by one might be a performance hell. You might want to consider using TRUNCATE TABLE instead:
TRUNCATE TABLE #wmsorder
Also, note that each delete is written to the database log, while truncate table doesn't get written to the log at all.
Testing with a temporary table containing 100,000 rows, deleting the rows one by one took me 9 seconds, while truncate table completed immediately:
-- create and populate sample table
SELECT TOP 100000 IDENTITY(int,1,1) AS Number
INTO #wmsorder
FROM sys.objects s1
CROSS JOIN sys.objects s2
-- delete rows one by one
WHILE EXISTS(select 1 FROM #wmsorder)
BEGIN
DELETE TOP (1)
FROM #wmsorder
END
-- clean up
DROP TABLE #wmsorder
-- create and populate sample table
SELECT TOP 100000 IDENTITY(int,1,1) AS Number
INTO #wmsorder
FROM sys.objects s1
CROSS JOIN sys.objects s2
-- truncate the table
TRUNCATE TABLE #wmsorder
-- clean up
DROP TABLE #wmsorder
DECLARE #t TABLE (a INT PRIMARY KEY)
INSERT INTO #t
VALUES (1), (2), (3)
Variant #1:
label:
DELETE TOP(1)
FROM #t
OUTPUT DELETED.a
IF ##ROWCOUNT > 0
GOTO label
Variant #2:
WHILE ##ROWCOUNT != 0
DELETE TOP(1)
FROM #t
OUTPUT DELETED.a
Variant #3:
DECLARE #a TABLE(a INT)
WHILE ##ROWCOUNT != 0 BEGIN
DELETE FROM #a
DELETE TOP(1)
FROM #t
OUTPUT DELETED.a INTO #a
SELECT * FROM #a
END
See the below code. I just corrected the SQL statements shared by you
WHILE 1=1
BEGIN
IF NOT EXISTS (SELECT 1 FROM #wmsorder)
BREAK
SELECT TOP 1 #WAOR_CODE = WAOR_.WAOR_CODE
,#WAOD_INVENTORYITEMID = WAOD_.WAOD_INVENTORYITEMID
FROM #wmsorder WAOR_
SELECT #WAOR_CODE
,#WAOD_INVENTORYITEMID
DELETE #wmsorder WHERE WAOR_CODE = #WAOR_CODE AND WAOD_INVENTORYITEMID = #WAOD_INVENTORYITEMID
END
But as Zohar Peled mentioned, it will be a pain to the engine if you are deleting the records one by one from a table. So below I have shared another query, through this even you can track the records before deleting from the table
DECLARE #TableVar AS TABLE (WAOR_CODE VARCHAR(100), WAOD_INVENTORYITEMID VARCHAR(100))
WHILE 1=1
BEGIN
SELECT TOP 1 #WAOR_CODE = WAOR_.WAOR_CODE
,#WAOD_INVENTORYITEMID = WAOD_.WAOD_INVENTORYITEMID
FROM #wmsorder WAOR_
WHERE NOT EXISTS (SELECT 1 FROM #TableVar t WHERE t.WAOR_CODE = WAOR_.WAOR_CODE AND t.WAOD_INVENTORYITEMID = WAOR_.WAOD_INVENTORYITEMID)
IF #WAOR_CODE IS NULL AND #WAOD_INVENTORYITEMID IS NULL
BREAK
INSERT INTO #TableVar
(WAOR_CODE, WAOD_INVENTORYITEMID)
SELECT #WAOR_CODE
,#WAOD_INVENTORYITEMID
END
DELETE #wmsorder WHERE EXISTS (SELECT 1 FROM #TableVar t WHERE t.WAOR_CODE = #wmsorder.WAOR_CODE AND t.WAOD_INVENTORYITEMID = #wmsorder.WAOD_INVENTORYITEMID)
Sorry I did not test the second code. Please forgive me if it breaks something. But I am pretty sure it may require a small repair to make this query functional. All the best.

Is it possible to use a stored procedure to update three different tables?

I have an proc doing a select and update statements as follows. I need to incorporate the update statements and select statement which put the data into a temp table into a single sql select statement.
Is this possible?
'
Yes:
CREATE TABLE TableA
(
valueA int
)
INSERT INTO
TableA
VALUES
(1),
(2),
(3)
GO
CREATE PROCEDURE test_procedure (#in_value int)
AS
BEGIN
--insert into temp table
SELECT
#in_value [out_value]
INTO
#TestTable
-- update with join on temp table
UPDATE
T
SET
T.[out_value] = 2
FROM
#TestTable T
INNER JOIN TableA A on A.valueA = T.out_value
WHERE
A.valueA = 1
-- update with join on temp table again
UPDATE
T
SET
[out_value] = 3
FROM
#TestTable T
INNER JOIN TableA A on A.valueA = T.out_value
WHERE
A.valueA = 2
--select results including the original "in_value"
SELECT
*,
#in_value [in_value]
FROM
TableA A
LEFT JOIN #TestTable T on T.out_value = A.valueA
END;
GO
--execute stored procedure
EXEC test_procedure 1
I have found one way out by using union.
this is bit wierd but since i am not able to find anything else, settled with this method

Using merge..output to get mapping between source.id and target.id

Very simplified, I have two tables Source and Target.
declare #Source table (SourceID int identity(1,2), SourceName varchar(50))
declare #Target table (TargetID int identity(2,2), TargetName varchar(50))
insert into #Source values ('Row 1'), ('Row 2')
I would like to move all rows from #Source to #Target and know the TargetID for each SourceID because there are also the tables SourceChild and TargetChild that needs to be copied as well and I need to add the new TargetID into TargetChild.TargetID FK column.
There are a couple of solutions to this.
Use a while loop or cursors to insert one row (RBAR) to Target at a time and use scope_identity() to fill the FK of TargetChild.
Add a temp column to #Target and insert SourceID. You can then join that column to fetch the TargetID for the FK in TargetChild.
SET IDENTITY_INSERT OFF for #Target and handle assigning new values yourself. You get a range that you then use in TargetChild.TargetID.
I'm not all that fond of any of them. The one I used so far is cursors.
What I would really like to do is to use the output clause of the insert statement.
insert into #Target(TargetName)
output inserted.TargetID, S.SourceID
select SourceName
from #Source as S
But it is not possible
The multi-part identifier "S.SourceID" could not be bound.
But it is possible with a merge.
merge #Target as T
using #Source as S
on 0=1
when not matched then
insert (TargetName) values (SourceName)
output inserted.TargetID, S.SourceID;
Result
TargetID SourceID
----------- -----------
2 1
4 3
I want to know if you have used this? If you have any thoughts about the solution or see any problems with it? It works fine in simple scenarios but perhaps something ugly could happen when the query plan get really complicated due to a complicated source query. Worst scenario would be that the TargetID/SourceID pairs actually isn't a match.
MSDN has this to say about the from_table_name of the output clause.
Is a column prefix that specifies a table included in the FROM clause of a DELETE, UPDATE, or MERGE statement that is used to specify the rows to update or delete.
For some reason they don't say "rows to insert, update or delete" only "rows to update or delete".
Any thoughts are welcome and totally different solutions to the original problem is much appreciated.
In my opinion this is a great use of MERGE and output. I've used in several scenarios and haven't experienced any oddities to date.
For example, here is test setup that clones a Folder and all Files (identity) within it into a newly created Folder (guid).
DECLARE #FolderIndex TABLE (FolderId UNIQUEIDENTIFIER PRIMARY KEY, FolderName varchar(25));
INSERT INTO #FolderIndex
(FolderId, FolderName)
VALUES(newid(), 'OriginalFolder');
DECLARE #FileIndex TABLE (FileId int identity(1,1) PRIMARY KEY, FileName varchar(10));
INSERT INTO #FileIndex
(FileName)
VALUES('test.txt');
DECLARE #FileFolder TABLE (FolderId UNIQUEIDENTIFIER, FileId int, PRIMARY KEY(FolderId, FileId));
INSERT INTO #FileFolder
(FolderId, FileId)
SELECT FolderId,
FileId
FROM #FolderIndex
CROSS JOIN #FileIndex; -- just to illustrate
DECLARE #sFolder TABLE (FromFolderId UNIQUEIDENTIFIER, ToFolderId UNIQUEIDENTIFIER);
DECLARE #sFile TABLE (FromFileId int, ToFileId int);
-- copy Folder Structure
MERGE #FolderIndex fi
USING ( SELECT 1 [Dummy],
FolderId,
FolderName
FROM #FolderIndex [fi]
WHERE FolderName = 'OriginalFolder'
) d ON d.Dummy = 0
WHEN NOT MATCHED
THEN INSERT
(FolderId, FolderName)
VALUES (newid(), 'copy_'+FolderName)
OUTPUT d.FolderId,
INSERTED.FolderId
INTO #sFolder (FromFolderId, toFolderId);
-- copy File structure
MERGE #FileIndex fi
USING ( SELECT 1 [Dummy],
fi.FileId,
fi.[FileName]
FROM #FileIndex fi
INNER
JOIN #FileFolder fm ON
fi.FileId = fm.FileId
INNER
JOIN #FolderIndex fo ON
fm.FolderId = fo.FolderId
WHERE fo.FolderName = 'OriginalFolder'
) d ON d.Dummy = 0
WHEN NOT MATCHED
THEN INSERT ([FileName])
VALUES ([FileName])
OUTPUT d.FileId,
INSERTED.FileId
INTO #sFile (FromFileId, toFileId);
-- link new files to Folders
INSERT INTO #FileFolder (FileId, FolderId)
SELECT sfi.toFileId, sfo.toFolderId
FROM #FileFolder fm
INNER
JOIN #sFile sfi ON
fm.FileId = sfi.FromFileId
INNER
JOIN #sFolder sfo ON
fm.FolderId = sfo.FromFolderId
-- return
SELECT *
FROM #FileIndex fi
JOIN #FileFolder ff ON
fi.FileId = ff.FileId
JOIN #FolderIndex fo ON
ff.FolderId = fo.FolderId
I would like to add another example to add to #Nathan's example, as I found it somewhat confusing.
Mine uses real tables for the most part, and not temp tables.
I also got my inspiration from here: another example
-- Copy the FormSectionInstance
DECLARE #FormSectionInstanceTable TABLE(OldFormSectionInstanceId INT, NewFormSectionInstanceId INT)
;MERGE INTO [dbo].[FormSectionInstance]
USING
(
SELECT
fsi.FormSectionInstanceId [OldFormSectionInstanceId]
, #NewFormHeaderId [NewFormHeaderId]
, fsi.FormSectionId
, fsi.IsClone
, #UserId [NewCreatedByUserId]
, GETDATE() NewCreatedDate
, #UserId [NewUpdatedByUserId]
, GETDATE() NewUpdatedDate
FROM [dbo].[FormSectionInstance] fsi
WHERE fsi.[FormHeaderId] = #FormHeaderId
) tblSource ON 1=0 -- use always false condition
WHEN NOT MATCHED
THEN INSERT
( [FormHeaderId], FormSectionId, IsClone, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
VALUES( [NewFormHeaderId], FormSectionId, IsClone, NewCreatedByUserId, NewCreatedDate, NewUpdatedByUserId, NewUpdatedDate)
OUTPUT tblSource.[OldFormSectionInstanceId], INSERTED.FormSectionInstanceId
INTO #FormSectionInstanceTable(OldFormSectionInstanceId, NewFormSectionInstanceId);
-- Copy the FormDetail
INSERT INTO [dbo].[FormDetail]
(FormHeaderId, FormFieldId, FormSectionInstanceId, IsOther, Value, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
SELECT
#NewFormHeaderId, FormFieldId, fsit.NewFormSectionInstanceId, IsOther, Value, #UserId, CreatedDate, #UserId, UpdatedDate
FROM [dbo].[FormDetail] fd
INNER JOIN #FormSectionInstanceTable fsit ON fsit.OldFormSectionInstanceId = fd.FormSectionInstanceId
WHERE [FormHeaderId] = #FormHeaderId
Here's a solution that doesn't use MERGE (which I've had problems with many times I try to avoid if possible). It relies on two memory tables (you could use temp tables if you want) with IDENTITY columns that get matched, and importantly, using ORDER BY when doing the INSERT, and WHERE conditions that match between the two INSERTs... the first one holds the source IDs and the second one holds the target IDs.
-- Setup... We have a table that we need to know the old IDs and new IDs after copying.
-- We want to copy all of DocID=1
DECLARE #newDocID int = 99;
DECLARE #tbl table (RuleID int PRIMARY KEY NOT NULL IDENTITY(1, 1), DocID int, Val varchar(100));
INSERT INTO #tbl (DocID, Val) VALUES (1, 'RuleA-2'), (1, 'RuleA-1'), (2, 'RuleB-1'), (2, 'RuleB-2'), (3, 'RuleC-1'), (1, 'RuleA-3')
-- Create a break in IDENTITY values.. just to simulate more realistic data
INSERT INTO #tbl (Val) VALUES ('DeleteMe'), ('DeleteMe');
DELETE FROM #tbl WHERE Val = 'DeleteMe';
INSERT INTO #tbl (DocID, Val) VALUES (6, 'RuleE'), (7, 'RuleF');
SELECT * FROM #tbl t;
-- Declare TWO temp tables each with an IDENTITY - one will hold the RuleID of the items we are copying, other will hold the RuleID that we create
DECLARE #input table (RID int IDENTITY(1, 1), SourceRuleID int NOT NULL, Val varchar(100));
DECLARE #output table (RID int IDENTITY(1,1), TargetRuleID int NOT NULL, Val varchar(100));
-- Capture the IDs of the rows we will be copying by inserting them into the #input table
-- Important - we must specify the sort order - best thing is to use the IDENTITY of the source table (t.RuleID) that we are copying
INSERT INTO #input (SourceRuleID, Val) SELECT t.RuleID, t.Val FROM #tbl t WHERE t.DocID = 1 ORDER BY t.RuleID;
-- Copy the rows, and use the OUTPUT clause to capture the IDs of the inserted rows.
-- Important - we must use the same WHERE and ORDER BY clauses as above
INSERT INTO #tbl (DocID, Val)
OUTPUT Inserted.RuleID, Inserted.Val INTO #output(TargetRuleID, Val)
SELECT #newDocID, t.Val FROM #tbl t
WHERE t.DocID = 1
ORDER BY t.RuleID;
-- Now #input and #output should have the same # of rows, and the order of both inserts was the same, so the IDENTITY columns (RID) can be matched
-- Use this as the map from old-to-new when you are copying sub-table rows
-- Technically, #input and #output don't even need the 'Val' columns, just RID and RuleID - they were included here to prove that the rules matched
SELECT i.*, o.* FROM #output o
INNER JOIN #input i ON i.RID = o.RID
-- Confirm the matching worked
SELECT * FROM #tbl t

Resources