Schema Change: Convert field to foreign key on a different table - sql-server

I currently have a table like this:
Stuff
----------
StuffId identity int not null
Description nvarchar(4000) null
...
I want to store the Description in a separate table that I have set aside specifically for user-generated content:
Content
----------
ContentId identity int not null
Content nvarchar(max) not null
...
(this table already exists, and other tables already reference entries in it.)
So I need to:
Create a DescriptionContentId field on the Stuff table with a foreign key constraint.
Copy the current Description content into the Content table.
Set each DescriptionContentId to have the ContentId value that was automatically generated when inserting values in step 2.
Drop the Description column.
I know how to do steps 1 and 4, but steps 2 and 3 are eluding me, because they need to be done pretty much simultaneously. This seems like it would be a fairly common schema change. What's the best way to do it?
Update
I'm a step closer thanks to the Output keyword, but I'm still missing something. Here's what I'd like to do:
create table #tmp (StuffId int, ContentId int)
insert into Content(Content)
output s.StuffId, inserted.ContentId
into #tmp(StuffId, ContentId)
select Description
from Stuff s
where Description IS NOT NULL
But I can't reference s.StuffId because it isn't one of the fields inserted into the Content table. How can I correlate the ID of the Stuff with the ID of the Content as I'm inserting a new Content item for each Stuff entry?

The output clause will come to your rescue.
It will output the description and the identity column from the insert into a table varaible and then you can use that data to update the other table.
If description is not unique, you may have to do the following:
add a column for the stuffID column to the content table. Then output the stuffid and content id from the insert, update the table using the stuffid to ensure uniqueness, Drop the stuffid column from the content table.
an example from Books Online as to how to use the OUTPUT
DECLARE #MyTableVar table(
LastName nvarchar(20) NOT NULL,
FirstName nvarchar(20) NOT NULL,
CurrentSales money NOT NULL
);
INSERT INTO dbo.EmployeeSales (LastName, FirstName, CurrentSales)
OUTPUT INSERTED.LastName,
INSERTED.FirstName,
INSERTED.CurrentSales
INTO #MyTableVar
SELECT c.LastName, c.FirstName, sp.SalesYTD
FROM HumanResources.Employee AS e
INNER JOIN Sales.SalesPerson AS sp
ON e.EmployeeID = sp.SalesPersonID
INNER JOIN Person.Contact AS c
ON e.ContactID = c.ContactID
WHERE e.EmployeeID LIKE '2%'
ORDER BY c.LastName, c.FirstName;

Related

How to make reference to another tables? (SQLite)

I am making a list of Images with Tags so I decided to make a SQLite Database with ImagesPath and Tags. I know that I have to combine these two tables in the state many do many but i really don't know how.
I have three tables:
1)ImagesPath (INT id, TEXT imagePath)
2)Tags(INT id, TEXT title)
3)ImageTag(INT id, image_id, tag_id)
in this fields I have to reference to ImagesPath.id and Tags.id but I dont know how. Can anybody help me?
You use JOIN's for example you could use :-
SELECT imagePath, title FROM ImagesPath
JOIN ImageTag ON image_id = ImagesPath.id
JOIN Tags ON tag_id = Tags.id
join-clause
Working Example
Consider the following which drops and creates the 3 tables and then populates the tables and then finally runs a query :-
DROP TABLE IF EXISTS ImagesPath;
DROP TABLE IF EXISTS Tags;
DROP TABLE IF EXISTS ImageTag;
CREATE TABLE IF NOT EXISTS ImagesPath (id INTEGER, ImagePath TEXT);
CREATE TABLE IF NOT EXISTS Tags (id INTEGER, title TEXT);
CREATE TABLE IF NOT EXISTS ImageTag (id INTEGER, Image_id INTEGER, Tag_id INTEGER);
INSERT INTO ImagesPath (id,Imagepath) VALUES (1,'Image1'),(2,'Image2'),(3,'Image3');
INSERT INTO Tags (id,title) VALUES (1,'TagA'),(2,'TagB'),(3,'TagC');
INSERT INTO ImageTag (Image_id,Tag_id) VALUES (1,3),(2,1),(2,2),(2,3),(3,1),(3,3);
SELECT imagePath, title FROM ImagesPath
JOIN ImageTag ON image_id = ImagesPath.id
JOIN Tags ON tag_id = Tags.id;
The result will be :-
Note that no values have been assigned to the id column of the ImageTag table, this value would probably be of little use as the ImageTag table is basically mapping image to tag(s) (allowing many to many relationships).
Note that you have to be a little careful with amibiguities e.g. there are 3 id columns hence id being prefixed with the table name and a period seperator as per ImagesPath.id and also Tags.id

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....

How to create a copy of data when the data has a hierarchy

I have a table structure where there are FK columns in child tables.
So say there is the following:
Company
-company_id
-name
Location
-location_id
-company_name
-name
Store
-store_id
-location_id
-name
Inventory
-inventory_id
-store_id
Now I want to create a copy of a company, along with all of location, store and inventory rows.
So say I want to create a copy of company_id=123, I have to duplicate all the rows.
I tried this:
DECLARE #OriginalCompanyId = 123
DECALRE #companyId AS INT
INSERT Companies (name)
select c.name
from companies c
where c.companyId = #OrignalCOmpanyId
SET #companyId = SCOPE_IDENTITY()
But this approach won't work because the other tables have multiple rows and I won't be able to linkup the newly inserted PK values.
What approach should I be taking?
I've actually been working on a project that does just this. My solution while not fancy, has so far proven effective.. the annoying part being the setup process. I am very open to critique and suggestions for improvement.
Create a "mirror" schema/db of all the necessary tables (I've gone with New[ApplicationTableName])
For each pKey/fKey, create a "placeholder" column (I've gone with p[ColumnName])
Map the existing data to placeholder keys, indexed at 1. (This is annoying, but doable with ranking functions.)
Insert into the application by your placeholder keys in descending order (descending is important!)
Update your "mirror" table using ranking functions (see example)
Repeat as necessary using the drived/inserted values across however many tables you need.
Example:
Given this schema...
CREATE TABLE Accounts (
AccountID int identity(1,1) not null,
Name varchar(500) not null
)
CREATE TABLE Users(
UserID int identity(1,1) not null,
AccountID int not null,
Name varchar(500) not null
)
CREATE TABLE NewUsers(
pUserID int not null,
UserID int not null,
AccountID int not null,
Name varchar(500)
)
And this data
INSERT INTO NewUsers VALUES
(1,0,0,'Bob'),
(2,0,0,'Sally'),
(3,0,0,'Jeff'),
(4,0,0,'Sam')
Say for each time we "create" an account we want to create these 4 default users... This will look something like this
DECLARE #AccountID int --this is scalar, so we'll use scope_identity() to grab it.
INSERT INTO Account VALUES('MyNewAccountID')
SELECT #AccountID = SCOPE_IDENTITY()
--Prepare NewUsers w/ derived accountID
UPDATE NewUsers SET AccountID = #AccountID
--Do our "application" insert
INSERT INTO Users(AccountID,Name)
SELECT AccountID,Name
FROM NewUsers
ORDER BY pUserID DESC;
--Capture inserted ID's for use in other tables (where we've derived pUserID)
WITH InsertedUsers AS(
SELECT
--use dense rank, it handles fkey mappings too
DENSE_RANK() OVER(ORDER BY UserID DESC) as pUserID,
UserID
FROM Users
)
UPDATE NewUsers SET UserID = iu.UserID
FROM NewUsers nu
JOIN InsertedUsers iu
ON iu.pUserID = nu.pUserID
SELECT TOP 100 * FROM Account ORDER BY 1 DESC
SELECT TOP 100 * FROM Users ORDER BY 1 DESC
So now if a future table needs UserID into the app (and has a derived pUserID,) we can grab it from NewUsers by joining on pUserID.

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