MS SQL select statement with "check table" - sql-server

I'm struggling to find out a solution for my select statement. I have two tables:
CREATE TABLE Things
(
Id int,
type int
)
CREATE TABLE Relations
(
Id int,
IdParent int,
IdChild int
)
What I need to select, based on given Thing Id:
All records from Things that has Type = -1
All records from Things that has Type matching IdChild where IdParent is Type of a row matching given Id.
If Type of a row matching given Id is -1 or doesn't exist in table Relations (as IdParent) I need to select all records from Things
I am having problem with the last scenario, I tried to do that by joining Relations table, but I can't come up with a condition that would satisfy all scenarios, any suggestions?
UPDATE
This is how I do it now. I need to solve the scenario where given id does not exists in table Relations - then I need to select all records.
CREATE TABLE Things (id int, type int)
CREATE TABLE Relations (id int, parent int, child int)
INSERT INTO Things VALUES (1, 1)
INSERT INTO Things VALUES (2, -1)
INSERT INTO Things VALUES (3, 3)
INSERT INTO Things VALUES (4, 3)
INSERT INTO Things VALUES (5, 2)
INSERT INTO Things VALUES (6, -1)
INSERT INTO Relations VALUES (1, 1, 2)
DECLARE #id int = 1
SELECT * FROM Things
JOIN Relations R ON R.parent = #id AND Type = R.child OR Type = -1
So I must solve the situation where #id = 2 for example - I need to retrieve all rows (same for #id = 5, unless it appears in Relations in parent column somewhere).
UPDATE 2
I came up with something like that:
DECLARE #id int = 2
DECLARE #type int
SELECT #type = type FROM Things WHERE Id = #id
IF #type > -1
BEGIN
SELECT T.* FROM Things T
JOIN Relations R ON (R.parent = #id AND T.Type = R.child) OR T.Type = -1
END
ELSE
BEGIN
SELECT * FROM Things
END
I'm quite sure that this can be done differently, without conditional IF, optimized.

from Things
left outer join Relations
on Relations.IdParent = Things.Type
where Things.Type = -1 or Relations.IdParent is null

Related

Recursive SQL query with multiple columns

I have a table with the following columns
idRelationshipType int,
idPerson1 int,
idPerson2 int
This table allows me to indicate records in a database that should be linked together.
I need to do a query returning all the unique ids where a person's id exists in idPerson1 or idPerson2 columns. Additionally, I need the query to be recursive so that the if I a match is found in idPerson1, the value for idPerson2 is included in the result set and used to repeat the query recursively until no more matches are found.
Example data:
CREATE TABLE [dbo].[tbRelationships]
(
[idRelationshipType] [int],
[idPerson1] [int] ,
[idPerson2] [int]
)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 1, 2)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 2, 3)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 3, 4)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 5, 1)
Four 'Relationships' are defined here. For this query, I will only know one of the ids to begin with. I need a query that in concept works like
SELECT idPerson
FROM [some query]
WHERE [the id i have to start with] = #idPerson
AND idRelationshipType = #idRelationshipType
The returned result should be a 5 rows with one column 'idPerson', with 1, 2, 3, 4, and 5 as the row values.
I have tried various combinations of UNPIVOT and recursive CTEs but I am not making much progress.
Any help would be greatly appreciated.
Thanks,
Daniel
I think this is what you want:
DECLARE #RelationshipType int
DECLARE #PersonId int
SELECT #RelationshipType = 1, #PersonId = 1
;WITH Hierachy (idPerson1, IdPerson2)
AS
(
--root
SELECT R.idPerson1, R.idPerson2
FROM tbRelationships R
WHERE R.idRelationshipType = #RelationshipType
AND (R.idPerson1 = #PersonId OR R.idPerson2 = #PersonId)
--recurse
UNION ALL
SELECT R.idPerson1, R.idPerson2
FROM Hierachy H
JOIN tbRelationships R
ON (R.idPerson1 = H.idPerson2
OR R.idPerson2 = H.idPerson1)
AND R.idRelationshipType = #RelationshipType
)
SELECT DISTINCT idPerson
FROM
(
SELECT idPerson1 AS idPerson FROM Hierachy
UNION
SELECT idPerson2 AS idPerson FROM Hierachy
) H
Essentially, get the first rows where the required id is in either column, and then recurse getting all of the child ids based on id column 2

Update two columns with list of data in sql server SP

i have one table that have Ads_Id And Priority in sql server
it possible the create procedure for update Ads_Id And Priority with a list ?
for example in a single query
update AdsPriority set [Priority] = 3 where Ads_Id = 1
change to
update AdsPriority set [Priority] = (List Of Id) where Ads_Id = (List of Priority)
thank you for your help
It's possible, but not the way you've described it.
You can use joins in an update statement, so I would go with something like this:
DECLARE #Priority AS TABLE (priority int, id int)
INSERT INTO #Priority VALUES (1, 1), (2, 2), (3, 3) .... (n,n)
UPDATE t
SET t.[Priority] = p.[Priority]
FROM AdsPriority t
INNER JOIN #Priority p ON(t.Ads_Id = p.id)
Since it's a stored procedure, you can use a table valued parameter:
CREATE TYPE udtTuple As TABLE (int1 int, int2 int)
GO
CREATE PROCEDURE stpUpdateAbsPriority
(
#Priority dbo.udtTuple readonly
)
AS
UPDATE t
SET t.[Priority] = p.[int1]
FROM AdsPriority t
INNER JOIN #Priority p ON(t.Ads_Id = p.int2)
GO

How do I reference a table twice when creating an indexed view? Can I enforce uniqueness based on 2 tables and multiple rows without it?

EDIT: Added in sample data that I am trying to disallow.
This question is similiar to this: Cannot create a CLUSTERED INDEX on a View because I'm referencing the same table twice, any workaround? but the answer there doesn't help me. I'm trying to enforce uniqueness, and so an answer of "don't do that" without an alternative doesn't help me progress.
Problem Example (Simplified):
CREATE TABLE [dbo].[Object]
(
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
OrgId UNIQUEIDENTIFIER
)
CREATE TABLE [dbo].[Attribute]
(
Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
Name NVARCHAR(256) NOT NULL
)
CREATE TABLE [dbo].[ObjectAttribute]
(
Id INT NOT NULL IDENTITY(1, 1),
ObjectId INT NOT NULL,
AttributeId INT NOT NULL,
Value NVARCHAR(MAX) NOT NULL,
CONSTRAINT FK_ObjectAttribute_Object FOREIGN KEY (ObjectId) REFERENCES [Object] (Id),
CONSTRAINT FK_ObjectAttribute_Attribute FOREIGN KEY (AttributeId) REFERENCES Attribute (Id)
)
GO
CREATE UNIQUE INDEX IUX_ObjectAttribute ON [dbo].[ObjectAttribute] ([ObjectId], [AttributeId])
GO
CREATE VIEW vObject_Uniqueness
WITH SCHEMABINDING
AS
SELECT
ObjectBase.OrgId
, CAST(OwnerValue.Value AS NVARCHAR(256)) AS OwnerValue
, CAST(NameValue.Value AS NVARCHAR(50)) AS NameValue
FROM [dbo].[Object] ObjectBase
INNER JOIN [dbo].ObjectAttribute OwnerValue
INNER JOIN [dbo].Attribute OwnerAttribute
ON OwnerAttribute.Id = OwnerValue.AttributeId
AND OwnerAttribute.Name = 'Owner'
ON OwnerValue.ObjectId = ObjectBase.Id
INNER JOIN [dbo].ObjectAttribute NameValue
INNER JOIN [dbo].Attribute NameAttribute
ON NameAttribute.Id = NameValue.AttributeId
AND NameAttribute.Name = 'Name'
ON NameValue.ObjectId = ObjectBase.Id
GO
/*
Cannot create index on view "[Database].dbo.vObject_Uniqueness". The view contains a self join on "[Database].dbo.ObjectAttribute".
*/
CREATE UNIQUE CLUSTERED INDEX IUX_vObject_Uniqueness
ON vObject_Uniqueness (OrgId, OwnerValue, NameValue)
GO
DECLARE #Org1 UNIQUEIDENTIFIER = NEWID();
DECLARE #Org2 UNIQUEIDENTIFIER = NEWID();
INSERT [dbo].[Object]
(
OrgId
)
VALUES
(#Org1) -- Id: 1
, (#Org2) -- Id: 2
, (#Org1) -- Id: 3
INSERT [dbo].[Attribute]
(
Name
)
VALUES
('Owner') -- Id: 1
, ('Name') -- Id: 2
--, ('Others')
-- Acceptable data.
INSERT [dbo].[ObjectAttribute]
(
AttributeId
, ObjectId
, Value
)
VALUES
(1, 1, 'Jeremy Pridemore') -- Owner for object 1 (Org1).
, (2, 1, 'Apple') -- Name for object 1 (Org1).
, (1, 2, 'John Doe') -- Owner for object 2 (Org2).
, (2, 2, 'Pear') -- Name for object 2 (Org2).
-- Unacceptable data.
-- Org1 already has an abject with an owner value of 'Jeremy' and a name of 'Apple'
INSERT [dbo].[ObjectAttribute]
(
AttributeId
, ObjectId
, Value
)
VALUES
(1, 3, 'Jeremy Pridemore') -- Owner for object 3 (Org1).
, (2, 3, 'Apple') -- Name for object 3 (Org1).
-- This is the bad data. I want to disallow this.
SELECT
OrgId, OwnerValue, NameValue
FROM vObject_Uniqueness
GROUP BY OrgId, OwnerValue, NameValue
HAVING COUNT(*) > 1
DROP VIEW vObject_Uniqueness
DROP TABLE ObjectAttribute
DROP TABLE Attribute
DROP TABLE [Object]
This example will create the error:
Msg 1947, Level 16, State 1, Line 2
Cannot create index on view "TestDb.dbo.vObject_Uniqueness". The view contains a self join on "TestDb.dbo.ObjectAttribute".
As this shows, I'm using an attribute system with 2 tables to represent one object and it's values. The existence of the object and the OrgId on an object are on the main table, and the rest of the values are attributes on the secondary table.
First of all, I don't understand why this says there is a self join. I'm joining from Object to ObjectAttribute twice. No where am I going from a table to that same table in an ON clause.
Second, is there a way to make this work? Or way to enforce the uniqueness that I'm going f or here? The end result that I want is that, by Object.OrgId, I have no two Object rows that have ObjectAttribute records referencing them providing the same 'Owner' and 'Name' values. So OrgId, Owner, and Name values need to be unique for any given Object.
I think you could create helper table for this:
CREATE TABLE [dbo].[ObjectAttributePivot]
(
Id int primary key,
OwnerValue nvarchar(256),
NameValue nvarchar(50)
)
GO
And then create helper trigger to keep data synchronized:
create view vw_ObjectAttributePivot
as
select
o.Id,
cast(ov.Value as nvarchar(256)) as OwnerValue,
cast(nv.Value as nvarchar(50)) as NameValue
from dbo.Object as o
inner join dbo.ObjectAttribute as ov on ov.ObjectId = o.Id
inner join dbo.Attribute as ova on ova.Id = ov.AttributeId and ova.Name = 'Owner'
inner join dbo.ObjectAttribute as nv on nv.ObjectId = o.Id
inner join dbo.Attribute as nva on nva.Id = nv.AttributeId and nva.Name = 'Name'
GO
create trigger utr_ObjectAttribute on ObjectAttribute
after update, delete, insert
as
begin
declare #temp_objects table (Id int primary key)
insert into #temp_objects
select distinct ObjectId from inserted
union
select distinct ObjectId from deleted
update ObjectAttributePivot set
OwnerValue = vo.OwnerValue,
NameValue = vo.NameValue
from ObjectAttributePivot as o
inner join vw_ObjectAttributePivot as vo on vo.Id = o.Id
where
o.Id in (select t.Id from #temp_objects as t)
insert into ObjectAttributePivot (Id, OwnerValue, NameValue)
select vo.Id, vo.OwnerValue, vo.NameValue
from vw_ObjectAttributePivot as vo
where
vo.Id in (select t.Id from #temp_objects as t) and
vo.Id not in (select t.Id from ObjectAttributePivot as t)
delete ObjectAttributePivot
from ObjectAttributePivot as o
where
o.Id in (select t.Id from #temp_objects as t) and
o.Id not in (select t.Id from vw_ObjectAttributePivot as t)
end
GO
After that, you can create unique view:
create view vObject_Uniqueness
with schemabinding
as
select
o.OrgId,
oap.OwnerValue,
oap.NameValue
from dbo.ObjectAttributePivot as oap
inner join dbo.Object as o on o.Id = oap.Id
GO
CREATE UNIQUE CLUSTERED INDEX IUX_vObject_Uniqueness
ON vObject_Uniqueness (OrgId, OwnerValue, NameValue)
GO
sql fiddle demo
The fundamental issue that we have here, enforcing the type of uniqueness you are going for, is in trying to answer the question, "When is it a violation?" Consider this:
Your database is loaded with the first two objects you reference in
your example (Org1 and Org2)
Now we INSERT ObjectAttribute(AttributeId, ObjectId, Value) VALUES (1, 3, 'Jeremy Pridemore')
Is this a violation? Based on what you have told me, I would say "no": we could go on to INSERT ObjectAttribute(AttributeId, ObjectId, Value) VALUES (2, 3, 'Cantalope'), and that would presumably be fine, right? So, we can't know whether the current statement is valid unless & until we know what the next statement is going to be. But there is no guarantee we will ever issue the second statement. Certainly there is no way of knowing what it will be at the time we are making up our minds whether the first statement is OK.
Should we, then, disallow free standing insertions of the type I am talking about-- where an "owner" entry is inserted, but with no simultaneous corrosponding "name" entry? To me, that is only workable approach to what you are trying to do here, and the only way to enforce that type of constraint is with a trigger.
Something like this:
DROP TRIGGER TR_ObjectAttribute_Insert
GO
CREATE TRIGGER TR_ObjectAttribute_Insert ON dbo.ObjectAttribute
AFTER INSERT
AS
DECLARE #objectsUnderConsideration TABLE (ObjectId INT PRIMARY KEY);
INSERT INTO #objectsUnderConsideration(ObjectId)
SELECT DISTINCT ObjectId FROM inserted;
DECLARE #expectedObjectAttributeEntries TABLE (ObjectId INT, AttributeId INT);
INSERT INTO #expectedObjectAttributeEntries(ObjectId, AttributeId)
SELECT o.ObjectId, a.Id AS AttributeId
FROM #objectsUnderConsideration o
CROSS JOIN Attribute a; -- cartisean join, objects * attributes
DECLARE #totalNumberOfAttributes INT = (SELECT COUNT(1) FROM Attribute);
-- ensure we got what we expect to get
DECLARE #expectedCount INT, #actualCount INT;
SET #expectedCount = (SELECT COUNT(*) FROM #expectedObjectAttributeEntries);
SET #actualCount = (
SELECT COUNT(*)
FROM #expectedObjectAttributeEntries e
INNER JOIN inserted i ON e.AttributeId = i.AttributeId AND e.ObjectId = i.ObjectId
); -- if an attribute is missing, we'll have too few; if an object is being entered twice, we'll have too many
IF #expectedCount < #actualCount
BEGIN
RAISERROR ('Invalid insertion: incomplete set of attribute values', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END
ELSE IF #expectedCount > #actualCount
BEGIN
RAISERROR ('Invalid insertion: multiple entries for same object', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END
-- passed the check that we have all the necessary attributes; now check for duplicates
ELSE
BEGIN
-- for each object, count exact duplicate preexisting entries; reject if every attribute is a dup
DECLARE #duplicateAttributeCount TABLE (ObjectId INT, DupCount INT);
INSERT INTO #duplicateAttributeCount(ObjectId, DupCount)
SELECT o.ObjectId, (
SELECT COUNT(1)
FROM inserted i
INNER JOIN ObjectAttribute oa
ON i.AttributeId = oa.AttributeId
AND i.ObjectId = oa.ObjectId
AND i.Value = oa.Value
AND i.Id <> oa.Id
WHERE oa.ObjectId = o.ObjectId
)
FROM #objectsUnderConsideration o
IF EXISTS (
SELECT 1
FROM #duplicateAttributeCount d
WHERE d.DupCount = #totalNumberOfAttributes
)
BEGIN
RAISERROR ('Invalid insertion: duplicates pre-existing entry', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END
END
GO
The above is not tested; thinking about it, you may need to join out to Object and organize your tests by OrgId instead of ObjectId. You would also need comparable triggers for UPDATE and DELETE. But, hopefully this is at least enough to get you started.
You should consider which Sql Sever edition do you use, this has limitations on indexed views.
see: http://msdn.microsoft.com/en-us/library/cc645993(SQL.110).aspx#RDBMS_mgmt
See indexed views direct querying.
The following steps are required to create an indexed view and are critical to the successful implementation of the indexed view:
1-Verify the SET options are correct for all existing tables that will be referenced in the view.
2-Verify the SET options for the session are set correctly before creating any new tables and the view.
3-Verify the view definition is deterministic.
4-Create the view by using the WITH SCHEMABINDING option.
5-Create the unique clustered index on the view.
Required SET Options for Indexed Views
Evaluating the same expression can produce different results in the Database Engine if different SET options are active when the query is executed. For example, after the SET option CONCAT_NULL_YIELDS_NULL is set to ON, the expression 'abc ' + NULL returns the value NULL. However, after CONCAT_NULL_YIEDS_NULL is set to OFF, the same expression produces 'abc '.

Inserting random number of rows in SQL Server via join to integer list is inconsistent

I am creating a database with sample data. Each time I run the stored procedure to generate some new data for my sample database, I would like to clear out and repopulate table B ("Item") based on all the rows in table A ("Product").
If table A contained the rows with primary key values 1, 2, 3, 4, and 5, I would want table B to have a foreign key for table A and insert a random number of rows into table B for each table A row. (We are essentially stocking the shelves with a random number of "item" for any given "product.")
I am using code from this answer to generate a list of numbers. I join to the results of this function to create the rows to insert:
WITH cte AS
(
SELECT
ROW_NUMBER() OVER (ORDER BY (select 0)) AS i
FROM
sys.columns c1 CROSS JOIN sys.columns c2 CROSS JOIN sys.columns c3
)
SELECT i
FROM cte
WHERE
i BETWEEN #p_Min AND #p_Max AND
i % #p_Increment = 0
Random numbers are generated in a view (to get around the limitations of functions) as follows:
-- Mock.NewGuid view
SELECT id = ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)))
And a function that returns the random numbers:
-- Mock.GetRandomInt(min, max) function definition
DECLARE #random int;
SELECT #random = Id % (#MaxValue - #MinValue + 1) FROM Mock.NewGuid;
RETURN #random + #MinValue;
However, when you look at this code and execute it...
WITH Products AS
(
SELECT ProductId, ItemCount = Mock.GetRandomInt(1,5)
FROM Product.Product
)
SELECT A = Products.ProductId, B = i
FROM Products
JOIN (SELECT i FROM Mock.GetIntList(1,5,1)) Temp ON
i < Products.ItemCount
ORDER BY ProductId, i
... this returns some inconsistent results!
A,B
1,1
1,2
1,3
2,1
2,2
3,2 <-- where is 1?
3,3
4,1
5,3 <-- where is 1, 2?
6,1
I would expect that, for every product id, the JOIN results in 1-5 rows. However, it seems like values get skipped! This is even more apparent with larger data sets. I was originally trying to generate 20-50 rows in Item for each Product row, but this resulted in only 30-40 rows for each product.
The question: Any idea why this is happening? Each product should have a random number of rows (between 1 and 5) inserted for it and the B value should be sequential! Instead, some numbers are missing!
This issue also happens if I store numbers in a table I created and then join to that, or if I use a recursive CTE.
I am using SQL Server 2008R2, but I believe I see the same issue on my 2012 database as well. Compatibility levels are 2008 and 2012 respectively.
This is a fun problem. I've dealt with this in a round about way a number of times. I am sure there is a way to not use a cursor. But why not. This is a cheap problem memory wise so long as the #RandomMaxRecords doesn't get huge or you have a significant amount of product records. If the data in the Items table is meaningless then I would suggest truncating any in memory table where I define the hash table for #Item. And obviously you will pull from your Product table not the hash I have created for testing.
This is a fantastic article and describes in detail how I arrive at my solution. Less Than Dot Blog
CODE
--This is your product table with 5 random products
IF OBJECT_ID('tempdb..#Product') IS NOT NULL DROP TABLE #Product
CREATE TABLE #Product
(
ProductID INT PRIMARY KEY IDENTITY(1,1),
ProductName VARCHAR(25),
ProductDescription VARCHAR(max)
)
INSERT INTO #Product (ProductName,ProductDescription) VALUES ('Product Name 1','Product Description 1'),
('Product Name 2','Product Description 2'),
('Product Name 3','Product Description 3'),
('Product Name 4','Product Description 4'),
('Product Name 5','Product Description 5')
--This is your item table. This would probably just be a truncate statement so that your table is reset for the new values to go in
IF OBJECT_ID ('tempdb..#Item') IS NOT NULL DROP TABLE #Item
CREATE TABLE #Item
(
ItemID INT PRIMARY KEY IDENTITY(1,1),
FK_ProductID INT NOT NULL,
ItemName VARCHAR(25),
ItemDescription VARCHAR(max)
)
--Declare a bunch of variables for the cursor and insert into the item table process
DECLARE #ProductID INT
DECLARE #ProductName VARCHAR(25)
DECLARE #ProductDescription VARCHAR(max)
DECLARE #RandomItemCount INT
DECLARE #RowEnumerator INT
DECLARE #RandomMaxRecords INT = 10
--We declare a cursor to iterate over the records in product and generate random amounts of items
DECLARE ItemCursor CURSOR
FOR SELECT * FROM #Product
OPEN ItemCursor
FETCH NEXT FROM ItemCursor INTO #ProductID, #ProductName, #ProductDescription
WHILE (##FETCH_STATUS <> -1)
BEGIN
--Get the Random Number into the variable. And we only want 1 or more records. Mod division will produce a 0.
SELECT #RandomItemCount = ABS(CHECKSUM(NewID())) % #RandomMaxRecords
SELECT #RandomItemCount = CASE #RandomItemCount WHEN 0 THEN 1 ELSE #RandomItemCount END
--Iterate on the RowEnumerator to the RandomItemCount and insert item rows
SET #RowEnumerator = 1
WHILE (#RowEnumerator <= #RandomItemCount)
BEGIN
INSERT INTO #Item (FK_ProductID,ItemName,ItemDescription)
SELECT #ProductID, REPLACE(#ProductName,'Product','Item'),REPLACE(#ProductDescription,'Product','Item')
SELECT #RowEnumerator = #RowEnumerator + 1
END
FETCH NEXT FROM ItemCursor INTO #ProductID, #ProductName, #ProductDescription
END
CLOSE ItemCursor
DEALLOCATE ItemCursor
GO
--Look at the result
SELECT
*
FROM
#Product AS P
RIGHT JOIN #Item AS I ON (P.ProductID = I.FK_ProductID)
--Cleanup
DROP TABLE #Product
DROP TABLE #Item
It looks like a LEFT OUTER JOIN to GetIntList (as opposed to INNER JOIN) fixes the problem I am having.

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