I have a table with categories, each category as an ID, a Name and a ParentID. The problem is that there are 3 levels, parent categories, sub categories and child categories.
I can extract parent categories with a simple SELECT and a WHERE ParentID IS NULL clause as such:
SELECT *
FROM Category
WHERE ParentID IS NULL
However, a WHERE ParentID IS NOT NULL clause will return both, sub categories as well as child categories.
I'm looking for a way to extract only sub categories, and only child categories.
Typically, for these sort of problems, it is better to use the Recursive Queries Using Common Table Expressions. Something like so:
;WITH CategoriesTree(CategoryID, CategoryName, ParentName, CategoryLevel)
AS
(
SELECT
c.ID,
c.Name,
CAST('No Parent' AS VARCHAR(50)) AS ParentName,
0 AS CategoryLevel
FROM #Categories c
WHERE c.ParentID IS NULL
UNION ALL
SELECT c.ID, c.Name, p.CategoryName, p.CategoryLevel + 1
FROM CategoriesTree p
INNER JOIN #Categories c ON c.ParentID = p.CategoryID
)
SELECT *
FROM CategoriesTree
Where CategoryLevel = some id;
SQL Fiddle Demo
This will give you:
CATEGORYID CATEGORYNAME PARENTNAME CATEGORYLEVEL
1 Root Cateogry No Parent 0
2 Sub Cateogry 1 Root Cateogry 1
3 Sub Cateogry 2 Root Cateogry 1
4 Sub Cateogry 3 Root Cateogry 1
8 sub Cateogry 1 of 3 Sub Cateogry 3 2
7 Sub Cateogry 1 of 2 Sub Cateogry 2 2
5 Sub Cateogry 1 of 1 Sub Cateogry 1 2
6 sub Cateogry 2 of 1 Sub Cateogry 1 2
How does this work?
Using this query, you can control what level of categories you want to select. For instance, for the sample data, I used in the previous demo, here is the categories tree:
1: RootCategory Category Level: 0
|
|
----------------------------
| | |
| | |
2: Sub1 3: Sub2 4: sub3 Category Level: 1
| | |
------------ | |
| | | |
| | | |
5: Sub1of1 6: Sub2of1 7: sub1of2 8: sub1of3 Category Level: 2
This query will give you this categories tree with the new generated CategoryLevel column.
Note that: In the sample data I used in the demo, there was only one parent category (the categories with parentid IS NULL). However, the query will for work fine in case there were a lot of parent categories. And this because of the anchor query of the CTE, which is:
SELECT
c.ID,
c.Name,
CAST('No Parent' AS VARCHAR(50)) AS ParentName,
0 AS CategoryLevel
FROM #Categories c
WHERE c.ParentID IS NULL;
Then, you can use the generated column CategoryLevel to select only the child categories of the level that you are interested in.
For example, if you need to select only the sub categories of the first sub categories of the root category, you can get these categories using the predicate CategoryLevel = 2:
;WITH CategoriesTree(CategoryID, CategoryName, ParentName, CategoryLevel)
AS
(
...
)
SELECT *
FROM CategoriesTree
WHERE CategoryLevel = 2;
This will give you:
CATEGORYID CATEGORYNAME PARENTNAME CATEGORYLEVEL
8 sub Cateogry 1 of 3 Sub Cateogry 3 2
7 Sub Cateogry 1 of 2 Sub Cateogry 2 2
5 Sub Cateogry 1 of 1 Sub Cateogry 1 2
6 sub Cateogry 2 of 1 Sub Cateogry 1 2
SQL Fiddle Demo
First level categories - you have it:
SELECT *
FROM Category
WHERE ParentID IS NULL
For the second level categories you can try:
SELECT * FROM Category
WHERE ParentID IN (SELECT ID FROM Category WHERE ParentID IS NULL).
For the third:
SELECT * FROM Category
WHERE ParentID IN (SELECT ID FROM Category WHERE ParentID IS NOT NULL)
(Not tested)
How about something like:
-- Root parents
select c.* from categories c where c.ParentID is null
-- Second level. Select where parentid is a root category.
select c.* from categories c
where c.ParentID in (select c1.ID from categories c1 where c1.ParentID is null);
-- Third level. Select where parentid is a second level category
with second_level_cats (ID) as (
select c.ID from categories c
where c.ParentID in (select c1.ID from categories c1 where c1.ParentID is null)
)
select c.* from categories c
where c.ParentID in (select l2.ID from second_level_cats l2)
May not be entirely optimal but it seems to work. If there's only a relatively low number of rows and you're only ever going to go to three levels then it should suffice.
Related
excuse the bad title but I couldn't find a good way to express what I want in abstract terms.
Anyway I have 3 tables
tbl_product:
PID | productname
1 | product 1
2 | product 2
3 | product 3
4 | product 4
..
tbl_categories, motherCategory allows me to nest categories:
CID | categoriename | motherCategory
1 | electronics | NULL
2 | clothing | NULL
3 | Arduino | 1
4 | Casings, extra's | 3
..
tbl_productInCategory PID and CID are foreign keys to PID and CID in tbl_product and tbl_categories respectively. A product can have multiple categories assigned to it so PID can occur more than once in this table.
PID | CID
1 | 1
2 | 1
3 | 3
4 | 4
Now I have a query that returns all categories if I give the mothercategory.
What I want to do is show ONLY the categories that have products in them recursively.
for instance on the example data above I show all categories(motherCategory is null), I want it to return only electronics since there are no products category 2, clothing.
However the problem I am having is that I also want this to work recursively. Consider this tbl_productInCategory:
PID | CID
1 | 2
2 | 2
3 | 2
4 | 4
Now it should return both clothing and electronics even though there are no products in electronics, because there are products in the nested category arduino->Casings, extra's. If I show all categories with motherCategory, electronics it should also return arduino.
I can't figure out how to do this and any help or pointers are appreciated.
First you should select all categories where products exist. On the next steps select mother categories.
WITH CTE AS
(
SELECT tbl_categories.*
FROM
tbl_categories
JOIN tbl_productInCategory on tbl_productInCategory.CID = tbl_categories.CID
UNION ALL
SELECT tbl_categories.*
FROM tbl_categories
JOIN CTE on tbl_categories.CID = CTE.motherCategory
)
SELECT DISTINCT * FROM CTE
Use a recursive CTE to get a derived table of your category tree, and then INNER JOIN it to your ProductCategory table.
It's not something I've done before, but some googling indicates it is possible.
https://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
The semantics of the recursive execution is as follows:
Split the CTE expression into anchor and recursive members.
Run the anchor member(s) creating the first invocation or base result set (T0).
Run the recursive member(s) with Ti as an input and Ti+1 as an output.
Repeat step 3 until an empty set is returned.
Return the result set. This is a UNION ALL of T0 to Tn.
USE AdventureWorks2008R2;
GO
WITH DirectReports (ManagerID, EmployeeID, Title, DeptID, Level)
AS
(
-- Anchor member definition
SELECT e.ManagerID, e.EmployeeID, e.Title, edh.DepartmentID,
0 AS Level
FROM dbo.MyEmployees AS e
INNER JOIN HumanResources.EmployeeDepartmentHistory AS edh
ON e.EmployeeID = edh.BusinessEntityID AND edh.EndDate IS NULL
WHERE ManagerID IS NULL
UNION ALL
-- Recursive member definition
SELECT e.ManagerID, e.EmployeeID, e.Title, edh.DepartmentID,
Level + 1
FROM dbo.MyEmployees AS e
INNER JOIN HumanResources.EmployeeDepartmentHistory AS edh
ON e.EmployeeID = edh.BusinessEntityID AND edh.EndDate IS NULL
INNER JOIN DirectReports AS d
ON e.ManagerID = d.EmployeeID
)
-- Statement that executes the CTE
SELECT ManagerID, EmployeeID, Title, DeptID, Level
FROM DirectReports
INNER JOIN HumanResources.Department AS dp
ON DirectReports.DeptID = dp.DepartmentID
WHERE dp.GroupName = N'Sales and Marketing' OR Level = 0;
GO
I have two tables, structures of the same is as below:
Table 1. Transactional data table
trx id.
1
2
3
4
5..etc
Table 2
table 2 has the parent child relationship as below .
id subject_id (Child) object_id (Parent)
1 2 1
2 3 1
3 4 1
4 5 1
Now using above tables, the expected output is as follow:
1
2
3
4
5
Please let me know how can I achieve the same. I need to get the details from the Table 1 along with parent and its all children in the hierarchy. I just have one level of hierarchy.
As you just have a one level hierarchy you can get it to work just ordering the results correctly. Try this one:
select obj.object_id, t.*
from
(
select
object_id,
object_id as parent_id
from table2
union
select
subject_id as object_id,
object_id as parent_id
from table2
) obj
inner join table1 t
on t.id = obj.object_id
order by
obj.parent_id,
case when obj.object_id = obj.parent_id then 0 else 1 end,
obj.object_id
;
I light variation on the comment of the #a_horse...
select * from table1
where id in
(select object_id from table2
union all
select subject_id from table2)
order by id;
as expected
1
2
3
4
5
If you want to constraint the parent, simple add WHERE predicates in the subquery.
I only have 1 table called Posts with 5 columns (much like StackExchange Data Explorer)
Id, Score, PostTypeId, ParentId, AcceptedAnswerId
1 5 1 null 3
2 3 1 null 5
3 9 2 1 null
4 3 2 1 null
....
As you can see, the rows have two levels of hierarchy (Parents, Childs). Let's say I want to select the childs (PostTypeId = 2) and for each one I would like to left join the score of the sibling that is currently accepted by the parent. The accepted sibling Id can only be found in the AcceptedAnswerId column of the parent row. The parent row can be found through the ParentId of the child row I am selecting. Is there an elegant way to do this?
The expected results is the following
Id, Score, AcceptedScore
3 9 9
4 3 9
....
My attempt (although it doesn't work and hurts my eyes as well as my brain):
select top 50 a.Id, a.Score, b.Score as AcceptedScore
from Posts as a
left join (
select c.Score from Posts as c
where c.Id = (
select d.AcceptedAnswerId from Posts as d
where d.Id = a.ParentId
)
) as b
on b.ParentId = a.ParentId
where a.PostTypeId = 2
The error I'm getting:
The multi-part identifier "a.ParentId" could not be bound. Invalid column name 'ParentId'.
Here's one that works:
SELECT p1.Id, p1.Score, p3.Score AS AcceptedScore
FROM Posts p1 LEFT JOIN Posts p2
ON p1.ParentId = p2.Id LEFT JOIN Posts p3
ON p2.AcceptedAnswerId = p3.Id
WHERE p1.PostTypeId = 2
See the SQLFiddle
I have a self-referencing table with content like this:
Self-referencing parent table
ID ParentID Name
---------------------
1 John
2 1 Mike
3 2 Erin
4 1 Janie
5 Eric
6 5 Peter
The tree hierarchy should look like this
John
Mike
Erin
Janie
Eric
Peter
And a child table that stores the leaf of parent table that looks like this:
ID Sales
3 100
3 100
4 200
4 200
6 300
6 300
6 300
I'm trying to roll-up the sum from the leaf node up to the hierarchy so it would return as ..
ID Name Sum
1 John 800
2 Mike 200
3 Erin 200
4 Janie 400
5 Eric 900
6 Peter 900
Any ideas how to achieve this in sql 2008? Thanks in advance.
EDIT All aggregation moved out of the CTE
WITH
tree AS
(
SELECT
id AS root_id,
name AS root_name,
id AS leaf_id
FROM
yourTreeTable
UNION ALL
SELECT
tree.root_id AS root_id,
tree.name AS root_name,
yourTreeTable.id AS leaf_id
FROM
tree
INNER JOIN
yourTreeTable
ON tree.leaf_id = yourTreeTable.ParentID
)
SELECT
tree.root_id,
tree.root_name,
COALESCE(SUM(yourScoresTable.score), 0) AS total
FROM
tree
LEFT JOIN
yourScoresTable
ON yourScoresTable.ID = tree.leafID
GROUP BY
tree.root_id,
tree.root_name
Here it is:
Let's supose this schema:
​create table #parent (
ID int,
ParentID int,
Name varchar(50) );
create table #child (
ID int,
Sales int );
The query is self explained:
WITH
tree AS
(
SELECT
id as id_parent,
id as id
FROM
#parent
UNION ALL
SELECT
tree.id_parent as id_parent,
#parent.id AS id
FROM
tree
INNER JOIN
#parent
ON tree.id = #parent.ParentID
)
SELECT
#parent.id,
#parent.name,
COALESCE(SUM(#child.Sales), 0) AS total
FROM
#parent
LEFT JOIN
tree
ON #parent.ID = tree.id_parent
LEFT JOIN
#child on tree.id = #child.id
GROUP BY
#parent.id,
#parent.name
CTE returns a list of 'leafs' for each employee (#parent), then query sums all sales for this 'leafs'. You can test it running.
EDITED
Query is fixed.
I have the following tables:
Categories
=================================
CategoryID | ParentID | Text
---------------------------------
1 NULL Text
2 1 Text
3 NULL Text
4 1 Text
Items
=================================
ItemID | CategoryID | Text
---------------------------------
1 1 Text
2 2 Text
3 4 Text
4 3 Text
Bear in mind this is not an n-level hierarchy, the categories have only 2 levels, so no category can have a parentID of 2 for instance.
What I am looking for is a way to return the categories with an extra column which displays the number of items that the category owns (including its subcategories).
i.e. I'm looking for a single query (or procedure) which can return something like the following:
Categories
============================================
CategoryID | ParentID | Text | Count
--------------------------------------------
1 NULL Text 3
2 1 Text 1
3 NULL Text 1
4 1 Text 1
My current method of getting the items associated to a category is as follows (Given a categoryID, #CategoryID):
SELECT * FROM Items
WHERE CategoryID
IN (SELECT CategoryID FROM Categories where ParentID = #CategoryID or CategoryID = #CategoryID)
My problem is that I cannot seem to link this to a select query for the categories themselves.
It's probably very simple, but I have tried methods using CTE's, various group by clauses, but the hierarchical nature of the categories seems to throw my logic off.
Thanks for any help!
EDIT: The query also needs to account for categories with no items associated to them
You'd have to join items to categories, and group by on any fields you need from the category table:
SELECT c.CategoryID
, c.ParentID
, c.Text
, count(distinct i.ItemID)
FROM Categories c
LEFT JOIN
Categories c2
ON c.CategoryID = c2.ParentID
LEFT JOIN
Items i
ON i.CategoryID = c.CategoryID
or i.CategoryID = c2.CategoryID
GROUP BY
c.CategoryID
, c.ParentID
, c.Text