What columns can be used in OUTPUT INTO clause? - sql-server

I'm trying to build a mapping table to associate the IDs of new rows in a table with those that they're copied from. The OUTPUT INTO clause seems perfect for that, but it doesn't seem to behave according to the documentation.
My code:
DECLARE #Missing TABLE (SrcContentID INT PRIMARY KEY )
INSERT INTO #Missing
( SrcContentID )
SELECT cshadow.ContentID
FROM Private.Content AS cshadow
LEFT JOIN Private.Content AS cglobal ON cshadow.Tag = cglobal.Tag
WHERE cglobal.ContentID IS NULL
PRINT 'Adding new content headers'
DECLARE #Inserted TABLE (SrcContentID INT PRIMARY KEY, TgtContentID INT )
INSERT INTO Private.Content
( Tag, Description, ContentDate, DateActivate, DateDeactivate, SortOrder, CreatedOn, IsDeleted, ContentClassCode, ContentGroupID, OrgUnitID )
OUTPUT cglobal.ContentID, INSERTED.ContentID INTO #Inserted (SrcContentID, TgtContentID)
SELECT Tag, Description, ContentDate, DateActivate, DateDeactivate, SortOrder, CreatedOn, IsDeleted, ContentClassCode, ContentGroupID, NULL
FROM Private.Content AS cglobal
INNER JOIN #Missing AS m ON cglobal.ContentID = m.SrcContentID
Results in the error message:
Msg 207, Level 16, State 1, Line 34
Invalid column name 'SrcContentID'.
(line 34 being the one with the OUTPUT INTO)
Experimentation suggests that only rows that are actually present in the target of the INSERT can be selected in the OUTPUT INTO. But this contradicts the docs in the books online. The article on OUTPUT Clause has example E that describes a similar usage:
The OUTPUT INTO clause returns values
from the table being updated
(WorkOrder) and also from the Product
table. The Product table is used in
the FROM clause to specify the rows to
update.
Has anyone worked with this feature?
(In the meantime I've rewritten my code to do the job using a cursor loop, but that's ugly and I'm still curious)

You can do this with a MERGE in Sql Server 2008. Example code below:
--drop table A
create table A (a int primary key identity(1, 1))
insert into A default values
insert into A default values
delete from A where a>=3
-- insert two values into A and get the new primary keys
MERGE a USING (SELECT a FROM A) AS B(a)
ON (1 = 0) -- ignore the values, NOT MATCHED will always be true
WHEN NOT MATCHED THEN INSERT DEFAULT VALUES -- always insert here for this example
OUTPUT $action, inserted.*, deleted.*, B.a; -- show the new primary key and source data
Result is
INSERT, 3, NULL, 1
INSERT, 4, NULL, 2
i.e. for each row the new primary key (3, 4) and the old one (1, 2). Creating a table called e.g. #OUTPUT and adding " INTO #OUTPUT;" at the end of the OUTPUT clause would save the records.

I've verified that the problem is that you can only use INSERTED columns. The documentation seems to indicate that you can use from_table_name, but I can't seem to get it to work (The multi-part identifier "m.ContentID" could not be bound.):
TRUNCATE TABLE main
SELECT *
FROM incoming
SELECT *
FROM main
DECLARE #Missing TABLE (ContentID INT PRIMARY KEY)
INSERT INTO #Missing(ContentID)
SELECT incoming.ContentID
FROM incoming
LEFT JOIN main
ON main.ContentID = incoming.ContentID
WHERE main.ContentID IS NULL
SELECT *
FROM #Missing
DECLARE #Inserted TABLE (ContentID INT PRIMARY KEY, [Content] varchar(50))
INSERT INTO main(ContentID, [Content])
OUTPUT INSERTED.ContentID /* incoming doesn't work, m doesn't work */, INSERTED.[Content] INTO #Inserted (ContentID, [Content])
SELECT incoming.ContentID, incoming.[Content]
FROM incoming
INNER JOIN #Missing AS m
ON m.ContentID = incoming.ContentID
SELECT *
FROM #Inserted
SELECT *
FROM incoming
SELECT *
FROM main
Apparently the from_table_name prefix is only allowed on DELETE or UPDATE (or MERGE in 2008) - I'm not sure why:
from_table_name
Is a column prefix that specifies a table included in the FROM clause of a DELETE or UPDATE statement that is used to specify the rows to update or delete.
If the table being modified is also specified in the FROM clause, any reference to columns in that table must be qualified with the INSERTED or DELETED prefix.

I'm running into EXACTLY the same problem as you are, I feel your pain...
As far as I've been able to find out there's no way to use the from_table_name prefix with an INSERT statement.
I'm sure there's a viable technical reason for this, and I'd love to know exactly what it is.
Ok, found it, here's a forum post on why it doesn't work:
MSDN forums

I think I found a solution to this problem, it sadly involves a temporary table, but at least it'll prevent the creation of a dreaded cursor :)
What you need to do is add an extra column to the table you're duplicating records from and give it a 'uniqueidentifer' type.
then declare a temporary table:
DECLARE #tmptable TABLE (uniqueid uniqueidentifier, original_id int, new_id int)
insert the the data into your temp table like this:
insert into #tmptable
(uniqueid,original_id,new_id)
select NewId(),id,0 from OriginalTable
the go ahead and do the real insert into the original table:
insert into OriginalTable
(uniqueid)
select uniqueid from #tmptable
Now to add the newly created identity values to your temp table:
update #tmptable
set new_id = o.id
from OriginalTable o inner join #tmptable tmp on tmp.uniqueid = o.uniqueid
Now you have a lookup table that holds the new id and original id in one record, for your using pleasure :)
I hope this helps somebody...

(MS) If the table being modified is also specified in the FROM clause, any reference to columns in that table must be qualified with the INSERTED or DELETED prefix.
In your example, you can't use cglobal table in the OUTPUT unless it's INSERTED.column_name or DELETED.column_name:
INSERT INTO Private.Content
(Tag)
OUTPUT cglobal.ContentID, INSERTED.ContentID
INTO #Inserted (SrcContentID, TgtContentID)
SELECT Tag
FROM Private.Content AS cglobal
INNER JOIN #Missing AS m ON cglobal.ContentID = m.SrcContentID
What worked for me was a simple alias table, like this:
INSERT INTO con1
(Tag)
OUTPUT **con2**.ContentID, INSERTED.ContentID
INTO #Inserted (SrcContentID, TgtContentID)
SELECT Tag
FROM Private.Content con1
**INNER JOIN Private.Content con2 ON con1.id=con2.id**
INNER JOIN #Missing AS m ON con1.ContentID = m.SrcContentID

Related

SQL capture output from Merge into temp table

I followed this code from another post:
MERGE [Destination] AS d
USING (SELECT * FROM #SourceForMerge) AS s
ON d.PrimaryKey = s.PrimaryKey
WHEN MATCHED THEN
-- UPDATE columns
WHEN NOT MATCHED THEN
-- INSERT columns
OUTPUT
-- columns
INTO
#OutputTempTable
I was using an actual table before (with a INSERT INTO <Table> before the merge, but I added a foreign key, which is not allowed. So I am trying to import into a temp table , then into my main table. The documentation says I can use a temp table and that the column list is not mandatory. However , I can't even get it to work , even if I declare the columns.
OUTPUT #BatchId , $action into #OutputTempTable ( batchid, what ) ;
I am getting error
Invalid object name '#OutputTempTable'
I know I am probably missing something basic, but been a long week.
Thanks

SQL Server : DELETE FROM table FROM table

I keep coming across this DELETE FROM FROM syntax in SQL Server, and having to remind myself what it does.
DELETE FROM tbl
FROM #tbl
INNER JOIN tbl ON fk = pk AND DATEDIFF(day, #tbl.date, tbl.Date) = 0
EDIT: To make most of the comments and suggested answers make sense, the original question had this query:
DELETE FROM tbl
FROM tbl2
As far as I understand, you would use a structure like this where you are restricting which rows to delete from the first table based on the results of the from query. But to do that you need to have a correlation between the two.
In your example there is no correlation, which will effectively be a type of cross join which means "for every row in tbl2, delete every row in tbl1". In other words it will delete every row in the first table.
Here is an example:
declare #t1 table(A int, B int)
insert #t1 values (15, 9)
,(30, 10)
,(60, 11)
,(70, 12)
,(80, 13)
,(90, 15)
declare #t2 table(A int, B int)
insert #t2 values (15, 9)
,(30, 10)
,(60, 11)
delete from #t1 from #t2
The result is an empty #t1.
On the other hand this would delete just the matching rows:
delete from #t1 from #t2 t2 join #t1 t1 on t1.A=t2.A
I haven't seen this anywhere before. The documentation of DELETE tells us:
FROM table_source Specifies an additional FROM clause. This
Transact-SQL extension to DELETE allows specifying data from
and deleting the corresponding rows from the table in
the first FROM clause.
This extension, specifying a join, can be used instead of a subquery
in the WHERE clause to identify rows to be removed.
Later in the same document we find
D. Using joins and subqueries to data in one table to delete rows in
another table The following examples show two ways to delete rows in
one table based on data in another table. In both examples, rows from
the SalesPersonQuotaHistory table in the AdventureWorks2012 database
are deleted based on the year-to-date sales stored in the SalesPerson
table. The first DELETE statement shows the ISO-compatible subquery
solution, and the second DELETE statement shows the Transact-SQL FROM
extension to join the two tables.
With these examples to demonstrate the difference
-- SQL-2003 Standard subquery
DELETE FROM Sales.SalesPersonQuotaHistory
WHERE BusinessEntityID IN
(SELECT BusinessEntityID
FROM Sales.SalesPerson
WHERE SalesYTD > 2500000.00);
-- Transact-SQL extension
DELETE FROM Sales.SalesPersonQuotaHistory
FROM Sales.SalesPersonQuotaHistory AS spqh
INNER JOIN Sales.SalesPerson AS sp
ON spqh.BusinessEntityID = sp.BusinessEntityID
WHERE sp.SalesYTD > 2500000.00;
The second FROM mentions the same table in this case. This is a weird way to get something similar to an updatable cte or a derived table
In the third sample in section D the documentation states clearly
-- No need to mention target table more than once.
DELETE spqh
FROM
Sales.SalesPersonQuotaHistory AS spqh
INNER JOIN Sales.SalesPerson AS sp
ON spqh.BusinessEntityID = sp.BusinessEntityID
WHERE sp.SalesYTD > 2500000.00;
So I get the impression, the sole reason for this was to use the real table's name as the DELETE's target instead of an alias.

How can I fix the error : Multi-Part ID when update a field of #tempTable

I want to compare #tempTable with tableA, if matched then update FieldValue of #tempTable equal to id of tableA; if not matched then insert values ,and get the id of tableA to update FieldValue of #tempTable.
The following is my SQL query:
create table [dbo].[TableA]([Id] int Indentity(1,1),[Data] [sql_variant] NULL)
declare #tempTable table (FieldValue nvarchar(1000),FieldType nvarchar(1000))
insert #tempTable values ('some content','A Type')
merge
tableA
using (
select
FieldValue
from
#tempTable
) x
on tableA.[Data] = x.FieldValue
when not matched then
insert values (x.FieldValue)
when matched then
update set x.FieldValue = tableA.[Id] ;
The following is the error message:
Unable to tie the Multi-Part ID "x.FieldValue".
The error looks like it's different data type between x.FieldValue and tableA.id, so I adjust them to the same data type, but it's still not work, and I don't know how to fix it.
What are you trying to achieve? Your target table is tableA, your source table is the x-aliased sub-query.
Next time please try to create a mcve. I had to modify your code slightly, but you will get the ghist:
declare #TableA TABLE([Id] int,[Data] [sql_variant] NULL)
declare #tempTable table (FieldValue nvarchar(1000),FieldType nvarchar(1000))
insert #tempTable values ('some content','A Type');
merge
#tableA a
using (
select
FieldValue
from
#tempTable
) x
on a.[Data] = x.FieldValue
when matched then
update set a.id = x.FieldValue; --fields swapped, probably not really your intention...
The point is: Your code tries to update a field of your source table. The general idea of MERGE is
We target one table where we want to insert/update/delete some records.
We use a second set with the data we want to compare and use for these operations
We find rows within the source which are missing in the target -> INSERT
We find rows within the source which are existing in the target -> UPDATE
We find rows whithin the target which are missing in the source -> DELETE
Saying this, I doubt, that the code above would need MERGE at all...
The following can achieve the results I want.
merge
tableA
using (
select
FieldValue
from
#tempTable
) x
on tableA.[Data] = x.FieldValue
when not matched then
insert values (x.FieldValue);
update #tempTable
set t.FieldValue = i.[Id]
from #tempTable t
join TableA i ON i.[Data] = t.FieldValue
select * from #tempTable

Copy complex table using stored procedure

I writing a stored procedure to copy rows in a table.
This is the table
I want to copy this but the ParentId should be linked to the new row.
If i do a simple INSERT INTO > SELECT FROM the ParentId will be linked to the ProductId 22 not the new ProductId as you can see above.
Any suggestion?
Your question is not completely clear, but if I understand it correctly, you are trying to copy several rows that build a hierarchy while preserving that hierarchy.
This cannot be done in one step. You need to first copy the rows and record the new and their matching old ids. Then you can update the references in the new rows to point to the new parents.
The simplest way to do this is using the MERGE statement:
CREATE TABLE dbo.tst(id INT IDENTITY(1,1), parent_id INT, other INT);
INSERT INTO dbo.tst(parent_id, other)VALUES(NULL,1);
INSERT INTO dbo.tst(parent_id, other)VALUES(1,2);
INSERT INTO dbo.tst(parent_id, other)VALUES(1,3);
INSERT INTO dbo.tst(parent_id, other)VALUES(3,4);
INSERT INTO dbo.tst(parent_id, other)VALUES(NULL,5);
INSERT INTO dbo.tst(parent_id, other)VALUES(5,6);
CREATE TABLE #tmp(old_id INT, new_id INT);
MERGE dbo.tst AS trg
USING dbo.tst AS src
ON (0=1)
WHEN NOT MATCHED
AND (src.id >= 1) --here you can put your own WHERE clause.
THEN
INSERT(parent_id, other)
VALUES(src.parent_id, src.other)
OUTPUT src.id, INSERTED.id INTO #tmp(old_id, new_id);
UPDATE trg SET
parent_id = tmp_translate.new_id
FROM dbo.tst AS trg
JOIN #tmp AS tmp_filter
ON trg.id = tmp_filter.new_id
JOIN #tmp AS tmp_translate
ON trg.parent_id = tmp_translate.old_id;
SELECT * FROM dbo.tst;
The line with the comment is the place where you can put your own where clause to select the rows that you want to copy. make sure to actually copy all referenced parents. If you copy a child without its parent the update will not catch it and it will point to the old parent in the end.
You also should wrap the MERGE and the UPDATE in a transaction to prevent someone else from reading the new and not yet finished records.
You can use SELECT to do this, you just need to manually specify the order of the columns. Here is an example, assuming that your table is called Product and that the ProductId is auto incremented. Notice that the first column returned in the SELECT is the primary key of the old row.
INSERT dbo.Product
SELECT
ProductId,
ArtNo,
[Description]
Specification,
Unit,
Account,
NetPrice
OhTime
FROM dbo.Product AS P
WHERE P.ParentId = 22
Does that help?

Copy table rows using OUTPUT INTO in SQL Server 2005

I have a table which I need to copy records from back into itself. As part of that, I want to capture the new rows using an OUTPUT clause into a table variable so I can perform other opertions on the rows as well in the same process. I want each row to contain its new key and the key it was copied from. Here's a contrived example:
INSERT
MyTable (myText1, myText2) -- myId is an IDENTITY column
OUTPUT
Inserted.myId,
Inserted.myText1,
Inserted.myText2
INTO
-- How do I get previousId into this table variable AND the newly inserted ID?
#MyTable
SELECT
-- MyTable.myId AS previousId,
MyTable.myText1,
MyTable.myText2
FROM
MyTable
WHERE
...
SQL Server barks if the number of columns on the INSERT doesn't match the number of columns from the SELECT statement. Because of that, I can see how this might work if I added a column to MyTable, but that isn't an option. Previously, this was implemented with a cursor which is causing a performance bottleneck -- I'm purposely trying to avoid that.
How do I copy these records while preserving the copied row's key in a way that will achieve the highest possible performance?
I'm a little unclear as to the context - is this in an AFTER INSERT trigger.
Anyway, I can't see any way to do this in a single call. The OUTPUT clause will only allow you to return rows that you have inserted. What I would recommend is as follows:
DECLARE #MyTable (
myID INT,
previousID INT,
myText1 VARCHAR(20),
myText2 VARCHAR(20)
)
INSERT #MyTable (previousID, myText1, myText2)
SELECT myID, myText1, myText2 FROM inserted
INSERT MyTable (myText1, myText2)
SELECT myText1, myText2 FROM inserted
-- ##IDENTITY now points to the last identity value inserted, so...
UPDATE m SET myID = i.newID
FROM #myTable m, (SELECT ##IDENTITY - ROW_NUMBER() OVER(ORDER BY myID DESC) + 1 AS newID, myID FROM inserted) i
WHERE m.previousID = i.myID
...
Of course, you wouldn't put this into an AFTER INSERT trigger, because it will give you a recursive call, but you could do it in an INSTEAD OF INSERT trigger. I may be wrong on the recursive issue; I've always avoid the recursive call, so I've never actually found out. Using ##IDENTITY and ROW_NUMBER(), however, is a trick I've used several times in the past to do something similar.

Resources