Recursive CTE possible for this? - sql-server

I have product data structured in the following format:
ProductID OptionID Lvl OptionDescription SubOptionID SubOptionDescription
HPH 6 1 Model 10 Studio
HPH 6 1 Model 11 DJ
HPH 7 2 Device 12 Bluetooth
HPH 7 2 Device 13 Cable
HPH 7 2 Device 14 Remote
There could be any number of levels to the product. I need to traverse the hierarchy and produce the following output - a description for each product option:
Studio-Bluetooth
Studio-Cable
Studio-Remote
DJ-Bluetooth
DJ-Cable
DJ-Remote
I've looked CTE's but the examples tend to incorporate adjacent lists (employeeID; managerID..etc) which don't seem appropriate here.
How can I achieve this output?
Thanks.
CREATE TABLE [dbo].[Products](
[ProductID] [varchar](50) NULL,
[OptionID] [int] NULL,
[Lvl] [int] NULL,
[OptionDescription] [varchar](50) NULL,
[SubOptionID] [int] NULL,
[SubOptionDescription] [varchar](50) NULL
) ON [PRIMARY]
insert into Products (ProductID, OptionID, Lvl, OptionDescription, SubOptionID, SubOptionDescription) values ('HPH', 6, 1, 'Model', 10, 'Studio')
insert into Products (ProductID, OptionID, Lvl, OptionDescription, SubOptionID, SubOptionDescription) values ('HPH', 6, 1, 'Model', 11, 'DJ')
insert into Products (ProductID, OptionID, Lvl, OptionDescription, SubOptionID, SubOptionDescription) values ('HPH', 7, 2, 'Device', 12, 'Bluetooth')
insert into Products (ProductID, OptionID, Lvl, OptionDescription, SubOptionID, SubOptionDescription) values ('HPH', 7, 2, 'Device', 13, 'Cable')
insert into Products (ProductID, OptionID, Lvl, OptionDescription, SubOptionID, SubOptionDescription) values ('HPH', 7, 2, 'Device', 14, 'Remote')

with cte as (
-- Root level
select p.Lvl, cast(p.SubOptionDescription as varchar(max)) as [ProductOption]
from #Products p where p.Lvl = 1
union all
-- Anchor part - cartesian here?
select p.Lvl, c.ProductOption + '-' + p.SubOptionDescription
from #Products p
inner join cte c on c.Lvl = p.Lvl - 1
)
select c.ProductOption from cte c;
A couple of notes.
Right now your sample answer implies that you need to create a cartesian product. I hope this is not the case, because the amount of rows will increase explosively. If there are other join conditions which are not apparent from your sample, you can introduce them in the anchor part of the CTE.
You would probably also want to return only leaf rows. There are several ways to do it - there may be some attribute in your actual data, or a combination of rank() and top (1) with ties will do the trick, although it won't be particularly efficient.

Related

Join most recent Date AND other fields that belong to that date from another table

I want to JOIN a different table that has DATE values in it, and I only want the most recent Date to be added and te most recent Value that corresponds with that Date.
I have a table in which certain RENTALOBJECTS in the RENTALOBJECTTABLE have a N:1 relationship with the OBJECTTABLE
RENTALOBJECTTABLE:
RENTALOBJECTID, OBJECTID
1, 1
2, 1
3, 2
4, 3
5, 4
6, 4
OBJECTTABLE:
OBJECTID
1
2
3
4
Every OBJECTID can (and usually has, more than 1) VALUE
VALUETABLE:
OBJECTID, VALUE, VALIDFROM, VALIDTO, CODE
1, 2000, 1-1-1950, 31-12-1980, A
1, 3000, 1-1-1981, 31-12-2010, A
1, 4000, 1-1-2013, NULL, A
2, 1000, 1-1-1970, NULL, A
3, 2000, 1-1-2010, NULL, A
4, 2000, 1-1-2000, 31-12-2009, A
4, 3100, 1-1-2010, NULL, B
4, 3000, 1-1-2010, NULL, A
And combined I want for every RentalObject the most recent VALUE to be shown. End result expected:
RENTALOBJECTTABLE_WITHVALUE:
RENTALOBJECTID, OBJECTID, VALUE, VALIDFROM, VALIDTO, CODE
1, 1, 4000, 1-1-2013, NULL, A
2, 1, 4000, 1-1-2013, NULL, A
3, 2, 1000, 1-1-1970, NULL, A
4, 3, 2000, 1-1-2010, NULL, A
5, 4, 3000, 1-1-2010, NULL, A
6, 4, 3000, 1-1-2010, NULL, A
I so far managed to get the Most Recent Date joined to the table with the code below. However, as soon as I want to INCLUDE VALUETABLE.VALUE then the rowcount goes from 5000 (what the original dataset has) to 48000.
SELECT
RENTALOBJECTTABLE.RENTALOBJECTID
FROM RENTALOBJECTTABLE
LEFT JOIN OBJECTTABLE
ON OBJECTTABLE.OBJECTID = RENTALOBJECTTABLE.OBJECTID
LEFT JOIN (
SELECT
OBJECTID,
CODE,
VALUE, --without this one it gives the same rows as the original table
MAX(VALIDFROM) VALIDFROM
FROM VALUETABLE
LEFT JOIN PMETYPE
ON VALUETABLE.CODE = PMETYPE.RECID
AND PMETYPE.REGISTERTYPENO = 6
WHERE PMETYPE.[NAME] = 'WOZ'
GROUP BY OBJECTID, CODE, VALUE
) VALUETABLE ON OBJECTTABLE.OBJECTID = VALUETABLE.OBJECTID
When I include MAX(VALUE) next to the MAX(Date) it obviously has the original 5000 dataset rows again, but now it only selects the most recent date + highest value, which is not always correct.
Anyone any clue about how to solve this issue?
I think I miss something very obvious.
This gets you close
WITH cte AS
(
SELECT
o.OBJECTID,
v.VALUE,
v.VALIDFROM,
v.VALIDTO,
v.CODE,
ROW_NUMBER() OVER (PARTITION BY o.OBJECTID ORDER BY v.VALIDFROM DESC ) rn
FROM dbo.OBJECTTABLE o
INNER JOIN dbo.VALUETABLE v ON v.OBJECTID = o.OBJECTID
)
SELECT ro.RENTALOBJECTID,
ro.OBJECTID,
cte.OBJECTID,
cte.VALUE,
cte.VALIDFROM,
cte.VALIDTO,
cte.CODE
FROM dbo.RENTALOBJECTTABLE ro
INNER JOIN cte ON cte.OBJECTID = ro.OBJECTID
AND rn=1;
However, this might pull out the 3100 value for object 4 - there is nothing to separate the two values with the same validfrom. If you have (or can add) an identity column to the value table, you can use this in the order by on the partitioning to select the row you want.
Your Sample Data
select * into #RENTALOBJECTTABLE from (
SELECT 1 AS RENTALOBJECTID, 1 OBJECTID
UNION ALL SELECT 2,1
UNION ALL SELECT 3,2
UNION ALL SELECT 4,3
UNION ALL SELECT 5,4
UNION ALL SELECT 6,4) A
SELECT * INTO #OBJECTTABLE FROM(
SELECT
1 OBJECTID
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4)AS B
SELECT * INTO #VALUETABLE FROM (
SELECT 1OBJECTID,2000 VALUE,'1-1-1950'VALIDFROM,'31-12-1980' VALIDTO, 'A' CODE
UNION ALL SELECT 1,3000,'1-1-1981','31-12-2010', 'A'
UNION ALL SELECT 1,4000,'1-1-2013',NULL, 'A'
UNION ALL SELECT 2,1000,'1-1-1970',NULL, 'A'
UNION ALL SELECT 3,2000,'1-1-2010',NULL, 'A'
UNION ALL SELECT 4,2000,'1-1-2000','31-12-2009', 'A'
UNION ALL SELECT 4,3100,'1-1-2010',NULL, 'B'
UNION ALL SELECT 4,3000,'1-1-2010',NULL, 'A'
) AS C
Query:
;WITH CTE AS (
SELECT * , ROW_NUMBER()OVER(PARTITION BY OBJECTID ORDER BY OBJECTID DESC)RN FROM #VALUETABLE
)
SELECT RO.RENTALOBJECTID,RO.OBJECTID,C.VALUE,C.VALIDFROM,C.VALIDTO,C.CODE
FROM CTE C
CROSS APPLY (SELECT OBJECTID,MAX(RN)RN FROM CTE C1 WHERE C.OBJECTID=C1.OBJECTID GROUP BY OBJECTID )AS B
INNER JOIN #RENTALOBJECTTABLE RO ON RO.OBJECTID=C.OBJECTID
WHERE C.OBJECTID=B.OBJECTID AND C.RN=B.RN
OutPut Data:
RENTALOBJECTID, OBJECTID, VALUE, VALIDFROM, VALIDTO, CODE
1, 1, 4000, 1-1-2013, NULL, A
2, 1, 4000, 1-1-2013, NULL, A
3, 2, 1000, 1-1-1970, NULL, A
4, 3, 2000, 1-1-2010, NULL, A
5, 4, 3000, 1-1-2010, NULL, A
6, 4, 3000, 1-1-2010, NULL, A

The maximum recursion 100 has been exhausted before statement completion (SQL Server)

In SQL Server, I have this simplified table and I'm trying to get a list of all employees with their domain manager:
IF OBJECT_ID('tempdb.dbo.#employees') IS NOT NULL DROP TABLE #employees
CREATE TABLE #employees (
empid int,
empname varchar(50),
mgrid int,
func varchar(50)
)
INSERT INTO #employees VALUES(1, 'Jeff', 2, 'Designer')
INSERT INTO #employees VALUES(2, 'Luke', 4, 'Head of designers')
INSERT INTO #employees VALUES(3, 'Vera', 2, 'Designer')
INSERT INTO #employees VALUES(4, 'Peter', 5, 'Domain Manager')
INSERT INTO #employees VALUES(5, 'Olivia', NULL, 'CEO')
;
WITH Emp_CTE AS (
SELECT empid, empname, func, mgrid AS dommgr
FROM #employees
UNION ALL
SELECT e.empid, e.empname, e.func, e.mgrid AS dommgr
FROM #employees e
INNER JOIN Emp_CTE ecte ON ecte.empid = e.mgrid
WHERE ecte.func <> 'Domain Manager'
)
SELECT * FROM Emp_CTE
So the output I want is:
empid empname func dommgr
1 Jeff Designer 4
2 Luke Head of designers 4
3 Vera Designer 4
Instead I get this error:
Msg 530, Level 16, State 1, Line 17
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
What am I doing wrong? Is it actually possible with CTE?
Edit: There was indeed an error in the data, the error has gone now, but the result isn't what I want:
empid empname func dommgr
1 Jeff Designer 2
2 Luke Head of designers 4
3 Vera Designer 2
4 Peter Domain Manager 5
5 Olivia CEO NULL
4 Peter Domain Manager 5
1 Jeff Designer 2
3 Vera Designer 2
You had two employees which were referenecing each other in the managerid, so one was the manager of the other. That caused the infinite recursion. There was also a gap in the recursion tree because the domain-manager was not referenced anywhere. You have fixed the sample data by changing Luke`s mgrid to 4. Now there is no gap and no lgical issue anymore.
But you also had no root entry for the recursion, the first query has no filter.
You can use this query:
WITH DomainManager AS (
SELECT empid, empname, func, dommgr = empid, Hyrarchy = 1
FROM #employees
WHERE func = 'Domain Manager'
UNION ALL
SELECT e.empid, e.empname, e.func, dommgr, Hyrarchy = Hyrarchy +1
FROM #employees e
INNER JOIN DomainManager dm ON dm.empid = e.mgrid
)
SELECT * FROM DomainManager
WHERE func <> 'Domain Manager'
ORDER BY empid
Note that the enry/root point for the CTE is the Domain Manager because you want to find every employees domain manager's ids. This id is transported down the hyrarchy. The final select needs to filter out the Domain Manager because you only want his ID for every employee, you dont want to include him in the result set.
The result of the query is:
empid empname func dommgr Hyrarchy
1 Jeff Designer 4 3
2 Luke Head of designers 4 2
3 Vera Designer 4 3
The error message is raised because the data contains a circular reference between Luke and Vera.
It's easier to perform hierarchical queries if you add a hierarchyid field. SQL Server provides functions that return descendants, ancestors and the level in a hierarchy. hierarchyid fields can be indexed resulting in improved performance.
In the employee example, you can add a level field :
declare #employees table (
empid int PRIMARY KEY,
empname varchar(50),
mgrid int,
func varchar(50),
level hierarchyid not null,
INDEX IX_Level (level)
)
INSERT INTO #employees VALUES
(1, 'Jeff', 2, 'Designer' ,'/5/4/2/1/'),
(2, 'Luke', 4, 'Head of designers','/5/4/2/'),
(3, 'Vera', 2, 'Designer' ,'/5/4/2/3/'),
(4, 'Peter', 5, 'Domain Manager' ,'/5/4/'),
(5, 'Olivia', NULL, 'CEO' ,'/5/')
;
` declare #employees table (
empid int PRIMARY KEY,
empname varchar(50),
mgrid int,
func varchar(50),
level hierarchyid not null,
INDEX IX_Level (level)
)
INSERT INTO #employees VALUES
(1, 'Jeff', 2, 'Designer' ,'/5/4/2/1/'),
(2, 'Luke', 4, 'Head of designers','/5/4/2/'),
(3, 'Vera', 2, 'Designer' ,'/5/4/2/3/'),
(4, 'Peter', 5, 'Domain Manager' ,'/5/4/'),
(5, 'Olivia', NULL, 'CEO' ,'/5/')
;
/5/4/2/1/ is the string representation of a hieararchyID value. It's essentially the path in the hierarchy that leads to a particular row.
To find all subordinates of domain managers, excluding the managers themselves, you can write :
with DMs as
(
select EmpID,level
from #employees
where func='Domain Manager'
)
select
PCs.empid,
PCs.empname as Name,
PCs.func as Class,
DMs.empid as DM,
PCs.level.GetLevel() as THAC0,
PCs.level.GetLevel()- DMs.level.GetLevel() as NextLevel
from
#employees PCs
inner join DMs on PCs.level.IsDescendantOf(DMs.level)=1
where DMs.EmpID<>PCs.empid;
The CTE is only used for convenience
The result is :
empid Name Class DM THAC0 NextLevel
1 Jeff Designer 4 4 2
2 Luke Head of designers 4 3 1
3 Vera Designer 4 4 2
The CTE returns all DMs and their hierarchyid value. The IsDescendantOf() query checks whether a row is a descentant of a DM or not. GetLevel() returns the level of the row in the hierarchy. By subtracting the DM's level from the employee's we get the distance between them
Like others said, you have here a problem with data (Vera).
IF OBJECT_ID('tempdb.dbo.#employees') IS NOT NULL
DROP TABLE #employees
CREATE TABLE #employees (
empid int,
empname varchar(50),
mgrid int,
func varchar(50)
)
INSERT INTO #employees VALUES(1, 'Jeff', 2, 'Designer')
INSERT INTO #employees VALUES(2, 'Luke', 3, 'Head of designers')
INSERT INTO #employees VALUES(3, 'Vera', 4, 'Designer') --**mgrid = 4 instead 2**
INSERT INTO #employees VALUES(4, 'Peter', 5, 'Domain Manager')
INSERT INTO #employees VALUES(5, 'Olivia', NULL, 'CEO')
;WITH Emp_CTE AS
(
SELECT empid, empname, func, mgrid AS dommgr, 0 AS Done
FROM #employees
UNION ALL
SELECT ecte.empid, ecte.empname, ecte.func,
CASE WHEN e.func = 'Domain Manager' THEN e.empid ELSE e.mgrid END AS dommgr,
CASE WHEN e.func = 'Domain Manager' THEN 1 ELSE 0 END AS Done
FROM Emp_CTE AS ecte
INNER JOIN #employees AS e ON
ecte.dommgr = e.empid
WHERE ecte.Done = 0--emp.func <> 'Domain Manager'
)
SELECT *
FROM Emp_CTE
WHERE Done = 1

SQL to select items not already in other table

I currently have sql table (with its columns) as follows:
ORDERS (orderid, orderno, orderdate, customer)
ORDERDETAILS (id, orderid, itemcode, qty, price)
DELIVERY (deliveryid, orderid, deliverydate)
DELIVERYDETAILS (id, deliveryid, itemcode, qty)
ORDERS and ORDERDETAILS contains order of items. DELIVERY and DELIVERYDETAILS contains items that have been delivered, and it is 1 to Many to order (ie. 1 order can have many deliveries).
Example.
ORDERS: 1, '001', '2016-07-01', 'CUST001'
ORDERDETAILS:
1, 1, 'ITEM001', 1, 2.50
2, 1, 'ITEM002', 3, 7.50
3, 1, 'ITEM003', 6, 8.50
DELIVERY: 1, 1, '2016-07-02'
DELIVERY DETAILS:
1, 1, 'ITEM001', 1
2, 1, 'ITEM002', 1
DELIVERY: 2, 1, '2016-07-03'
DELIVERY DETAILS:
3, 2, 'ITEM002', 1
4, 2, 'ITEM003', 2
How do I need to generate a list of items that have not been delivered of an order using sql as follows.
UNDELIVERED ITEM as of '2016-07-04': itemcode, qty
'ITEM001', 0
'ITEM002', 1
'ITEM003', 4
Appreciate any advice.
Updates: Change order to orders. Added examples and results
I am not sure that my understanding is correct. Because i am confused with order delivery relationship (ie. 1 to Many ).
But according to my understanding you need list of all order no that doesn't have an delivery Id
SELECT o.orderid,o.orderno,o.customer
FROM ORDER o
LEFT JOIN DELIVERY d
ON o.orderno = d.orderno
WHERE d.orderno IS NULL
So this query will return all the orderno that is not in delivery table.
Are you expecting anything like this ?
UPDATE
Query is may not optimized, But hope this will solve your issue.
SELECT dd.itemcode, SUM(dd.qty) as delivrdQty,
(SELECT od.qty FROM ORDERDETAILS od WHERE od.itemcode = dd.itemcode) as originalQty,
(SELECT od.qty FROM ORDERDETAILS od WHERE od.itemcode = dd.itemcode) - SUM(dd.qty) as remainQty
FROM DELIVERYDETAILS dd
INNER JOIN DELIVERY d ON dd.deliveryid = d.deliveryid
GROUP BY dd.itemcode ,dd.qty

Suggestions for improving slow performance of subquery

I've tried to illustrate the problem in the (made-up) example below. Essentially, I want to filter records in the primary table based on content in a secondary table. When I attempted this using subqueries, our application performance took a big hit (some queries nearly 10x slower).
In this example I want to return all case notes for a customer EXCEPT for the ones that have references to products 1111 and 2222 in the detail table:
select cn.id, cn.summary from case_notes cn
where customer_id = 2
and exists (
select 1 from case_note_details cnd
where cnd.case_note_id = cn.id
and cnd.product_id not in (1111,2222)
)
I tried using a join as well:
select distinct cn.id, cn.summary from case_notes cn
join case_note_details cnd
on cnd.case_note_id = cn.id
and cnd.product_id not in (1111,2222)
where customer_id = 2
In both cases the execution plan shows two clustered index scans. Any suggestions for other methods or tweaks to improve performance?
Schema:
CREATE TABLE case_notes
(
id int primary key,
employee_id int,
customer_id int,
order_id int,
summary varchar(50)
);
CREATE TABLE case_note_details
(
id int primary key,
case_note_id int,
product_id int,
detail varchar(1024)
);
Sample data:
INSERT INTO case_notes
(id, employee_id, customer_id, order_id, summary)
VALUES
(1, 1, 2, 1000, 'complaint1'),
(2, 1, 2, 1001, 'complaint2'),
(3, 1, 2, 1002, 'complaint3'),
(4, 1, 2, 1003, 'complaint4');
INSERT INTO case_note_details
(id, case_note_id, product_id, detail)
VALUES
(1, 1, 1111, 'Note 1, order 1000, complaint about product 1111'),
(2, 1, 2222, 'Note 1, order 1000, complaint about product 2222'),
(3, 2, 1111, 'Note 2, order 1001, complaint about product 1111'),
(4, 2, 2222, 'Note 2, order 1001, complaint about product 2222'),
(5, 3, 3333, 'Note 3, order 1002, complaint about product 3333'),
(6, 3, 4444, 'Note 3, order 1002, complaint about product 4444'),
(7, 4, 5555, 'Note 4, order 1003, complaint about product 5555'),
(8, 4, 6666, 'Note 4, order 1003, complaint about product 6666');
You have a clustered index scan because you are not accessing your case_note_details table by its id but via non-indexed columns.
I suggest adding an index to the case-note_details table on case_note_id, product_id.
If you are always accessing the case_note_details via the case_note_id, you might also restructure your primary key to be case_note_id, detail_id. There is no need for an independent id as primary key for dependent records. This would let you re-use your detail primary key index for joins with the header table.
Edit: add an index on customer_id as well to the case_notes table, as Manuel Rocha suggested.
When using "exists" I always limit results with "TOP" as bellow:
select cn.id
,cn.summary
from case_notes as cn
where customer_id = 2
and exists (
select TOP 1 1
from case_note_details as cnd
where cnd.case_note_id = cn.id
and cnd.product_id not in (1111,2222)
)
In table case_notes create index for customer_id and on table case_note_details create index for case_note_id and case_note_id.
Then try execute both query. Should have better performance now.
Try also this query
select
cn.id,
cn.summary
from
case_notes cn
where
cn.customer_id = 2 and
cn.id in
(
select
distinct cnd.case_note_id
from
case_note_details cnd
where
cnd.product_id not in (1111,2222)
)
Did you try "in" instead of "exists". This sometimes performs differently:
select cn.id, cn.summary from case_notes cn
where customer_id = 2
and cn.id in (
select cnd.case_note_id from case_note_details cnd
where cnd.product_id not in (1111,2222)
)
Of course, check indexes.

CTE Recursion to get tree hierarchy

I need to get an ordered hierarchy of a tree, in a specific way. The table in question looks a bit like this (all ID fields are uniqueidentifiers, I've simplified the data for sake of example):
EstimateItemID EstimateID ParentEstimateItemID ItemType
-------------- ---------- -------------------- --------
1 A NULL product
2 A 1 product
3 A 2 service
4 A NULL product
5 A 4 product
6 A 5 service
7 A 1 service
8 A 4 product
Graphical view of the tree structure (* denotes 'service'):
A
___/ \___
/ \
1 4
/ \ / \
2 7* 5 8
/ /
3* 6*
Using this query, I can get the hierarchy (just pretend 'A' is a uniqueidentifier, I know it isn't in real life):
DECLARE #EstimateID uniqueidentifier
SELECT #EstimateID = 'A'
;WITH temp as(
SELECT * FROM EstimateItem
WHERE EstimateID = #EstimateID
UNION ALL
SELECT ei.* FROM EstimateItem ei
INNER JOIN temp x ON ei.ParentEstimateItemID = x.EstimateItemID
)
SELECT * FROM temp
This gives me the children of EstimateID 'A', but in the order that it appears in the table. ie:
EstimateItemID
--------------
1
2
3
4
5
6
7
8
Unfortunately, what I need is an ordered hierarchy with a result set that follows the following constraints:
1. each branch must be grouped
2. records with ItemType 'product' and parent are the top node
3. records with ItemType 'product' and non-NULL parent grouped after top node
4. records with ItemType 'service' are bottom node of a branch
So, the order that I need the results, in this example, is:
EstimateItemID
--------------
1
2
3
7
4
5
8
6
What do I need to add to my query to accomplish this?
Try this:
;WITH items AS (
SELECT EstimateItemID, ItemType
, 0 AS Level
, CAST(EstimateItemID AS VARCHAR(255)) AS Path
FROM EstimateItem
WHERE ParentEstimateItemID IS NULL AND EstimateID = #EstimateID
UNION ALL
SELECT i.EstimateItemID, i.ItemType
, Level + 1
, CAST(Path + '.' + CAST(i.EstimateItemID AS VARCHAR(255)) AS VARCHAR(255))
FROM EstimateItem i
INNER JOIN items itms ON itms.EstimateItemID = i.ParentEstimateItemID
)
SELECT * FROM items ORDER BY Path
With Path - rows a sorted by parents nodes
If you want sort childnodes by ItemType for each level, than you can play with Level and SUBSTRING of Pathcolumn....
Here SQLFiddle with sample of data
This is an add-on to Fabio's great idea from above. Like I said in my reply to his original post. I have re-posted his idea using more common data, table name, and fields to make it easier for others to follow.
Thank you Fabio! Great name by the way.
First some data to work with:
CREATE TABLE tblLocations (ID INT IDENTITY(1,1), Code VARCHAR(1), ParentID INT, Name VARCHAR(20));
INSERT INTO tblLocations (Code, ParentID, Name) VALUES
('A', NULL, 'West'),
('A', 1, 'WA'),
('A', 2, 'Seattle'),
('A', NULL, 'East'),
('A', 4, 'NY'),
('A', 5, 'New York'),
('A', 1, 'NV'),
('A', 7, 'Las Vegas'),
('A', 2, 'Vancouver'),
('A', 4, 'FL'),
('A', 5, 'Buffalo'),
('A', 1, 'CA'),
('A', 10, 'Miami'),
('A', 12, 'Los Angeles'),
('A', 7, 'Reno'),
('A', 12, 'San Francisco'),
('A', 10, 'Orlando'),
('A', 12, 'Sacramento');
Now the recursive query:
-- Note: The 'Code' field isn't used, but you could add it to display more info.
;WITH MyCTE AS (
SELECT ID, Name, 0 AS TreeLevel, CAST(ID AS VARCHAR(255)) AS TreePath
FROM tblLocations T1
WHERE ParentID IS NULL
UNION ALL
SELECT T2.ID, T2.Name, TreeLevel + 1, CAST(TreePath + '.' + CAST(T2.ID AS VARCHAR(255)) AS VARCHAR(255)) AS TreePath
FROM tblLocations T2
INNER JOIN MyCTE itms ON itms.ID = T2.ParentID
)
-- Note: The 'replicate' function is not needed. Added it to give a visual of the results.
SELECT ID, Replicate('.', TreeLevel * 4)+Name 'Name', TreeLevel, TreePath
FROM MyCTE
ORDER BY TreePath;
I believe that you need to add the following to the results of your CTE...
BranchID = some kind of identifier that uniquely identifies the branch. Forgive me for not being more specific, but I'm not sure what identifies a branch for your needs. Your example shows a binary tree in which all branches flow back to the root.
ItemTypeID where (for example) 0 = Product and 1 = service.
Parent = identifies the parent.
If those exist in the output, I think you should be able to use the output from your query as either another CTE or as the FROM clause in a query. Order by BranchID, ItemTypeID, Parent.

Resources