Recursive SQL for a menu system - sql

I have a table for a menu system with the following structure, and some data.
ID, Text, ParentID, DestinationID
1, Applications, (null), (null)
2, Games, (null), (null)
3, Office, 1, (null)
4, Text Editing, 1, (null)
5, Media, (null), (null)
6, Word, 3, 1
7, Excel, 3, 2
8, Crysis, 2, 3
What I need is a query to which I can pass the menu ID, and it will return a list of items that have that ID as a child. BUT, I need it to only return children that have a valid path to a destination. So in the example above, the user will be presented initially with (Applications, Games), when he selects Applicaions, he is presented with (Office). Text Editing and Media should be omitted, because there are no valid destinations beneath them.
The trickiest thing about this, is that there is no predetermined depth to any given menu.
EDIT:
Today, the problem came up for MS SQL 2008, but in the past 2 weeks I've needed similar solutions for SQLite and SQL CE. The ideal solution should not be tied to any specific SQL engine.

SQL server only, but it sounds like a job for Common Table Expressions.

In Oracle:
SELECT m.*, level
FROM my_table m
START WITH
Id = :startID
CONNECT BY
ParentID = PRIOR Id
AND DestinationID IS NOT NULL
There is no way to do it in ANSI SQL with a single query. You may create an additional column AccessPath for you table:
ID, Text, ParentID, DestinationID AccessPath
1, Applications, (null), (null), "1"
2, Games, (null), (null), "2"
3, Office, 1, (null), "1.3"
4, Text Editing, 1, (null), "1.4"
5, Media, (null), (null), "5"
6, Word, 3, 1, "1.3.6"
7, Excel, 3, 2, "1.3.7"
8, Crysis, 2, 3, "1.2.8"
, and query:
SELECT mp.Id, mp.Text
FROM my_table mp, my_table mc
WHERE mp.parentID = #startingParent
AND mc.Id <> mp.Id
AND SUBSTR(mc.AccessPath, LENGTH(mp.AccessPath)) = mp.AccessPath
GROUP BY
mp.Id, mp.Text
It's a bad idea to start with NULL, as the index on ParentID cannot be used in this case. For a start, use a fake parentID of 0 instead of NULL.

If the hierarchy/tree that you are stroing in your database does not change very often, I would recommend to use the modified preorder tree traversal (MPTT) algorithm. That would require a different table schema, but would allow you to request a whole subtree with a simple SQL statement (no recursion, etc.).
The article on Storing Hierarchical Data in a Database describes this method in detail.
In your example you would get the following tree, where I call the red numbers the left value and the green right value of a node.
Now, if you want to select the Office subtree, you can do this with:
SELECT * FROM tree WHERE left BETWEEN 10 AND 15 AND destination IS NOT NULL
If your DB does not support the BETWEEN statement, you can of course write left > 10 AND left < 15 instead.
Your table would look like this:
name | left | right | destination
------------------------------------------
root | 1 | 17 | NULL
Applications | 7 | 16 | ...
...

If this is a problem that interests you (or plagues you), you may want to check out: Joe Celko's Trees and Hierarchies in SQL for Smarties.

As others have pointed out, there's no way in standard ANSI SQL to do what you want. For something like this, I once implemented on SQL 2000 a system for tracking components of products an ex employer made - each "product" could be atomic component like, say, screw A500. This component could be used in "composite" components: some A500 screws plus 6 B120 wood boards conformed a C90 "stylish tool box". That box, plus more screws and a motor "M500" could conform a carpetry tool.
I designed a table "Product" like this:
ID, PartName, Description
1, A500, "Screw A500"
2, B120, "Wood panel B120"
3, C90, "Stylish tool box C90"
4, M500, "Wood cutter M500"
And a "ProductComponent" table as follows:
Hierarchy, ComponentID, Amount
0301, 1, 24
0302, 2, 6
0401, 1, 3
0402, 3, 1
0403, 4, 1
040201, 1, 24
040202, 2, 6
The trick is: field hierarchy is a VARCHAR with first 2 chars representing each product's ID, each next pair of chars identify a node in the tree. So we see that product 3 depends on 2 other products. Product 4 depends on 2 others, also, one of which depends on its part on two others.
There's lots of redundancy in this model, but allows to easily calculate how many screws you need for a particular product, determine fastly which parts need wood panels or get the list of all components a product ultimately depends on (including indirect dependencies), etc. And scanning the tree below a certain level is a simple LIKE query!
By using 2 chars in a hexadecimal representation I limited a product to depend directly on maximum 256 other prods (which on turn can depend on something else). You could change that to use base 36 (the 26 letters plus 10 numbers) or base-64 if you need more than that.
Besides, this table model works very well on Access and mySQL, too. What you can not have is circular dependencies in any way.

SQL is not very good at walking arbitrary depth hierarchies.
If there's less than 1000 of these records, I would grab them all to the application and construct the graph there.
If there's more than 1000 of these records, I'd group them into raw subtrees of approx 1000 (by adding a SubtreeID foreign key) and fetch each subtree... then construct the graph of the subtree in the application.

The first thing I'd do is strip out the destination column - it's meaningless in terms of hierarchy (it actually appears to be a kind of second primary key to signal a live child row the way you've represented it)
this would give
ID, Item, parentID
1, Applications, (null)
2, Games, (null)
3, Office, 1
4, Text Editing, 1
5, Media, (null)
6, Word, 3
7, Excel, 3
8, Crysis, 2
e.g...
word > office > applications and...
excel > office > applications
...should presumably be on the same menu item (parent id 3)
I'm not sure how you're selecting the menu but I'll work on the principle that there's an initial menu button set with (null) as it's parameter and each subsequent click holds the next parameter in sequence dynamically (which seems to match your comments)
e.g.
click top level menu :- value is (null)
click applications :- value is 1
click Office :- value is 3
etc.
Assuming the destinationID is doing nothing apart from showing an active child link (allowing you to remove it), the code would then be as follows:
with items (nodeID, PID, list) as
(select id, ParentID, item
from menu
where id = 9
union all
select id, ParentID, item
from menu
inner join items on nodeID = menu.ParentID
)
select *
from items
where (pid = 9)
and nodeID in (select parentid from menu)
This works in MSSQL 2005+
If you need the destination id for some other reason then you can amend the code as follows (if you need to return the lowest level where a node id hasn't been set as a parent id, for instance):
with items (nodeID, PID, list, dest) as
(select id, ParentID, item, destinationID
from menu
where id = 9
union all
select id, ParentID, item, destinationID
from menu
inner join items on nodeID = menu.ParentID
)
select *
from items
where (pid = 9)
and (nodeID in (select parentid from menu)
or dest is not null)

https://geeks.ms/jirigoyen/2009/05/22/recursividad-con-sql-server/
ALTER PROCEDURE [dbo].[Usuarios_seguridad_seleccionar]
AS
BEGIN
DECLARE #minClave int
SELECT #minClave = MIN(Clave) FROM dbo.Usuarios_seguridad;
WITH UsuariosAccesos AS(
SELECT top 1 us1.Padre,us1.Clave,us1.Variable,us1.Modulo,us1.Contenido,us1.Acceso,us1.Imagen
FROM dbo.Usuarios_seguridad us1
WHERE us1.Clave = #minClave
UNION ALL
SELECT top 100 percent us2.Padre,us2.Clave,us2.Variable,us2.Modulo,us2.Contenido,us2.Acceso,us2.Imagen
FROM dbo.Usuarios_seguridad us2
INNER JOIN UsuariosAccesos AS us3 ON us3.Clave = us2.Padre
WHERE us2.Clave <> #minClave
)
SELECT TOP 100 PERCENT ia.Padre,ia.Clave,ia.Variable,ia.Modulo,ia.Contenido,ia.Acceso,ia.Imagen
FROM UsuariosAccesos ia
ORDER BY padre, clave
END
GO

Related

Self join a table showing parent child relationship

I have a table called sales. In it there are columns catid, desc, parentforeignkey and the records are something like this:
catid desc parentforeignkey
1, clothes, 1
2, shoes, 1
3, socks, 1
4, gloves, 1
5, mittens, 4
6, leather gloves, 4
7, plain gloves, 4
...
How do I build a query to show this relationship?
I'ma take a shot at this, but I'm struggling to see the question. I feel like you want a query that groups the selection by the parent foreignkey and then lists the catid and desc for each parent. So basically something like
SELECT t.parentforeignkey, t.catid, t.desc
FROM table1 as t
GROUP BY t.parentforeignkey, t.catid, t.desc;
NOTE: be careful with "desc" as a column name as DESC is a reserved word for descending (used for sorting)
That will give you a result like:
ParentForeignKey | CatID | Desc
1 1 Clothes
1 2 Shoes
2 2 Shoes
3 1 Clothes
So the trick is to use GROUP BY to assign the parent and child groups. Be careful though, because the order of the group by command matters (GROUP BY Catid, ParentforeignKey yields a different result than what I listed above). Also, you need to explicitely say how each column is related to the grouping. If you leave a column out, you'll likely get an error (depending on your DBMS) that says something like "You tried to specify a query that does not include the specified expression as part of the aggregate function"
EDIT: I now see that you've included the DBMS in your question. If you're using the BIDS or SSRS then this is supremely easy, you'll what a query that just selects the data (and filters whatever you want out) and then you'll go to the tablix controls and define the parent group to the details as catid, and the parent of catid as foreignparentkey and then the table should take care of itself!

Access ancestors

I have an Access db table, that holds couples of values: (SourceId, DestinationId). Both values are taken from the same list of Id's.
I want to create a list (query result?) of all item's ancestors. Ie, if the user enters Id=15, I'd like to return all Id's that are destinations for source-15, but also their destinations etc.
For example, if my table hold
15, 3 |
15, 4 |
4, 7 |
4, 8 |
3, 5 |
5, 2 |
1, 9
Id like to return 3, 4, 7, 8, 5, 2 (but not return 9).
I guess the solution should include some VBA code with loops or recursion, but I got confused by recordsets versus collections.
Any idea?
Thanks,
Aviram
Unfortunately Access SQL lacks the CONNECT BY syntax that Oracle uses to do hierarchical queries. However, if you are prepared to create a temporary table you can emulate it in Access with a loop.
In this example your original table is "LinkTab" and the temporary table will be "TmpTree" an you are starting from SourceID 15.
First execute:
SELECT SourceID, DestID, 1 as Lvl INTO TmpTree FROM LinkTab WHERE SourceID = 15
Then in a loop, repeatedly execute:
INSERT INTO TmpTree ( SourceID, DestID, Lvl )
SELECT newrows.SourceID, newrows.DestID, TmpTree.Lvl + 1
FROM TmpTree INNER JOIN LinkTab newrows ON TmpTree.DestID = newrows.SourceID
WHERE TmpTree.Lvl = (SELECT MAX(Lvl) FROM TmpTree)
until the statement returns zero affected rows (or if you can't detect this, count the rows in TmpTree each time and stop when they don't increase)
Now your results can be retrieved with :
SELECT DestID FROM TmpTree
And finally, to tidy up:
DROP TABLE TmpTree
I've tested these statements in the Access Query designer and they get the desired result.

How to Order By within an Order By in SQL?

I'm trying to create a SQL statement which will recreate the hierarchical container/folder/test structure in SilkCentral Test Manager.
Test containers have no ParentID
Test folders contain a ParentID and IsLeaf = 0
Tests contain a ParentID and IsLeaf = 1
This Query results in all of the test containers, folders, and tests:
SELECT "NodeID", "ParentID", "Name", "IsLeaf", "OrderNumber"
FROM "Silk"."TM_TestPlanNodes" AS TPN
WHERE PROJECTID = 36
ORDER BY "ParentID", "OrderNumber", "IsLeaf"
Here are some of the Results:
NodeID ParentID Name IsLeaf OrderNumber
65408 Installation and Upgrades 0 0
65445 Connectivity 0 1
65448 Focus 0 2
65409 GINA / PLAP 0 3
65446 Graphical User Interface 0 4
71038 Login Properties 0 5
65449 Miscellaneous 0 6
70636 Net Firewall 0 7
70998 Software Updates 0 8
65447 Third-party Services 0 9
70805 SilkTest Automated Tests 0 10
68812 65408 0. Setup 0 0
65454 65408 1. Installations & Upgrades 0 1
65450 65408 Typical/Custom Installation 0 2
I would like this ordering instead:
The ParentID is sorted, but if there exists a Node with the ParentID=thePreviousNode'sID, then that is chosen next. If there are multiple of those nodes, they should be ordered by IsLeaf and then, OrderNumber.
How to accomplish this? I'm very limited in what I can do, because I think very complicated syntax will end up throwing errors in Silk. I was going to try a nested SELECT statement:
SELECT "NodeID", "ParentID", "Name","IsLeaf"
FROM "Silk"."TM_TestPlanNodes"
WHERE PROJECTID = '36'AND ParentID LIKE (
SELECT ParentID
FROM "Silk"."TM_TestPlanNodes"
WHERE NAME = 'Installation and Upgrades')
But this is getting this error: "Could not execute report query: Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression."
This is why I'm fiddling with Order By.
You can use a recursive cte to create a hidden column and orderby that column. The hidden column should be Something like:
WITH cte (NodeID, ParentID, Name, IsLeaf, [Order])
AS
(
SELECT NodeID, ParentID, Name, IsLeaf, cast(NodeID as nvarchar(10))
FROM "Silk"."TM_TestPlanNodes"
WHERE PROJECTID = '36'
UNION ALL
SELECT "NodeID", "ParentID", "Name","IsLeaf", cast(leftNode.ParentID as nvarchar(10)) + cast(leftNode.NodeID as nvarchar(10))
FROM "Silk"."TM_TestPlanNodes" as leftNode
INNER JOIN cte on cte.NodeID = leftNode.ParentID
WHERE leftNode.ParentID = cte.NodeID
)
select "NodeID", "ParentID", "Name","IsLeaf" from cte
order by cast([Order] as nvarchar(50))
This was written in notepad so is possible to have some errors, but the idea is to make an [order] column that for example for 65530 would be 654086554569530 (the parent_parent, the parent and the node)
EDIT:
this only works if the ids are all 5 characters long, but from here you can make the proper tweaks.
Although it might not be a PERFECT fit, it is very close with a nested hierarchical representation of parent-child records in a self-joined list and incorporated proper ordering concerns. You may have to tweak it a bit for your table, but here's a link to a prior solution
To clarify that problem with the menu and the corresponding data.
id | parentid | name
1 | 0 | CatOne
2 | 0 | CatTwo
3 | 0 | CatThree
4 | 1 | SubCatOne
5 | 1 | SubCatOne2
6 | 3 | SubCatThree
Desired output
CatOne 1
--SubCatOne 4
--SubCatOne2 5
CatTwo 2
CatThree 3
--SubCatThree 6
The FIRST case is pre-grouping all the like ID's based on the parent... So, when the parent ID is 0, it IS the top-most level, so we keep it's ID. Then, any children under it, we want their respective PARENT IDs so all of the same are correctly pre-grouped.
The purpose of the SECOND group by is to force the entry that represents the actual TOP LEVEL menu item to the top of the list regardless of the child entries.
Say you have a table where IDs are already established, and you now add a new item into position ID = 7 for "New Top Level" and want to move ID #s 2 and 3 into the new "top-level section. If you just to the query with the first CASE, your records would be simulated returned as
ID Parent Name (natural order from the table)
2 7 CatTwo
3 7 CatThree
7 0 New Category. (we want THIS one in FIRST POSITION of the group)
As you can see, this would be a bad representation of the sub-grouping order. The top-level item actually is in the 3rd position... To bring it to the front, we are now sub-grouping and saying... if the Parent ID of the record = 0, then sort it as if it were a '1' priority. Anything else is considered a '2' priority and would simulate the result like
ID Parent Name SubPrioritySort
7 0 New Category. 1
2 7 CatTwo 2
3 7 CatThree 2
Since you are not actually returning these "CASE" values in your result query, you wouldn't otherwise visually see it... but for grins, add them as columns to your query to see the impact. Hopefully this clarified the answer for you.
In your question, you would obviously be able to add your sort order column to the basis of this query.

Finding matching parents where all children also match

I'm trying to write a sql query in MS SQL Server 2008 that will match parent rows where the parents match and all their children match.
Assuming I have this basic table structure:
ParentTable:
ParentID, Item, Price
ChildTable:
ChildID, ParentID, Accessory, Price
I want to get a grouping of ParentIDs where the parents match on Item and Price and they have the same number of children, each of which match on Accessory and Price.
For example:
ParentTable:
---------------------
1, "Computer", 1000
2, "Stereo", 500
3, "Computer", 500
4, "Computer", 1000
ChildTable:
---------------------
1, 1, "Mouse", 10
2, 1, "Keyboard", 10
3, 2, "Speakers", 50
4, 3, "Keyboard", 10
4, 3, "Mouse", 10
5, 4, "Keyboard", 10
6, 4, "Mouse", 10
The expected results would be something like
ParentID, Grouping
---------------------
1, 1
2, 2
3, 3
4, 1
This would imply that ParentID 1 and 4 are exactly the same and 2 and 3 are unique. I dont really care about the format of the result, as long as I get a list of parents that match.
I'm not opposed to doing (some or all of) this in .net either.
your question is a little ambiguous, but I thought I'd give it a shot anyway.
here goes. Free form SQL. Hard to get it exactly right without access to some DML.
So this would be my general approach. This should work in SQL Server, probably Oracle as well. I'm not claiming this is perfect. My mental schema doesn't match above exactly, I'll leave that as an exercise for the reader. I typed it straight in.
SELECT DISTINCT p.id,p.name,p.dte,q.cnt
FROM parent p
JOIN
(
select p.id, p.dte, count(*) cnt
from parent p
join child ch
on ch.pid = p.id
group by p.id, p.dte
) q
ON p.id=q.id and p.dte=q.dte
GROUP BY p.id,p.name,q.cnt
ORDER BY p.id,p.name,q.cnt
btw: your question is a little ambiguous.
UPDATE:
this function looks promissing for the child rows to csv direction
http://sql-ution.com/function-to-convert-rows-to-csv/
OK if you can do this with temp tables, then this may give you ideas. Off the top of my head, so syntax not checked. Also, this is limited, as bigint typically only goes up to 2**63 or something.
First put unique child accessory and price into #child
create table #child ( accessory varchar, price decimal, id identity,
bnumber bigint null)
insert into #child(accessory, price)
select accesory,price from childtable group by accessory price
assuming id will be 1,2,3,4 etc
update #child set bnumber = 2**(id-1)
sets bnumber to 1,2,4,8 etc (this is where the bigint limitation may kick in). So now you have
mouse, 10,1,1
keyboard,10,2,2
speakers,50,3,4
Now you can sum these numbers by parent
select p.item, sum(ctemp.bnumber)
from parent p, child c, #child ctemp
where p.parentid = c.parentid
and c.accessory = ctemp.accessory
and c.price = ctemp.price
group by p.item
giving
1, 3
2, 4
3, 3
4, 3
..which I think is the answer you want. This is a bit clunky, and it's been a long day(!), but it might help.

How to implement high performance tree view in SQL Server 2005

What is the best way to build the table that will represent the tree?
I want to implement a select ,insert ,update and delete that will work well with big data.
The select for example will have to support "Expand ALL" - getting all the children (and there children) for a given node.
Use CTE's.
Given the tree-like table structure:
id parent name
1 0 Electronics
2 1 TV
3 1 Hi-Fi
4 2 LCD
5 2 Plasma
6 3 Amplifiers
7 3 Speakers
, this query will return id, parent and depth level, ordered as a tree:
WITH v (id, parent, level) AS
(
SELECT id, parent, 1
FROM table
WHERE parent = 0
UNION ALL
SELECT id, parent, v.level + 1
FROM v
JOIN table t
ON t.parent = v.id
)
SELECT *
FROM v
id parent name
1 0 Electronics
2 1 TV
4 2 LCD
5 2 Plasma
3 1 Hi-Fi
6 3 Amplifiers
7 3 Speakers
Replace parent = 0 with parent = #parent to get only a branch of a tree.
Provided there's an index on table (parent), this query will efficiently work on a very large table, since it will recursively use INDEX LOOKUP to find all chilrden for each parent.
To update a certain branch, issue:
WITH v (id, parent, level) AS
(
SELECT id, parent, 1
FROM table
WHERE parent = 0
UNION ALL
SELECT id, parent, v.level + 1
FROM v
JOIN table t
ON t.parent = v.id
)
UPDATE table t
SET column = newvalue
WHERE t.id IN
(
SELECT id
FROM v
)
where #parent is the root of the branch.
You have to ask yourself these questions first :
1) What is ratio of modifications vs reads ? (= mostly static tree or changing constantly?)
2) How deep and how large do you expect the tree to grow ?
Nested sets are great for mostly-static trees where you need operations on whole branches. It handles deep trees without problems.
Materialized path works well for dynamic (changing) trees with constrained/predictable depth.
Recursive CTEs are ideal for very small trees, but the branch operations ("get all children in this branch..") get very costly with deep / large tree.
Check out Joe Celko's book on trees and hierarchies for multiple ways to tackle the hierarchy problem. The model that you choose will depend on how you weight lookups vs. updates vs. complexity. You can make the lookups pretty fast (especially for getting all children in a node) using the adjacency list model, but updates to the tree are slower.
If you have many updates and selects, the best option seems to be the Path Enumeration Model, which is briefly described here:
http://www.sqlteam.com/article/more-trees-hierarchies-in-sql
I'm surprised no one has mentioned going with a Closure Table. Very efficient for reads and pretty simple to write.