SQL Server : concatenate the primary key ID within a grouped statement - sql

I am trying to select a grouped set of rows and concatenate those rows primary key values into the select statement and count the rows also and select that value.
Tables:
JobTable - JobID, ExpressJob, ItemID
ItemTable - ItemID, Colour, Size
Values in Jobs:
10001, true, 3
10002, true, 3
10003, false, 4
Values in Items:
3, Blue, 1-2
4, Pink, 5-6
Result set:
3,Blue,1-2,10001|10002
3,Pink,5-6,10003
I've explored the following within the select statement:
SELECT
i.ItemID, i.Colour, i.Size,
COUNT(i.ItemID) AS Quantity,
j.ExpressJob,
JobIDArray = STUFF((SELECT CONVERT(VARCHAR(10), jb.JOBID)
FROM Jobs jb
WHERE jb.JobID = j.JobID
FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, ''))
FROM
Jobs j
INNER JOIN
Items i ON i.ItemID = j.ItemID
GROUP BY
i.ItemID, i.Colour, i.Size, j.ExpressJob
But I keep getting an aggregate group error on JobID. From what I have researched online FROM XML is the way to go but for some reason not effective when selecting the ID column.

Small tweak to what you already have will get you there.
Give this a try:
DECLARE #Jobs TABLE
(
[JobID] INT
, [ExpressJob] NVARCHAR(100)
, [ItemID] INT
);
DECLARE #Items TABLE
(
[ItemID] INT
, [Colour] NVARCHAR(100)
, [Size] NVARCHAR(100)
);
INSERT INTO #Jobs (
[JobID]
, [ExpressJob]
, [ItemID]
)
VALUES ( 10001, 'true', 3 )
, ( 10002, 'true', 3 )
, ( 10003, 'false', 4 );
INSERT INTO #Items (
[ItemID]
, [Colour]
, [Size]
)
VALUES ( 3, 'Blue', '1-2' )
, ( 4, 'Pink', '5-6' );
SELECT [i].[ItemID]
, [i].[Colour]
, [i].[Size]
, [j].[ExpressJob]
, COUNT([i].[ItemID]) AS [Quantity]
--Added '|' as that was how you wanted the results delimited
, STUFF((
SELECT '|' + CONVERT(VARCHAR(10), [jb].[JobID])
FROM #Jobs [jb]
WHERE [jb].[ItemID] = [i].[ItemID] --Change here as you're looking for JobID associated to the Item.
FOR XML PATH('') --No need to set TYPE or use '.value'
)
, 1
, 1
, ''
) AS JobIDArray
FROM #Jobs [j]
INNER JOIN #Items [i]
ON [i].[ItemID] = [j].[ItemID]
GROUP BY [i].[ItemID]
, [i].[Colour]
, [i].[Size]
, [j].[ExpressJob];

Related

How to create SQL Server tree in one row?

create table Test
(
Id int identity,
Name varchar(50) not null,
SName varchar(50) null,
ParentId int null
)
insert into Test
values ('aaa', 'bbb', null), ('adf', '22b', null), ('aad', 'bbsd',2),('asdsaa', 'bf', 3),('sdfs','sdf',3),('iopio','uiopio',3)
select * from Test
I have a table with parentid, and I want to get something like that
Name SName "aaa" "bbb" , "adf" {"aad":{"asdsaa":"bf"}}
from the selected values
I have been trying to get it with a recursive query, but...
WITH tree_view AS
(
SELECT
Id, ParentId, Name, SName
FROM
Test
UNION ALL
SELECT
parent.Id, parent.ParentId, parent.Name, parent.SName
FROM
Test parent
JOIN
tree_view tv ON parent.ParentId = tv.Id
)
SELECT DISTINCT *
FROM tree_view
I want to get this
if the parent id is not null, in one sell I want to get Name:SName in Sname field
I think you need to use PIVOT:
Below is similar solution which you can try
Sample Table creation:
CREATE TABLE [dbo].[TestTable](
[Id] [int] NULL,
[Name] [nvarchar](50) NULL,
[Description] [nvarchar](50) NULL)
Inserting sample values:
INSERT INTO TestTable VALUES (1,'Test1a','TestDesc1a')
INSERT INTO TestTable VALUES (2,'Test1b','TestDesc1b')
INSERT INTO TestTable VALUES (3,'Test2a','TestDesc2a')
INSERT INTO TestTable VALUES (4,'Test2b','TestDesc2b')
Query to get the desired output using Pivot:
SELECT 'Name' AS [Column], [1], [2],[3],[4]
FROM
(SELECT Name, id from TestTable) AS ST
PIVOT
(Max(Name) FOR ID IN ([1], [2],[3],[4])) AS PT
UNION
SELECT 'Description' AS [Column], [1], [2],[3],[4]
FROM
(SELECT id,[Description] from TestTable) AS ST
PIVOT
(Max([Description]) FOR ID IN ([1], [2],[3],[4])) AS PT
ORDER BY [Column] DESC
OutPut:
Column 1 2 3 4
Name Test1a Test1b Test2a Test2b
Description TestDesc1a TestDesc1b TestDesc2a TestDesc2b
Hope this helps to solve your question.
If you ar sure any parent leads to only 1 leaf, it's possible to generate the JSON you want by playing only with string manipulation:
with rel_hier(id, parentid, name, sname, js) as (
select m.id, m.parentid, m.name, m.sname,
CAST(CASE WHEN m.parentid IS NULL THEN
CONCAT('"' , m.sname , '"') ELSE
CONCAT('{"' , m.name , '" : "' , m.sname , '"}') END AS VARCHAR(2000))
js
from Test m
where not exists(select 1 from Test t1 where t1.parentid = m.id)
union all
select m.id, m.parentid, m.name, m.sname,
CAST(CASE WHEN m.parentid IS NULL THEN r.js ELSE CONCAT('{"' , m.name ,
'" : ' , r.js , '}') END AS VARCHAR(2000)) js
from rel_hier r
join Test m on m.id = r.parentid
)
SELECT name, js as sname FROM rel_hier r
WHERE parentid IS NULL
;
name sname
aaa "bbb"
adf {"aad" : {"asdsaa" : "bf"}}
But you should also better explain the substitution rules for a number of levels > 3.

TSQL - How to avoid UNION ALL

Sample Data:
DECLARE #Parent TABLE
(
[Id] INT
, [Misc_Val] VARCHAR(5)
) ;
DECLARE #Children TABLE
(
[Id] INT
, [P_ID] INT
) ;
INSERT INTO #Parent
VALUES
( 1, 'One' )
, ( 2, 'Two' )
, ( 3, 'Three' )
, ( 5, 'Four' ) ;
INSERT INTO #Children
VALUES
( 10, 1 )
, ( 11, 1 )
, ( 21, 2 )
, ( 23, 2 )
, ( 30, 3 )
, ( 40, 4 ) ;
Goal:
To efficiently output three fields ( [Id] and [IsChild], [Misc_Val] ). Output all records from #Parent table with [IsChild] = 0 and output all MATCHING records from #Child table (#Parent.Id = #Children.P_Id) with [IsChild] = 1.
Expected Output
Id IsChild Misc_Val
1 0 One
2 0 Two
3 0 Three
5 0 Four
10 1 One
11 1 One
21 1 Two
23 1 Two
30 1 Three
My try:
SELECT [P].[Id]
, 0 AS [IsChild]
, [P].[Misc_Val]
FROM #Parent AS [P]
UNION ALL
SELECT [C].[Id]
, 1
, [P].[Misc_Val]
FROM #Parent AS [P]
JOIN #Children AS [C]
ON [C].[P_ID] = [P].[Id] ;
Is there a better way to do this than using UNION ALL? #Parent and #Children tables are quite big and so am trying to avoid querying the #Parent table twice.
UPDATE: The below answer made me realized something I missed out when creating the post with mocked data. We do need some additional data from #Parent table regardless in the final output.
You can use CROSS APPLY to add the child table to the parent table.
This may or may not be faster, it can depend on indexing and so forth. You need to check the query plan.
SELECT v.Id
, v.IsChild
, P.Misc_Val
FROM #Parent AS P
CROSS APPLY (
SELECT
P.Id,
0 AS IsChild
UNION ALL
SELECT
C.Id,
1
FROM #Children AS C
WHERE C.P_ID = P.Id
) v;
Note that the first SELECT in the apply has no FROM and therefore does not do any table access.

TSQL - Mapping Parent/Child Hierarchy

Sample Data:
DECLARE #Hierarchy TABLE
(
[ParentId] INT
, [ChildId] INT
) ;
INSERT INTO #Hierarchy
VALUES
( 1, 2 )
, ( 1, 3 )
, ( 2, 4 )
, ( 3, 5 )
, ( 4, 3 )
, ( 4, 6 )
, ( 5, 6 )
, ( 7, 3 ) ;
Current Query:
; WITH CTE AS
(
SELECT [ParentId]
, [ChildId]
, 1 AS [Level]
, CONCAT ( CAST ( [ParentId] AS VARCHAR(MAX) ), '.', CAST ( [ChildId] AS VARCHAR(MAX) ) ) AS [Path]
FROM #Hierarchy
UNION ALL
SELECT [C].[ParentId]
, [T].[ChildId]
, [C].[Level] + 1
, CAST ( [C].[Path] + '.' + CAST([T].[ChildId] AS VARCHAR(MAX) ) AS VARCHAR(MAX) )
FROM CTE AS [C]
JOIN #Hierarchy AS [T]
ON [C].[ChildId] = [T].[ParentId]
)
SELECT *
FROM CTE
ORDER BY [ParentId]
, [Level]
, [ChildId] ;
Goal:
distinctly group levels of shared "path" together
find the shallowest and the deepest levels of the shared "path"
Expected Output:
NOTICE: the records with Orange highlight at the end are manually inserted to show what I'm expecting, but haven't figure out yet.
Group: Basically a "dense rank" of each "groups" of nodes that follow the same path. I think if you look at the values of Group in the above image and relate it to Level and Path field's data, it'll make more sense.
IsShallowest: 1st level (I can see that now that someone brought it up). Just need to figure out how to derive those missing (repeating) records
IsDeepest: max level within the group.
Think IsShallowest and IsDeepest is easy to figure out once "Group" logic is figured out and adding missing (repeated) records.
Please check this solution. It provide the requested solution except adding the extra row which more information is needed
;WITH CTE AS (
SELECT
[ParentId]
, [ChildId]
, 1 AS [Level]
, CONCAT ( CAST ( [ParentId] AS VARCHAR(MAX) ), '.', CAST ( [ChildId] AS VARCHAR(MAX) ) ) AS [Path]
, MyGroup1 = ROW_NUMBER() OVER(ORDER BY [ParentId])
, MyGroup2 = ROW_NUMBER() OVER(ORDER BY [ParentId])
FROM Hierarchy
UNION ALL
SELECT
[C].[ParentId]
, [T].[ChildId]
, [C].[Level] + 1
, CAST ( [C].[Path] + '.' + CAST([T].[ChildId] AS VARCHAR(MAX) ) AS VARCHAR(MAX) )
, MyGroup1 = C.MyGroup1
, MyGroup2 = [C].[MyGroup1] + ROW_NUMBER() OVER(ORDER BY [T].[ParentId]) - 1
FROM CTE AS [C]
JOIN Hierarchy AS [T] ON [C].[ChildId] = [T].[ParentId]
)
, MyCTE2 as (
SELECT
[ParentId]
, [ChildId]
, [Level]
, [Path]
-- un-comment bellow 2 rows to see the logic
--, MyGroup1
--, MyGroup2
, MyGroup = DENSE_RANK() OVER (ORDER BY MyGroup1, MyGroup2)
FROM CTE
),
MyCTE3 as (
SELECT
[ParentId]
, [ChildId]
, [Level]
, [Path]
, MyGroup
, shallowest = ROW_NUMBER() OVER(PARTITION BY MyGroup ORDER BY [Path])
, deepest = ROW_NUMBER() OVER(PARTITION BY MyGroup ORDER BY [Path] DESC)
FROM MyCTE2
)
SELECT
[ParentId]
, [ChildId]
, [Level]
, [Path]
, MyGroup
, ISshallowest = CASE WHEN shallowest = 1 then 1 else 0 END
, Isdeepest = CASE WHEN deepest = 1 then 1 else 0 END
FROM MyCTE3
ORDER BY
--[Path]
[ParentId]
, [Level]
, [ChildId];

SQL - Prepend a value if missing

Code/data:
DECLARE #T TABLE
(
[Col1] VARCHAR(20)
, [RowNum] INT
) ;
INSERT INTO #T
VALUES
( N'second', 1 )
, ( N'fifth', 4 )
, ( N'fourth', 3 )
--, ( N'zzz', 1 )
, ( N'third', 2 )
---- OR when "zzz" is part of this list
--VALUES
-- ( N'second', 2 )
-- , ( N'fifth', 5 )
-- , ( N'fourth', 4 )
-- , ( N'zzz', 1 )
-- , ( N'third', 3 )
SELECT STUFF ((
SELECT ',' + [SQ].[Col1]
FROM
(
SELECT N'zzz' AS [Col1]
, 1 AS [RowNum]
UNION
SELECT [Col1]
, [RowNum]
FROM #T
) AS [SQ]
FOR XML PATH ( '' ), TYPE
).[value] ( '.', 'varchar(MAX)' ), 1, 1, ''
) ;
Current output:
fifth,fourth,second,third,zzz
Goal:
Prepend "zzz," in the output string if missing in the 2nd part of the union AND the values should be in ASC ordered based on the values specified in [rownum] field defined in the 2nd part of the union. If "zzz" exists in the 2nd part of the input already (it will always be RowNum 1 in that case), it should return it only once as the first value.
Expected output:
zzz,second,third,fourth,fifth
UPDATED the requirement due to an error on my part when creating this post. Updated code/data represents more accurate scenario. Please note the RowNum seq in the 2nd part of the UNION, it also starts with 1, but this time, it might or might not be associated to "zzz" Basically, I want to prepend "zzz" in the comma-delimited & ordered output if it doesn't exist.
Hope the below one will help you.
SELECT ',' + [SQ].[Col1]
FROM
(
SELECT N'first' AS [Col1],1 AS [RowNum]
UNION
SELECT [ABC].[Col1],[ABC].[RowNum]
FROM
(
VALUES
( N'second', 2 )
, ( N'fifth', 5 )
, ( N'fourth', 4 )
--, ( N'first', 1 )
, ( N'third', 3 )
) AS [ABC] ( [Col1], [RowNum] )
) AS [SQ]
ORDER BY [RowNum]
FOR XML PATH ( '' ), TYPE
).[value] ( '.', 'varchar(MAX)' ), 1, 1, ''
) ;
Returns an output
first,second,third,fourth,fifth
Attached the Answer for the updated Scenario-
DECLARE #T TABLE
(
[Col1] VARCHAR(20)
, [RowNum] INT
) ;
INSERT INTO #T
VALUES
( N'second', 1 )
, ( N'fifth', 4 )
, ( N'fourth', 3 )
--, ( N'zzz', 1 )
, ( N'third', 2 )
---- OR when "zzz" is part of this list
--VALUES
-- ( N'second', 2 )
-- , ( N'fifth', 5 )
-- , ( N'fourth', 4 )
-- , ( N'zzz', 1 )
-- , ( N'third', 3 )
SELECT STUFF ((
SELECT ',' + [SQ].[Col1]
FROM
(
SELECT N'zzz' AS [Col1]
, 0 AS [RowNum]
UNION
SELECT [Col1]
, [RowNum]
FROM #T
) AS [SQ]
ORDER BY [RowNum]
FOR XML PATH ( '' ), TYPE
).[value] ( '.', 'varchar(MAX)' ), 1, 1, ''
) ;
Returns
zzz,second,third,fourth,fifth
Common Table Expressions (CTEs) provide a handy way of breaking queries down into simpler steps. Note that you can view the results of each step by switching out the last select statement.
with
Assortment as (
-- Start with the "input" rows.
select Col1, RowNum
from ( values ( N'second', 2 ), ( N'fifth', 5 ), ( N'fourth', 4 ),
-- ( N'first', 1 ),
( N'third', 3 ) ) ABC ( Col1, RowNum ) ),
ExtendedAssortment as (
-- Conditionally add "first".
select Col1, RowNum
from Assortment
union all -- Do not remove duplicate rows.
select N'first', 1
where not exists ( select 42 from Assortment where Col1 = N'first' ) )
-- Output the result.
-- Intermediate results may be seen by uncommenting one of the alternate select statements.
-- select * from Assortment;
-- select * from ExtendedAssortment;
select Stuff(
( select N',' + Col1 from ExtendedAssortment order by RowNum for XML path(N''), type).value( N'.[1]', 'NVarChar(max)' ),
1, 1, N'' ) as List;
The same logic can be performed using tables for input:
-- Rows to be included in the comma delimited string.
declare #Input as Table ( Col1 NVarChar(20), RowNum Int );
insert into #Input ( Col1, RowNum ) values
( N'second', 2 ), ( N'fifth', 5 ),
--( N'ZZZ', 17 ), -- Test row.
( N'fourth', 4 ), ( N'third', 3 );
select * from #Input;
-- Mandatory value that must appear in the result. One row only.
declare #Mandatory as Table ( Col1 NVarChar(20), RowNum Int );
-- By using the maximum negative value for an Int this value will be prepended
-- (unless other rows happen to have the same RowNum value).
insert into #Mandatory ( Col1, RowNum ) values ( N'ZZZ', -2147483648 );
select * from #Mandatory;
-- Process the data.
with
AllRows as (
select Col1, RowNum
from #Input
union all
select Col1, RowNum
from #Mandatory
where not exists ( select 42 from #Mandatory as M inner join #Input as I on M.Col1 = I.Col1 ) )
-- Output the result.
-- Intermediate results may be seen by uncommenting the alternate select statement.
--select * from AllRows;
select Stuff(
( select N',' + Col1 from AllRows order by RowNum for XML path(N''), type).value( N'.[1]', 'NVarChar(max)' ),
1, 1, N'' ) as List;

Insert Data with Foreign Keys

I have two tables connected with a foreign key constraint. Each table has a primary Key (ProductID ,BaseProductID ) that is an autoincrementing identity column.
Every BaseProduct has only 1 Product during the import.
BaseProduct
- BaseProductID
- BaseProductName
Product
- ProductID
- BaseProductID
- ProductName
I try to insert rows into both tables from a different Database that has the same Tables, but different seed Values. I dont want to keep the ids from the source database.
Is there a way to do this in one statement?
EDIT
The Select would be
Select ProductName FROM #SourceProduct
WHERE Not ProductName in (
select ProductName FROM #TargetBaseProduct
)
I tried the solution by Giorgi which is close, but the matching by
tgt.BaseProductID = src.BaseProductID
leads to result where source rows with BaseProductID= 1 are not inserted if the target already has an entry with BaseProductID = 1.
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
INSERT INTO #TargetBaseProduct
(
BaseProductName
)
VALUES ( 'Existing Product Base' )
INSERT INTO #TargetProduct(
BaseProductID ,
ProductName)
VALUES ( ##IDENTITY, 'Existing Product' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product2
3 3 Product3
Expected result
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct1
3 BaseProduct2
4 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product1
3 3 Product2
4 4 Product3
EDIT
using
ON 1 = 0
like Giorgi suggested gave the right result
If I get you right, there is way with MERGE:
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output:
BaseProductID BaseProductName
1 BaseProduct1
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Product1
2 2 Product2
3 3 Product3
EDIT: if you want to insert existing rows too, then change
ON tgt.BaseProductID = src.BaseProductID
to
ON 1 = 0