I have two tables.
The chapters table has the columns id and name.
The chapters_chapter table has columns id, master_id, and slave_id.
Lets say that the chapters table has 7 records:
id name
1 test01
2 test02
3 test03
4 test04
5 test05
6 test06
7 test07
And in the chapters_chapters table I have these records:
id master_id slave_id
1 1 5
2 1 6
3 6 7
4 7 2
Given that data, how can I extract the hierarchy of that data so that it looks like this?
test01
test05
test06
test07
test02
test03
test04
So this was kind of a pain because of the fact that we had to have the hierarchy stored in the DB. Because of this, each item can have multiple children, and each child can have multiple parents.
This second part means we cannot simply loop through the list once and be done with it. We might have to insert an item in multiple places in the hierarchy. While you probably won't actually structure your data that way, the database schema you've described supports this scenario, so the code must support it too.
Here's a high-level version of the algorithm:
Query both tables
Create a map (array) of a parent (number) to its children (another array)
Create a set of items that are children (array of numbers)
Create a function that displays a single item, indenting it to the desired depth.
If that item has children, this function increases the depth by one, and calls itself recursively
Loop through all items that aren't children (root items).
Call the function for each of those items, with a desired depth of 0 (no indent).
Here's two hours work. Enjoy :)
Note that I stuck it within a <pre> block, so you might have to mess with how the indentation is done (output something other than two spaces, mess with the style of the divs, etc).
<?php
$con = mysql_connect("localhost", "test_user", "your_password");
if(!$con)
{
die("could not connect to DB: " . mysql_error());
}
mysql_select_db("your_db", $con);
// get chapters
$chapters = array();
$result = mysql_query("SELECT * FROM chapters");
while($row = mysql_fetch_array($result))
{
$id = $row["id"];
$name = $row["name"];
$chapters[$id] = $name;
}
// get chapters_chapters - We'll call it "parent/child" instead of "master/slave"
$parent_child_map = array();
$is_child = array();
$result = mysql_query("SELECT master_id, slave_id FROM chapters_chapters");
while($row = mysql_fetch_array($result))
{
$parent_id = $row["master_id"];
$child_id = $row["slave_id"];
$children = $parent_child_map[$parent_id];
if($children == null)
{
$children = array();
}
$children[] = $child_id;
$parent_child_map[$parent_id] = $children;
$is_child[$child_id] = true;
}
// display item hierarchically
$display_item_and_children = function($id, $name, $depth)
use ($chapters, $parent_child_map, &$display_item_and_children)
{
echo "<div><pre>";
// indent up to depth
for($i = 0; $i < $depth; $i++)
{
echo " ";
}
echo "id: " . $id
. " name: " . $name
. "</pre></div>";
// if there are children, display them recursively
$children = $parent_child_map[$id];
if($children != null)
{
foreach($children as $child_id)
{
$child_name = $chapters[$child_id];
$display_item_and_children($child_id, $child_name, $depth + 1);
}
}
};
// display all top-level items hierarchically
foreach($chapters as $id => $name)
{
// if it is a top-level item, display it
if($is_child[$id] != true)
{
$display_item_and_children($id, $name, 0);
}
}
mysql_close($con);
?>
And here's a screenshot:
The question becomes how complex you want your solution to be. I'd do it with the following pseudo code.
SELECT all the chapters
SELECT all the *chapters_chapters*
loop over the chapters to create an array chapter objects
loop over the `chapters_chapters* and create the relationships using the chapter objects
Essentially you're creating a link-list.
Related
I am creating a simple Affiliate System (5 Level max). So basically the database have this kind of structure:
"aff level" column is just extra things so that i know that i listed out correctly.
I have successfully listed out (in bullet format) the parent & child of each agent. Below is my code:
function list_current_agents($aff_parent_id, $aff_level){
$max_level = 5;
if($aff_level <= 5)
{
$query = "SELECT aff_id, agent_code FROM affiliate WHERE aff_parent_id = '$aff_parent_id' AND aff_level = '$aff_level'";
$result = db_query($query);
$row = $result-> fetch_object();
if($result-> num_rows > 0)
{
echo '<ul>';
do{
$aff_id = $row-> aff_id;
$agent_code = $row-> agent_code;
echo '<li>';
echo $aff_id .' - '.$agent_code . ' - (level '.$aff_level.')';
echo '</li>';
$aff_level = $aff_level+1;
if($aff_level <= 5)
{
list_current_agents($aff_id, $aff_level+1);
}
}while($row = $result-> fetch_object());
echo '</ul>';
}
}
return $count;
}
and the output will be like this:
And now im stucked where i want to list out the total number of the child below if i select one of the parent agent. For example, if i select:
Agent1
total child : 8
Agent3
total child : 3
I have tried any method i can think of but cant work out with the logic.
Any help is very much appreciated.
thanks in advance.
Create an actual tree data structure and it will be easy to count the children.
Or you can use one of the many available on Github. This one is even made for you specific database structure.
I have the following query which pulls from 3 tables. I'll end up with 1 family per row but with multiple children for each. I want to be able to show all children ages within the family row. I thought about opening another connection/query, but figured there is a smarter way.
Query:
SELECT
families.*, job.*, children.*, families.first_name AS fam_firstname, children.first_name AS child_firstname
FROM job
LEFT OUTER JOIN families ON job.fam_id = families.fam_id
LEFT OUTER JOIN children ON families.fam_id = children.fam_id
WHERE
job.published = 2
GROUP BY job.job_id
ORDER BY job.created_on DESC
Loop:
if ($result = $mysqli->query($query)) {
$from = new DateTime($row['dob']);
$to = new DateTime('today');
while ($row = $result->fetch_assoc()) {
echo '<tr>';
echo '<td>' .$row['fam_firstname']. '</td>';
echo '<td>' .$row['last_name'].'</td>';
/* Looking to list all children ages. Separate by comma or break */
echo '<td>' . $from->diff($to)->y .'</td>';
echo '</tr>';
}
$result->free();
}
Desired Output:
Family First Name | Family Last Name | Child 1 Age, Child 2 Age
You need to use the mysql group_concat function to achieve this:
SELECT
families.*, group_concat(children.age)
FROM job
LEFT OUTER JOIN families ON job.fam_id = families.fam_id
LEFT OUTER JOIN children ON families.fam_id = children.fam_id
WHERE
job.published = 2
group by families.fam_id
ORDER BY job.created_on DESC
Follow this question: Nested Array from multiple tables.
Refer to the second option in the question, that explains how you subtract your data from the JOIN query.
P.S.
I'ts a question I've asked myself, with an implementation with what your'e trying to do here. If you need more lead on how to implement it here, ask in comments...
Here is a way to implement it in your code (notice you should order your JOIN query by "fam_firstname", for this code to work for you):
/* init temp vars to save current family's data */
$current = null;
$fam_firstname = null;
$children = array();
while ($row = mysql_fetch_assoc($result))
{
/*
if the current id is different from the previous id:
you've got to a new family.
print the previous family (if such exists),
and create a new one
*/
if ($row['fam_firstname'] != $fam_firstname )
{
// in the first iteration,
// current (previous family) is null,
// don't print it
if ( !is_null($current) )
{
$current['children'] = $children;
/*
Here you print the whole line
I'm just dumping it all here, but you can print
it more nicer...
*/
var_dump($current);
$current = null;
$fam_firstname = null;
$children = array();
}
// create a new family
$current = array();
$current['fam_firstname'] = $row['fam_firstname'];
/*
Add more columns value here...
*/
// set current as previous id
$fam_firstname = $current['fam_firstname'];
}
// you always add the phone-number
// to the current phone-number list
$children[] = $row['child_firstname'] . " is " . $row['child_age'] . " years old";
}
}
// don't forget to print the last family (saved in "current")
if (!is_null($current))
/*
Here you print the whole line
I'm just dumping it all here, but you can print
it more nicer...
*/
var_dump($current);
I am trying to make un-order list for parent child categories where if there is any child category than it will create another un-order list ( like indented text) so user can understand properly.
I have fetch sql but with foreach I don't understand how to set so where child category only will display under parent category by creating another un-order list under the parent category.
Here is my code
$query_cat = "SELECT * FROM ^categories";
$query = qa_db_query_sub($query_cat);
$catsid = qa_db_read_all_assoc($query);
echo '<UL>';
foreach ($catsid as $catid){
echo '<LI>'. $catid['title'].' '. $catid['categoryid'].'</LI>';
}
echo '</UL>';
So final result would be
First Category
Sub Category1
Second Category
EDIT:
After modified code with #vlcekmi3 answer https://stackoverflow.com/a/13451136/1053190 I am getting this result
Now how to exclude subcategory from parent list?
There's no really easy solution for this with your design. The most effective way would be to add column like order_in_list (and maybe depth_in_list).
They would be pre calculated in loop (pseudocode):
START TRANSACTION
UPDATE t1 SET order_in_list = 0 // Restart whole loop
$ids = array(0);
while $id = array_shift($ids){
$record = SELECT * FROM t1 WHERE id = $id // Get id details, order_in_list is important
$children = SELECT * FROM t1 WHERE parent_id = $id // get list of all childs
// If it's root element, start indexing from 0
$root_order = ($record ? $record->order_in_list : 1)
$child_no = count($children) // How many child will be adding
// No children, nothing to do:
if $child_no < 1{
continue;
}
append_to_array($ids, $children) // Store ids to process
// Shift all later records, we'll be creating gap in order_in_list 1,2,3,4,5
// To 1,2,5,6,7 to insert items on places 3,4
UPDATE t1 SET order_in_list = (order_in_list + $child_no)
WHERE order_in_list > $record->order_in_list
// Okay, set IDs for direct children
foreach( $children as $child){
UPDATE t1 SET order_in_list = $root_order, depth_in_list = $record->depth_in_list+1
WHERE id = $child->id
$root_order++;
}
}
COMMIT
This way you'll get records like:
First category, 1, 1
Second category 3, 1
Sub category, 2, 2
Which you could display with simple loop:
$last_depth = 0;
foreach( (SELECT * FROM t1 ORDER by `order_in_list`) as $row){
if( $last_detph > $row['depth_in_list'])){
// Close level </ul>
} else if($last_detph < $row['depth_in_list']){
// Opening level <ul>
} else {
// The same depth
}
$last_depth = $row['depth_in_list'];
}
Without modifying database
It would be probably most effective to build two arrays containing root elements and all elements:
$root_elements = array();
$all_elements = array();
foreach( (SELECT * FROM t1) as $row){
// Store details into all_elements, note that entry may have already be created when
// processing child node
if( isset( $all_elements[$row['id']])){
// set details
} else {
$all_elements[$row['id']] = $row;
$all_elements[$row['id']]['children'] = array(); // Array of child elements
}
if( $row['parent_id'] == NULL){
$all_elements[] = $row['id']; // Add row element
} else {
if( isset( $all_elements[ $row[ 'parent_id']])){
$all_elements[ $row[ 'parent_id']]['children'][] = $row['id'];
} else {
// Create new record:
$all_elements[ $row[ 'parent_id']] = array();
$all_elements[ $row[ 'parent_id']]['children'] = array($row['id']);
}
}
}
And then write it as:
foreach( $root_elements as $element_id){
write_recursive( $all_elements[ $element_id]);
}
// And display
function write_recursive( $element)
{
echo '<ul>...';
if( count( $element['children'])){
foreach( $element['children'] as $child){
write_recursive( $all_elements[ $child]);
}
}
echo '</ul>';
}
You better create class for that (to replace using global variables), but you should have a solid way to do this. Anyway try avoid using this with large number of records (I wouldn't go past 2000-5000 menu entries), try to at least cache it.
Note: solutions are oriented towards minimal number of requests on database when displaying list.
you can use complicated query or something like this
foreach ($catsid as $catid) {
...
$subquery_cat = "SELECT * FROM ^categories WHERE parentid='".$catid['categoryid']."'";
$query = qa_db_query_sub($subquery_cat);
$subcatsid = qa_db_read_all_assoc($query);
// wrap into html
...
}
I have Adjacency list mode structure like that and i want to count all title of parent according level like Food = (2,4,3), Fruit = (3,3)
tree tabel structure
after that make tree like that
by this code i m getting right total like for Food =9, Fruit = 6
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0;
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1);
}
return $count;
}
call function
display_children(Food, 0)
Result : 9 // but i want to get result like 2,4,3
But i want to get count total result like that For Food 2,4,3 and For Fruit 3,3 according level
so plz guide how to get total according level
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = "";
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
if($count!="")
$count .= (1 + $this->display_children($row['title'], $level+1));
else
$count = ", ".(1 + $this->display_children($row['title'], $level+1));
}
return $count;
}
Lets try this once..
If you want to get amounts by level, then make the function return them by level.
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree WHERE parent="'.$parent.'"');
$count = array(0=>0);
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count[0]++;
$children= $this->display_children($row['title'], $level+1);
$index=1;
foreach ($children as $child)
{
if ($child==0)
continue;
if (isset($count[$index]))
$count[$index] += $child;
else
$count[$index] = $child;
$index++;
}
}
return $count;
}
Note that its hard for me to debug the code as i dont have your table. If there is any error let me know and i will fix it.
Anyways result will be array
which should contain amounts of levels specified by indices:
$result=display_children("Food", 0) ;
var_export($result);//For exact info on all levels
echo $result[0];//First level, will output 2
echo $result[1];//Second level, will output 4
echo $result[2];//Third level, will output 3
And by the way there is typo in your database, id 10 (Beef) should have parent "Meat" instead of "Beat" i guess.
If you want to see testing page, its here.
This article has all you need to creates a tree with mysql, and how count item by level
If you don't mind changing your schema I have an alternative solution which is much simpler.
You have your date in a table like this...
item id
-------------+------
Food | 1
Fruit | 1.1
Meat | 1.2
Red Fruit | 1.1.1
Green Fruit | 1.1.2
Yellow Fruit | 1.1.3
Pork | 1.2.1
Queries are now much simpler, because they're just simple string manipulations. This works fine on smallish lists, of a few hundred to a few thousand entries - it may not scale brilliantly - I've not tried that.
But to count how many things there are at the 2nd level you can just do a regexp search.
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
Third level is just
select count(*) from items
where id regexp '^[0-9]+.[0-9]+.[0-9]+$'
If you just want one sub-branch at level 2
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
and id like "1.%"
It has the advantage that you don't need to run as many queries on the database, and as a bonus it's much easier to read the data in the tables and see what's going on.
I have a nagging feeling this might not be considered "good form", but it does work very effectively. I'd be very interested in any critiques of this method, do DB people think this is a good solution? If the table were very large, doing table scans and regexps all the time would get very inefficient - your approach would make better use of the any indexes, which is why I say this probably doesn't scale very well, but given you don't need to run so many queries, it may be a trade off worth taking.
An solution by a php class :
<?php
class LevelDepCount{
private $level_count=array();
/**
* Display all child of an element
* #return int Count of element
*/
public function display_children($parent, $level, $isStarted=true)
{
if($isStarted)
$this->level_count=array(); // Reset for new ask
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0; // For the level in the section
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1,false);
}
if(array_key_exists($level, $this->level_count))
$this->level_count[$level]+=$count;
else
$this->level_count[$level]=$count;
return $count;
}
/** Return the count by level.*/
public function getCountByLevel(){
return $this->level_count;
}
}
$counter=new LevelDepCount();
$counter->display_children("Food",0);
var_dump($counter->getCountByLevel());
?>
If you modify your query you can get all the data in one swoop and without that much calculations (code untested):
/* Get all the data in one swoop and arrange it for easy mangling later */
function populate_data() {
$result = mysql_query('SELECT parent, COUNT(*) AS amount, GROUP_CONCAT(title) AS children FROM tree GROUP BY parent');
$data = array();
while ($row = mysql_fetch_assoc($result)) {
/* Each node has the amount of children and their names */
$data[$row['parent']] = array($row['children'], int($row['amount']));
}
return $data;
}
/* The function that does the whole work */
function get_children_per_level($data, $root) {
$current_children = array($root);
$next_children = array();
$ret = array();
while(!empty($current_children) && !empty($next_children)) {
$count = 0;
foreach ($current_children as $node) {
$count += $data[$node][0]; /* add the amount */
$next_children = array_merge($next_children, explode($data[$node][1])); /* and its children to the queue */
}
ret[] = $count;
$current_children = $next_children;
$next_children = array();
}
return $ret;
}
$data = populate_data();
get_children_per_level($data, 'Food');
It shouldn't be difficult to modify the function to make a call per invocation or one call per level to populate the data structure without bringing the whole table into memory. I'd suggest against that if you have deep trees with just a few children as it is a lot more efficient to get all the data in one swoop and calculate it. If you have shallow trees with a lot of children, then it may be worth changing.
It would also be possible to put everything together in a single function, but I'd avoid re-calculating data for repeated calls when they are not needed. A possible solution for this would be to make this a class, use the populate_data function as the constructor that stores it as an internal private property and a single method that is the same as get_children_per_level without the first parameter as it would get the data off its internal private property.
In any case, I'd also suggest you use the ID column as a "parent" reference instead of other columns. To start with, my code will break if any of the names contains a comma :P. Besides, you may have two different elements with the same name. For example, you could have Vegetables -> Red -> Pepper and the Red will get slumped together with the Fruit's Red.
Another thing to note is that my code will enter an infinite loop if your DB data is not a tree. If there is any cycle in the graph, it will never finish. That bug could be easily solved by keeping a $visited array with all the nodes that have already been visited and not pushing them into the $next_children array within the loop (probably using array_diff($data[$node][1], $visited).
I'm trying to generate a tree structure from a table in a database. The table is stored flat, with each record either having a parent_id or 0. The ultimate goal is to have a select box generated, and an array of nodes.
The code I have so far is :
function init($table, $parent_id = 0)
{
$sql = "SELECT id, {$this->parent_id_field}, {$this->name_field} FROM $table WHERE {$this->parent_id_field}=$parent_id ORDER BY display_order";
$result = mysql_query($sql);
$this->get_tree($result, 0);
print_r($this->nodes);
print_r($this->select);
exit;
}
function get_tree($query, $depth = 0, $parent_obj = null)
{
while($row = mysql_fetch_object($query))
{
/* Get node */
$this->nodes[$row->parent_category_id][$row->id] = $row;
/* Get select item */
$text = "";
if($row->parent_category_id != 0) {
$text .= " ";
}
$text .= "$row->name";
$this->select[$row->id] = $text;
echo "$depth $text\n";
$sql = "SELECT id, parent_category_id, name FROM product_categories WHERE parent_category_id=".$row->id." ORDER BY display_order";
$nextQuery = mysql_query($sql);
$rows = mysql_num_rows($nextQuery);
if($rows > 0) {
$this->get_tree($nextQuery, ++$depth, $row);
}
}
}
It's almost working, but not quite. Can anybody help me finish it off?
You almost certainly, should not continue down your current path. The recursive method you are trying to use will almost certainly kill your performance if your tree ever gets even slightly larger. You probably should be looking at a nested set structure instead of an adjacency list if you plan on reading the tree frequently.
With a nested set, you can easily retrieve the entire tree nested properly with a single query.
Please see these questions for a a discussion of trees.
Is it possible to query a tree structure table in MySQL in a single query, to any depth?
Implementing a hierarchical data structure in a database
What is the most efficient/elegant way to parse a flat table into a tree?
$this->nodes[$row->parent_category_id][$row->id] = $row;
This line is destroying your ORDER BY display_order. Change it to
$this->nodes[$row->parent_category_id][] = $row;
My next issue is the $row->parent_category_id part of that. Shouldn't it just be $row->parent_id?
EDIT: Oh, I didn't read your source closely enough. Get rid of the WHERE clause. Read the whole table at once. You need to post process the tree a second time. First you read the database into a list of arrays. Then you process the array recursively to do your output.
Your array should look like this:
Array(0 => Array(1 => $obj, 5 => $obj),
1 => Array(2 => $obj),
2 => Array(3 => $obj, 4 => $obj),
5 => Array(6 => $obj) );
function display_tree() {
// all the stuff above
output_tree($this->nodes[0], 0); // pass all the parent_id = 0 arrays.
}
function output_tree($nodes, $depth = 0) {
foreach($nodes as $k => $v) {
echo str_repeat(' ', $depth*2) . $v->print_me();
// print my sub trees
output_tree($this->nodes[$k], $depth + 1);
}
}
output:
object 1
object 2
object 3
object 4
object 5
object 6
I think it's this line here:
if($row->parent_category_id != 0) {
$text .= " ";
}
should be:
while ($depth-- > 0) {
$text .= " ";
}
You are only indenting it once, not the number of times it should be indented.
And this line:
$this->get_tree($nextQuery, ++$depth, $row);
should be:
$this->get_tree($nextQuery, $depth + 1, $row);
Note that you should probably follow the advice in the other answer though, and grab the entire table at once, and then process it at once, because in general you want to minimize round-trips to the database (there are a few use cases where the way you are doing it is more optimal, such as if you have a very large tree, and are selecting a small portion of it, but I doubt that is the case here)