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>' . $name . '
<ul>
<li>' . $prodName . '</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?
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.
One query
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.
Two steps variant
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>' . $cat['name'] . '<ul>';
foreach($cat['products'] as $prod) {
echo '<li>' . $prod['name'] . '</li>';
}
echo '</ul></li>';
}
One step variant
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>' . $row['catname'] . '<ul>';
}
echo '<li>' . $row['prodName'] . '</li>';
if($actualcategory != $row['catid']) {
echo '</ul></li>';
$actualcategory = $row['catid'];
}
}
n+1 query
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>' . $cat['name'] . '<ul>';
$sql2 = "SELECT products.id, products.name
FROM `products`
WHERE `products`.category = ".$cat['id'];
$products = execute_select($sql2);
foreach($products as $prod) {
echo '<li>' . $prod['name'] . '</li>';
}
echo '</ul></li>';
}
Dry coding warning
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 :)
Conclusion
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.
Disclaimer
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 !
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.)
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
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
Related
I have a categories table:
id | name | parent_id
1 | Camaro | 0
2 | Chevelle | 0
3 | Sale - Camaro Parts | 1
4 | Bestselling Parts | 1
My first request looks like:
'SELECT
*
FROM
`categories`
WHERE
parent_id = :parent_id';
And after I'm fetching result set I make sub query to check if row has child elements:
foreach($result as $r) {
$r->hasChild = count(ORM::forTable('categories')->where('parent_id', $r->id)->findArray());
$data[] = $r;
}
Is any way to avoid multiple connection to DB in foreach loop and get data in first query?
Thanks!
This isn't awful to do, so long as you only want the count of children below the selected rows. If you want the entire hierarchy, you'll need to use a better RDMS.
The main part of the solution here is self joining the same table. Then we can use the count() aggregate function to see how many children are attached to each item.
select
categories.id
, categories.name
, categories.parent_id
, count(chld.id)
from
categories
left join categories chld
on categories.id = chld.parent_id
where
parent_id = :parent_id
group by
categories.id
, categories.name
, categories.parent_id
You can do a self join to the table on parent_id and id. Based on whether you want categories with child or not you can do a left join or inner join. Kind of a similar question is mentioned here -
Mysql Self Join to find a parent child relationship in the same table
How can I combine 3 tables in a INNER JOIN?
The end result I am after is getting a list of CATEGORIES belonging to a PRODUCT - including the CATEGORY'S PARENT ID CATEGORY value (ie: Sneakers and Nike).
The CATEGORIES table and PRODUCTS table are joined in the PRODUCTS & CATEGORIES table. A product can belong to many categories and a category can have many products.
Here's more-or-less the setup I have in my database...
CATEGORIES TABLE:
CAT ID | PARENT ID | CATEGORY
1 | 0 | Sneakers
2 | 1 | Nike
3 | 2 | Jordan
PRODUCTS TABLE:
PROD ID
1
2
3
PRODUCTS & CATEGORIES TABLE:
CAT ID | PROD ID
1 | 0
1 | 1
2 | 3
I am running these queries and I am getting some results, but at the moment I am running 2 separate queries...
$q1 = "SELECT prodid, GROUP_CONCAT(catid SEPARATOR ' // ') as catid FROM products_categories group by prodid order by prodid";
$result1 = $conn->query($q1);
if ($result1->num_rows > 0) {
while($prods = $result1->fetch_assoc()) {
echo "Product Id:" . $prods["prodid"] . " ––> " . "Categories Id:" . $prods["catid"];
}
} else {
echo "0 results";
}
$q2 =
" SELECT `ID`.`category` as `IDName`, `LABEL`.`category` as `LabelName`, `LABEL`.`catid` as `LabelId`
FROM `categories` as ID
INNER JOIN `categories` as LABEL
ON `ID`.`catid` = `LABEL`.`parentid`";
$result2 = $conn->query($q2);
if ($result2->num_rows > 0) {
while($prods = $result2->fetch_assoc()) {
echo "ID# " . $prods["LabelId"] . " is called: ". $prods["LabelName"] . "<br>";
}
} else {
echo "0 results";
}
$conn->close();
I have tried adding another INNER JOIN with no luck in the results.
The end result I am after would be: PROD ID #0 belongs to Sneakers, Nike, Jordan.
Anyone can point me in the right direction?
Thank you so much,
Sergio
UPDATE - 10/11/16
The Query:
$q =
" SELECT PC.productid as productid, concat_WS('~',C1.category, C2.category, C3.category) as breadcrumb
FROM xcart_categories as C1
INNER JOIN xcart_products_categories as PC
ON C1.categoryid = PC.categoryid
LEFT JOIN xcart_categories as C2
ON C1.categoryid = C2.parentid
AND C1.parentid = 0
LEFT JOIN xcart_categories as C3
ON C2.categoryid = C3.parentid
WHERE C1.parentid = 0
";
The Fetch:
$result = $conn->query($q);
if ($result->num_rows > 0) {
while($prods = $result->fetch_assoc()) {
echo $prods['productid'] . ' Belongs in these categories: ' . $prods['breadcrumb'] . '<br>';
}
} else {
echo "0 results";
}
This assumes 3 levels of hierarchy no more and a separate join is needed to "put each record on the same line" so they can be combined into a single value result. I thin you were trying to use Group_concat but I can't see how that's going to work as you don't have a way to walk the hierarchy.
SELECT PC.ProductID, concat_WS('-',C1.Category, C2.Category, C3.Category) as breadcrumb
FROM categories C1
INNER JOIN ProductsCategories PC
on C1.categoryID = PC.CategoryID
LEFT JOIN categories C2
on c1.CategoryID = C2.ParentID
and C1.parentID = 0
LEFT Join Categories C3
on C2.CategoryID = C3.ParentID
WHERE C1.ParentID = 0
Working SQL Fiddle example ( this only supports 3 levels deep, but could be altered with added left joins to support a max level but not a undetermined max level..)
I see you're trying to use group concat to bring all the rows for the same product category.productID of 0 to the same line
However as 0 references catID of 1 it would only return "sneakers" on the inner join. You would need to traverse the tree (all of it) somehow, thus the above, or you have to take multiple trips to the db or use some sort of dynamic SQL or method mentioned in link in comments.
This would be fairly simple in SQL Server, Oracle or other Enterprise RDBMS systems, however without recursive queries or engine specific hierarchy queries, this is no easy feat in MySQL on a single trip.
Maybe I'm missing something so it may help to see the actual expected results for your sample data. What is the record set look like that you want back?
I have 2 tables - products and productimages.
product has unique id and title.
productimages has productid and imageurl. these are examples of my tables:
products:
|id|title |
_____________
|1 |Laptop |
|2 |Speakers |
productimages:
|productid|imageurl|
___________________
| 1 |lap1.png|
| 1 |lap2.png|
| 1 |lap3.png|
| 2 |spe1.png|
Right now I have a nested loop in PHP.
loop through all rows of -> select * from products
and for every product inside the loop -> select * from productimages where productid = id which is basically another loop inside the first loop.
and then I take all productimages into array and decode to JSON [title,photos].
Now imagine you have 2 million rows in productimages, the query times are too high, is there any way to make it more efficient?
$query = "SELECT * FROM products ORDER BY id LIMIT 10;
$result = mysqli_query($con,$query);
if(mysqli_num_rows($result)>0)
{
$response = array();
while($row = mysqli_fetch_assoc($result)) {
$photos = array();
$id = $row["id"];
$title = $row["title"];
$queryp = "select imageurl from productimages where productid= '".$id."';";
$resultp = mysqli_query($con,$queryp);
if(mysqli_num_rows($resultp)>0)
{
while($row2 = mysqli_fetch_assoc($resultp)) {
$photourl = $row2["imageurl"];
array_push($photos,$photourl);
}
}
}
}
Some betterment for you could be:
1) Don't use select *. Use column names instead. e.g. select products.id, products.title, productimages.imageurl
2) Use JOIN instead of nested loop
So, you can try querying data like:
select products.id, products.title, productimages.imageurl
from products
join productimages on products.id = productimages.productid
ORDER BY products.id LIMIT 10
This case is not uncommon - you have two tables in a one to many relationship.
You should never nest an SQL call in a loop if you can possibly avoid it but there is a decision to be made about one SQL call or two.
A single SQL call could be:
SELECT id, title, imageURL
FROM products LEFT JOIN productImages ON id=productid
The disadvantage of this is that you are extracting the title several times for each product and this is wasteful.
Using two SQL statements you can download the titles once for each product:
SELECT id, title FROM products
The results of this query can be stored in an associative array - so that you can look up the title for each id.
The second query is:
SELECT productid, imageURL FROM productImages ORDER BY productid, imageURL
You can loop through the results of this query, spitting out the title as you go.
To save the images with product you can add a column imageurl in the products table.collect the image names with , and insert that image name string to the products table.
your table looks like below.
+--------------+--------------+---------------------------+
| id | title | imageurl |
+--------------+--------------+---------------------------+
| 1 | Laptop | lap1.png,lap2.png,lap3.png|
+--------------+--------------+---------------------------+
| 2 | Speakers | spe1.png |
Hope you understood what i explain.
In my MySQL-Database, I have two tables, one for the projects and one for the categories:
The projects-table looks something like this:
id | project_title | category_id
---|----------------------|------------
1 | My Book Project | 101
2 | My Comic Project | 102
3 | My Magazine Project | 104
Then I have the categories. These can have a parent category which is stored in the same table:
id | category_title | parent_id
---|--------------------|----------
101 | Books | 0
102 | Comics | 101
103 | DVDs | 0
104 | Magazines | 101
I like to fetch all the projects with the according category and (optional) sub-category (if parent_id is provided). If the category has no sub-category, the sub-category should be something like "-" or "none".
Now I know how I can get all these values with several statements:
First Statement: Fetch the projects with the indicated category (which can be a main-category or a sub category, therefor I fetch also the category's parent_id):
SELECT
p.project_title,
c.category_title,
c.parent_id as cat_parent_id
FROM
projects p,
categories c
WHERE
p.category_id = c.id
Second Statement(s): After that, I could fetch the possible sub-category of a project within a loop, or assign the found category as main-category, e.g. with php:
<?php
foreach( $rows as $project ) {
if ( $project['cat_parent_id'] > 0 ) {
$project['sub_category'] = $project['category_title'];
// query main-category here
}
else {
$project['main_category'] = $project['category_title'];
$project['sub_category'] = 'none';
}
// do things with this project ...
}
?>
The problem is that I will have another query for each project found, and this is not nice. I think there should be a way to fetch all the required values in one statement. I found this question on SO which is nearly the same, but in my case, the sub-category is obtional.
As John Cleese would say: Could someone please give me a push?
You can join categories twice:
SELECT
p.project_title,
c.category_title,
COALESCE(c0.category_title, '-')
FROM projects p join categories c on p.category_id = c.id
left join categories c0 on c.parent_id = c0.id;
But it will work only for two levels hierarchy (any category may have zero or one parent).
Accoding to the comments:
SELECT
p.project_title,
COALESCE(c0.category_title, c.category_title),
case when c0.id is not null then c.category_title else '-' end
FROM projects p join categories c on p.category_id = c.id
left join categories c0 on c.parent_id = c0.id;
SELECT
p.project_title,
IF(c2.category_title is null,
c1.category_title,
c2.category_title) as main_category,
IF(c2.category_title is null,
"none",
c1.category_title) as sub_category
FROM projects p
LEFT JOIN categories c1 ON (c1.id = p.category_id)
LEFT JOIN categories c2 ON (c2.id = c1.parent_id)
http://sqlfiddle.com/#!2/85020/1
This returns the requested result:
PROJECT_TITLE MAIN_CATEGORY SUB_CATEGORY
My Book Project Books none
My Comic Project Books Comics
My Magazine Project Books Magazines
I have 2 tables:
categories (id, categoryName),
menu (id, menuname, category_id)
I would like to display all categories, which have one or more records in the menu.
And after every categoryName to show 5 menuname.
Is it possibe, to do this in the one recordset?
Thank you!
These are my 2 recordsets:
$query = "select a.id, a.name from categories as a where a.id in (select count(*) from menu as b on b.category_id = a.id)";
$result = mysql_query($query);
while ($row = mysql_fetch_array($result)) {
echo $row['name'];
$category_id = intval($row['id']);
$query = "select menuname from menu where category_id = $category_id limit 0, 5";
$resultmenu = mysql_query($query);
while ($rowmenu = mysql_fetch_array($resultmenu)) {
echo $rowmenu['menuname'];
}
}
As mentioned above, i'm not sure what is meant by "And after every categoryName to show 5 menuname".
But to show a list of all category/menu names in alphabetical order you could use the following:
SELECT C.categoryName,
M.menuname
FROM categories C
INNER JOIN menu M ON M.category_id = C.id
ORDER BY C.categoryName,
M.menuname
Update:
At the moment your first query will only be returning at best one row. The subquery is currently counting the number of menu rows and then this figure is being used to pull a row from the category table, which isn't what you want.
The following query joins onto the menu table to ensure that at least one item exists, and then groups by the category fields to ensure that each item is only returned once:
$query = "select a.id, a.name from categories as a inner join menu as b on b.category_id = a.id group by a.id, a.name"
Update 2
Ah sorry, I understand now. No I don't think it's possible to achieve what you want in a single query. Even if it were possible I wouldn't recommend it. Looking at your code, you only want to print the categoryName once for each set of menu items. If you were able to pull back the categoryName and menuname items in one result set like so:
| categoryName | menuname |
---------------------------
| category1 | menu1 |
| category1 | menu2 |
| category1 | menu3 |
| category2 | menu4 |
When iterating through the results you would need to manually check when the categoryName had changed in order to print it out once for that set of menuname items.