i have a table with 3 columns id,name and parent_id representing categories. The root categories(categories with no parents) have parent_id 0. All other categories have parent_id as the id of their immediate parent. There is no limit on the depth of categories i mean that a category can be 3,4 or even 10 levels down from a root category. What i need now is a PHP multi-dimensional array that contains all the root categories at the first level and then their immediate sub-categories at the next level,each sub-category under its parent category,and their sub-categories under them 1 level down. So its a tree like structure. there can be many levels in the tree
I dont need the exact code but need an idea. one such idea is get all the root categories with a select query and then fire a select query for each root query to get its subcategoies and so on recursively but that would be too much select queries.
Or if i know that my table is going to contain say 300 rows at the maximum, how can i do something like
$categories=GetResultAsArray(select * from categories);
and now manipulate the $categories array in memory to get the desired tree.
You're right, using the solution with a "parentid" column is simple but it makes you write recursive queries. There are several other designs for storing hierarchical data in a database that let you do queries more efficiently.
See:
What is the most efficient/elegant way to parse a flat table into a tree?
My other answers to SO questions on hierarchical-data
My presentation Models for Hierarchical Data with SQL and PHP
My book SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming
Sql Antipatterns Strike Back
It's possible duplicate (http://stackoverflow.com/questions/8431463/more-efficient-hierarchy-system/8431551) but here is a snippet to query an adjacent tree (parent_id,id,title):
$q = mysql_query("SELECT id, parent_id, name FROM categories");
while ($r = mysql_fetch_row($q)) {
$names[$r[0]] = $r[2];
$children[$r[0]][] = $r[1];
}
function render_select($root=0, $level=-1) {
global $names, $children;
if ($root != 0)
echo '<option>' . strrep(' ', $level) . $names[$root] . '</option>';
foreach ($children[$root] as $child)
render_select($child, $level+1);
}
echo '<select>';
render_select();
echo '</select>';
You can be inspired by this solution that retrieves the breadcrumb trail of a url and its parents (infinite depth of hierarchy levels).
$q = "SELECT T2.*
FROM (
SELECT
#r AS parent_id,
(SELECT #r := `parent`
FROM `route` WHERE id = parent_id)
AS `parent`, #l := #l + 1 AS `depth`
FROM
(SELECT #r := $route_id, #l := 0) vars, `route` T3
WHERE #r <> 0) T1
JOIN `route` T2
ON T1.parent_id = T2.id
ORDER BY T1.`depth` DESC";
if($res = $db->query($q)) {
if($res = $res->fetchAll(PDO::FETCH_ASSOC)) {
if($size = sizeof($res) > 0) {
// push results in breadcrumb items array
$breadcrumb['items'] = $res;
$breadcrumb['levels'] = $size;
// retrieve only titles
$titles = array_column($res, 'title');
// construct html result seperated by '>' or '/'
$breadcrumb['html']['gt'] = implode(' > ', $titles);
$breadcrumb['html']['sl'] = implode(' / ', $titles);
}
}
}
Entire get_breadcrumb() function available here : https://stackoverflow.com/a/63578607/2282880
Related
This question already has answers here:
How to create a MySQL hierarchical recursive query?
(16 answers)
Closed 4 years ago.
I have a MYSQL table called collections when viewed and implemented as a table could be something like this:
I needed to know whether one mysql query will be able to get all the products under a collection type entry (a given) which could have collections under it. For example, if I select 10, it should return 14, 12, 13, and 15.
I implemented a solution that involves a do..while loop...
$concatted = 10;
$products = [];
do {
$sql = "SELECT id, type FROM collections WHERE parent IN ($id_concatted)";
$result = $mysqli->query($sql);
if($result) {
while($row = $result->fetch_object()){
if($row->type == 'product') {
apply_changes_to_product($row->id);
} elseif ($row->type=='collection'){
$collections[] = $row->id;
}
}
}
if(count($collections) > 0){
$id_concatted = implode($collections, ",");
$continue = true;
$collections = [];
} else {
$continue = false;
}
} while ($continue);
I think that the above code is not efficient. I think it is doable with one query but I don't know how.
UPDATE: I mark this as a duplicate of How to create a MySQL hierarchical recursive query although in that post there is NO accepted solution. I got myself this solution based on one reply there (Mysql 5.6):
SELECT id, `type` FROM (
select id, `type`
from (select * from collections
order by parent, id) products_sorted,
(select #pv := '10') initialisation
where find_in_set(parent, #pv)
and length(#pv := concat(#pv, ',', id))
) products
WHERE
products.`type` = 'product'
The fiddle is http://sqlfiddle.com/#!9/ea214f/2.
yes, you may need to use subquery and first fetch id where parent = selectedId and type = 'collection' and then select id where parent in the subquery id and type = 'product'
Like below:
SELECT id, type FROM collections WHERE parent IN (select id from collections where
parent = $id_concatted and type = 'collection') and type = 'product'
For Multiple level, Use Recursive feature of MySql. Like below:
WITH RECURSIVE COLLECTIONS_PRODUCTS (ID, TYPE, PATH)
AS
(
SELECT ID, TYPE, CAST(ID AS CHAR(200))
FROM COLLECTIONS
WHERE PARENT IN ($id_concatted)
UNION ALL
SELECT S.ID, S.TYPE, CONCAT(M.PATH, ",", S.ID)
FROM COLLECTIONS_PRODUCTS M JOIN COLLECTIONS S ON M.ID=S.PARENT
)
SELECT * FROM COLLECTIONS_PRODUCTS WHERE TYPE = 'product' ORDER BY PATH;
I have a category tree that contains up to 3 levels of children-categories, like this:
houseproducts->livingroom->sofas->twoseats
houseproducts->livingroom->sofas->threeseats
houseproducts->livingroom->sofas->fourseats
So for each sublevel, I do a SELECT based on the mothers category-id. This is done in a PHP-loop like the code below, but I guess it could be done in one single Mysql-query, for better performance. I have tried different JOINS but find it really difficualt. Any advice will be highly appreciated.
function build_category_tree()
{
$cat = array();
// main category loop
$r1 = mysql_query("SELECT cat_id,cat_name FROM categories WHERE cat_mother=0 OR cat_mother='' ORDER BY cat_name");
while ($row=mysql_fetch_assoc($r1))
{
$cat[$row['cat_id']] = $row['cat_name'];
// check for subcategories
$r2 = mysql_query("SELECT cat_id,cat_name FROM categories WHERE cat_mother='".$row['cat_id']."'");
while ($subrow=mysql_fetch_assoc($r2))
{
$cat[$subrow['cat_id']] = ' - '.$subrow['cat_name'];
// check if there is subcats for the current subcategory
$r3 = mysql_query("SELECT cat_id,cat_name FROM categories WHERE cat_mother='".$subrow['cat_id']."'");
while ($subrow2=mysql_fetch_assoc($r3))
{
$cat[$subrow2['cat_id']] = ' -- '.$subrow2['cat_name'];
// check if there is subcats for the current subcategory
$r4 = mysql_query("SELECT cat_id,cat_name FROM categories WHERE cat_mother='".$subrow2['cat_id']."'");
while ($subrow3=mysql_fetch_assoc($r4))
{
$cat[$subrow3['cat_id']] = ' --- '.$subrow3['cat_name'];
}
}
}
}
return $cat;
}
I would read your entire table into an array and segment that array by the mother's keys
Try this:
SELECT l1.cat_id AS l1_cat_id
,l1.cat_name AS l1_cat_name
,l2.cat_id AS l2_cat_id
,l2.cat_name AS l2_cat_name
,l3.cat_id AS l3_cat_id
,l3.cat_name AS l3_cat_name
,l4.cat_id AS l4_cat_id
,l4.cat_name AS l4_cat_name
FROM categories AS l1
JOIN categories AS l2
ON l2.cat_mother = l1.cat_id
JOIN categories AS l3
ON l3.cat_mother = l2.cat_id
JOIN categories AS l4
ON l4.cat_mother = l3.cat_id
WHERE l1.cat_mother=0 OR l1.cat_mother=''
ORDER BY l1_cat_name, l2_cat_name, l3_cat_name, l4_cat_name
I have category table which stores all category info [parent and child both] , category_child table which stores parent and child category relation, and product_category table which stores relation between child_category and product.
category - All Category {cid, cname, isParent, status} column.
category_child - Realtion {parent_id, child_id}.
category_product - Relation {product_id, child_category_id}
product - All product details {pid, pname, pimage, pprice,pstock}
I am displaying all Parent Category Link in Front page. Now, Whenever any person will click on parent category Link, I want to display 4 product information from each child category of parent category.
here is my code, which is horrible at the moment, and i am looking for some help in minimising it as much as possible.
$fetchChild = $mysqli->query("SELECT child_id from category_child where parent_id='".$mysqli->real_escape_string($pid)."'");
$listchild = array();
if($fetchChild->num_rows > 0) {
$n = 1;
while($storeChild = $fetchChild->fetch_assoc()) {
$listchild['child_id'][$n] = $mysqli->query("SELECT product_id from category_product where child_category_id='".$mysqli->real_escape_string($storeChild[$n])."'");
if($listchild['child_id'][$n]->num_rows > 0) {
$i = 1;
while($storeMore = $listchild['child_id'][$n]->fetch_assoc()) {
$listchild['product_id'][$i] = $mysqli->query("SELECT pid, pname, pimage, pprice, pstock from product where pid='".$mysqli->real_escape_string($storeMore[$i])."'");
if($listchild['child_id'][$n]['product_id'][$i]->num_rows > 0) {
$me = 1;
while($smeLast = $storeMore[$i]->fetch_assoc()) {
$listchild['child_id'][$n]['product_id'][$i]['pid'] = $smeLast['pid'];
$listchild['child_id'][$n]['product_id'][$i]['pid'] = $smeLast['pname'];
$listchild['child_id'][$n]['product_id'][$i]['pid'] = $smeLast['pimage'];
$listchild['child_id'][$n]['product_id'][$i]['pid'] = $smeLast['pprice'];
$listchild['child_id'][$n]['product_id'][$i]['pid'] = $smeLast['pstock'];
$me++;
}
} else {
echo '<meta http-equiv="refresh" content="0; url=index.php?error=Something+Went+Wrong+We+are+Fixing+it" />';
}
$listchild['product_id'][$i]->free();
$i++;
}
}
$listchild['child_id'][$n]->free();
$n++;
}
} else {
echo '<meta http-equiv="refresh" content="0; url=index.php" />';
}
$fetchChild->free();
Kindly help in minimizing nested while and query in my code.
Thanks
If you want, you can put everything into one SQL query using JOIN statement.
SELECT `category`.*, `product`.* FROM `product`
LEFT JOIN `category_product` ON `category_product`.`product_id` = `product`.`pid`
LEFT JOIN `category_child` ON `category_child`.`child_id` = `category_product`.`child_id`
LEFT JOIN `category` ON `category`.`cid` = `category_child`.`child_id`
WHERE `category_child`.`parent_id`='".$mysqli->real_escape_string($pid)."'
But I don't think it's the best solution.
PS. By the way, there is no LIMIT of 4 products per child category in your code, so I haven't put it either.
You can reduce all your queries to one query with JOINS. Using joins will allow you to return results from one table (e.g. product) basing on the conditions provided in another table or tables (e.g. category_product, category_child).
Read more about joins somewhere at Stack Overflow or browse some other resources. For example http://www.codinghorror.com/blog/2007/10/a-visual-explanation-of-sql-joins.html .
You should never use SQL query in a loop! It is very slow, you can overload the sql server etc...
Your database structure is bad. If you want to store hiearchical tree there are better options:
Tree 1 level:
1A2
Tree 2 level:
1A6
2B3 4C5
Tree 3 level:
1A12
2B7 8C9 10D11
3E4 5F6
You have a left and a right value by each node, and you can have the parent id too. You can check whether a node is leaf or branch if you check the difference of the right-left. By leaves it is one. You can check whether a leaf is under a branch if its left is bigger than the left of the branch and its right is smaller than the right of the branch. etc... This structure is described on several sites, for example here: nested set model
After you transformed your model to nested set, you can use a simple sql to ask for your products. Something like this:
SELECT
p.*
FROM
categories c, categories b, products p, product_categories pc
WHERE
b.category_id = ?
AND
c_category_id <> b.category_id
AND
c.category_left BETWEEN b.category_left AND b.category_right
AND
c.category_right - c.category_left = 1
AND
c.category_id = pc.category_id
AND
p.product_id = pc.product_id
This is good for beginning, your code will contain group by, limit, etc... because you want to ask only a single product per category... (or you can simply use order by rand() and limit 4 ...)
You should use prepared statements instead of manual escaping... http://php.net/manual/en/mysqli.prepare.php
Please see the data tables and query below ..
Items
Id, Name
1, Item 1
2, Item 2
Categories
Id, Name, Parent ID
1, Furniture , 0
2, Tables, 1
3, Beds, 1
4, Dining Table, 2
5, Bar Table, 2
4, Electronics, 0
5, Home, 4
6, Outdoors, 4
7, Table lamp, 4
ItemCategory
ItemId, CategoryId
1, 2 .. Row1
2, 4 .. Row 2
2, 5 .. Row 3
ItemCategory table stores which items belongs to which category. An item can belong to top level and or sub category. there are about 3 level deep categories, that is, Tob level, sub level, and sub sub level.
Users select all of the categories they want to view and submit and I can query the database by using a sample query below..
SELECT * FROM items i INNER JOIN ItemCategory ic ON
ic.itemId = i.itemId AND ic.itemId IN ('comma separated category ids')
This works fine.
My question is that Is it possible to view all the items under a top level category even though it has not been directly assigned to the item. For example, if users select Furniture above, then it lists all the items belonging to its sub categories (even though the ItemCategory doesn't contain any record for it)??
I'm open to making necessary amendements to the data table or queries, please suggest a solution. Thank you.
Watcher has given a good answer, but I'd alter my approach somewhat to the following, so you have a structured recursive 2-dimensional array with categories as keys and items as values. This makes it very easy to print back to the user when responding to their search requirements.
Here is my approach, which I have tested:
$items = getItemsByCategory($topCategory);
//To print contents
print_r($items);
function getItemsByCategory($sid = 0) {
$list = array();
$sql = "SELECT Id, Name FROM Categories WHERE ParentId = $sid";
$rs = mysql_query($sql);
while ($obj = mysql_fetch_object($rs)) {
//echo $obj->id .", ".$parent." >> ".$obj->name."<br/>";
$list[$obj->name] = getItems($obj->id);
if (hasChildren($obj->id)) {
array_push($list[$obj->name],getItemsByCategory($obj->id));
}
}
return $list;
}
function getItems($cid) {
$list = array();
$sql = "SELECT i.Id, i.Name FROM Items p INNER JOIN ItemCategory ic ON i.id = ic.ItemId WHERE ic.CategoryId = $cid";
$rs = mysql_query($sql);
while ($obj = mysql_fetch_object($rs)) {
$list[] = array($obj->id, $obj->name);
}
return $list;
}
function hasChildren($pid) {
$sql = "SELECT * FROM Categories WHERE ParentId = $pid";
$rs = mysql_query($sql);
if (mysql_num_rows($rs) > 0) {
return true;
} else {
return false;
}
}
Hope this helps.
With recursion, anything is possible:
function fetchItemsByCat($cat, &$results) {
$itemsInCat = query("SELECT Items.Id FROM Items INNER JOIN ItemCategory ON ItemCategory.ItemId = Items.Id WHERE CategoryId = ?", array($cat));
while($row = *_fetch_array($itemsInCat))
array_push($results, $row['Id']);
$subCategories = query("SELECT Id FROM Categories WHERE Parent = ?", array( $cat ));
while($row = *_fetch_array($subCategories))
$results = fetchItemsByCat($row['Id'], $results);
return $results;
}
$startCat = 1; // Furniture
$itemsInCat = fetchItemsByCat($startCat, array());
The function is somewhat pseudo-code. Replace *_fetch_array with whatever Database extension you are using. The query function is however you are querying your database.
Also, this is untested, so you should test for unexpected results due to using an array reference, although I think it's good to go.
After calling the function, $itemsInCat will be an array of integer ids of all of the items/subitems that exist in the given start category. If you wanted to get fancy, you can instead return an array of arrays with each 2nd level array element having an item id as well as that item's assigned category id, item name, etc.
If you use MySQL, you're out of luck short of indexing your tree using typical techniques, which usually means pre-calculating and storing the paths, or using nested sets:
http://en.wikipedia.org/wiki/Nested_set_model
If you can switch to PostgreSQL, you can alternatively use a recursive query:
http://www.postgresql.org/docs/9.0/static/queries-with.html
Evidently, you can also recursively query from your app, but it's a lot less efficient.
I'm building a simple web app at the moment that I'll one day open source. As it stands at the moment, the nav is generated on every page load (which will change to be cached one day) but for the moment, it's being made with the code below. Using PHP 5.2.6 and MySQLi 5.0.7.7, how more efficient can the code below be? I think joins might help, but I'm after advice. Any tips would be greatly appreciated.
<?php
$navQuery = $mysqli->query("SELECT id,slug,name FROM categories WHERE live=1 ORDER BY name ASC") or die(mysqli_error($mysqli));
while($nav = $navQuery->fetch_object()) {
echo '<li>';
echo ''. $nav->name .'';
echo '<ul>';
$subNavQuery = $mysqli->query("SELECT id,name FROM snippets WHERE category='$nav->id' ORDER BY name ASC") or die(mysqli_error($mysqli));
while($subNav = $subNavQuery->fetch_object()) {
echo '<li>';
echo ''. $subNav->name .'';
echo '</li>';
}
echo '</ul>';
echo '</li>';
}
?>
You can run this query:
SELECT c.id AS cid, c.slug AS cslug, c.name AS cname,
s.id AS sid, s.name AS sname
FROM categories AS c
LEFT JOIN snippets AS s ON s.category = c.id
WHERE c.live=1
ORDER BY c.name, s.name
Then iterate thru the results to create the proper heading like:
// last category ID
$lastcid = 0;
while ($r = $navQuery->fetch_object ()) {
if ($r->cid != $lastcid) {
// new category
// let's close the last open category (if any)
if ($lastcid)
printf ('</li></ul>');
// save current category
$lastcid = $r->cid;
// display category
printf ('<li>%s', $r->cslug, $r->cname);
// display first snippet
printf ('<li>%s</li>', $r->cslug, $r->sname, $r->sname);
} else {
// category already processed, just display snippet
// display snippet
printf ('<li>%s</a>', $r->cslug, $r->sname, $r->sname);
}
}
// let's close the last open category (if any)
if ($lastcid)
printf ('</li></ul>');
Note that I used printf but you should use your own function instead which wraps around printf, but runs htmlspecialchars thru the parameters (except the first of course).
Disclaimer: I do not necessarily encourage such use of <ul>s.
This code is just here to show the basic idea of processing hierarchical data got with one query.
First off, you shouldn't query your database in your view. That would be mixing your business logic and your presentation logic. Just assign the query results to a variable in your controller and iterate through it.
As for the query, yup a join can do that in 1 query.
SELECT * -- Make sure you only select the fields you want. Might need to use aliases to avoid conflict
FROM snippets S LEFT JOIN categiries C ON S.category = C.id
WHERE live = 1
ORDER BY S.category, C.name
This will get you an initial result set. But this won't give you the data nicely ordered like you expect. You'll need to use a bit of PHP to group it into some arrays that you can use in your loops.
Something along the lines of
$categories = array();
foreach ($results as $result) {
$snippet = array();
//assign all the snippet related data into this var
if (isset($categories[$result['snippets.category']])) {
$categories[$result['snippets.category']]['snippet'][] = $snippet;
} else {
$category = array();
//assign all the category related data into this var;
$categories[$result['snippets.category']]['snippet'] = array($snippet);
$categories[$result['snippets.category']]['category'] = $category;
}
}
This should give you an array of categories which have all the related snippets in an array. You can simply loop through this array to reproduce your list.
I'd try this one:
SELECT
c.slug,c.name,s.name
FROM
categories c
LEFT JOIN snippets s
ON s.category = c.id
WHERE live=1 ORDER BY c.name, s.name
I didnt test it, though. Also check the indexes using the EXPLAIN statement so MySQL doesnt do a full scan of the table.
With these results, you can loop the results in PHP and check when the category name changes, and build your output as you wish.
Besides a single combined query you can use two separate ones.
You have a basic tree-structure here with branch elements (categories table) and leaf elements (snippets table). The shortcoming of the single-query solution is that you get owner brach-element repeatedly for every single leaf element. This is redundant information and depending on the number of leafs and the amount of information you query from each branch element can produce large amount of additional traffic.
The two-query solution looks like:
$navQuery = $mysqli->query ("SELECT id, slug, name FROM categories WHERE live=1 ORDER BY name")
or die (mysqli_error ($mysqli));
$subNavQuery = $mysqli->query ("SELECT c.id AS cid, s.id, s.name FROM categories AS c LEFT JOIN snippets AS s ON s.category=c.id WHERE c.live=1 ORDER BY c.name, s.name")
or die (mysqli_error ($mysqli));
$sub = $subNavQuery->fetch_object (); // pre-reading one record
while ($nav = $navQuery->fetch_object ()) {
echo '<li>';
echo ''. $nav->name .'';
echo '<ul>';
while ($sub->cid == $nav->id) {
echo '<li>';
echo ''. $sub->name .'';
echo '</li>';
$sub = $subNavQuery->fetch_object ();
}
echo '</ul>';
}
It should print completely the same code as your example
$navQuery = $mysqli->query("SELECT t1.id AS cat_id,t1.slug,t1.name AS cat_name,t2.id,t2.name
FROM categories AS t1
LEFT JOIN snippets AS t2 ON t1.id = t2.category
WHERE t1.live=1
ORDER BY t1.name ASC, t2.name ASC") or die(mysqli_error($mysqli));
$current = false;
while($nav = $navQuery->fetch_object()) {
if ($current != $nav->cat_id) {
if ($current) echo '</ul>';
echo ''. $nav->cat_name .'<ul>';
$current = $nav->cat_id;
}
if ($nav->id) { //check for empty category
echo '<li>'. $nav->name .'</li>';
}
}
//last category
if ($current) echo '</ul>';