Building a Mysql table-tree using query instead of loops in PHP - php

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

Related

COUNT() Function Does Not Return Number Of Rows

I'm working on a blogging system website. In this blogging system, I have two tables. One is called blogs which simply contains the information of a blog (such as blog_title, blog_author, blog_category).
And the other one is called categories which only contains the blog_category names of blog posts.
Now I have made a page where users can see the blog categories and the number of blog posts within that custom category name.
So here is how it looks like:
And this is the code behind of that:
foreach($catShow as $cat){
echo "
<tr>
<td>".$cat['table_id']."</td>
<td>".$cat['cat_title']."</td>
<td>".$cat['cat_id']."</td>
<td>".num_cats($cat['cat_id'])."</td>
<td></td>
</tr>";
}
So the function num_cats() basically counts the number of blog posts at blogs table which has the same blog category id (cat_id):
function num_cats($id){
$num_cats = "SELECT COUNT(*) FROM blogs WHERE blog_category = '$id'";
$run_num = mysqli_query($GLOBALS['con2'],$num_cats);
$return = '';
if (!$run_num) {
die(mysqli_error($GLOBALS['con2']));
}
$numCat = mysqli_num_rows($run_num);
$return .= "
$numCat
";
return $return;
}
But now the problem is, the result is incorrect. I mean the table only shows 1 result for each category however some of them have more than one item at the blogs table.
So what is wrong with this code, can you please help me with that!
"SELECT COUNT() FROM blogs WHERE blog_category = '$id'"
will return one row. So $numCat becomes 1.
You need to fetch your query result and get the value from COUNT()
function num_cats($id){
$num_cats = "SELECT COUNT(*) AS count FROM blogs WHERE blog_category = '$id'";
if ($result = mysqli_query($GLOBALS['con2'], $num_cats)) {
$row = mysqli_fetch_assoc($result);
return $row['count'];
} else {
die(mysqli_error($GLOBALS['con2']));
}
}
Use join for better performance
$sql = "Select table_id,cat_title,bc.cat_id,count(b.category_id) as total_post from blog b inner join blog_category bc on b.blog_category_id=bc.blog_category_id group by b.category_id";
$result = mysqli_query($GLOBALS['con2'],$sql);
while($cat = $result->mysqli_fetch_row()){
echo "
<tr>
<td>".$cat['table_id']."</td>
<td>".$cat['cat_title']."</td>
<td>".$cat['cat_id']."</td>
<td>".$cat['total_post']."</td>
<td></td>
</tr>";
}
COUNT will give number of blog_category in your database. so in the result have only one recode with category count.
but you calling mysqli_num_rows this again. so, result will be 1 always.
just remove mysqli_num_rows($run_num); and update query with result col name,
SELECT COUNT(*) AS `total` FROM blogs WHERE blog_category = '$id'
then call
$row = mysqli_fetch_object($run_num);
$numCat = $row->total;

Conditional Sorting of Data using php codes from sql database

I'm having problem to sort table values by different categories of books and I'm having 10 categories of books.
Categories are mentioned in a table entitled "categories" as (1, 2, 3,...10) and each category contains more than 100 books.
I just want to sort books only by one category such as 1 from my database and ignore all the other categories.
I used following php codes but it sorted the results as whole of column "categories" that means all the categories are sorted one by one but
I just want to sort data by single category.
$orderby = "ORDER BY books.category DESC";
$addparam = "";
if ($_GET["incldead"] == 2)
$wherea[] = "visible = 'yes'";
$res = mysql_query("SELECT COUNT(*) FROM books $where") or die(mysql_error());
$row = mysql_fetch_array($res,MYSQL_NUM);
$count = $row[0];
if ($count)
{
$pager = pager($booksperpage, $count, "browse.php?" . $addparam);
$query = "SELECT books.id, books.category, books.name, books.times_completed, books.size, books.added, books.type, books.comments,books.numfiles,books.filename,books.owner,IF(books.nfo <> '', 1, 0) as nfoav," .
"categories.name AS cat_name, categories.image AS cat_pic, users.username FROM books LEFT JOIN categories ON category = categories.id LEFT JOIN users ON books.owner = users.id $where $orderby {$pager['limit']}";
$res = mysql_query($query) or die(mysql_error());
Output of above code similar to below:
My desired output:
Please guide me how it'll be possible by amending in above php codes...

Storing categories and subcategories with the possibility of infinite sub-'s, and a function to return them all from MYSQL into JSON

My current function:
$categories = array();
$db -> new PDO(connect);
foreach($db->query('SELECT * FROM categories') as $row) {
$category = array();
array_push($category, $row['categoryID']);
foreach($db->query("SELECT * FROM subcategories WHERE categoryID = {$row['categoryID']} ") as $row2)
{
if ($row2["subcatparentID"] == null) {
$category[$row['categoryID']][] = $row2['subcatID'];
}
//Here I need to get all the subcats with parent subcats, and
//then I need to add the subcategory to $category finding the parent subcategory
//mentioned before. So, I need a infinite multi-dimensional search function or something..
//Plus, I need to use a repeating while loop to make sure all subcategories with
//parents are added into the array, because if 14 is linked to 11 and 11 is linked to
}
$categories[] = $category;
}
return json_encode($categories);
That's my only idea, currently. How do I find the parent subcategory if it can be in any dimension? For example, subcategory 11 is linked to 12, 12 is linked to 14, 14 is linked to 13, I will have $category[13][14][12][11]
My table structure is like this:
table "subcategories": subcatID, categoryID, name, subcatparentID
table "categories": categoryID, name //these are main categories
in return with JSON format I would like something like this:
{"1":[41[43,42[45, 44]]],"2":[26,27,28,29,30,31,32,33,34,35,36,37,38,39,40],"3":[56,57,58,59,60,61],"4":[62,63,64],"5":[1,2,3,4,5,6,21[22,23]]}
although that doesn't seem to work with JSON.parse, strangely enough.
Idea for a recursive way of doing this:-
<?php
$categories = array();
$db -> new PDO(connect);
foreach($db->query('SELECT * FROM categories') as $row)
{
$categories[$row['categoryID']] = get_sub_categories($db, $row['categoryID']);
}
return json_encode($categories);
function get_sub_categories($db, $parent_category)
{
$return_categories = array();
foreach($db->query("SELECT * FROM subcategories WHERE categoryID = $parent_category ") as $row2)
{
$return_categories[$row2['subcatID']] = get_sub_categories($db, $row2['subcatID']);
}
return $return_categories;
}
?>
Not tested, but this is to give you an array where the key is the category and the value is an array of catgories under that category (which in turn could each be arrays, etc)
EDIT - Still not 100%, but doing this with a loop around but starting at the bottom of the tree, using array_unshift to prepend the sub categories to the array for each category.
This is relying on subcatparentID being set to 0 when a sub category is the highest one under that category, but if something else that is easy to change.
Not tested
<?php
$categories = array();
$db -> new PDO(connect);
$categories = array();
foreach($db->query('SELECT categories.categoryID, a.subcatID, a.subcatparentID
FROM categories
INNER JOIN subcategories a ON categories.categoryID = a.categoryID
LEFT OUTER JOIN subcategories b ON a.categoryID = b.categoryID AND a.subcatID = b.subcatparentID
WHERE b.subcatID IS NULL
') as $row)
{
$categories[$row['categoryID']] = array();
$parent = $row['subcatparentID']
while($parent != 0)
{
// following should only get a single row
foreach($db->query("SELECT subcatID, subcatparentID FROM subcategories WHERE categoryID = $row['categoryID'] AND subcatID = $parent") as $row2)
{
array_unshift($categories[$row['categoryID']], $row2['subcatID']);
$parent = $row2['subcatparentID']
}
}
}
?>

How do I retrieve items belonging to sub categories by selecting a top level category?

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.

Inefficient SQL Query

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>';

Categories