问题
Let's say I have a table such as this:
CREATE TABLE IF NOT EXISTS `node_list` (
`nid` int(11) NOT NULL AUTO_INCREMENT,
`parent` int(11)
COMMENT \'Node`s parent (nid).\',
PRIMARY KEY (`nid`)
)
For a given node id, I want to get a count of all of its descendants. However:
SELECT COUNT(*) FROM `node_list` WHERE `parent`=?
Only returns the count of immediate children. What might a good way of doing this without a mess of for
loops look like?
回答1:
For a hierarchy with a known maximum number of tiers, you can do a single query with a cascade of joins to find the record count. If the number of tiers is greater than three or four, this might not be pretty, but it should work.
select count(*)
from node_list n1
outer join node_list n2 on n2.parent = n1.nid
outer join node_list n3 on n3.parent = n2.nid
outer join node_list n4 on n4.parent = n3.nid
...and so on for as many levels as you need. Try not to make it too many though, or the performance may suffer.
In the real world, most hierarchy systems are actually fairly limited in their depth; even if they are theoretically unlimited. For example, a site menu may allow unlimited levels of structure, but beyond three or four it gets hard to use. It's up to you whether you impose a limit on the nesting, but it may make things easier.
However, if you do have an open-ended hierarchy where you don't know how deep it could go, or if the above query is too slow, then you are going to need a loop. Whether that loop is in a MySQL stored procedure or whether it's in PHP is immaterial; you'll need a loop one way or the other. It certainly doesn't need to be the mess of for
loops you're worried about, though.
I would do it with a recursive PHP function. Maybe something like this:
function countDescendants($db, $nid) {
$total = 0;
$query = "select nid from Nodes where parent = ".(int)$nid;
$res = $db->query($query);
foreach($res as $data) {
$total += countDescendants($db, $data['nid']);
}
$total += $res->num_rows;
return $total;
}
Then you can call it and get your answer with a single line of code:
$number_of_descendants = countDescendants($starting_nid);
A fairly simple recursive function (I've made an assumption that you're using mysqli
for your DB and you've got your connection already sorted to pass into the function).
Granted, if you have a really huge hierarchy or you're querying it lots of times, it may get a bit slow, but there are ways of speeding it up by improving on this basic example I've given. For example, you could use a prepared statement query, and just populate the same statement with different nid values: this would save a large part of the DB work. But for simple usage on a small hierarchy, the above code should be fine.
The one big pitfall with any of these techniques is if you have a loop in your node structure -- ie a node having one of its own descendants as it's parent ID. This scenario will cause an infinite loop with the above PHP code, and will cause the record count to be badly skewed in the case of the nested joins SQL query. In either event, if it is possible for your system to have this situation, you will need to code against it. But that does complicate things, so I won't go into it here.
Hope that helps.
(NB: above code not tested: I typed it straight into the answer without running it; apologies if there's any typos)
回答2:
Given those constraints, there is no way. In such a case you would either retrieve all the tree and build a 'hill' client side, or perform recursive queries, whatever would be most performant in the specific case.
With the additional constraint of having a fixed number of hierarchy levels, you can do this with a multiple JOIN.
In the general case, there are several structure modifications to allow overcoming those constraints. In practice you relax the constraint "THIS is my table structure", allowing the addition of additional fields.
For example, you could supplement the node structure with a left_id
value, and ensure that all node IDs are in sequence when you visit the tree depth-first:
1 --- 2 -+- 3 -+- 4
| |
| +- 5
+- 6 --- 7
In this case, node 3 would store the value "5", node 6 would store the value "7", and node 2 would store the value "7" too. Each node stores in LeftID the maximum between its children's LeftIDs and its own ID.
So childless nodes have LeftID equal to their IDs. Node 1 will have LeftID 7 since that's the LeftID of 2, which got it from 6.
In this situation, counting nodes is easy if there are no holes in the sequence; all descendants of a node are those nodes whose ID is between the starting node's ID and its LeftID; and leafs are identified by having LeftID equal to ID.
So "all leafs descending from node id 17" would be
SELECT child.* FROM table AS parent JOIN table AS child ON (child.id > parent.id AND child.id <= parent.leftid ) /* Descendant / WHERE child.id = child.leftid / Leaf / AND parent.id = 17; / Parent is 17
This structure is awkward to maintain if you want to be able to do prune and branch, since you need to renumber all nodes between the point of prune up to the point of branch, as well as the moved nodes.
Another possibility if you're only interested in counting is to keep a child counter. This can be maintained by updating it iteratively, selecting all leaves and setting their counter to 0 (you identify leaves through a LEFT JOIN); then all those parents with NULL counters that have children with non-NULL counters, updating their counters to the SUM()
of children's counters plus the COUNT()
of children themselves; and continuing until the number of updated rows becomes zero, for all nodes have non-NULL counters. After a prune-and-branch, you just set all counters to NULL and repeat.
This last approach costs a reflective join for each hierarchy level.
回答3:
Little more digging...
With Nodes As
(
Select s.nid, s.parent
From nodelist s
Where s.nid = @ParentID
Union All
Select s2.nid, s2.parent
From nodelist s2
Join Nodes
On Nodes.nid = s2.parent
)
Select Count(*)
From Nodes
回答4:
$tree = array();
$tree[0][1] = 'Electronics';
$tree[0][0][1] = 'Mobile';
$tree[0][0][0][2] = 'Samsung';
$tree[0][0][0][0][3] = ' Toppings Flip Cover for Samsung Galaxy S6';
$tree[0][0][0][0][0][4] = 'Maac Online Flip Cover for Samsung Galaxy S6';
$tree[0][0][0][0][0][0][5] = 'Shine Flip Cover for Samsung Galaxy S6';
$tree[0][0][0][0][0][0][0][6] = 'SNE Flip Cover for Samsung Galaxy S6';
print_r($tree);
来源:https://stackoverflow.com/questions/12734879/querying-a-count-of-items-of-a-tree