Reputation: 713
I`ve a problem with grouping and displaying results from two tables in MySQL db.
I`m trying to make a shopping cart, and I have realised a shop menu which contains products categories and via jquery when category is clicked, menu expands and show products from that category.
Example of categories table:
+------------+
| ID | NAME |
+------------+
| 1 | name1 |
| 2 | name2 |
| 3 | name3 |
| 4 | name4 |
+------------+
Example of products table:
+------------------------------------------------------------------------+
| ID | PR_CODE | NAME | DIMENSIONS | COLORS | OFFER | PRICE | CATEGORY |
+------------------------------------------------------------------------+
| 1 | pr_code1 | prod1 | 40 x 40 | blue | 1 | 11.00 | 1 |
| 2 | pr_code2 | prod2 | 120 x 120 | white | 1 | 12.00 | 1 |
| 3 | pr_code3 | prod3 | 60 x 120 | yellow | 0 | 13.00 | 2 |
| 4 | pr_code4 | prod4 | 40 x 60 | orange | 0 | 14.00 | 3 |
+------------------------------------------------------------------------+
Category row in products table telling which category product belongs.
Code for menu:
<div class="shopMenu">
<ul>
<?php
$sql = "SELECT categories.*, products.id as prodId, products.name as prodName FROM `categories` LEFT JOIN `products` ON categories.id = products.category ORDER BY categories.id DESC";
$query = execute_select($sql);
foreach($query as $row) {
$id = $row['id'];
$name = $row['name'];
$prodId = $row['prodId'];
$prodName = $row['prodName'];
echo '<li><a href="#">' . $name . '</a>
<ul>
<li><a href="&id=' . $prodId . '">' . $prodName . '</a></li>
</ul>
</li>'; }
?>
</ul>
</div>
This code works, but works wrong. If I put sql query as:
$sql = "SELECT categories.*, products.id as prodId, products.name as prodName FROM `categories` LEFT JOIN `products` ON categories.id = products.category ORDER BY categories.id DESC";
Result is: all categories are listed but categories which contains more products are repeated in main menu as many times as there are products in this category.
If I put sql query as:
$sql = "SELECT categories.*, products.id as prodId, products.name as prodName FROM `categories` LEFT JOIN `products` ON categories.id = products.category GROUP BY categories.id ORDER BY categories.id DESC";
Result is: all categories are listed once, but only one of products from chosen category is listed, missing other products.
Can somebody help with this query?
Upvotes: 1
Views: 2362
Reputation: 26597
There is basically two ways of doing what you want, both of them have been explained in the other response and in the comments. I will try to expand a little about them in this answer.
You retrieve all the data in one query and then use PHP to do some transformation in order to show them in the appropriate way.
There's two approaches to do that. You can modify the data first and then loop on them to display the menu or you can do everything in just one loop.
First, we create an array containing the data in a more "usable" way for us, and then we display the content of this new array :
$sql = "SELECT categories.id as catid, categories.name as catname, products.id as prodId, products.name as prodName
FROM `categories`
INNER JOIN `products` ON categories.id = products.category
ORDER BY categories.id DESC";
$result = execute_select($sql);
$categories = array();
foreach($query as $row) {
if( ! isset($categories[$row['catid']])) {
$categories[$row['catid']] = array(
'id' => $row['catid'],
'name' => $row['catname'],
'products' => array(),
);
}
$categories[$row['catid']]['products'][] = array(
'id' => $row['prodId'],
'name' => $row['prodName'],
);
}
foreach($categories as $cat) {
echo '<li><a href="#">' . $cat['name'] . '</a><ul>';
foreach($cat['products'] as $prod) {
echo '<li><a href="&id=' . $prod['id'] . '">' . $prod['name'] . '</a></li>';
}
echo '</ul></li>';
}
We store the current category, and when the category changes, we close the current list and open a new one :
$sql = "SELECT categories.id as catid, categories.name as catname, products.id as prodId, products.name as prodName
FROM `categories`
INNER JOIN `products` ON categories.id = products.category
ORDER BY categories.id DESC";
$result = execute_select($sql);
$actualcategory = null;
foreach($query as $row) {
if($actualcategory != $row['catid']) {
echo '<li><a href="#">' . $row['catname'] . '</a><ul>';
}
echo '<li><a href="&id=' . $row['prodId'] . '">' . $row['prodName'] . '</a></li>';
if($actualcategory != $row['catid']) {
echo '</ul></li>';
$actualcategory = $row['catid'];
}
}
In this solution, we retrieve the list of categories, and then, for each one, retrieves the list of products :
$sql = "SELECT categories.id, categories.name
FROM `categories`
ORDER BY categories.id DESC";
$categories = execute_select($sql);
foreach($categories as $cat) {
echo '<li><a href="#">' . $cat['name'] . '</a><ul>';
$sql2 = "SELECT products.id, products.name
FROM `products`
WHERE `products`.category = ".$cat['id'];
$products = execute_select($sql2);
foreach($products as $prod) {
echo '<li><a href="&id=' . $prod['id'] . '">' . $prod['name'] . '</a></li>';
}
echo '</ul></li>';
}
I dry coded the preceding piece of PHP code, I'm not even sure I didn't made some kind of silly mistakes. It is possible you will have to adapt them to your needs. If something is wrong, please point it out in the comments, I will fix it ASAP :)
The first two possibilities executes only one query and then parse the results to display meaningful information. The code is, in my opinion, fairly hard to understand and error prone.
The last possibility is much more clearer and, I think, easier to modify and extend.
From a performance point of view, we have only one query in the first two versions, but we retrieves much more data and a join in necessary. I think there's no easy answer about which solution is best, it will greatly depends on the number of categories and products for each of them. I think the best to do is to test each of the solution on various data set to determine the quicker one.
If I would have to develop this kind of menu, I will personally use the n+1 query approach. Even if the performance are slightly off (which I'm not sure), the solution is so much clearer that it compensates the weaknesses.
Kudos to every other poster on this question, I didn't provide a "new" answer, I just put the already provided one in PHP code. Hope this will help !
Upvotes: 1
Reputation: 40673
The SQL Grouping operator roughly works like this: You group by an attribute, and you (under the hood) make groups of rows which have the same value in that attribute. The query result will only contain the top of each those groups. You can use aggregate functions on each of those groups but that's about it.
You have two options:
(a) Either perform separate queries for each category (as previously suggested) or
(b) Using your first query, order by category and in your loop, while the current row is in the same category as the previous row then add it to your current list. When you reach a result which belongs to another category than the previous one then close the current list (echo </ul>;
) and start a new one
Upvotes: 0
Reputation: 16923
SELECT
*, products.id as prodId, products.name as prodName
FROM
products
LEFT JOIN
categories ON categories.id = products.category
You want products
table so take it FROM products
not FROM categories
Upvotes: 0
Reputation: 26753
You want two queries.
First get a list of categories, and loop through them. Then within each category do a second query and get the products.
(Yes, it's technically possible to get all the data in one query and check if the category was already output, but I recommend against it.)
Upvotes: 0