T-SQL - Concatenation of names on TWO tables/orphans - sql

I'm prepared to be crucified for asking my first question on SO and what is a potentially duplicate question, but I cannot find it for the life of me.
I have three tables, a product table, a linking table, and a child table with names. Preloaded on SQLFiddle >> if I still have your attention.
CREATE TABLE Product (iProductID int NOT NULL PRIMARY KEY
, sProductName varchar(50) NOT NULL
, iPartGroupID int NOT NULL)
INSERT INTO Product VALUES
(10001, 'Avionic Tackle', '1'),
(10002, 'Eigenspout', '2'),
(10003, 'Impulse Polycatalyst', '3'),
(10004, 'O-webbing', '2'),
(10005, 'Ultraservo', '3'),
(10006, 'Yttrium Coil', '5')
CREATE TABLE PartGroup (iPartGroupID int NOT NULL
, iChildID int NOT NULL)
INSERT INTO PartGroup VALUES
(1, 1),
(2, 2),
(3, 1),
(3, 2),
(3, 3),
(3, 4),
(4, 5),
(4, 6),
(5, 1)
CREATE TABLE PartNames (iChildID int NOT NULL PRIMARY KEY
, sPartNameText varchar(50) NOT NULL)
INSERT INTO PartNames VALUES
(1, 'Bulbcap Lube'),
(2, 'Chromium Deltaquartz'),
(3, 'Dilation Gyrosphere'),
(4, 'Fliphose'),
(5, 'G-tightener Bypass'),
(6, 'Heisenberg Shuttle')
I am trying to find out how to list all the part groups (that may or may not belong to a product), and translate their child names. That is, how do I use only the linking table and child name table to list all the translated elements of the linking table. I am trying to find orphans.
I have two queries:
SELECT P.iPartGroupID
,STUFF(
(SELECT
CONCAT(', ', PN.sPartNameText)
FROM PartGroup PG
INNER JOIN PartNames PN ON PN.iChildID = PG.iChildID
WHERE PG.iPartGroupID = P.iPartGroupID
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)')
, 1, 2, ''
) AS [Child Elements]
FROM Product P
GROUP BY P.iPartGroupID
This lists all the part groups that belong to a product, and their child elements by name. iPartGroupID = 4 is not here.
I also have:
SELECT PG.iPartGroupID
,STUFF(
(SELECT
CONCAT(', ', PGList.iChildID)
FROM PartGroup PGList
WHERE PGList.iPartGroupID = PG.iPartGroupID
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)')
, 1, 2, ''
) AS [Child Elements]
FROM PartGroup PG
GROUP BY PG.iPartGroupID
This lists all the part groups, and their child elements by code. iPartGroupID = 4 is covered here, but the names aren't translated.
What query can I use to list the orphan part groups (and also the orphan parts):
4 G-tightener Bypass, Heisenberg Shuttle
Ideally it is included in a list of all the other part groups, but if not, I can union the results.
Every other SO question I've looked up uses either 3 tables, or only 1 table, self joining with aliases. Does anyone have any ideas?
No XML in the part names, no particular preference for CONCAT or SELECT '+'.
I would link to other posts, but I can't without points :(

I'm not entirely sure what do you mean, exactly, when you use the word "translate". And your required output seems to contradict your sample data (if I'm not lost something).
Nevertheless, try this query, maybe it's what you need:
select sq.iPartGroupID, cast((
select pn.sPartNameText + ',' as [data()] from #PartNames pn
inner join #PartGroup p on pn.iChildID = p.iChildID
where p.iPartGroupID = sq.iPartGroupID
order by pn.iChildID
for xml path('')
) as varchar(max)) as [GroupList]
from (select distinct pg.iPartGroupID from #PartGroup pg) sq
left join #Product pr on sq.iPartGroupID = pr.iPartGroupID
where pr.iProductID is null;

Following way you can use to get the answer you want
SELECT pg.iPartGroupID,
CASE COUNT(pg.iPartGroupID)
WHEN 1 THEN (
SELECT pn2.sPartNameText
FROM PartNames pn2
WHERE pn2.iChildID = pg.iPartGroupID
)
ELSE (
SELECT CASE ROW_NUMBER() OVER(ORDER BY(SELECT 1))
WHEN 1 THEN ''
ELSE ','
END + pn2.sPartNameText
FROM PartNames pn2
INNER JOIN PartGroup pg2
ON pg2.iChildID = pn2.iChildID
WHERE pg2.iPartGroupID = pg.iPartGroupID
FOR XML PATH('')
)
END
FROM PartGroup pg
GROUP BY
pg.iPartGroupID

Related

Use Left Join Alias in Column Select in SQL Views

I am working on creating a view in SQL server one of the columns for which needs to be a comma separated value from a different table. Consider the tables below for instance -
CREATE TABLE Persons
(
Id INT NOT NULL PRIMARY KEY,
Name VARCHAR (100)
)
CREATE TABLE Skills
(
Id INT NOT NULL PRIMARY KEY,
Name VARCHAR (100),
)
CREATE TABLE PersonSkillLinks
(
Id INT NOT NULL PRIMARY KEY,
SkillId INT FOREIGN KEY REFERENCES Skills(Id),
PersonId INT FOREIGN KEY REFERENCES Persons(Id),
)
Sample data
INSERT INTO Persons VALUES
(1, 'Peter'),
(2, 'Sam'),
(3, 'Chris')
INSERT INTO Skills VALUES
(1, 'Poetry'),
(2, 'Cooking'),
(3, 'Movies')
INSERT INTO PersonSkillLinks VALUES
(1, 1, 1),
(2, 2, 1),
(3, 3, 1)
What I want is something like shown in the image
While I have been able to get the results using the script below, I have a feeling that this is not the best (and certainly not the only) way to do as far as performance goes -
CREATE VIEW vwPersonsAndTheirSkills
AS
SELECT p.Name,
ISNULL(STUFF((SELECT ', ' + s.Name FROM Skills s JOIN PersonSkillLinks psl ON s.Id = psl.SkillId WHERE psl.personId = p.Id FOR XML PATH ('')), 1, 2, ''), '') AS Skill
FROM Persons p
GO
I also tried my luck with the script below -
CREATE VIEW vwPersonsAndTheirSkills
AS
SELECT p.Name,
ISNULL(STUFF((SELECT ', ' + skill.Name FOR XML PATH ('')), 1, 2, ''), '') AS Skill
FROM persons p
LEFT JOIN
(
SELECT s.Name, psl.personid FROM Skills s
JOIN PersonSkillLinks psl ON s.Id = psl.SkillId
) skill ON skill.personId = p.Id
GO
but it is not concatenating the strings and returning separate rows for each skill as shown below -
So, is my assumption about the first script correct? If so, what concept am I missing about it and what should be the most efficient way to achieve it.
I would try with APPLY :
SELECT p.Name, STUFF(ss.skills, 1, 2, '') AS Skill
FROM Persons p OUTER APPLY
(SELECT ', ' + s.Name
FROM Skills s JOIN
PersonSkillLinks psl
ON s.Id = psl.SkillId
WHERE psl.personId = p.Id
FOR XML PATH ('')
) ss(skills);
By this way, optimizer will call STUFF() once not for all rows returned by outer query.

SQL Server CTE: How to select the lowest level only

I want to select/display the lowest level of the CTE only. Please help. I am using SQL Server 2016.
Create Table Location
(
Id int
Name varchar(20)
Parent int
)
Insert into location
values (1, Location1, null), (2, Location1child, 1),
(3, Location1grandchild, 2), (4, Location2, null),
(5, Location3, null), (6, Locationchild3, 5)
I need to display only records 3, 4, 6 which is the lowest level.
Update: I already created the query, but record number 4 didn't display. I am expecting record number 4 to be displayed because the record is the lowest level in the group.
With CTE (id, cte_level, cte_name, cte_longname) as
(
Select
A.ID, 1,
cast(A.name as varchar(max)),
cast(A.name as varchar(max))
from
Location A
Union All
Select
A.ID, cte_level + 1,
replicate(' ยท ' , cte_level ) + cast(A.name as varchar(max)),
cte.cte_longname + ' . ' + cast(A.name as varchar(max))
from
Location A
inner join
CTE ON A.Parent = CTE.id
)
select
CTE_2.id,
CTE_2.cte_longname [name]
--, A.cte_name [name]
from
CTE as CTE_1
inner join
CTE as CTE_2 on CTE_1.id = cte_2.id
where
CTE_1.cte_level = 1
And CTE_2.cte_level = (Select MAX(CTE.cte_level) From CTE)
order by
cte_2.cte_longname
It has nothing to do with CTE. Just use LEFT OUTER JOIN with IS NULL check.
SELECT P.*
FROM Location P
LEFT OUTER JOIN Location C ON P.Id = C.Parent
WHERE C.Id IS NULL;

SQL return only distinct IDs from LEFT JOIN

I've inherited some fun SQL and am trying to figure out how to how to eliminate rows with duplicate IDs. Our indexes are stored in a somewhat columnar format and then we pivot all the rows into one with the values as different columns.
The below sample returns three rows of unique data, but the IDs are duplicated. I need just two rows with unique IDs (and the other columns that go along with it). I know I'll be losing some data, but I just need one matching row per ID to the query (first, top, oldest, newest, whatever).
I've tried using DISTINCT, GROUP BY, and ROW_NUMBER, but I keep getting the syntax wrong, or using them in the wrong place.
I'm also open to rewriting the query completely in a way that is reusable as I currently have to generate this on the fly (cardtypes and cardindexes are user defined) and would love to be able to create a stored procedure. Thanks in advance!
declare #cardtypes table ([ID] int, [Name] nvarchar(50))
declare #cards table ([ID] int, [CardTypeID] int, [Name] nvarchar(50))
declare #cardindexes table ([ID] int, [CardID] int, [IndexType] int, [StringVal] nvarchar(255), [DateVal] datetime)
INSERT INTO #cardtypes VALUES (1, 'Funny Cards')
INSERT INTO #cardtypes VALUES (2, 'Sad Cards')
INSERT INTO #cards VALUES (1, 1, 'Bunnies')
INSERT INTO #cards VALUES (2, 1, 'Dogs')
INSERT INTO #cards VALUES (3, 1, 'Cat')
INSERT INTO #cards VALUES (4, 1, 'Cat2')
INSERT INTO #cardindexes VALUES (1, 1, 1, 'Bunnies', null)
INSERT INTO #cardindexes VALUES (2, 1, 1, 'playing', null)
INSERT INTO #cardindexes VALUES (3, 1, 2, null, '2014-09-21')
INSERT INTO #cardindexes VALUES (4, 2, 1, 'Dogs', null)
INSERT INTO #cardindexes VALUES (5, 2, 1, 'playing', null)
INSERT INTO #cardindexes VALUES (6, 2, 1, 'poker', null)
INSERT INTO #cardindexes VALUES (7, 2, 2, null, '2014-09-22')
SELECT TOP(100)
[ID] = c.[ID],
[Name] = c.[Name],
[Keyword] = [colKeyword].[StringVal],
[DateAdded] = [colDateAdded].[DateVal]
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
ORDER BY [DateAdded]
Edit:
While both solutions are valid, I ended up using the MAX() solution from #popovitsj as it was easier to implement. The issue of data coming from multiple rows doesn't really factor in for me as all rows are essentially part of the same record. I will most likely use both solutions depending on my needs.
Here's my updated query (as it didn't quite match the answer):
SELECT TOP(100)
[ID] = c.[ID],
[Name] = MAX(c.[Name]),
[Keyword] = MAX([colKeyword].[StringVal]),
[DateAdded] = MAX([colDateAdded].[DateVal])
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
GROUP BY c.ID
ORDER BY [DateAdded]
You could use MAX or MIN to 'decide' on what to display for the other columns in the rows that are duplicate.
SELECT ID, MAX(Name), MAX(Keyword), MAX(DateAdded)
(...)
GROUP BY ID;
using row number windowed function along with a CTE will do this pretty well. For example:
;With preResult AS (
SELECT TOP(100)
[ID] = c.[ID],
[Name] = c.[Name],
[Keyword] = [colKeyword].[StringVal],
[DateAdded] = [colDateAdded].[DateVal],
ROW_NUMBER()OVER(PARTITION BY c.ID ORDER BY [colDateAdded].[DateVal]) rn
FROM #cards AS c
LEFT JOIN #cardindexes AS [colKeyword] ON [colKeyword].[CardID] = c.ID AND [colKeyword].[IndexType] = 1
LEFT JOIN #cardindexes AS [colDateAdded] ON [colDateAdded].[CardID] = c.ID AND [colDateAdded].[IndexType] = 2
WHERE [colKeyword].[StringVal] LIKE 'p%' AND c.[CardTypeID] = 1
ORDER BY [DateAdded]
)
SELECT * from preResult WHERE rn = 1

SQL Building Pathway using With Union in SQL Server

This is SQL Server Question
I have a set of categories, and their relationship results in nested categories.
I want to build a pathway keeping the relationship and build the SEF urls. Here is what I am looking for:
Category table:
ID, Name
1, Root
2, Cat1
3, Cat2
4, Cat1.1
5, Cat1.2
6, Cat2.1
7, Cat2,2
CategoryChild table: ParentCategoryID, ChildCategoryID
1, 2
1, 3
2, 4
2, 5
3, 6
3, 7
It is an unlimited nested structure. Here is what I am doing (I know its wrong but want something like this):
WITH MenuItems
AS (
SELECT
CAST((ItemPath) AS VARCHAR(1000)) AS 'ItemPath',
CategoryID, Category, ChildID
FROM #Mapping
WHERE CategoryID = 1
UNION ALL
SELECT
CAST((items.ItemPath + '-/' + MenuItem.Category) AS VARCHAR(1000)) AS 'ItemPath',
MenuItem.CategoryID, MenuItem.Category, MenuItem.ChildID
FROM #Mapping AS MenuItem
JOIN MenuItems AS items
ON items.ChildID = MenuItem.CategoryID
)
select * from MenuItems
It gives me something like this:
root--------|1---|root---|2
root--------|1---|root---|3
root/Cat2---|3---|Cat2---|6
root/Cat2---|3---|Cat2---|7
root/Cat1---|2---|Cat1---|4
root/Cat1---|2---|Cat1---|5
So ideally the path should be like this:
root/parent/child (and so on)!
I'm not sure if this is what you're looking for but I've played with recursive cte's in the past and so this might be helpful in building the items path.
NOTE: I've included additional information like the Root Id and Level for each item, so that you can change the ordering of the output.
declare #Category table (Id int, Name varchar(10))
insert into #Category values (1, 'Root'),(2, 'Cat1'), (3, 'Cat2'), (4, 'Cat1.1'), (5, 'Cat1.2'), (6, 'Cat2.1'), (7, 'Cat2.2')
declare #CategoryChild table (ParentCategoryID int, ChildCategoryID int)
insert into #CategoryChild values (1, 2), (1, 3), (2, 4), (2, 5), (3, 6), (3, 7)
;with cte as
(
-- root part
select
ccParent.ChildCategoryID Id,
ccParent.ParentCategoryID ParentId,
c.Name Name,
CAST(parentCategory.Name + '/' + c.Name as varchar(1000)) as Path,
ccParent.ChildCategoryID Root,
0 as Level
from
#CategoryChild ccParent
inner join
#Category c on c.Id = ccParent.ChildCategoryID
inner join
#Category parentCategory on parentCategory.Id = ccParent.ParentCategoryID
where
ccParent.ParentCategoryID = 1
union all
-- recursive part
select
ccChild.ChildCategoryID Id,
ccChild.ParentCategoryID ParentId,
c.Name Name,
CAST((cte.Path + '/' + c.Name) as varchar(1000)) as Path,
cte.Root Root,
cte.Level + 1 as Level
from
#CategoryChild ccChild
inner join
#Category c on c.Id = ccChild.ChildCategoryID
inner join
cte on cte.Id = ccChild.ParentCategoryID
)
select cte.Path
from cte
order by cte.Root, cte.Level
Running the above within my environment gives the following results
Root/Cat1
Root/Cat1/Cat1.1
Root/Cat1/Cat1.2
Root/Cat2
Root/Cat2/Cat2.1
Root/Cat2/Cat2.2
If you were looking to include the Root category in your result set as a standalone item then you can change the first part of the cte to hard code the select of the root item.
;with cte as
(
-- root part
select
c.Id Id,
null ParentId,
c.Name Name,
CAST(c.Name as varchar(1000)) as Path,
c.Id Root,
0 as Level
from
#Category c
where
c.Name = 'Root'
union all
... same as before
Giving the follow
Root
Root/Cat1
Root/Cat1/Cat1.1
Root/Cat1/Cat1.2
Root/Cat2
Root/Cat2/Cat2.1
Root/Cat2/Cat2.2

SQL - Ordering by multiple criteria

I have a table of categories. Each category can either be a root level category (parent is NULL), or have a parent which is a root level category. There can't be more than one level of nesting.
I have the following table structure:
Categories Table Structure http://img16.imageshack.us/img16/8569/categoriesi.png
Is there any way I could use a query which produced the following output:
Free Stuff
Hardware
Movies
CatA
CatB
CatC
Software
Apples
CatD
CatE
So the results are ordered by top level category, then after each top level category, subcategories of that category are listed?
It's not really ordering by Parent or Name, but a combo of the two. I'm using SQL Server.
It seems to me like you are looking to flatten and order your hierarchy, the cheapest way to get this ordering would be to store an additional column in the table that has the full path.
So for example:
Name | Full Path
Free Stuff | Free Stuff
aa2 | Free Stuff - aa2
Once you store the full path, you can order on it.
If you only have a depth of one you can auto generate a string to this effect with a single subquery (and order on it), but this solution does not work that easily when it gets deep.
Another option, is to move this all over to a temp table and calculate the full path there, on demand. But it is fairly expensive.
You could make the table look at itself, ordering by the parent Name then the child Name.
select categories.Name AS DisplayName
from categories LEFT OUTER JOIN
categories AS parentTable ON categories.Parent = parentTable.ID
order by parentTable.Name, DisplayName
Ok, here we go :
with foo as
(
select 1 as id, null as parent, 'CatA' as cat from dual
union select 2, null, 'CatB' from dual
union select 3, null, 'CatC' from dual
union select 4, 1, 'SubCatA_1' from dual
union select 5, 1, 'SubCatA_2' from dual
union select 6, 2, 'SubCatB_1' from dual
union select 7, 2, 'SubCatB_2' from dual
)
select child.cat
from foo parent right outer join foo child on parent.id = child.parent
order by case when parent.id is not null then parent.cat else child.cat end,
case when parent.id is not null then 1 else 0 end
Result :
CatA
SubCatA_1
SubCatA_2
CatB
SubCatB_1
SubCatB_2
CatC
Edit - Solution change inspire from van's order by ! Much simpler that way.
Not entirely sure of your questions but it sounds like PARTITION BY might be useful for you. There's a good introductory post on PARTITION BY here.
Here you have a complete working example using a resursive common table expression.
DECLARE #categories TABLE
(
ID INT NOT NULL,
[Name] VARCHAR(50),
Parent INT NULL
);
INSERT INTO #categories VALUES (4, 'Free Stuff', NULL);
INSERT INTO #categories VALUES (1, 'Hardware', NULL);
INSERT INTO #categories VALUES (3, 'Movies', NULL);
INSERT INTO #categories VALUES (2, 'Software', NULL);
INSERT INTO #categories VALUES (10, 'a', 0);
INSERT INTO #categories VALUES (12, 'apples', 2);
INSERT INTO #categories VALUES (8, 'catD', 2);
INSERT INTO #categories VALUES (9, 'catE', 2);
INSERT INTO #categories VALUES (5, 'catA', 3);
INSERT INTO #categories VALUES (6, 'catB', 3);
INSERT INTO #categories VALUES (7, 'catC', 3);
INSERT INTO #categories VALUES (11, 'aa2', 4);
WITH categories(ID, Name, Parent, HierarchicalName)
AS
(
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(c.[Name] AS VARCHAR(200)) AS HierarchicalName
FROM #categories c
WHERE c.Parent IS NULL
UNION ALL
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(pc.HierarchicalName + c.[Name] AS VARCHAR(200))
FROM #categories c
JOIN categories pc ON c.Parent = pc.ID
)
SELECT c.*
FROM categories c
ORDER BY c.HierarchicalName
SELECT
ID,
Name,
Parent,
RIGHT(
'000000000000000' +
CASE WHEN Parent IS NULL
THEN CONVERT(VARCHAR, Id)
ELSE CONVERT(VARCHAR, Parent)
END, 15
)
+ '_' + CASE WHEN Parent IS NULL THEN '0' ELSE '1' END
+ '_' + Name
FROM
categories
ORDER BY
4
The long padding is to account for the fact that SQL Server's INT data type goes from 2,147,483,648 through 2,147,483,647.
You can ORDER BY the expression directly, no need to use ORDER BY 4. It was just to show what it is sorting on.
It is worth noting that this expression cannot use any index. This means sorting a large table will be slow.