Getting more results from left jointed query - php

I have 2 tables named collections and posts. I need to display the collection name and 3 posts from each collection collected by users. Some of the collection has less than 3 posts and some have no posts at all. Also I need to count the posts (Not total number of posts but the posts produce by the query)
MySQL Tables
Collections
| collection_id | collection_name | uid |
| 1 | My collection 01 | 1 |
| 2 | My collection 02 | 1 |
| 3 | My collection 03 | 1 |
| 4 | My collection 04 | 2 |
| 5 | My collection 05 | 2 |
| 6 | My collection 06 | 1 |
Posts
| posts_id | post_title | cid |
| 1 | post title 1 | 1 |
| 2 | post title 2 | 1 |
| 3 | post title 3 | 1 |
| 4 | post title 4 | 3 |
| 5 | post title 5 | 2 |
| 6 | post title 6 | 3 |
cid is the collection id. So what I want to and uid is the user id. I want the results to be display
3 posts from My collection 01
post title 1
post title 2
post title 3
1 posts from My collection 02
post title 5
2 posts from My collection 02
post title 4
post title 6
Just made the example according to the dummy data I added in the table above.
I tried with left join with no luck
SELECT * FROM collections LEFT JOIN posts ON posts.cid= collections. collection_id WHERE posts ON posts.cid= collections. collection_id AND collections. uid=1 ORDER BY collections. collection_id DESC LIMIT 0, 16
With this query I can get the collection name and 1 post.
But if i run two queries it will work (1 inside the other)
SELECT * FROM collections WHERE uid=1 ORDER BY collection_id DESC LIMIT 0, 16
Then I get the collection id and run another query inside while loop of above query
SELECT * FROM posts WHERE cid=$collection_id ORDER BY post_id DESC LIMIT 0, 3
I really love to do it with a single query. Your help is greatly appreciated.

There is no easy way to do that. Maybe with very complex query, but it will be difficult to maintain and may be even less efficient than doing that with several simpler queries.
The solution described by you costs 1 + (number of categories) queries, not two, of course. You could union them easily, and then you would have two queries and less trips do database, but similar load for db (comparing to your solution).
Even if you would assume, that there is a way to fetch everything with single query, then db has to do almost the same work (fetch 3 newest posts from every category). So having 2 queries vs 1 hypothetical is not a big penalty in terms of performance. Moreover, I can imagine that DB engine could have some issues with finding most optimal execution plan, especially if you would add there functions etc.
And the last solution. There is a way for fetching up to 3 posts from each category, but that require modifying schema and some application-side work. You can add a boolean column "most_recent" and have always 3 posts per cat. with true and false for the rest. You would have to keep updating it every time when you are adding/deleting posts. That is achievable as well with db triggers. Then your problem is trivial to resolve, but only because you have done some precomputation.

Related

Can SELECT, SELECT COUNT and cross reference tables be handled by just one query?

I have a page that displays a list of projects. With each project is displayed the following data retrieved from a mysqli database:
Title
Subtitle
Description
Part number (1 of x)
The total number of photos associated with that project
A randomly selected photo from the project
A list of tags
Projects are displayed 6 per page using a pagination system
As this is based on an old project of mine, it was originally done with sloppy code (I was just learning and did not know any better) using many queries. Three, in fact, just for items 5-7, and those were contained within a while loop that worked with the pagination system. I'm now quite aware that this is not even close to being the right way to do business.
I am familiar with INNER JOIN and the use of subqueries, but I'm concerned that I may not be able to get all of this data using just one select query for the following reasons:
Items 1-4 are easy enough with a basic SELECT query, BUT...
Item 5 needs a SELECT COUNT AND...
Item 6 needs a basic SELECT query with an ORDER by RAND LIMIT 1 to
select one random photo out of all those associated with each project
(using FilesystemIterator is out of the question, because the photos
table has a column indicating 0 if a photo is inactive and 1 if it is
active)
Item 7 is selected from a cross reference table for the tags and
projects and a table containing the tag ID and names
Given that, I'm not certain if all this can (r even should for that matter) be done with just one query or if it will need more than one query. I have read repeatedly how it is worth a swat on the nose with a newspaper to nest one or more queries inside a while loop. I've even read that multiple queries is, in general, a bad idea.
So I'm stuck. I realize this is likely to sound too general, but I don't have any code that works, just the old code that uses 4 queries to do the job, 3 of which are nested in a while loop.
Database structure below.
Projects table:
+-------------+---------+----------+---------------+------+
| project_id | title | subtitle | description | part |
|---------------------------------------------------------|
| 1 | Chevy | Engine | Modify | 1 |
| 2 | Ford | Trans | Rebuild | 1 |
| 3 | Mopar | Diff | Swap | 1 |
+-------------+---------+----------+---------------+------+
Photos table:
+----------+------------+--------+
| photo_id | project_id | active |
|--------------------------------|
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 1 |
| 4 | 2 | 1 |
| 5 | 2 | 1 |
| 6 | 2 | 1 |
| 7 | 3 | 1 |
| 8 | 3 | 1 |
| 9 | 3 | 1 |
+----------+------------+--------+
Tags table:
+--------+------------------+
| tag_id | tag |
|---------------------------|
| 1 | classic |
| 2 | new car |
| 3 | truck |
| 4 | performance |
| 5 | easy |
| 6 | difficult |
| 7 | hard |
| 8 | oem |
| 9 | aftermarket |
+--------+------------------+
Tag/Project cross-reference table:
+------------+-----------+
| project_id | tag_id |
|------------------------|
| 1 | 1 |
| 1 | 3 |
| 1 | 4 |
| 2 | 2 |
| 2 | 5 |
| 3 | 6 |
| 3 | 9 |
+------------+-----------+
I'm not asking for the code to be written for me, but if what I'm asking makes sense, I'd sincerely appreciate a shove in the right direction. Often times I struggle with both the PHP and MySQLi manuals online, so if there's any way to break this down, then fantastic.
Thank you all so much.
You're able to do subqueries inside your SELECT clause, like this:
SELECT
p.title, p.subtitle, p.description, p.part,
(SELECT COUNT(photo_id) FROM Photos where project_id = p.project_id) as total_photos,
(SELECT photo_id FROM Photos where project_id = p.project_id ORDER BY RAND LIMIT 1) as random_photo
FROM projects as p
Now, for the list of tags, as it returns more than one row, you can't do a subquery and you should do one query for every project. Well, in fact you can if you return all the tags in some kind of concatenation, like a comma separated list: tag1,tag2,tag3... but I don't recommend this one time that you will need to explode the column value. Do it only if you have many many projects and the performance to retrieve the list of tags for each individual project is fairly low. If you really want, you can:
SELECT
p.title, p.subtitle, p.description, p.part,
(SELECT COUNT(photo_id) FROM Photos where project_id = p.project_id) as total_photos,
(SELECT photo_id FROM Photos where project_id = p.project_id ORDER BY RAND LIMIT 1) as random_photo,
(SELECT GROUP_CONCAT(tag SEPARATOR ', ') FROM tags WHERE tag_id in (SELECT tag_id FROM tagproject WHERE project_id = p.project_id)) as tags
FROM projects as p
As you said from item 1 to 4 you already have the solution.
Add to the same query a SQL_CALC_FOUND_ROWS instead of a SELECT COUNT to solve the item 5.
For the item 6 you can use a subquery or maybe a LEFT JOIN limiting to one result.
For the latest item you can also use a subquery joining all the tags in a single result (separated by comma for instance).

Mysql Limit rows by field value

I'm working on simple application using PHP and MySQL. Up to this point we needed to display items from database in HTML table. Simple pagination was implemented as well. It looks something like this:
+----+---------------------+
| 1 | Item 1 |
+----+---------------------+
| 2 | Item 2 |
+----+---------------------+
| 3 | Item 3 |
+----+---------------------+
| 4 | Item 4 |
+----+---------------------+
....
+----+---------------------+
| 5 | Item 25 |
+----+---------------------+
Not a rocket science. Now we add new functionality so we can (optionally) group items - We really create a 'lot' of identical items. We decided to add new column in database called groupID - which can be number or NULL for items not contained in any group. On web page we must display it as one element which expands when you click on it.
+----+---------------------+
| 1 | Item 1 |
+----+---------------------+
| 2 | Item 2 |
+----+---------------------+
| 3 | Item 3 |
+----+---------------------+
| 4 | Group 1 (Expanded) |
+----+---------------------+
| Group 1 Item 1 |
+---------------------+
| Group 1 Item 2 |
+---------------------+
| Group 1 Item 3 |
+----+---------------------+
....
+----+---------------------+
| 25| Item 25 |
+----+---------------------+
As you can see Number of items on one page may vary so we must treat items in group as one item, so simple 'limit 25' not working anymore. I wonder if I can make some clever mysql query which will work this way. I rather want to avoid to create new table in database which consists groups and relation to item table, because most of the groups will have only 1 Item. I don't believe this functionality will be used a lot, but You know - client. Also this system works on production for some time so I'd rather avoid such changes. Any Idea how to make it work? Also please keep it simple as possible, because this example is simplified. Real query is already bit complicated.
I also want avoid parsing it via PHP code, because it's just dumb to query all few thousands of rows and then discard all but 25-50 elements.
As you said some group ids can be null, I thought we should fix that.
When null, we use the item id to for the group and we use a prefix to make sure our new group_id is unique.
This is my solution using subqueries (not pretty, but seems to work):
SELECT
i1.id,
i1.itemgroup1
FROM (
SELECT
items.id,
IF(ISNULL(items.group_id),
CONCAT('alone-', items.id),
CONCAT('group-', items.group_id)) as itemgroup1
FROM
items
) as i1
RIGHT JOIN (
SELECT
items.id,
IF(ISNULL(items.group_id),
CONCAT('alone-', items.id),
CONCAT('group-', items.group_id)) as itemgroup2
FROM
items
GROUP BY itemgroup2
LIMIT 2
) as i2 on i2.itemgroup2 = i1.itemgroup1
** UPDATE **
Removed
WHERE
items.group_id IS NOT NULL

how can i pin a new in my site?

in my php news script
i use this code for show title of news :
<?php
$sql = mysql_query("SELECT newsid,title FROM news WHERE cat='1' ORDER BY newsid DESC LIMIT 15");
?>
and show 10 news at database order by send date
but i want pin 3 news to show first :
for example:
newstitle3 (pined)
newstitle4 (pined)
newstitle6 (pined)
newstitle1
newstitle2
newstitle5
newstitle7
....
you see 3,4 and 6 are pinned news then show unpinned news
how can i add code in my database query for this news display?
Lets think one step ahead -
Taking into consideration that there might be other areas on your site that require this pinned items behavior - I recomend a slightly more complex yet robust solution.
A more generic way of settings "pinned" items would be to create a new "pinned_items" table in your database. You mention only news items but perhaps there are other areas in your site that will also (perhaps in the future) require this "pinned" behavior.
You could have a pinned_items table with this structure -
+----+----------+--------+
| id | obj_type | obj_id |
+----+----------+--------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 4 |
| 5 | 2 | 5 |
| 6 | 2 | 6 |
+----+----------+--------+
The obj_type would reference the type of the item (lets consider type 1 to be a news item) and the obj_id would be a reference to your news table.
So to retrieve all the "pinned" news items, you would do a query similar to this -
SELECT newsItem.* FROM news AS newsItem
LEFT JOIN `pinned_items` AS pinnedItem ON pinnedItem.obj_id = newsItem.id
WHERE pinnedItem.obj_type = 1
An easy way would be to add a 'pinned' field to your table, and then order by pinned, newsid.

Mapping products with similar names

I am trying to find a good solution to accomplish the following:
I have a table which includes the name of various products, such as:
"Tide - Original Scent".
In addition, I also have the amount in there for e.g. 50 fl oz.
The problem I have right now is, that the product not only comes in containers of 50 fl oz but also in different sizes such as 75 fl oz and 100 fl oz. For each of these I have new rows in the product table:
id| productName | amount | unit
1 |"Tide - Original Scent" | 50 | "fl oz"
2 |"Tide - Original Scent" | 75 | "fl oz"
3 |"Tide - Original Scent" | 100 | "fl oz"
Now I have a web interface to perform a search on this table and whenever I search for "tide" I get all three rows - which is supposed to be like that of course. However I would like a different behavior and this is where I need your help:
Instead of returning all three rows I would like one row only. I would then need to be able to process it in php so that if the user clicks on "Tide - Original Scent" that the user is then prompted to select the size.
To add even more complexity to the task:
I also have products in the same table named:
"Tide - Mountain Spring".
In this case, it would be great to have some relations set up so I know that "Tide - Original Scent" is linked with "Tide - Mountain Spring". Within php I would then like to not only give the user the choice of selecting the size but also the (in this case) scent.
What would your recommendation be on how I can accomplish this (not the php part)?
How would your database look like?
Do I need to create a table where I map these products? How would this look like if you would create this :)
2 possibilities:
Don't store the sizes in that table - along with the other specific information. Move that to another table. Denormalize your structure.
or
Query but group by the name. For the size column, do a count(amount). If it's more than one, you can then populate a drop down with choices. This is good temporary fix.
SELECT productName, count(amount) AS numOfChoices FROM YOUR_TABLE
WHERE LOWER(productName) LIKE 'tide%'
GROUP BY productName
then after the choice is made
SELECT id, amount FROM YOUR_TABLE
WHERE id = "$selectedId"
to present a choice of sizes that will pin point which one.
I would personally setup my tables like this..
Products Table:
ID| Product ID | Product Name | Description
1 | 0404888282 | Tide - Original Scent | Smells Good
Quantity Table:
ID| Product ID | Size| Price | Quantity
1 | 0404888282 | 50 | 4.99 | 23
2 | 0404888282 | 75 | 5.99 | 120
3 | 0404888282 | 100 | 7.99 | 10
This structure you have a table for each unique item, another for the sizes and quantity of each size. Keeps the structure clean and easy to understand.
"Instead of returning all three rows I would like one row only."
SELECT DISTINCT productName FROM YOUR_TABLE WHERE LOWER(productName) LIKE 'tide%'
And you'll need a functional index on LOWER(productName) for good performance. Alternatively a case-insensitive collation sequence could be used on DBMSes that support that (e.g. MS SQL Server).
"I would then need to be able to process it in php so that if the user clicks on "Tide - Original Scent" that the user is then prompted to select the size."
SELECT amount FROM YOUR_TABLE WHERE productName = whatever_user_selected
"To add even more complexity to the task: I also have products in the same table named:
"Tide - Mountain Spring"."
The query above will also return that.
What you can do is :
$Query = 'SELECT productName, amount, unit FROM products';
$Data = array();
while($Assoc = mysql_fetch_assoc($Query)){
if(!isset($Data[$Assoc['productName']])){
$Data[$Assoc['productName']] = array();
}
$Size = $Assoc['amount'].' '$Assoc['unit'];
$Data[$Assoc['productName']][] = $Size;
}
// Now what you can do is :
foreach($Data as $ProductName => $Amount){
echo $ProductName.' has :<br />';
if(count($Amount) > 0){
foreach($Amount as $Key => $Value){
echo $Value.'<br />';
}
} else {
echo 'Nothing<br />';
}
}
This however doesn't solve the problem on MySQL's side. IT will work in PHP wihtout problem. It's not beautiful but it's working.
For the first problem, you could create two tables:
products - this is where you store all the information about a product except for the specifics such as different sizes, colors, etc.
attributes - you would link to the product and for each attribute you specify a value
products
id | description
---+------------
1 | crazy shirt
2 | clown shoe
attributes
product | name | value
--------+-------+-------
1 | color | green
1 | color | blue
1 | size | medium
2 | size | large
You can optimize the attributes table further by creating a attribute_names table (and even an attribute_values table).
For your second problem, you could either:
create a related product id column inside the products table; this would limit you to only one related product per product.
create a related product table in which you store combinations between two products.
related_products
product_id | related_product_id
-----------+-------------------
1 | 2
1 | 3
That would create a relationship between product 1 and products 2 and 3.
Hope this helps.
Create a foreign key to sublabels, and order them with a counter. This will look like.
Product Table:
Product ID (key)
Brand ID
Price
Brand Table:
Brand ID (key)
Brand
Sublable Table:
ID
Product ID
Order Index
Value
Size Table:
Size ID
Value
Unit
ProductSize Table:
Size ID
Product ID
Then, you'll divide into subcategories using subsequent sublabels.
Products
10 | 6 | 1.99
11 | 6 | 2.99
12 | 6 | 3.99
13 | 6 | 1.99
14 | 6 | 2.99
15 | 6 | 3.99
Brand
6 | Tide
Sublabel
30 | 10 | 1 | Original Scent
30 | 11 | 1 | Original Scent
30 | 12 | 1 | Original Scent
30 | 13 | 1 | Mountain Spring
30 | 14 | 1 | Mountain Spring
30 | 15 | 1 | Mountain Spring
Size Table
1 | 50 | fl.oz.
2 | 75 | fl.oz.
3 | 100 | fl.oz.
Product Size Table
1 | 10
1 | 13
2 | 11
2 | 14
3 | 12
3 | 15

Sub categories Hierarchy

I designed a SQL structure to represent categories and their subcategories.
I have 3 tables:
articles
articles_categories
categories
Articles table:
id,title,content
Categories table:
id, title, parent_id
articles_categories:
id,article_id,category_id
No problem with SQL, but now - lets say i'm on article id 5
article id 5 has - 3 categories, that 2 of them has parents, and the main has '0' as parent.
How do I fetch them all efficiently? (lets say for - breadcrumbs).
thanks!
Unless the depth of the category hierarchy is fixed, you cannot do this in MySQL with your current model (adjacency list). You'd have to traverse the hierarchy using several SQL statements in a loop.
If the category hierarchy is fairly static, you can "precompute" the tree by using:
Path enumeration
Nested sets
Closure table
All of the above, trades write performance for read performance.
Google or search SO for any of the above and you will find examples of how to implement it.
Quite often, I find that storing the data in a adjacency list (because of best matches the data model) and caching a copy of the tree in the application is good enough, but that depends on your requirements of course :)
This should do the job:
select * from articles_categories
left join categories on categories.id = articles_categories.category_id
where article_id=1;
+------+------------+-------------+------+--------+-----------+
| id | article_id | category_id | id | title | parent_id |
+------+------------+-------------+------+--------+-----------+
| NULL | 1 | 1 | 1 | first | 0 |
| NULL | 1 | 2 | 2 | second | 1 |
| NULL | 1 | 3 | 3 | third | 2 |
+------+------------+-------------+------+--------+-----------+
Additionally, I would remove the "id" column from associative table articles_categories.

Categories