Duplicate parent/child data - sql-server

I have some parent/child data across two tables. I need to copy the parent rows back into the parent table, but then also copy the child rows as child rows of the new rows created.
I have been searching this site and Google, but can only find examples from Oracle or that use XML (or have many warnings about not being reliable), so am posting here for a complete easy-to-refer-back-to solution.
Take the following code (SqlFiddle):
DECLARE #tbl_person TABLE
(
ID int IDENTITY(1,1),
person nvarchar(20)
);
DECLARE #tbl_drinks TABLE
(
ID int IDENTITY(1,1),
personID int,
drink nvarchar(20)
);
DECLARE #i int;
INSERT INTO #tbl_person (person) VALUES ('Bob');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Beer');
INSERT INTO #tbl_person (person) VALUES ('Wendy');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Champage');
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Water');
INSERT INTO #tbl_person (person) VALUES ('Mike');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Beer');
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Lemonade');
SELECT * FROM #tbl_person;
SELECT * FROM #tbl_drinks;
This produces this output:
ID person
----------- --------------------
1 Bob
2 Wendy
3 Mike
ID personID drink
----------- ----------- --------------------
1 1 Beer
2 2 Champage
3 2 Water
4 3 Beer
5 3 Lemonade
I know how to easily duplicate a single person plus their drinks, but not multiple people. Assuming I need to duplicate Bob and Wendy I need to get to this output:
ID person
----------- --------------------
1 Bob
2 Wendy
3 Mike
4 Bob
5 Wendy
ID personID drink
----------- ----------- --------------------
1 1 Beer
2 2 Champage
3 2 Water
4 3 Beer
5 3 Lemonade
6 4 Beer
7 5 Champagne
8 5 Water
I cannot figure out how to compare the old and new parent ID columns in order to get the child data.

The problem is that INSERT doesn't really have a "from table" that you could reference in the OUTPUT clause. But you could achieve the same with MERGE statement:
declare #tbl_IDmap table (newID int, oldID int)
merge #tbl_person as target
using (
select ID, person from #tbl_person where ID in (1,2)
) as source(ID, person)
on 1=0
when not matched then
insert (person) values(person)
output inserted.ID, source.ID into #tbl_IDmap;
And then duplicate the drinks with the new IDs:
insert into #tbl_drinks(personID, drink)
select m.newID, d.drink
from #tbl_drinks d
inner join #tbl_IDmap m
on m.oldID = d.personID
Here is your SqlFiddle updated.

Determined to add some additional solutions (and having thought about this most of the night!) I am posting an additional solution that doesn't use MERGE, hopefully to help users with older versions of SQL. It's more verbose than #TomT's suggestion but works okay.
SQLFiddle
-- Gather the people we need to copy
DECLARE #tbl_IdsToCopy TABLE
(
[counter] int IDENTITY(1,1),
[existingId] int
);
INSERT INTO #tbl_IdsToCopy (existingId) VALUES (1),(2); -- Bob & Wendy
-- Table to save new person ID's
DECLARE #tbl_newIds TABLE
(
[counter] int IDENTITY(1,1),
[newId] int
);
-- Create new people and save their new Id's
INSERT INTO #tbl_person
(
person
)
OUTPUT
INSERTED.ID
INTO
#tbl_newIds
(
[newId]
)
SELECT
p.person
FROM
#tbl_person p INNER JOIN
#tbl_IdsToCopy c ON c.existingId = p.ID
ORDER BY
c.[counter]; -- use counter to preserve ordering
-- map the old ID's to the new ID's and find the drinks for the old ID's
INSERT INTO #tbl_drinks
(
personID,
drink
)
SELECT
n.[newId],
d.drink
FROM
#tbl_IdsToCopy c INNER JOIN
#tbl_newIds n ON c.[counter] = n.[counter] INNER JOIN -- <-- map the old person ID to the new person Id
#tbl_drinks d ON d.personID = c.existingId; -- <-- find the drinks of the old person Id
-- Results
SELECT
p.ID,
p.person,
d.ID,
d.drink
FROM
#tbl_person p INNER JOIN
#tbl_drinks d ON d.personID = p.ID;

Related

Students enrolled in 3 or more course

I new here
I have 2 tables Subject and student 123 and I want to list the name(s) of subject and number of students taking it only if the subject has 3 or more students enrolled
SELECT
B.Subject_Name ,A.Student_Name ,A.Subjct_ID
FROM [dbo].[Student 123] AS A
LEFT JOIN [dbo].[Subject] AS B ON A.Subjct_ID = B.Subject_ID
GROUP BY A.Student_Name, B.Subject_Name, A.Subjct_ID
I'm stuck here, not quite sure how to get the list; any help will be appreciated
First step is to identify the Subject_ID with 3 or more students. You use GROUP BY and HAVING to do this
SELECT A.Subject_ID
FROM Subject as A
INNER JOIN [Student 123] as B
ON A.Subject_ID = B.Subject_ID
GROUP BY A.Subject_ID
HAVING COUNT(*) >= 3
Next, with the above result, you JOIN back to the Student 123 to show the Student taking these Subject_ID
You want subject and number of students taking it. You can apply GROUP BY with HAVING clause.
SELECT B.Subjct_Name, COUNT(A.StudentName) as StudentCount
FROM [dbo].[Student 123] AS A
LEFT JOIN [dbo].[Subject] AS B ON A.Subjct_ID = B.Subject_ID
GROUP BY B.Subjct_ID, B.Subjct_Name
HAVING COUNT(A.StudentName) > 3
I have provided sample below:
DECLARE #subject table(subjectid int, subjectname varchar(10))
declare #student table(studentid int, subjectid varchar(10))
insert into #subject
values (1, 'maths'), (2, 'science')
insert into #student
values (1,1),(1,2),(2,1),(3,1),(4,1);
SELECT sub.subjectname,count(*) as studentcount
FROM #student as stu
left join #subject as sub
on sub.subjectid = stu.subjectid
group by sub.subjectid, sub.subjectname
having count(*) > 3
subjectname
studentcount
maths
4

How to get desired result in SQL Server

In my application there is a table to store text and another table to store it's respective images..
My table structure goes as follows (tbl_article):
article_id | Page_ID | article_Content
-----------+---------+-----------------
1 | 1 | hello world
2 | 1 | hello world 2
where article_id is the pk and auto incremented.
Now in my other table (tbl_img):
image_id| image_location|article_id | page_id
--------+---------------+-----------+---------
1 | imgae locat | 1 | 1
2 | image loc2 | 2 | 1
where image_id is the pk and auto incremented.
In both table I am inserting data through table valued parameter, and in second table article_id is referencing article_id of the first table.
To get auto incremented column value I am using output clause:
DECLARE #TableOfIdentities TABLE
(
IdentValue INT,
PageId INT
)
INSERT INTO tbl_article(page_id, article_content)
OUTPUT Inserted.article_id, #pageId INTO #TableOfIdentities (IdentValue, PageId)
SELECT page_id, slogan_body_header
FROM #dtPageSlogan
INSERT INTO tbl_img(page_id, image_location)
SELECT page_id, image_location
FROM #dtPageImageContent
But now I have to insert values from #TableOfIdentities into article_id of tbl_img - how to do that?
You need an additional column , a temporary article id generated from your code to link images and related articles properly. So you can use MERGE with OUTPUT, because with merge you can refer to columns from both the target and the source and build your TableOfIdentities tvp properly, then join it with dtPageImageContent to insert on tbl_img.
CREATE TABLE tbl_article (
article_id INT IDENTITY(1, 1) PRIMARY KEY
, Page_ID INT
, article_Content NVARCHAR(MAX)
);
CREATE TABLE tbl_img (
image_id INT IDENTITY(1, 1) PRIMARY KEY
, image_location VARCHAR(256)
, article_id INT
, Page_ID INT
);
DECLARE #TableOfIdentities TABLE
(
IdentValue INT,
PageId INT,
tmp_article_id INT
);
DECLARE #dtPageSlogan TABLE(
tmp_article_id INT -- generated in your code
, page_id INT
, slogan_body_header NVARCHAR(MAX)
);
DECLARE #dtPageImageContent TABLE (
page_id INT
, image_location VARCHAR(256)
, tmp_article_id INT -- needed to link each image to its article
)
-- create sample data
INSERT INTO #dtPageSlogan(tmp_article_id, page_id, slogan_body_header)
VALUES (10, 1, 'hello world');
INSERT INTO #dtPageSlogan(tmp_article_id, page_id, slogan_body_header)
VALUES (20, 1, 'hello world 2');
INSERT INTO #dtPageImageContent(page_id, image_location, tmp_article_id)
VALUES (1, 'image loc1', 10);
INSERT INTO #dtPageImageContent(page_id, image_location, tmp_article_id)
VALUES (1, 'image loc2', 20);
-- use merge to insert tbl_article and populate #TableOfIdentities
MERGE INTO tbl_article
USING (
SELECT ps.page_id, ps.slogan_body_header, ps.tmp_article_id
FROM #dtPageSlogan as ps
) AS D
ON 1 = 2
WHEN NOT MATCHED THEN
INSERT(page_id, article_content) VALUES (page_id, slogan_body_header)
OUTPUT Inserted.article_id, Inserted.page_id, D.tmp_article_id
INTO #TableOfIdentities (IdentValue, PageId, tmp_article_id)
;
-- join using page_id and tmp_article_id fields
INSERT INTO tbl_img(page_id, image_location, article_id)
-- select the "IdentValue" from your table of identities
SELECT pic.page_id, pic.image_location, toi.IdentValue
FROM #dtPageImageContent pic
-- join the "table of identities" on the common "page_id" column
INNER JOIN #TableOfIdentities toi
ON pic.page_Id = toi.PageId AND pic.tmp_article_id = toi.tmp_article_id
;
You can try it on fiddle
You need to join the #dtPageImageContent table variable with the #TableOfIdentities table variable on their common page_id to get those values:
-- add the third column "article_id" to your list of insert columns
INSERT INTO tbl_img(page_id, image_location, article_id)
-- select the "IdentValue" from your table of identities
SELECT pic.page_id, pic.image_location, toi.IdentValue
FROM #dtPageImageContent pic
-- join the "table of identities" on the common "page_id" column
INNER JOIN #TableOfIdentities toi ON pic.page_Id = toi.page_id

How to create a new "shared" group in a single SELECT query in MS SQL

I'm trying to group a SELECT like you'd normally do - AND at the same time make a new "shared/aggregate group" adding that to the original result-set without a secondary SELECT and UNION.
The secondary SELECT and UNION is out of the question since the real use of this is with some very big tables, with a lot of joins, so it would be waay to slow. So the UNION way is definitely out of the question.
I've tried my best to illustrate this with the following simplified example:
BEGIN TRAN
CREATE TABLE #MyTable
(
id INT,
name VARCHAR(255)
)
INSERT INTO #MyTable VALUES (1,'cola');
INSERT INTO #MyTable VALUES (2,'cola');
INSERT INTO #MyTable VALUES (3,'cola');
INSERT INTO #MyTable VALUES (4,'fanta');
INSERT INTO #MyTable VALUES (5,'fanta');
INSERT INTO #MyTable VALUES (6,'fanta');
INSERT INTO #MyTable VALUES (7,'water');
INSERT INTO #MyTable VALUES (8,'water');
INSERT INTO #MyTable VALUES (9,'water');
INSERT INTO #MyTable VALUES (10,'cola');
INSERT INTO #MyTable VALUES (11,'cola');
SELECT
CASE
WHEN name = 'cola' OR name = 'fanta'
THEN 'soda'
ELSE
name
END as name,
COUNT(distinct id) as count
FROM #MyTable
GROUP BY name
ROLLBACK TRAN
Actual output:
soda 5
soda 3
water 3
Desired output:
cola 5
fanta 3
soda 8 <- this is the "shared/aggregate group"
water 3
As Panagiotis Kanavos correctly pointed out in the comment above, this can be done using ROLLUP:
BEGIN TRAN
CREATE TABLE #BeverageType
(
name VARCHAR(255)
)
INSERT INTO #BeverageType VALUES ('Soda');
INSERT INTO #BeverageType VALUES ('Other');
CREATE TABLE #UserBeverage
(
id INT,
name VARCHAR(255)
)
INSERT INTO #UserBeverage VALUES (1,'cola');
INSERT INTO #UserBeverage VALUES (2,'cola');
INSERT INTO #UserBeverage VALUES (3,'cola');
INSERT INTO #UserBeverage VALUES (1,'fanta'); -- <- NOTE: user 1 drinks both cola and fanta so the as intended the user is only counted 1 time in the ROLLUP 'Soda' group (7)
INSERT INTO #UserBeverage VALUES (5,'fanta');
INSERT INTO #UserBeverage VALUES (6,'fanta');
INSERT INTO #UserBeverage VALUES (7,'water');
INSERT INTO #UserBeverage VALUES (8,'water');
INSERT INTO #UserBeverage VALUES (9,'water');
INSERT INTO #UserBeverage VALUES (10,'cola');
INSERT INTO #UserBeverage VALUES (11,'cola');
SELECT ub.name, bt.name AS groupName, COUNT(distinct id) as uniqueUserCount
FROM #UserBeverage as ub
JOIN #BeverageType as bt
ON CASE
WHEN (ub.name = 'water')
THEN 'Other'
ELSE
'Soda'
END = bt.name
GROUP BY ROLLUP(bt.name, ub.name)
ROLLBACK TRAN
Outputs:
cola Soda 5
fanta Soda 3
water Other 3
NULL Other 3
NULL Soda 7
NULL NULL 10
You should repeat CASE statement everywhere.
SELECT
CASE WHEN name = 'cola' OR name = 'fanta'
THEN 'soda' ELSE name END as name,
COUNT((CASE WHEN name = 'cola' OR name = 'fanta'
THEN 'soda' ELSE name END)) as count
FROM #MyTable
GROUP BY CASE WHEN name = 'cola' OR name = 'fanta'
THEN 'soda' ELSE name END
+-------+-------+
| name | count |
+-------+-------+
| soda | 8 |
+-------+-------+
| water | 3 |
+-------+-------+
Can I suggest to use a subquery:
SELECT name, count(*) AS count
FROM (SELECT CASE WHEN name = 'cola' OR name = 'fanta'
THEN 'soda' ELSE name END as name
FROM #MyTable) x
GROUP BY name;
If you need the aggregate as well as the individual products, then an alternative may be to use a UNION and select the aggregates as a second query.
SELECT name, count(distinct id) as count
FROM #MyTable
GROUP BY name
UNION
SELECT 'SODA', COUNT(distinct id) as count
FROM #MyTable
WHERE name = 'cola' or name ='fanta'
You might also use Søren Høyer Kristensen's summary table to get the aggregate names if you need more groupings.

XML Query Attach appropriate ID

I seem struggle with XML. I am looking to get appropriate ID attached to each row
Declare #User table (id int,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50))
Insert into #User values
(1,'John','Smith','john.smith#gmail.com'),
(2,'Jane','Doe' ,'jane.doe#gmail.com')
Declare #XML xml
Set #XML = (Select * from #User for XML RAW)
Select ID = 1 -- < dummy need actual id
,Item = cast(x.v.query('local-name(.)') as varchar(100))
,Value = x.v.value('.','varchar(150)')
From #xml.nodes('//#*') x(v)
My current result is.
ID Item Value
1 id 1
1 First_Name John
1 Last_Name Smith
1 EMail john.smith#gmail.com
1 id 2
1 First_Name Jane
1 Last_Name Doe
1 EMail jane.doe#gmail.com
My Desired result would be.
ID Item Value
1 id 1
1 First_Name John
1 Last_Name Smith
1 EMail john.smith#gmail.com
2 id 2
2 First_Name Jane
2 Last_Name Doe
2 EMail jane.doe#gmail.com
Try it like this:
Btw: You were pretty close!
Declare #User table (id int,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50))
Insert into #User values
(1,'John','Smith','john.smith#gmail.com'),
(2,'Jane','Doe' ,'jane.doe#gmail.com')
Declare #XML xml
Set #XML = (Select * from #User for XML RAW)
SELECT #XML;
/*
<row id="1" First_Name="John" Last_Name="Smith" EMail="john.smith#gmail.com" />
<row id="2" First_Name="Jane" Last_Name="Doe" EMail="jane.doe#gmail.com" />
*/
The first .nodes() will return with all row elements in single rows
The CROSS APPLY .nodes(./#*) will do a row based search for all attributes and deliver them as single rows.
Select r.value('#id','int') AS ID
,Attr.value('local-name(.)','varchar(max)') AS Item
,Attr.value('.','varchar(max)') AS Value
FROM #XML.nodes('/row') AS A(r)
CROSS APPLY A.r.nodes('./#*') AS B(Attr)
Just came across this question. Though answer to this question was already given, thought to answer it with different approach without using xquery.
You can achieve the same result using CROSS APPLY and Table Value Constructor like this -
Declare #User table (id int,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50))
Insert into #User values
(1,'John','Smith','john.smith#gmail.com'),
(2,'Jane','Doe' ,'jane.doe#gmail.com')
SELECT r.ID, t.* FROM #User r
CROSS APPLY (
VALUES ('ID', cast(id as varchar)),
('First_Name', First_Name),
('Last_Name', Last_Name),
('EMail', EMail)
) t(Item, Value)
Result
ID Item Value
---------------------
1 ID 1
1 First_Name John
1 Last_Name Smith
1 EMail john.smith#gmail.com
2 ID 2
2 First_Name Jane
2 Last_Name Doe
2 EMail jane.doe#gmail.com

Sorting SQL table

can anyone help me with T-SQL to sort this table
ID Comment ParentId
-- ------- --------
3 t1 NULL
4 t2 NULL
5 t1_1 3
6 t2_1 4
7 t1_1_1 5
to look like this
ID Comment ParentId
-- ------- --------
3 t1 NULL
5 t1_1 3
7 t1_1_1 5
4 t2 NULL
6 t2_1 4
Kind regards,
Lennart
try this:
DECLARE #YourTable table (id int, Comment varchar(10), parentID int)
INSERT INTO #YourTable VALUES (3, 't1' , NULL)
INSERT INTO #YourTable VALUES (4, 't2' , NULL)
INSERT INTO #YourTable VALUES (5, 't1_1' , 3)
INSERT INTO #YourTable VALUES (6, 't2_1' , 4)
INSERT INTO #YourTable VALUES (7, 't1_1_1', 5)
;with c as
(
SELECT id, comment, parentid, CONVERT(varchar(8000),RIGHT('0000000000'+CONVERT(varchar(10),id),10)) as SortBy
from #YourTable
where parentID IS NULL
UNION ALL
SELECT y.id, y.comment, y.parentid, LEFT(c.SortBy+CONVERT(varchar(8000),RIGHT('0000000000'+CONVERT(varchar(10),y.id),10)),8000) AS SortBy
FROM c
INNER JOIN #YourTable y ON c.ID=y.PArentID
)
select * from C ORDER BY SortBy
EDIT
here is output
id comment parentid SortBy
----------- ---------- ----------- ---------------------------------
3 t1 NULL 0000000003
5 t1_1 3 00000000030000000005
7 t1_1_1 5 000000000300000000050000000007
4 t2 NULL 0000000004
6 t2_1 4 00000000040000000006
(5 row(s) affected)
humm order by?
http://t-sql.pro/t-sql/ORDER-BY.aspx
SELECT ID, Comment, ParentId
FROM TestTable
ORDER BY Comment, ParentId asc
This sounds very much like a homework question, but here's some hints on where to go with this:
You'll want to do a quick google or StackOverflow search for the ORDER BY clause to be able to get a set of results ordered by the column you want to use (i.e. the 'Comment' column).
Once you've got that, you can start writing a SQL statement to order your results.
If you need to then place re-order the actual table (and not just get the results in a specific order), you'll need to look up using temporary tables (try searching for 'DECLARE TABLE'). Much like any temp swap, you can place the results you have in a temporary place, delete the old data, and then replace the table contents with the temporary data you have, but this time in the order you want.
But just ordering by Comment will give you that? Or have I missed the point?!
declare #table table
(
Comment varchar(10)
)
insert into #table (Comment) values ('t1')
insert into #table (Comment) values ('t2')
insert into #table (Comment) values ('t1_1')
insert into #table (Comment) values ('t2_1')
insert into #table (Comment) values ('t1_1_1')
select * from #table order by comment

Resources