How to get ID from one table and associate with record in another table in SQL Server - sql-server

I've tried searching for the answer to this one to no avail. There is no good logic behind the way this was setup. The guy does not know what he's doing, but it's what I have to work with (long story).
I'm using SQL Server 2008R2 I need to take records from one table and transfer the data to 4 separate tables all with a one to one relationship (I know - not smart). I need to get the value from the Identity field in the first table the data is inserted into, then populate the other 3 tables with the same ID and disperse the data accordingly. for example:
OldTable: Field1, Field2, Field3, Field4
NewTable1: Identity field, Field1
NewTable2: ID, Field2
NewTable3: ID, Field3
NewTable4: ID, Field4
I'd like to handle this in a stored procedure. I'd like to do a loop, but I read that loops in SQL are inadvisable.
Loop moving through each record in OldTable... (??)
INSERT INTO NewTable1
(Field1)
Select Field1 from OldTable
INSERT INTO NewTable2
(ID, Field2)
Select SCOPE_IDENTITY?, Field2 From OldTable Where OldTable.ID = ??
etc for other 2 tables
Loop to next record in OldTable
I am not sure how to use SCOPE_IDENTITY, but I have a feeling this will be involved in how I accomplish this.
Also, I'm probably going to need to setup a trigger for whenever a new record is created in NewTable1. I know, it's insanity, but I can't do anything about it, just have to work around it.
So, I need to know
1: the best way to initially populate the tables
2: how to make triggers for new records
The solution to 1 might involve 2.
Please help!

You can use the output clause of the merge statement to get a mapping between the existing primary key in OldTable and the newly generated identity ID in NewTable1.
-- Temp table to hold the mapping between OldID and ID
create table #ID
(
OldID int primary key,
ID int
);
-- Add rows to NewTable1 and capture the ID's in #ID
merge NewTable1 as T
using OldTable as S
on 1 = 0
when not matched by target then
insert(Field1) values(S.Field1)
output S.ID, inserted.ID into #ID(OldID, ID);
-- Add rows to NewTable2 using #ID to get the correct value for each row
insert into NewTable2(ID, Field2)
select I.ID, O.Field2
from #ID as I
inner join OldTable as O
on I.OldID = O.ID
insert into NewTable3(ID, Field3)
select I.ID, O.Field3
from #ID as I
inner join OldTable as O
on I.OldID = O.ID
insert into NewTable4(ID, Field4)
select I.ID, O.Field4
from #ID as I
inner join OldTable as O
on I.OldID = O.ID
drop table #ID;
SQL Fiddle
See also Using merge..output to get mapping between source.id and target.id

How about using the OUTPUT clause of the insert statement? Assuming that Field1 is a unique key on the OldTable...
Declare #IDinserted table(ID int, Field1 varchar(255));
Insert Into NewTable1(Field1)
Output inserted.ID, inserted.Field1 into #IDinserted
Select OldID, Field1 from OldTable;
Insert Into NewTable2(RowID, Field2)
Select i.ID, o.#Field2
from #IDinserted i Inner Join OldTable o
on i.Field1=o.Field1;

Related

Copy whole row excluding identifier column

I'm trying to insert a new row into a table which is an exact copy of another row except for the identifier. Previously I hadn't had the issue because there was an ID-column which didn't fill automatically. So I could just copy a row like this
INSERT INTO table1 SELECT * FROM table1 WHERE Id = 5
And then manually change the ID like this
WITH tbl AS (SELECT ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Id) AS RNr, Id
FROM table1 WHERE Id = 5
UPDATE tbl SET Id = (SELECT MAX(Id) FROM table1) + 1
WHERE RNr = 2
Recently the column Id has changed to not be filled manually (which I also like better). But this leads to the error that I obviously can't fill that column while IDENTITY_INSERT is false. Unfortunately, I don't have the right to use SET IDENTITY_INSERT IdentityTable ON/OFF before and after the statement, which I also would like to avoid anyways.
I'm looking for a way to insert the whole row excluding the Id column without naming each other column in the INSERT and SELECT statement (which are quite a lot).
In the code below the maximum value of the ID gets added by one, so your integrity is not violated.
insert into table1
select a.Top_ID, Column2, Column3, ColumnX
from table1 t1
outer apply (select max(id_column) + 1as Top_ID from table1) as a
where t1.Id = 1
Okay, I found a way by using a temporary table
SELECT * INTO #TmpTbl
FROM table1 WHERE Id = 5;
ALTER TABLE #TmpTbl
DROP COLUMN Id;
INSERT INTO table1 SELECT * FROM #TmpTbl;
DROP TABLE #TmpTbl

SQL trigger with IDENTITY_INSERT

I have two tables: Table1 is all the companies, Table2 is companies whose name start with A.
Table1 company (companyId int, companyName varchar(50), companySize int)
Table2 companyStartWithA (companyId int, companyName varchar(50), companySize int)
What I want to do is to create a trigger so that when I insert/update/delete something in Table1, it will automatically do the same in Table2
My code:
CREATE TRIGGER A_TRG_InsertSyncEmp
ON company
AFTER INSERT
AS
BEGIN
INSERT INTO companyStartWithA
SELECT *
FROM INSERTED
WHERE inserted.companyName LIKE 'A%'
END
And I get an error:
An explicit value for the identity column in table 'companyStartWithA' can only be specified when a column list is used and IDENTITY_INSERT is ON.
What can I do?
Thanks
The problem is the fact that you're not explicitly specifying the column in the INSERT statement, and using a SELECT * to fill the data. Both are big no-no's - you should always explicitly specify the column that you want to insert into, and you should always explicitly specify the columns that you want to select. Doing so will fix this problem:
CREATE TRIGGER A_TRG_InsertSyncEmp
ON company
AFTER INSERT
AS
BEGIN
INSERT INTO companyStartWithA (companyName, companySize)
SELECT companyName, companySize
FROM INSERTED
WHERE inserted.companyName LIKE 'A%'
END
But as Sean Lange absolutely correctly commented - this should really be just a view rather than a separate table.....
CREATE VIEW dbo.CompanyStartsWithA
AS
SELECT companyId, companyName, companySize
FROM dbo.Company
WHERE Name LIKE 'A%'
and then you don't need any messy triggers or anything - just insert into dbo.Company and all companies with a name that starts with an A will be visible in this view....

SQL Server can you use EXCEPT or INTERSECT and ignore a column?

Here's my question,
CREATE TABLE
#table1(ID int, Fruit varchar(50), Veg varchar(50))
INSERT INTO #table1 (ID,Fruit,Veg)
VALUES (1,'Apple', 'Potato')
CREATE TABLE
#table2(ID int, Fruit varchar(50), Veg varchar(50))
INSERT INTO #table2 (ID,Fruit,Veg)
VALUES (2,'Apple', 'Potato')
SELECT * FROM #table1 INTERSECT SELECT * FROM #table2
I have two tables and I want to find rows which are the same in both, but both tables have different and unrelated ID columns. Is there any way to use INTERSECT or EXCEPT on two tables, but ignore the ID in the comparison?
I need to keep the ID's on the returned rows, so on the example above, two rows would be returned, one with ID = 1 and another with ID=2
If anything other than the ID's is different, then nothing would be returned.
Thanks!
I don't think this can be done with INTERSECT. Maybe with a join instead?
SELECT t1.id, t2.id, t1.veg, t1.fruit
FROM Table1 as t1
INNER JOIN Table2 as t2
ON t1.veg = t2.veg AND t1.fruit = t2.fruit

T-SQL: Best way to copy hierarchy data?

My database looks like this:
Questionnaire
Id
Description
Category
id
description
QuestionnaireId (FK)
Question
id
CategoryId (FK)
field
When I copy a questionnaire, I'd like to copy all the underlying tables. So this means that the table Questionnaire gets a new Id. Then, all the belonging categories of the questionnaire must also be copied. So the newly inserted categories must get the new questionnaire Id. After the categories, the questions must be copied. But the categoryId must be updated to the newly inserted category.
How can I do this using t-sql?
This is pretty easy to accomplish, but you have to keep track of everything as you go. I would generally create a single SP for this, which takes as an input the questionnaire to copy.
DECLARE #newQuestionnaireId INT
INSERT INTO Questionnaire
(Id,Description)
SELECT Id, Description
FROM Questionnaire
WHERE ID = #sourceQuestionnaireID
SET #newquestionnaireId = SCOPE_IDENTITY()
At this point you have a new header record, and the newly generated Id for the copy. The next step is to load the categories into a temp table which has an extra field for the new Id
DECLARE #tempCategories TABLE (id INT, description VARCHAR(50),newId INT)
INSERT INTO #tempCategories(id,description)
SELECT id, description FROM Category
WHERE questionnaireId = #sourceQuestionnaireId
Now, you have a temp table with all the categories to insert, along with a field to backfill the new ID for this category. Use a cursor to go over the list inserting the new record, and use a similar SCOPE_IDENTITY call to backfill the new Id.
DECLARE cuCategory CURSOR FOR SELECT Id, Description FROM #tempCategories
DECLARE #catId INT, #catDescription, #newCatId INT
OPEN cuCategory
FETCH NEXT FROM cuCategory INTO #catId,#catDescription
WHILE ##FETCH_STATUS<>0
BEGIN
INSERT INTO Category(description,questionnaireId)
VALUES(#catDescription,#newQuestionnaireId)
SET #newCatId = SCOPE_IDENTITY()
UPDATE #tempCategories SET newCatId=#newCatId
WHERE id=#catId
FETCH NEXT FROM cuCategory INTO #catId,#catDescription
END
CLOSE cuCategory
DEALLOCATE cuCategory
At this point you now have a temp table which maps the catId from the original questionnaire to the catId for the new questionnaire. This can be used to fill the final table in much the same way - which i'll leave as an excercise for you, but feel free to post back here if you have difficulty.
Finally, I would suggest that this whole operation is carried out within a transaction to save you from half completed copies when something goes wrong.
A couple of disclaimers: The above was all typed quickly, dont expect it to work off the bat. Second, Ive assumed that all your PK's are identity fields, which they should be! If they're not just replace the SCOPE_IDENTITY() calls with the appropriate logic to generate the next ID.
Edit: documentation for Cursor operations can be foundhere
I had a problem like this and began to implement the solution suggested by #Jamiec but I quickly realised that I needed a better solution because my model is much larger than that in the example cited here. I have one master table with three intermediate tables, each of which have one or more tertiary tables. And the three intermediates each had something like 50 columns. This would mean a lot of work to type all that up, particularly in the fetch part with the temporary memvars. I tried to find a way to FETCH directly into the temp table but it seems you cannot do that.
What I did was add a column to the intermediate tables called OriginalId. Here is my code translated into the model used by the asker:
DECLARE #newQuestionnaireId INT
INSERT INTO Questionnaire (Id,Description)
SELECT Id, Description FROM Questionnaire
WHERE ID = #sourceQuestionnaireID
SET #newquestionnaireId = SCOPE_IDENTITY()
INSERT INTO Category(QuestionnaireId, description, originalId)
SELECT #newquestionnaireId, description, id FROM Category
WHERE questionnaireId = #sourceQuestionnaireId
INSERT INTO Question SELECT Category.Id, Question.Field
FROM Question join Category on Question.CategoryId = Category.OriginalId
WHERE Category.QuestionnaireId = #newquestionnaireId
In my model the id fields are all Identities so you do not supply them in the inserts.
Another thing I discovered before I gave up on the CURSOR approach was this clever little trick to avoid having to type the FETCH statement twice by using an infinite WHILE loop with a BREAK:
here is a way that does not have cursors, it relies on remembering the order of events, and then using that to resolve the children.
Declare #Parrent TABLE( ID int PRIMARY KEY IDENTITY, Value nvarchar(50))
Declare #Child TABLE( ID int PRIMARY KEY IDENTITY, ParrentID int, Value nvarchar(50))
insert into #Parrent (Value) Values ('foo'),('bar'),('bob')
insert into #Child (ParrentID, Value) Values (1,'foo-1'),(1,'foo-2'),(2,'bar-1'),(2,'bar-2'),(3,'bob')
declare #parrentToCopy table (ID int) -- you can me this a collection
insert into #parrentToCopy values (2)
select * from #Parrent p inner join #Child c on p.ID = c.ParrentID order by p.ID asc, c.ID asc
DECLARE #Ids TABLE( nID INT);
INSERT INTO #Parrent (Value)
OUTPUT INSERTED.ID
INTO #Ids
SELECT
Value
FROM #Parrent p
inner join #parrentToCopy pc on pc.ID=p.ID
ORDER BY p.ID ASC
INSERT INTO #Child (ParrentID, Value)
SELECT
nID
,Value
FROM #Child c
inner join (select ID, ROW_NUMBER() OVER (ORDER BY ID ASC) AS 'RowNumber' from #parrentToCopy) o ON o.ID = c.ParrentID
inner join (select nID, ROW_NUMBER() OVER (ORDER BY nID ASC) AS 'RowNumber' from #Ids) n ON o.RowNumber = n.RowNumber
select * from #Parrent p inner join #Child c on p.ID = c.ParrentID order by p.ID asc, c.ID asc
full post is here http://bashamer.wordpress.com/2011/10/04/copying-hierarchical-data-in-sql-server/

What columns can be used in OUTPUT INTO clause?

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

Resources