SQL - ordering a common table expression - sql

Using this sample table:
drop table Population
CREATE TABLE [dbo].[Population](
[PersonId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL,
[MotherId] [int] NULL,
[FatherId] [int] NULL
) ON [PRIMARY]
insert Population (PersonId, [Name], MotherId, FatherId) values (1, 'Baby', 2, 3)
insert Population (PersonId, [Name], MotherId, FatherId) values (2, 'Mother', 4, 5)
insert Population (PersonId, [Name], MotherId, FatherId) values (3, 'Father', 6, 7)
insert Population (PersonId, [Name], MotherId, FatherId) values (4, 'Mothers Mother', 8, 9)
insert Population (PersonId, [Name], MotherId, FatherId) values (5, 'Mothers Father', 99, 99)
insert Population (PersonId, [Name], MotherId, FatherId) values (6, 'Fathers Mother', 99, 99)
insert Population (PersonId, [Name], MotherId, FatherId) values (7, 'Father Father', 99, 99)
insert Population (PersonId, [Name], MotherId, FatherId) values (8, 'Mothers GrandMother', 99, 99)
insert Population (PersonId, [Name], MotherId, FatherId) values (9, 'Mothers GrandFather', 99, 99)
I can return all the correct people I needed for a family tree using this SQL
;WITH FamilyTree
AS
(
SELECT *, CAST(NULL AS VARCHAR(50)) AS childName, 0 AS Generation
FROM Population
WHERE [PersonId] = '1'
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[motherId]
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[fatherId]
)
SELECT childName, space(generation*2)+name, generation FROM FamilyTree
It gives me:
-baby
--mother
--father
---fathers mother
---fathers father
---mothers mother
---mothers father
But how can I (just using sql) put the tree in the correct order - so that I get:
-baby
--mother
---mothers mother
---mothers father
--father
---fathers mother
---fathers father

NB: This answer was written after an extremely superficial glance at the Hierarchies chapter in the book "Inside Microsoft SQL Server T-SQL Querying" Hopefully I didn't miss any vital caveats!
;WITH FamilyTree
AS
(
SELECT *, CAST(NULL AS VARCHAR(50)) AS childName, 0 AS Generation, '.' + CAST([PersonId] AS VARCHAR(max)) + '.' as Path
FROM Population
WHERE [PersonId] = '1'
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1, Path + '0.' + CAST(Fam.[PersonId] AS VARCHAR(max)) + '.' as Path
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[MotherId]
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1, Path + '1.' + CAST(Fam.[PersonId] AS VARCHAR(max)) + '.' as Path
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[FatherId]
)
SELECT childName, space(Generation*2)+Name, Generation, Path
FROM FamilyTree
ORDER BY Path

;WITH FamilyTree
AS
(
SELECT *, CAST(NULL AS VARCHAR(50)) AS childName, 0 AS Generation, CAST(RIGHT('0000000000' + CAST(PersonId as varchar(10)),10) as varchar(max)) as Descendents
FROM Population
WHERE [PersonId] = '1'
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1,FamilyTree.Descendents + '|' + RIGHT('0000000000' + CAST(Fam.PersonId as varchar(10)),10)
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[motherId]
UNION ALL
SELECT Fam.*, FamilyTree.[Name] AS childName, Generation + 1,FamilyTree.Descendents + '|' + RIGHT('0000000000' + CAST(Fam.PersonId as varchar(10)),10)
FROM Population AS Fam
INNER JOIN FamilyTree
ON Fam.[PersonId] = FamilyTree.[fatherId]
)
SELECT childName, space(generation*2)+name, generation FROM FamilyTree order by Descendents
Basically, you build up a big sort key, ensuring all Ids at the same level differ at the same position within the string.

Related

Matrix table SQL

I have three tables:
Table tCity
(Id int, City nvarchar(50))
Table tLocation
(Id int, Location nvarchar(50))
Table tCityLocation
(Id int, CityId int, LocationId int)
I would like to generate matrix table like in image below.
If City belongs to location-> in appropriate cell in table, char X will be written down.
I would like to about any sophisticated approach how to reach it. I had issue in similar logic and processed by iteration in cursors with dynamically added column to result set table.
Exists any sophisticated approach instead of cursor and dynamically added column?
Thank you.
It should be something like this:
DECLARE #DymanimcTSQLSatement NVARCHAR(MAX)
,#DynamicColumns NVARCHAR(MAX);
SET #DynamicColumns = STUFF
(
(
SELECT ',' + QUOTENAME([Location])
FROM tLocation
GROUP BY [Location]
ORDER BY [Location]
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1
,1
,''
);
SET #DymanimcTSQLSatement = N'
SELECT *
FROM
(
SELECT L.Location
,C.City
,1
FROM tCityLocation CL
INNER JOIN tLocation L
ON CL.[LocationId] = L.[id]
INNER JOIN tCity C
ON CL.[CityId] = C.[id]
) DS (country, city, value)
PIVOT
(
MAX([value]) FOR [country] IN (' + #DynamicColumns +')
) PVT;';
EXECUTE sp_executesql #DymanimcTSQLSatement;
and here is the data I have used:
-- Creating Table tCity
CREATE TABLE tCity (
Id int,
City nvarchar(50)
);
-- Populating Table tCity with 10 records
INSERT INTO tCity (Id, City) VALUES (1, 'New York');
INSERT INTO tCity (Id, City) VALUES (2, 'Los Angeles');
INSERT INTO tCity (Id, City) VALUES (3, 'Paris');
INSERT INTO tCity (Id, City) VALUES (4, 'Tokyo');
INSERT INTO tCity (Id, City) VALUES (5, 'Sydney');
INSERT INTO tCity (Id, City) VALUES (6, 'Philadelphia');
INSERT INTO tCity (Id, City) VALUES (7, 'Rio de Janeiro');
INSERT INTO tCity (Id, City) VALUES (8, 'Cape Town');
INSERT INTO tCity (Id, City) VALUES (9, 'Beijing');
INSERT INTO tCity (Id, City) VALUES (10, 'Singapore');
-- Creating Table tLocation
CREATE TABLE tLocation (
Id int,
Location nvarchar(50)
);
-- Populating Table tLocation with 10 records
INSERT INTO tLocation (Id, Location) VALUES (1, 'United States');
INSERT INTO tLocation (Id, Location) VALUES (2, 'United States');
INSERT INTO tLocation (Id, Location) VALUES (3, 'France');
INSERT INTO tLocation (Id, Location) VALUES (4, 'Japan');
INSERT INTO tLocation (Id, Location) VALUES (5, 'Australia');
INSERT INTO tLocation (Id, Location) VALUES (6, 'United States');
INSERT INTO tLocation (Id, Location) VALUES (7, 'Brazil');
INSERT INTO tLocation (Id, Location) VALUES (8, 'South Africa');
INSERT INTO tLocation (Id, Location) VALUES (9, 'China');
INSERT INTO tLocation (Id, Location) VALUES (10, 'Singapore');
-- Creating Table tCityLocation
CREATE TABLE tCityLocation (
Id int,
CityId int,
LocationId int
);
INSERT INTO tCityLocation (Id, CityId, LocationId)
SELECT
ROW_NUMBER() OVER (ORDER BY tCity.Id),
tCity.Id, tLocation.Id
FROM tCity
JOIN tLocation
ON tCity.Id = tLocation.Id;
The idea is to built a dynamic PIVOT, where the PIVOT columns are the unique countries (in your case locations). If your SQL Server supports STRING_AGG, you can do this in different way:
SELECT #DynamicColumns = STRING_AGG(CAST(QUOTENAME([Location]) AS VARCHAR(MAX)), ',') WITHIN GROUP (ORDER BY [Location])
FROM
(
SELECT DISTINCT [Location]
FROM tLocation
) DS ([Location]);
Better way for answer to this question use pivot
First I get list Location of table Location
Second I get list Location of table tLocation For use in select
Because I need to use ISNULL for replace null with space
after use pivot for get result
result :Rows include List City and Column include Location
city
Asia
CRezah Rep.
Europe
France
GB
Germany
Japan
N.America
USA
NY
x
x
Prague
x
x
x
London
x
x
Tokio
x
x
--Get List Columns for Pivot
DECLARE #cols AS NVARCHAR(MAX),#scols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(Location) from tLocation
FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)') ,1,1,'' )
--can to use below Code for Replace null with space
select #scols = STUFF((SELECT distinct ',ISNULL(' + QUOTENAME(Location) +','' '') as '+ QUOTENAME(Location)
from tLocation FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)') ,1,1,'')
set #query = 'SELECT city ,'+#scols+' from
(
select cl.CityId,c.City,l.Location,''X'' as Value from tCityLocation cl
inner join tCity c on cl.CityId=c.Id
inner join tLocation l on cl.LocationId=l.Id
) x
pivot
(
max( value) for Location in (' + #cols + ')
) p
order by CityId
'
execute(#query)
You can to insert the basic data with the following codes
create table tCity(Id int, City nvarchar(50))
create table tLocation(Id int, Location nvarchar(50))
create table tCityLocation(Id int, CityId int, LocationId int)
insert into tCity
(id,City)
select 1 as id,'NY' union all select 2 as id,'Prague'
union all select 3 as id,'London' union all select 4 as id,'Tokio'
insert into tLocation (id,Location)
select 1,'USA' union all select 2,'CRezah Rep.' union all select 3,'France' union all select 4,'GB'
union all select 5,'Germany' union all select 6,'Europe' union all select 7,'N.America'
union all select 9,'Asia' union all select 10,'Japan'
insert into tCityLocation
(id,CityId,LocationId)
select 1,1,1 union all select 2,1,7 union all select 3,2,2 union all select 4,2,6 union all select 5,3,4 union all
select 6,3,6 union all select 7,4,9 union all select 8,4,10

SQL LEFT JOIN to many categories

Suppose the following easy scenario, where a product row gets connected to one primary category, subcategory, and sub-subcategory.
DECLARE #PRODUCTS TABLE (ID int, DESCRIPTION varchar(50), CAT varchar(30), SUBCAT varchar(30), SUBSUBCAT varchar(30));
INSERT #PRODUCTS (ID, DESCRIPTION, CAT, SUBCAT, SUBSUBCAT) VALUES
(1, 'NIKE MILLENIUM', '1', '10', '100'),
(2, 'NIKE CORTEZ', '1', '12', '104'),
(3, 'ADIDAS PANTS', '2', '27', '238'),
(4, 'PUMA REVOLUTION 5', '3', '35', '374'),
(5, 'SALOMON SHELTER CS', '4', '15', '135'),
(6, 'NIKE EBERNON LOW', '2', '14', '157');
DECLARE #CATS TABLE (ID int, DESCR varchar(100));
INSERT #CATS (ID, DESCR) VALUES
(1, 'MEN'),
(2, 'WOMEN'),
(3, 'UNISEX'),
(4, 'KIDS'),
(5, 'TEENS'),
(6, 'BACK TO SCHOOL');
DECLARE #SUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBCATS (ID, DESCR) VALUES
(10, 'FOOTWEAR'),
(12, 'OUTERWEAR'),
(14, 'SWIMWEAR'),
(15, 'HOODIES'),
(27, 'CLOTHING'),
(35, 'SPORTS');
DECLARE #SUBSUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBSUBCATS (ID, DESCR) VALUES
(100, 'RUNNING'),
(104, 'ZIP TOPS'),
(135, 'FLEECE'),
(157, 'BIKINIS'),
(238, 'PANTS'),
(374, 'JOGGERS');
SELECT prod.ID,
prod.DESCRIPTION,
CONCAT(cat1.DESCR, ' > ', cat2.DESCR, ' > ', cat3.DESCR) AS CATEGORIES
FROM #PRODUCTS AS prod
LEFT JOIN #CATS AS cat1 ON cat1.ID = prod.CAT
LEFT JOIN #SUBCATS AS cat2 ON cat2.ID = prod.SUBCAT
LEFT JOIN #SUBSUBCATS AS cat3 ON cat3.ID = prod.SUBSUBCAT;
Now suppose that the foreign keys on #PRODUCTS table aren't just indices to their respective tables. They are comma-separated indices to more than one categories, subcategories, and sub-subcategories like here.
DECLARE #PRODUCTS TABLE (ID int, DESCRIPTION varchar(50), CAT varchar(30), SUBCAT varchar(30), SUBSUBCAT varchar(30));
INSERT #PRODUCTS (ID, DESCRIPTION, CAT, SUBCAT, SUBSUBCAT) VALUES
(1, 'NIKE MILLENIUM', '1, 2', '10, 12', '100, 135'),
(2, 'NIKE CORTEZ', '1, 5', '12, 15', '104, 374'),
(3, 'ADIDAS PANTS', '2, 6', '27, 35', '238, 374');
DECLARE #CATS TABLE (ID int, DESCR varchar(100));
INSERT #CATS (ID, DESCR) VALUES
(1, 'MEN'),
(2, 'WOMEN'),
(3, 'UNISEX'),
(4, 'KIDS'),
(5, 'TEENS'),
(6, 'BACK TO SCHOOL');
DECLARE #SUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBCATS (ID, DESCR) VALUES
(10, 'FOOTWEAR'),
(12, 'OUTERWEAR'),
(14, 'SWIMWEAR'),
(15, 'HOODIES'),
(27, 'CLOTHING'),
(35, 'SPORTS');
DECLARE #SUBSUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBSUBCATS (ID, DESCR) VALUES
(100, 'RUNNING'),
(104, 'ZIP TOPS'),
(135, 'FLEECE'),
(157, 'BIKINIS'),
(238, 'PANTS'),
(374, 'JOGGERS');
SELECT prod.ID,
prod.DESCRIPTION
--CONCAT(cat1.DESCR, ' > ', cat2.DESCR, ' > ', cat3.DESCR) AS CATEGORIES
FROM #PRODUCTS AS prod
--LEFT JOIN #CATS AS cat1 ON cat1.ID = prod.CAT
--LEFT JOIN #SUBCATS AS cat2 ON cat2.ID = prod.SUBCAT
--LEFT JOIN #SUBSUBCATS AS cat3 ON cat3.ID = prod.SUBSUBCAT;
In this case I want to achieve the following:
Be able to retrieve the respective names of the cats, subcats, sub-subcats, ie. for cats '1, 2' be able to retrieve their names (I tried LEFT JOIN #CATS AS cat1 ON cat1.ID IN prod.CAT but it doesn't work)
Create triplets of the corresponding cats, subcats, sub-subcats, ie. for
cats '1, 2'
subcats '12, 17'
sub-subcats '239, 372'
(after retrieving the appropriate names) create pipe-separated category routes like name of cat 1 > name of subcat 12 > name of sub-subcat 239 | name of cat 2 > name of subcat 17 > name of sub-subcat 372
So, for a row like (1, 'NIKE MILLENIUM', '1, 2', '10, 12', '100, 135'),
I would like to get the following result
ID
DESCRIPTION
CATEGORIES
1
NIKE MILLENIUM
MEN > FOOTWEAR > RUNNING # WOMEN > OUTERWEAR > FLEECE (I had to use # as the delimiter of the two triplets because pipe messed with the table's columns)
In case the user stupidly stores more cat IDs than subcat IDs, or sub-subcat IDs, the query should just match the ones that have a corresponding position match, ie for
cats '1, 2'
subcats '12'
sub-subcats '239, 372'
it should just create one triplet, like name of 1 > name of 12 > name of 239
STRING_SPLIT() does not promise to return the values in a specific order, so it won't work in this case as ordinal position matters.
Use OPENJSON() split the string into separate rows to ensure the values are returned in the same order.
OPENJSON() also returns a key field, so you can join on the row number within each grouping. You'll want an INNER JOIN since your requirement is that all values in that "column" must exist.
Use STUFF() to assemble the various cat>subcat>subsubcat values.
DECLARE #PRODUCTS TABLE (ID int, DESCRIPTION varchar(50), CAT varchar(30), SUBCAT varchar(30), SUBSUBCAT varchar(30));
INSERT #PRODUCTS (ID, DESCRIPTION, CAT, SUBCAT, SUBSUBCAT) VALUES
(1, 'NIKE MILLENIUM', '1, 2', '10, 12', '100, 135'),
(2, 'NIKE CORTEZ', '1, 5', '12, 15', '104, 374'),
(3, 'ADIDAS PANTS', '2, 6, 1', '27, 35, 10', '238, 374, 100'),
(4, 'JOE THE PLUMBER JEANS', '1, 5', '27', '238, 374');
DECLARE #CATS TABLE (ID int, DESCR varchar(100));
INSERT #CATS (ID, DESCR) VALUES
(1, 'MEN'),
(2, 'WOMEN'),
(3, 'UNISEX'),
(4, 'KIDS'),
(5, 'TEENS'),
(6, 'BACK TO SCHOOL');
DECLARE #SUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBCATS (ID, DESCR) VALUES
(10, 'FOOTWEAR'),
(12, 'OUTERWEAR'),
(14, 'SWIMWEAR'),
(15, 'HOODIES'),
(27, 'CLOTHING'),
(35, 'SPORTS');
DECLARE #SUBSUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBSUBCATS (ID, DESCR) VALUES
(100, 'RUNNING'),
(104, 'ZIP TOPS'),
(135, 'FLEECE'),
(157, 'BIKINIS'),
(238, 'PANTS'),
(374, 'JOGGERS');
;
with prod as (
SELECT p.ID,
p.DESCRIPTION
--CONCAT(cat1.DESCR, ' > ', cat2.DESCR, ' > ', cat3.DESCR) AS CATEGORIES
, c.value as CatId
, c.[key] as CatKey
, sc.value as SubCatId
, sc.[key] as SubCatKey
, ssc.value as SubSubCatId
, ssc.[key] as SubSubCatKey
FROM #PRODUCTS p
cross apply OPENJSON(CONCAT('["', REPLACE(cat, ', ', '","'), '"]')) c
cross apply OPENJSON(CONCAT('["', REPLACE(subcat, ', ', '","'), '"]')) sc
cross apply OPENJSON(CONCAT('["', REPLACE(subsubcat, ', ', '","'), '"]')) ssc
where c.[key] = sc.[key]
and c.[key] = ssc.[key]
)
, a as (
select p.ID
, p.DESCRIPTION
, c.DESCR + ' > ' + sc.DESCR + ' > ' + ssc.DESCR as CATEGORIES
, p.CatKey
from prod p
inner join #CATS c on c.ID = p.CatId
inner join #SUBCATS sc on sc.ID = p.SubCatId
inner join #SUBSUBCATS ssc on ssc.ID = p.SubSubCatId
)
select DISTINCT ID
, DESCRIPTION
, replace(STUFF((SELECT distinct ' | ' + a2.CATEGORIES
from a a2
where a.ID = a2.ID
FOR XML PATH(''))
,1,2,''), '>', '>') CATEGORIES
from a
Totally separate answer because of the change to older technology. I think my original answer is still good for folks using current SQL Server versions, so I don't want to remove it.
I don't remember where I got the function. When I found it today it was named split_delimiter. I changed the name, added some comments, and incorporated the ability to have a delimiter that is more than one character long.
CREATE FUNCTION [dbo].[udf_split_string](#delimited_string VARCHAR(8000), #delimiter varchar(10))
RETURNS TABLE AS
RETURN
WITH cte10(num) AS ( -- 10 rows
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
, cte100(num) AS ( -- 100 rows
SELECT 1
FROM cte10 t1, cte10 t2
)
, cte10000(num) AS ( -- 10000 rows
SELECT 1
FROM cte100 t1, cte100 t2
)
, cte1(num) AS ( -- 1 row per character
SELECT TOP (ISNULL(DATALENGTH(#delimited_string), 0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM cte10000
)
, cte2(num) AS ( -- locations of strings
SELECT 1
UNION ALL
SELECT t.num + len(replace(#delimiter, ' ', '_'))
FROM cte1 t
WHERE SUBSTRING(#delimited_string, t.num, len(replace(#delimiter, ' ', '_'))) = #delimiter
)
, cte3(num, [len]) AS (
SELECT t.num
, ISNULL(NULLIF(CHARINDEX(#delimiter, #delimited_string, t.num), 0) - t.num, 8000)
FROM cte2 t
)
SELECT [Key] = ROW_NUMBER() OVER (ORDER BY t.num)
, [Value] = SUBSTRING(#delimited_string, t.num, t.[len])
FROM cte3 t;
GO
DECLARE #PRODUCTS TABLE (ID int, DESCRIPTION varchar(50), CAT varchar(30), SUBCAT varchar(30), SUBSUBCAT varchar(30));
INSERT #PRODUCTS (ID, DESCRIPTION, CAT, SUBCAT, SUBSUBCAT) VALUES
(1, 'NIKE MILLENIUM', '1, 2', '10, 12', '100, 135'),
(2, 'NIKE CORTEZ', '1, 5', '12, 15', '104, 374'),
(3, 'ADIDAS PANTS', '2, 6, 1', '27, 35, 10', '238, 374, 100'),
(4, 'JOE THE PLUMBER JEANS', '1, 5', '27', '238, 374');
DECLARE #CATS TABLE (ID int, DESCR varchar(100));
INSERT #CATS (ID, DESCR) VALUES
(1, 'MEN'),
(2, 'WOMEN'),
(3, 'UNISEX'),
(4, 'KIDS'),
(5, 'TEENS'),
(6, 'BACK TO SCHOOL');
DECLARE #SUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBCATS (ID, DESCR) VALUES
(10, 'FOOTWEAR'),
(12, 'OUTERWEAR'),
(14, 'SWIMWEAR'),
(15, 'HOODIES'),
(27, 'CLOTHING'),
(35, 'SPORTS');
DECLARE #SUBSUBCATS TABLE (ID int, DESCR varchar(100));
INSERT #SUBSUBCATS (ID, DESCR) VALUES
(100, 'RUNNING'),
(104, 'ZIP TOPS'),
(135, 'FLEECE'),
(157, 'BIKINIS'),
(238, 'PANTS'),
(374, 'JOGGERS');
;
with prod as (
SELECT p.ID,
p.DESCRIPTION
, c.value as CatId
, c.[key] as CatKey
, sc.value as SubCatId
, sc.[key] as SubCatKey
, ssc.value as SubSubCatId
, ssc.[key] as SubSubCatKey
FROM #PRODUCTS p
cross apply dbo.udf_split_string(cat, ', ') c
cross apply dbo.udf_split_string(subcat, ', ') sc
cross apply dbo.udf_split_string(subsubcat, ', ') ssc
where c.[key] = sc.[key]
and c.[key] = ssc.[key]
)
, a as (
select p.ID
, p.DESCRIPTION
, c.DESCR + ' > ' + sc.DESCR + ' > ' + ssc.DESCR as CATEGORIES
, p.CatKey
from prod p
inner join #CATS c on c.ID = p.CatId
inner join #SUBCATS sc on sc.ID = p.SubCatId
inner join #SUBSUBCATS ssc on ssc.ID = p.SubSubCatId
)
select DISTINCT ID
, DESCRIPTION
, replace(STUFF((SELECT distinct ' | ' + a2.CATEGORIES
from a a2
where a.ID = a2.ID
FOR XML PATH(''))
,1,2,''), '>', '>') CATEGORIES
from a
Well that should do work, i changed your character ">" for "-" just for see the data more simple.
the design of your tables is not perfect but the first try almost never is.
select mainp.ID, mainp.DESCRIPTION, stuff(ppaths.metapaths, len(ppaths.metapaths),1,'') metalinks
from #PRODUCTS mainp
cross apply(
select
(select
c.DESCR + '-' + sc.DESCR + '-' + sbc.DESCR + '|'
from #PRODUCTS p
cross apply (select row_number() over(order by Value) id, Value from split(p.CAT, ','))cat_ids
inner join #cats c on c.ID = cat_ids.Value
cross apply (select row_number() over(order by Value) id, Value from split(p.SUBCAT, ','))subcat_ids
inner join #SUBCATS sc on sc.ID = subcat_ids.Value
and subcat_ids.id = subcat_ids.id
cross apply (select row_number() over(order by Value) id, Value from split(p.SUBSUBCAT, ','))subsubcat_ids
inner join #SUBSUBCATS sbc on sbc.ID = subsubcat_ids.Value
and subsubcat_ids.id = subcat_ids.id
where p.id = mainp.ID
for xml path('')) metapaths
) ppaths
the link for split function
https://desarrolladores.me/2014/03/sql-server-funcion-split-para-dividir-un-string/

How to make hierarchy path from bottom SQL Server

I'm trying to make reverse hierarchy path.
This is for path manager-subordinate
WITH CTE_Path AS
(
SELECT
P1.EmployeeID, P1.ManagerID,
CAST(FirstName + LastName AS NVARCHAR(MAX)) AS PathHierarchy,
1 AS level
FROM
EmployeesManagers AS P1
WHERE
ManagerID IS NULL
UNION ALL
SELECT
P2.EmployeeID, P2.ManagerID,
CONCAT(CAST(C.PathHierarchy AS NVARCHAR(MAX)), N' > ',
CAST(P2.FirstName+LastName AS NVARCHAR(MAX))), level + 1
FROM
EmployeesManagers AS P2
JOIN
CTE_Path AS C ON C.EmployeeID = P2.ManagerID
)
SELECT PathHierarchy
FROM CTE_Path
Data for this
CREATE TABLE dbo.EmployeesManagers
(
EmployeeID int NOT NULL PRIMARY KEY,
FirstName varchar(50) NOT NULL,
LastName varchar(50) NOT NULL,
ManagerID int NULL
)
GO
INSERT INTO EmployeesManagers VALUES (101, 'Ken', 'Sánchez', NULL)
INSERT INTO EmployeesManagers VALUES (102, 'Terri', 'Duffy', 101)
INSERT INTO EmployeesManagers VALUES (103, 'Roberto', 'Tamburello', 101)
INSERT INTO EmployeesManagers VALUES (104, 'Rob', 'Walters', 102)
INSERT INTO EmployeesManagers VALUES (105, 'Gail', 'Erickson', 102)
INSERT INTO EmployeesManagers VALUES (106, 'Jossef', 'Goldberg', 103)
INSERT INTO EmployeesManagers VALUES (107, 'Dylan', 'Miller', 103)
INSERT INTO EmployeesManagers VALUES (108, 'Diane', 'Margheim', 105)
INSERT INTO EmployeesManagers VALUES (109, 'Gigi', 'Matthew', 105)
INSERT INTO EmployeesManagers VALUES (110, 'Michael', 'Raheem', 106)
And I need subordinate-manager-manager1 - ...
What to change here? Anchor or recursive part?
Is it possible to create function where you can give argument EmployeeID and it will return path employee-manager-manager1-ceoOfCompany?
You can reverse the logic by starting where employees are not a manager and then tweak the join in the second part of the recursive subquery:
WITH CTE AS (
SELECT P1.EmployeeID, P1.ManagerID, CAST(FirstName+LastName AS NVARCHAR(MAX)) AS PathHierarchy, 1 as level
FROM EmployeesManagers AS P1
WHERE NOT EXISTS (SELECT 1 FROM EmployeesManagers em2 WHERE EM2.ManagerID = p1.EmployeeId)
UNION ALL
SELECT P2.EmployeeID, P2.ManagerID, CONCAT(CAST(C.PathHierarchy AS NVARCHAR(MAX)), N' > ', CAST(P2.FirstName+LastName AS NVARCHAR(MAX))), level+1
FROM CTE c JOIN
EmployeesManagers P2
ON P2.EmployeeID = C.ManagerID
)
SELECT PathHierarchy
FROM CTE;
Here is a db<>fiddle.

Getting Paged Distinct Records Using SQL (Not Duplicate)

Following #mdb's answer to apply pagination using SQL SERVER, I find it hard to retrieve distinct records when the main table is joined to other tables for a one-to-many relationship, i.e, A person has many addresses.
Use case, suppose I want to retrieve all persons which has an address in New York given tables #temp_person and #temp_addresses, I would join them on PersonID and OwnerID.
The problem arises when there are multiple addresses for a person, the result set contains duplicate records.
To make it clearer, here's a sample query with data:
Sample Data:
create table #temp_person (
PersonID int not null,
FullName varchar(max) not null
)
create table #temp_addresses(
AddressID int identity not null,
OwnerID int not null,
Address1 varchar(max),
City varchar(max)
)
insert into #temp_person
values
(1, 'Sample One'),
(2, 'Sample Two'),
(3, 'Sample Three')
insert into #temp_addresses (OwnerID, Address1, City)
values
(1, 'Somewhere East Of', 'New York'),
(1, 'Somewhere West Of', 'New York'),
(2, 'blah blah blah', 'Atlantis'),
(2, 'Address2 Of Sample Two', 'New York'),
(2, 'Address3 Of Sample Two', 'Nowhere City'),
(3, 'Address1 Of Sample Three', 'New York'),
(3, 'Address2 Of Sample Three', 'Seattle')
--drop table #temp_addresses, #temp_person
Pagination Query:
SELECT
(
CAST( RowNum as varchar(MAX) )
+ '/'
+ CAST(TotalCount as varchar(MAX))
) as ResultPosition
, PersonID
, FullName
FROM (
SELECT DISTINCT
ROW_NUMBER() OVER(ORDER BY p.FullName ASC) as RowNum
, p.PersonID
, p.FullName
, Count(1) OVER() as TotalCount
FROM #temp_person p
LEFT JOIN #temp_addresses a
ON p.PersonID = a.OwnerID
WHERE City = 'New York'
) as RowConstrainedResult
WHERE RowNum > 0 AND RowNum <= 3
ORDER BY RowNum
Expected Results:
ResultPosition PersonID FullName
1/3 1 Sample One
2/3 2 Sample Two
3/3 3 Sample Three
Actual Results:
ResultPosition PersonID FullName
1/4 1 Sample One
2/4 1 Sample One
3/4 3 Sample Three
As you can see, the inner query is returning multiple records due to the join with #temp_addresses.
Is there a way we could only return unique records by PersonID?
UPDATE:
Actual use case is for an "Advanced Search" functionality where the user can search using different filters, i.e, name, firstname, last name, birthdate, address, etc.. The <WHERE_CLAUSE> and <JOIN_STATEMENTS> in the query are added dynamically so GROUP BY is not applicable here.
Also, please address the "Pagination" scheme for this question. That is, I want to retrieve only N number of results from Start while also retrieving the total count of the results as if they are not paged. i.e, I retrieve only 25 rows out of a total of 500 results.
Just do group by PersonID and no need to use subquery
SELECT
cast(row_number() over (order by (select 1)) as varchar(max)) +'/'+
cast(Count(1) OVER() as varchar(max)) ResultPosition,
p.PersonID,
max(p.FullName) FullName
FROM #temp_person p
LEFT JOIN #temp_addresses a ON p.PersonID = a.OwnerID
WHERE City = 'New York'
group by p.PersonID
EDIT : I would use CTE for the pagination
;with cte as
(
SELECT
row_number() over(order by (select 1)) rn,
cast(row_number() over (order by (select 1)) as varchar(max)) +'/'+
cast(Count(1) OVER() as varchar(max)) ResultPosition,
p.PersonID,
max(p.FullName) FullName
FROM #temp_person p
LEFT JOIN #temp_addresses a ON p.PersonID = a.OwnerID
WHERE City = 'New York'
group by p.PersonID
)
select * from cte
where rn > 0 and rn <= 2
Result:
ResultPosition PersonID FullName
1/3 1 Sample One
2/3 2 Sample Two
3/3 3 Sample Three
You need to have distinct rows before using ROW_NUMBER().
If you will filter by City, there are no need to use LEFT JOIN. Use INNER JOIN instead.
select ResultPosition = cast(row_number() over (order by (r.PersonID)) as varchar(max)) +'/'+ cast(Count(r.PersonID) OVER() as varchar(max)), *
from(
SELECT distinct p.PersonID,
p.FullName
FROM #temp_person p
JOIN #temp_addresses a ON
p.PersonID = a.OwnerID
WHERE City = 'New York') r
EDIT:
Considering pagination
declare #page int =1, #rowsPage int = 25
select distinct position, ResultPosition = cast(position as varchar(10)) + '/' + cast(count(*) OVER() as varchar(10)), *
from(
SELECT position = DENSE_RANK () over (order by p.PersonID),
p.PersonID,
p.FullName
FROM #temp_person p
LEFT JOIN #temp_addresses a ON
p.PersonID = a.OwnerID
WHERE City = 'New York'
) r
where position between #rowsPage*(#page-1)+1 and #rowsPage*#page
Geoman Yabes, Check if this help... Gives results expected in your example and you can have pagination using RowNum:-
SELECT *
FROM
(SELECT ROW_NUMBER() OVER(ORDER BY RowConstrainedResult.PersonId ASC) As RowNum,
Count(1) OVER() As TotalRows,
RowConstrainedResult.PersonId,
RowConstrainedResult.FullName
FROM (
SELECT
RANK() OVER(PARTITION BY p.PersonId ORDER BY a.Address1 ASC) as Ranking
, p.PersonID
, p.FullName
FROM #temp_person p
INNER JOIN #temp_addresses a ON p.PersonID = a.OwnerID WHERE City = 'New York'
) as RowConstrainedResult WHERE Ranking = 1) Filtered
Where RowNum > 0 And RowNum <= 4
Sample Data:
insert into #temp_person
values
(1, 'Sample One'),
(2, 'Sample Two'),
(3, 'Sample Three'),
(4, 'Sample 4'),
(5, 'Sample 5'),
(6, 'Sample 6')
insert into #temp_addresses (OwnerID, Address1, City)
values
(1, 'Somewhere East Of', 'New York'),
(1, 'Somewhere West Of', 'New York'),
(2, 'blah blah blah', 'Atlantis'),
(2, 'Address2 Of Sample Two', 'New York'),
(2, 'Address3 Of Sample Two', 'Nowhere City'),
(3, 'Address1 Of Sample Three', 'New York'),
(3, 'Address2 Of Sample Three', 'Seattle'),
(4, 'Address1 Of Sample 4', 'New York'),
(4, 'Address1 Of Sample 4', 'New York 2'),
(5, 'Address1 Of Sample 5', 'New York'),
(6, 'Address1 Of Sample 6', 'New York')

Building Matrix via SQL

I am using two query from a schema which counts companies, but I am struggling how to combine them give Columns as Country and Industry as row and give the company counts accordingly.
select g.simpleindustrydescription, count(c.companyid) as companycount from ciqcompany c
join ciqsimpleindustry g on g.simpleIndustryid = c.simpleIndustryid
join ciqbusinessdescription b on b.companyid = c.companyid
group by g.simpleindustrydescription
select g.country, count(c.companyid) as companycount from ciqcompany c
join ciqcountrygeo g on g.countryid = c.countryid
join ciqbusinessdescription b on b.companyid = c.companyid
group by g.country
Expected Output:
Country A Country B Country C
Industry A 5 5 6
Industry B 3 3 4
Industry C 4 8 6
Due to lack of real example data, here a simple pivot example basing on some dummy records:
CREATE TABLE #tCountry
(
ID INT
,Name NVARCHAR(100)
);
INSERT INTO #tCountry VALUES (1, 'Country A'), (2, 'Country B'), (3, 'Country C');
CREATE TABLE #tIndustry
(
ID INT
,Name NVARCHAR(100)
);
INSERT INTO #tIndustry VALUES (1, 'Industry A'), (2, 'Industry B'), (3, 'Industry C');
CREATE TABLE #tMapping
(
ID INT
,CountryID INT
,IndustryID INT
,Name NVARCHAR(100)
);
INSERT INTO #tMapping VALUES (1, 1, 1, 'Country A Industry A - 1'), (2, 1, 1, 'Country A Industry A - 2');
INSERT INTO #tMapping VALUES (3, 1, 2, 'Country A Industry B - 1'), (4, 1, 2, 'Country A Industry b - 2'), (5, 1, 2, 'Country A Industry b - 3');
INSERT INTO #tMapping VALUES (6, 2, 1, 'Country B Industry A - 1');
DECLARE #lCountries NVARCHAR(max) = N'';
DECLARE #stmt NVARCHAR(MAX) = N'';
SELECT #lCountries += N', ' + QUOTENAME(CountryName)
FROM(
SELECT DISTINCT tc.Name CountryName
FROM #tCountry tc
JOIN #tMapping tm ON tm.CountryID = tc.ID
) x;
SELECT #lCountries = STUFF(#lCountries, 1, 2, '');
SELECT #stmt = 'SELECT *
FROM
(
SELECT ti.Name IndustryName, tc.Name CountryName, COUNT(*) MappingCounter
FROM #tMapping tm
JOIN #tCountry tc ON tm.CountryID = tc.ID
JOIN #tIndustry ti ON tm.IndustryID = ti.ID
GROUP BY ti.Name, tc.Name
) t
PIVOT (MAX(t.MappingCounter) FOR CountryName in (' + #lCountries + ')) AS x';
EXEC sp_executesql #stmt
Anyways, if you are dealing with an unknown number of countries, you might want to extend this example a little and use dynamic SQL in order to build the pivot statement. Got an example somewhere, but would have to search for it...
Result of my example:
IndustryName Country A Country B Country C
Industry A 2 1 NULL
Industry B 3 NULL NULL