Names of nodes at depth d for every descendant leaf - sql

I have a category hierarchy that products are attached to. That category hierarchy is saved as an adjacency list. Products can be attached to any category nodes at any level. The category hierarchy is a tree.
I would like to...
get the name of every level 3 category...
per product...
where that product is attached to any level 3 category node...
or a descendant of a level 3 node.
I know I can materialize the hierarchy, and from that I've been able to satisfy all requirements but the last. I always lose some products or categories.
Given
CREATE TABLE product (p_id varchar PRIMARY KEY);
CREATE TABLE category (c_id varchar PRIMARY KEY, parent_c_id varchar);
CREATE TABLE product_category (
p_id varchar,
c_id varchar,
PRIMARY KEY (p_id, c_id),
FOREIGN KEY (p_id) REFERENCES product (p_id)
ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (c_id) REFERENCES category (c_id)
ON UPDATE CASCADE ON DELETE CASCADE
);
INSERT INTO product (p_id) VALUES
('p_01'),
('p_02'),
('p_03'),
('p_04'),
('p_05');
INSERT INTO category (c_id, parent_c_id) VALUES
('c_0_1', NULL),
-- L1
('c_1_1', 'c_0_1'),
('c_1_2', 'c_0_1'),
('c_1_3', 'c_0_1'),
-- L2
('c_2_1', 'c_1_1'),
('c_2_2', 'c_1_1'),
('c_2_3', 'c_1_2'),
('c_2_4', 'c_1_3'),
-- L3
('c_3_1', 'c_2_1'),
('c_3_2', 'c_2_2'),
('c_3_3', 'c_2_3'),
('c_3_4', 'c_2_4'),
-- L4
('c_4_1', 'c_3_1'),
('c_4_2', 'c_3_2'),
('c_4_3', 'c_3_3'),
('c_4_4', 'c_3_4');
INSERT INTO product_category (p_id, c_id) VALUES
-- p_01 explicitly attached to every level in path 1; include.
('p_01', 'c_0_1'),
('p_01', 'c_2_1'),
('p_01', 'c_3_1'),
('p_01', 'c_4_1'),
-- p_02 explicitly attached to desired level in paths 1 and 3; include both.
('p_02', 'c_3_3'),
('p_02', 'c_3_4'),
-- p_03 explicitly attached to super-level in path 3; exclude.
('p_03', 'c_2_4'),
-- p_04 explicitly attached to sub-level in path 1,
-- transitively to desired level in path 1; include.
('p_04', 'c_4_2');
-- p_05 not attached at all.
I would like to end up with something like
p_id | c_id
------+----------------
p_01 | {c_3_1}
p_02 | {c_3_3, c_3_4}
p_04 | {c_3_2}
(3 rows)
but the closest I have gotten is
WITH RECURSIVE category_tree (c_id, parent_c_id, depth, path) AS (
SELECT c_id, parent_c_id, 0 AS depth, ARRAY[]::varchar[]
FROM category
WHERE parent_c_id IS NULL
UNION ALL
SELECT c.c_id, c.parent_c_id, ct.depth + 1, path || c.c_id
FROM category_tree AS ct
INNER JOIN category AS c ON c.parent_c_id = ct.c_id
)
SELECT *
INTO TEMP TABLE t_category_path
FROM category_tree;
SELECT p.p_id, ARRAY_AGG(c_id) category_names
FROM product AS p,
(SELECT DISTINCT t1.c_id, p_id
FROM product_category AS pc
INNER JOIN t_category_path AS t1 ON pc.c_id = t1.c_id
WHERE t1.depth = 3
ORDER BY c_id) x
WHERE p.p_id = x.p_id
GROUP BY p.p_id;
p_id | category_names
------+----------------
p_01 | {c_3_1}
p_02 | {c_3_4,c_3_3}
(2 rows)
The order of categories is irrelevent (I want a set, not a list).
I can tolerate duplicate categories far better than missing categories or products.
I have some liberty to adjust the schema.
> select version();
version
--------------------------------------------------------------------------------------------------------------
PostgreSQL 10.12 on x86_64-redhat-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39), 64-bit

step-by-step demo:db<>fiddle
WITH RECURSIVE cte AS (
SELECT c_id, parent_c_id, 0 as level, NULL AS level3_category
FROM category
WHERE parent_c_id IS NULL
UNION
SELECT
c.c_id,
cte.parent_c_id,
cte.level + 1,
CASE -- 1
WHEN cte.level + 1 = 3 THEN c.c_id
ELSE cte.level3_category
END
FROM
category c
JOIN
cte
ON c.parent_c_id = cte.c_id
)
SELECT
p_id,
ARRAY_AGG(DISTINCT level3_category) as c_id -- 2
FROM
cte
JOIN
product_category pc
ON cte.c_id = pc.c_id AND cte.level3_category IS NOT NULL
GROUP BY p_id
This CASE clause stores the current name if and only if it is level 3. If it is less, than it returns NULL, if it is greater, it takes the level 3 value.
DISTINCT is allowed in GROUP BY aggregates to eliminate non-distinct values.

You can use exists and not exists with joins to get a particular depth:
select p.p_id, array_agg(pc.c_id)
from products p join
product_category pc
on p.p_id = pc.p_id
where exists (select 1
from category_tree ct join
category_tree ctp
on ct.parent_cid = ctp.cid join
category_tree ctp2
on ctp.parent_cid = ctp2.cid
where ct.cid = pc.c_id
) and
not exists (select 1
from category_tree ct join
category_tree ctp
on ct.parent_cid = ctp.cid join
category_tree ctp2
on ctp.parent_cid = ctp2.cid join
category_tree ctp3
on ctp2.parent_cid = ctp3.cid
where ct.cid = pc.c_id
)
group by p.p_id;

Related

recursive select and join related table

CREATE TABLE IF NOT EXISTS "product_category"(
"id" SERIAL NOT NULL,
"parent_id" integer DEFAULT NULL,
"name" varchar DEFAULT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "product"(
"id" SERIAL NOT NULL,
"product_category_id" integer DEFAULT NULL,
PRIMARY KEY ("id")
);
I have two table like above,
the product_category is hierarchy,
and product.product_category_id fk product_category.id.
How to select all product under specific product_category id,
e.g
if input product_category 1,
output -> product:1, product:2
if input product_category 2
output -> product:2
product_category
id | parent_id | name
1 | | parent
2 | 1 | child
3 | 2 | child child
product
id | product_category_id
1 | 1
2 | 3
query
like this ?? but this return only product_category list .... I want product list
WITH RECURSIVE pc AS (
SELECT pc.id AS id
FROM product_category pc
LEFT JOIN product p ON p.product_category_id = pc.id
WHERE id = $1
UNION ALL
SELECT child.id
FROM product_category AS child
LEFT JOIN product p ON p.product_category_id = child.id
JOIN pc ON pc.id = child.parent_id
)
SELECT * FROM product_category WHERE id IN (SELECT * FROM pc)
You should first build up the list of categories, then join that to the products.
WITH RECURSIVE pc AS (
SELECT id
FROM product_category
WHERE id = $id
UNION ALL
SELECT child.id
FROM product_category AS child
JOIN pc ON pc.id = child.parent_id
)
SELECT pr.*
FROM product pr
JOIN pc on pr.product_category_id = pc.id
;
Note:I do not have postgresql installed on my system so I am talking based on concepts. I do not see where you have defined the foreign key constraint ... still I am assuming you have done so. And I have not checked the correctness of the CTE/Common Table Expression (- basically with recursive portion of your SQL). Assuming that CTE is correct -
How is about replacing
SELECT * FROM product_category WHERE id IN (SELECT * FROM pc)
with
SELECT * FROM product WHERE product_category_id IN (SELECT * FROM pc)
The correctness of the CTE is the next thing I am going to check.
So apparently the following should work:
;
WITH RECURSIVE pc AS (
SELECT pc.id AS id
FROM
product_category pc
LEFT JOIN product p
ON p.product_category_id = pc.id
WHERE pc.id = <Your product category id of interest>
UNION ALL
SELECT child.id
FROM
product_category AS child
LEFT JOIN product p
ON p.product_category_id = child.id
JOIN pc
ON pc.id = child.parent_id
)
SELECT id as product_id FROM product WHERE product_category_id
IN (
SELECT * FROM pc
);

SQL Server: querying hierarchical and referenced data

I'm working on an asset database that has a hierarchy. Also, there is a "ReferenceAsset" table, that effectively points back to an asset. The Reference Asset basically functions as an override, but it is selected as if it were a unique, new asset. One of the overrides that gets set, is the parent_id.
Columns that are relevant to selecting the heirarchy:
Asset: id (primary), parent_id
Asset Reference: id (primary), asset_id (foreignkey->Asset), parent_id (always an Asset)
---EDITED 5/27----
Sample Relevent Table Data (after joins):
id | asset_id | name | parent_id | milestone | type
3 3 suit null march shape
4 4 suit_banker 3 april texture
5 5 tie null march shape
6 6 tie_red 5 march texture
7 7 tie_diamond 5 june texture
-5 6 tie_red 4 march texture
the id < 0 (like the last row) signify assets that are referenced. Referenced assets have a few columns that are overidden (in this case, only parent_id is important).
The expectation is that if I select all assets from april, I should do a secondary select to get the entire tree branches of the matching query:
so initially the query match would result in:
4 4 suit_banker 3 april texture
Then after the CTE, we get the complete hierarchy and our result should be this (so far this is working)
3 3 suit null march shape
4 4 suit_banker 3 april texture
-5 6 tie_red 4 march texture
and you see, the parent of id:-5 is there, but what is missing, that is needed, is the referenced asset, and the parent of the referenced asset:
5 5 tie null march shape
6 6 tie_red 5 march texture
Currently my solution works for this, but it is limited to only a single depth of references (and I feel the implementation is quite ugly).
---Edited----
Here is my primary Selection Function. This should better demonstrate where the real complication lies: the AssetReference.
Select A.id as id, A.id as asset_id, A.name,A.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 0 as reference, W.phase_name, W.status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on A.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Left Join Workflow as W on W.asset_id = A.id
where A.deleted <= #showDeleted
UNION
Select -1*AR.id as id, AR.asset_id as asset_id, A.name, AR.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 1 as reference, NULL as phase_name, NULL as status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on AR.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Inner Join AssetReference AR on AR.asset_id = A.id
where A.deleted <= #showDeleted
I have a stored procedure that takes a temp table (#temp) and finds all the elements of the hierarchy. The strategy I employed was this:
Select the entire system heirarchy into a temp table (#treeIDs) represented by a comma separated list of each entire tree branch
Get entire heirarchy of assets matching query (from #temp)
Get all reference assets pointed to by Assets from heirarchy
Parse the heirarchy of all reference assets
This works for now because reference assets are always the last item on a branch, but if they weren't, i think i would be in trouble. I feel like i need some better form of recursion.
Here is my current code, which is working, but i am not proud of it, and I know it is not robust (because it only works if the references are at the bottom):
Step 1. build the entire hierarchy
;WITH Recursive_CTE AS (
SELECT Cast(id as varchar(100)) as Hierarchy, parent_id, id
FROM #assetIDs
Where parent_id is Null
UNION ALL
SELECT
CAST(parent.Hierarchy + ',' + CAST(t.id as varchar(100)) as varchar(100)) as Hierarchy, t.parent_id, t.id
FROM Recursive_CTE parent
INNER JOIN #assetIDs t ON t.parent_id = parent.id
)
Select Distinct h.id, Hierarchy as idList into #treeIDs
FROM ( Select Hierarchy, id FROM Recursive_CTE ) parent
CROSS APPLY dbo.SplitIDs(Hierarchy) as h
Step 2. Select the branches of all assets that match the query
Select DISTINCT L.id into #RelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE #treeIDs.id in (Select id FROM #temp)
Step 3. Get all Reference Assets in the branches
(Reference assets have negative id values, hence the id < 0 part)
Select asset_id INTO #REFLinks FROM #AllAssets WHERE id in
(Select #AllAssets.asset_id FROM #AllAssets Inner Join #RelativeIDs
on #AllAssets.id = #RelativeIDs.id Where #RelativeIDs.id < 0)
Step 4. Get the branches of anything found in step 3
Select DISTINCT L.id into #extraRelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE
exists (Select #REFLinks.asset_id FROM #REFLinks WHERE #REFLinks.asset_id = #treeIDs.id)
and Not Exists (select id FROM #RelativeIDs Where id = #treeIDs.id)
I've tried to just show the relevant code. I am super grateful to anyone who can help me find a better solution!
--getting all of the children of a root node ( could be > 1 ) and it would require revising the query a bit
DECLARE #AssetID int = (select AssetId from Asset where AssetID is null);
--algorithm is relational recursion
--gets the top level in hierarchy we want. The hierarchy column
--will show the row's place in the hierarchy from this query only
--not in the overall reality of the row's place in the table
WITH Hierarchy(Asset_ID, AssetID, Levelcode, Asset_hierarchy)
AS
(
SELECT AssetID, Asset_ID,
1 as levelcode, CAST(Assetid as varchar(max)) as Asset_hierarchy
FROM Asset
WHERE AssetID=#AssetID
UNION ALL
--joins back to the CTE to recursively retrieve the rows
--note that treelevel is incremented on each iteration
SELECT A.Parent_ID, B.AssetID,
Levelcode + 1 as LevelCode,
A.assetID + '\' + cast(A.Asset_id as varchar(20)) as Asset_Hierarchy
FROM Asset AS a
INNER JOIN dbo.Batch AS Hierarchy
--use to get children, since the parentId of the child will be set the value
--of the current row
on a.assetId= b.assetID
--use to get parents, since the parent of the Asset_Hierarchy row will be the asset,
--not the parent.
on Asset.AssetId= Asset_Hierarchy.parentID
SELECT a.Assetid,a.name,
Asset_Hierarchy.LevelCode, Asset_Hierarchy.hierarchy
FROM Asset AS a
INNER JOIN Asset_Hierarchy
ON A.AssetID= Asset_Hierarchy.AssetID
ORDER BY Hierarchy ;
--return results from the CTE, joining to the Asset data to get the asset name
---that is the structure you will want. I would need a little more clarification of your table structure
It would help to know your underlying table structure. There are two approaches which should work depending on your environment: SQL understands XML so you could have your SQL as an xml structure or simply have a single table with each row item having a unique primary key id and a parentid. id is the fk for the parentid. The data for the node are just standard columns. You can use a cte or a function powering a calculated column to determin the degree of nesting for each node. The limit is that a node can only have one parent.

SQL - Find duplicates with equivalencies

I'm having trouble wrapping my mind around developing this SQL query. Given the following two tables:
ACADEMIC_HISTORY ( STUDENT_ID, TERM, COURSE_ID, COURSE_GRADE )
COURSE_EQUIVALENCIES ( COURSE_ID, COURSE_ID_EQUIVALENT )
What would be the best way to detect if students have taken the same (or an equivalent) course in the past with a passing grade (C or better)?
Example
Student #1 took the course ABC001 and received a grade of C. Ten years later, the course was renamed ABC011 and the appropriate entry was made in COURSE_EQUIVALENCIES. The student retook the course under this new name and received a grade of B. How can I construct a SQL query that will detect the duplicate courses and only count the first passing grade?
(The actual case is significantly more complicated, but this should get me started.)
Thanks in advance.
EDIT:
It's not even necessary to keep or discard any information. A query that simply shows classes with duplicates will be sufficient.
you could use something like:
SELECT
STUDENT_ID
,MIN (COURSE_GRADE)
FROM (
SELECT * FROM
ACADEMIC_HISTORY
WHERE COURSE_ID =1
UNION
SELECT
h.STUDENT_ID
,h2.COURSE_ID
,h2.COURSE_GRADE
FROM
ACADEMIC_HISTORY AS h
LEFT OUTER JOIN COURSE_EQUIVELANCIES as e
ON e.COURSE_ID = h.COURSE_ID
LEFT OUTER JOIN ACADEMIC_HISTORY as h2
ON h.STUDENT_ID = h2.STUDENT_ID
AND h2.COURSE_ID = e.COURSE_ID_EQUIVELANT
WHERE
h.COURSE_ID =1
) AS t
WHERE STUDENT_ID =1
GROUP BY STUDENT_ID
http://sqlfiddle.com/#!3/d608f/20
Sorry posted with a bug.. it preferred the score of the actual course requested over any equivalencies - fixed now
this only looks for one level of equivalencies.. but maybe you want to enforce that and have that part of the data entry process.. review all possible equivalencies and enter the valid ones
EDIT: for first pass of qualifying course (using numbered terms..)
SELECT TOP 1
STUDENT_ID
,MIN (COURSE_GRADE)
FROM (
SELECT * FROM
ACADEMIC_HISTORY
WHERE COURSE_ID =1
UNION
SELECT
h.STUDENT_ID
,h2.COURSE_ID
,h2.TERM
,h2.COURSE_GRADE
FROM
ACADEMIC_HISTORY AS h
LEFT OUTER JOIN COURSE_EQUIVELANCIES as e
ON e.COURSE_ID = h.COURSE_ID
LEFT OUTER JOIN ACADEMIC_HISTORY as h2
ON h.STUDENT_ID = h2.STUDENT_ID
AND h2.COURSE_ID = e.COURSE_ID_EQUIVELANT
WHERE
h.COURSE_ID =1
) AS t
WHERE STUDENT_ID =1
GROUP BY STUDENT_ID, TERM
ORDER BY TERM ASC
http://sqlfiddle.com/#!3/fdded/6
(note TOP is a t-sql command for MySQL you need LIMIT)
The data (in LOWERCASE)
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp;
SET search_path='tmp';
CREATE TABLE academic_history
( student_id INTEGER NOT NULL
, course_id CHAR(6)
, course_grade CHAR(1)
, PRIMARY KEY(student_id,course_id)
);
INSERT INTO academic_history ( student_id,course_id,course_grade) VALUES
(1, 'ABC001' , 'C' )
, (1, 'ABC011' , 'B' )
, (2, 'ABC011' , 'A' )
;
CREATE TABLE course_equivalencies
( course_id CHAR(6)
, course_id_equivalent CHAR(6)
);
INSERT INTO course_equivalencies(course_id,course_id_equivalent) VALUES
( 'ABC011' , 'ABC001' )
;
The query:
-- EXPLAIN ANALYZE
WITH canon AS (
SELECT ah.student_id AS student_id
, ah.course_id AS course_id
, COALESCE (eq.course_id_equivalent,ah.course_id) AS course_id_equivalent
FROM academic_history ah
LEFT JOIN course_equivalencies eq ON eq.course_id = ah.course_id
)
SELECT h.student_id
, c.course_id_equivalent
, MIN(h.course_grade) AS the_grade
FROM academic_history h
JOIN canon c ON c.student_id = h.student_id AND c.course_id = h.course_id
GROUP BY h.student_id, c.course_id_equivalent
ORDER BY h.student_id, c.course_id_equivalent
;
The output:
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table tmp.academic_history
drop cascades to table tmp.course_equivalencies
DROP SCHEMA
CREATE SCHEMA
SET
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "academic_history_pkey" for table "academic_history"
CREATE TABLE
INSERT 0 3
CREATE TABLE
INSERT 0 1
student_id | course_id_equivalent | the_grade
------------+----------------------+-----------
1 | ABC001 | B
2 | ABC001 | A
(2 rows)

Help with generating a report from data in a parent-children model

I need help with a problem regarding data saved in a parent-children model table and a report I need to build upon it. I've already tried searching for topics about parent-children issues, but I couldn't find anything useful in my scenario.
What I have
A Microsoft SQL Server 2000 database server.
A categories table, which has four columns: category_id, category_name, father_id and visible; the categories have x root categories (where x is variable), and could be y level deep (where y is variable), if a category is a root level one it has father_id null otherwise it's filled with the id of the father category.
A sales table, which has z columns, one of which is category_id, a foreign key to categories.category_id; a sale must always have a category, and it could be linked anywhere in the aforementioned y level.
What I need
I've been asked a report displaying only the root (first level) categories, and the quantity of sales belongings to each of these, or their children, no matter how deep. I.e. if one of the root categories is food, which has a children category named fruit, which has a children category named apple, I need to count every item belonging to food or fruit or apple.
Couldn't you use the nested set data model?
I know of the nested set model, but I already have the table this way, and migrating it to the nested set model would be a pain (let alone I didn't even fully grasp how nested set works), not counting the changes needed in the application using the database. (If someone thinks this is still the least pain way, please explain why and how the current data could be migrated.)
Couldn't you use CTE (Common Table Expressions)?
No, it's a Microsoft SQL Server 2000, and Common Table Expressions are introduced in the 2005 edition.
Thanks in advance, Andrea.
SQL 2000 Based solution
DECLARE #Stack TABLE (
StackID INTEGER IDENTITY
, Category VARCHAR(20)
, RootID INTEGER
, ChildID INTEGER
, Visited BIT)
INSERT INTO #Stack
SELECT [Category] = c.category_name
, [RootID] = c.category_id
, [ChildID] = c.category_id
, 0
FROM Categories c
WHILE EXISTS (SELECT * FROM #Stack WHERE Visited = 0)
BEGIN
DECLARE #StackID INTEGER
SELECT #StackID = MAX(StackID) FROM #Stack
INSERT INTO #Stack
SELECT st.Category
, st.RootID
, c.category_id
, 0
FROM #Stack st
INNER JOIN Categories c ON c.father_id = st.ChildID
WHERE Visited = 0
UPDATE #Stack
SET Visited = 1
WHERE StackID <= #StackID
END
SELECT st.RootID
, st.Category
, COUNT(s.sales_id)
FROM #Stack st
INNER JOIN Sales s ON s.category_id = st.ChildID
GROUP BY st.RootID, st.Category
ORDER BY st.RootID
SQL 2005 Based solution
A CTE should get you what you want
Select each category from Categories to be the root item
recursively add each child of every root item
INNER JOIN the results with your sales table. As every root is in the result of the CTE, a simple GROUP BY is sufficient to get a count for each item.
SQL Statement
;WITH QtyCTE AS (
SELECT [Category] = c.category_name
, [RootID] = c.category_id
, [ChildID] = c.category_id
FROM Categories c
UNION ALL
SELECT cte.Category
, cte.RootID
, c.category_id
FROM QtyCTE cte
INNER JOIN Categories c ON c.father_id = cte.ChildID
)
SELECT cte.RootID
, cte.Category
, COUNT(s.sales_id)
FROM QtyCTE cte
INNER JOIN Sales s ON s.category_id = cte.ChildID
GROUP BY cte.RootID, cte.Category
ORDER BY cte.RootID
Something like this?
CREATE TABLE #SingleLevelCategoryCounts
{
category_id,
count,
root_id
}
CREATE TABLE #ProcessedCategories
{
category_id,
root_id
}
CREATE TABLE #TotalTopLevelCategoryCounts
{
category_id,
count
}
INSERT INTO #SingleLevelCategoryCounts
SELECT
category_id, SUM(*), category_id
FROM
Categories
INNER JOIN Sales ON Categories.category_id = sales.category_id
WHERE
Categories.father_id IS NULL
GROUP BY
Categories.category_id
WHILE EXISTS (SELECT * FROM #SingleLevelCategoryCounts)
BEGIN
IF NOT EXISTS(SELECT * FROM #TopLevelCategoryCounts)
BEGIN
INSERT INTO #TopLevelCategoryCounts
SELECT
root_id, count
FROM
#SingleLevelCategoryCounts
END
ELSE
BEGIN
UPDATE top
SET
top.count = top.count + level.count
FROM
#TopLevelCategoryCounts top
INNER JOIN #SingleLevelCategoryCounts level ON top.category_id = level.count
END
INSERT INTO #ProcessedCategories
SELECT category_id, root_id FROM #SingleLevelCategoryCounts
DELETE #SingleLevelCategoryCounts
INSERT INTO #SingleLevelCategoryCounts
SELECT
category_id, SUM(*), pc.root_id
FROM
Categories
INNER JOIN Sales ON Categories.category_id = sales.category_id
INNER JOIN #ProcessedCategories pc ON Categories.father_id = pc.category_id
WHERE
Categories.category_id NOT IN
(
SELECT category_id in #ProcessedCategories
)
GROUP BY
Categories.category_id
END

How do I create a recursive query in MSSQL 2005?

Let's say I have the following table:
CustomerID ParentID Name
========== ======== ====
1 null John
2 1 James
3 2 Jenna
4 3 Jennifer
5 3 Peter
6 5 Alice
7 5 Steve
8 1 Larry
I want to retrieve in one query all the descendants of James (Jenna,Jennifer,Peter, Alice, Steve).
Thanks,
Pablo.
On SQL Server 2005 you can use CTEs (Common Table Expressions) :
with Hierachy(CustomerID, ParentID, Name, Level)
as
(
select CustomerID, ParentID, Name, 0 as Level
from Customers c
where c.CustomerID = 2 -- insert parameter here
union all
select c.CustomerID, c.ParentID, c.Name, ch.Level + 1
from Customers c
inner join Hierachy ch
on c.ParentId = ch.CustomerID
)
select CustomerID, ParentID, Name
from Hierachy
where Level > 0
For bottom up use mathieu's answer with a little modification:
with Hierachy(CustomerID, ParentID, Name, Level)
as
(
select CustomerID, ParentID, Name, 0 as Level
from Customers c
where c.CustomerID = 2 -- insert parameter here
union all
select c.CustomerID, c.ParentID, c.Name, ch.Level + 1
from Customers c
inner join Hierachy ch
-- EDITED HERE --
on ch.ParentId = c.CustomerID
-----------------
)
select CustomerID, ParentID, Name
from Hierachy
where Level > 0
You can't do recursion in SQL without stored procedures. The way to solve this is using Nested Sets, they basically model a tree in SQL as a set.
Notice that this will require a change to the current data model or possibly figuring out how to create a view on the original model.
Postgresql example (using very few postgresql extensions, just SERIAL and ON COMMIT DROP, most RDBMSes will have similar functionality):
Setup:
CREATE TABLE objects(
id SERIAL PRIMARY KEY,
name TEXT,
lft INT,
rgt INT
);
INSERT INTO objects(name, lft, rgt) VALUES('The root of the tree', 1, 2);
Adding a child:
START TRANSACTION;
-- postgresql doesn't support variables so we create a temporary table that
-- gets deleted after the transaction has finished.
CREATE TEMP TABLE left_tmp(
lft INT
) ON COMMIT DROP; -- not standard sql
-- store the left of the parent for later use
INSERT INTO left_tmp (lft) VALUES((SELECT lft FROM objects WHERE name = 'The parent of the newly inserted node'));
-- move all the children already in the set to the right
-- to make room for the new child
UPDATE objects SET rgt = rgt + 2 WHERE rgt > (SELECT lft FROM left_tmp LIMIT 1);
UPDATE objects SET lft = lft + 2 WHERE lft > (SELECT lft FROM left_tmp LIMIT 1);
-- insert the new child
INSERT INTO objects(name, lft, rgt) VALUES(
'The name of the newly inserted node',
(SELECT lft + 1 FROM left_tmp LIMIT 1),
(SELECT lft + 2 FROM left_tmp LIMIT 1)
);
COMMIT;
Display a trail from bottom to top:
SELECT
parent.id, parent.lft
FROM
objects AS current_node
INNER JOIN
objects AS parent
ON
current_node.lft BETWEEN parent.lft AND parent.rgt
WHERE
current_node.name = 'The name of the deepest child'
ORDER BY
parent.lft;
Display the entire tree:
SELECT
REPEAT(' ', CAST((COUNT(parent.id) - 1) AS INT)) || '- ' || current_node.name AS indented_name
FROM
objects current_node
INNER JOIN
objects parent
ON
current_node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY
current_node.name,
current_node.lft
ORDER BY
current_node.lft;
Select everything down from a certain element of the tree:
SELECT
current_node.name AS node_name
FROM
objects current_node
INNER JOIN
objects parent
ON
current_node.lft BETWEEN parent.lft AND parent.rgt
AND
parent.name = 'child'
GROUP BY
current_node.name,
current_node.lft
ORDER BY
current_node.lft;
Unless I'm missing something, recursion isn't necessary...
SELECT d.NAME FROM Customers As d
INNER JOIN Customers As p ON p.CustomerID = d.ParentID
WHERE p.Name = 'James'