First you have to separate the nodes subordination from the nodes itself. You need a pivot table trees
having exactly two columns: nodeID -- parentID
. The node having parentID
equal to NULL is a root node for certain tree. Pivot table trees
can contain more than one tree in it. This table requires a primary key (nodeID) and an unique multicolumn key (nodeID, parentID) - each node should be listed once and each node can have only one parent. An additional restriction - node can not refer to itself as a parent.
CREATE TABLE trees (
nodeID INT UNSIGNED NOT NULL,
parentID INT UNSIGNED, -- nullable for root nodes
PRIMARY KEY ( nodeID ), -- each node is listed once
UNIQUE KEY ( nodeID, parentID ), -- no duplicates
CONSTRAINT treeselfref CHECK ( nodeID <> parentID ) -- can't be parent for itself
)
Then you need a set of procedures for nodes manipulations:
AddNode( nodeID, parentID )
DeleteNode( nodeID ) -- you have to manage orphan childs
DeleteBranch( nodeID ) -- and all its childs/subchilds
GetParent( nodeID ) -- for the given node
GetChilds( parentID ) -- whos parent is parentID
GetBranch( parentID ) -- childs and their childs up to the leaves, ranked
GetLeaves( nodeID ) -- all leaves attached to the node
GetPath( nodeID ) -- full subordination path from the root up to the node, ranked
MergeBranches( nodeIDa, nodeIDb )
MoveBranch( nodeID, newParentID )
. . . . .
An exact set of procedures and their logic depend on the items/categories nature. So here is the sample code for the GetLeaves( nodeID )
procedure, just for illustration of the basic principle:
WITH RECURSIVE cte (nodeID, parentID) AS
( SELECT w.*
FROM trees AS w
WHERE w.nodeID = _nodeID -- seed node for branch
UNION ALL
SELECT q.*
FROM cte AS z
JOIN trees AS q ON q.parentID = z.nodeID -- childs, recurrent
) SELECT s.nodeID
FROM cte AS s
LEFT JOIN trees AS r ON r.parentID = s.nodeID
WHERE r.nodeID IS NULL; -- nodes not listed as parents for other nodes i.e. leaves
Here is the code for GetPath( nodeID )
procedure that returns a list of all parent nodes from the tree root up to the given node, with rank (distance):
WITH RECURSIVE cte (rank, nodeID, parentID) AS
( SELECT 0, w.*
FROM trees AS w
WHERE w.nodeID = _nodeID
UNION ALL
SELECT z.rank+1, q.*
FROM cte AS z
JOIN trees AS q ON q.nodeID = z.parentID
) SELECT s.*, f.name
FROM cte AS s
JOIN nodes AS n ON n.ID = s.nodeID
JOIN categories AS f ON f.ID = n.categoryID
ORDER BY s.rank DESC
When invoked for nodeID = 123 it will return something like that:
+----+------+--------+----------+
|rank|nodeID|parentID|name |
+----+------+--------+----------+
| 5 | 1 | NULL | root |
| 4 | 7 | 1 | edible |
| 3 | 11 | 7 | fruit |
| 2 | 31 | 11 | domestic |
| 1 | 42 | 31 | apple |
| 0 | 123 | 42 | mcintosh |
+----+------+--------+----------+