MySQL sort images by most viewed - php

I have an images table which stores an image id, title, the user who uploads it, a timestamp and file extension.
I also have a views table which records when an image has been viewed. the columns are id, image id, timestamp and the user who viewed the image.
What i need and have no idea how to do is to run an SQL query which will return a set of images in order of the most viewed.
I've been looking around on here and Google but just don't know what to actually search.

SELECT images.*, count(views.view_id) view_count
FROM images
LEFT JOIN views on views.image_id = images.image_id
GROUP BY images.image_id
ORDER BY view_count DESC
http://sqlfiddle.com/#!2/a669b/5
The GROUP BY clause tells the "aggregate functions" (count, sum, min, max, etc) how to behave. In this case, we are grouping by image and grabbing the count of views per image. COUNT only count non-null values, and we don't want to count the views that don't exist, so we're keying off views.view_id.
If you do an INNER JOIN rather than a LEFT JOIN, it will only return the images which have views. The LEFT JOIN allows us to get counts of zero.

You need to be looking at joins, join the images table to the images viewed table, then you can run one query across both tables.

Related

How to get 1 row per user with INNER JOIN

I have two tables one is user and another is images. I gave users the option to select multiple images. I can store multiple images with same user_id in database, but when I am trying to get one image from every user I am getting all the images.
My query is something like this:
$query = "
SELECT *
FROM images i
JOIN users u
ON u.user_id = i.user_id
LIMIT 1";
When I run this query in while() loop, I only get very first image from images table.
I am really sorry if I am not able to clarify what I am try to ask.
Have you tried something like this:
SELECT * FROM users u INNER JOIN images i ON u.user_id = i.user_id GROUP BY u.user_id;
This should return you only one record from user/image tables for each user that has an image.
Don't run queries in a while loop. Instead, use one query to get all the desired records.
If you insist on running your query in a loop, then you are missing WHERE users.user_id = ? part in your query, so you can get a different result for each user in a loop.
you can do this without using join. simple select user and fetch data and on the bases of 'id' add query to get image. i hope this will help you;
Your current query:-
SELECT *
FROM images i
JOIN users u
ON u.user_id = i.user_id
LIMIT 1
uses LIMIT 1. This tells the query to bring back 1 row.
Removing the LIMIT 1 will return 1 or more records per user (who has at least 1 image), one for each image.
If you want a single user then it is possible (although not recommended) to (ab)use the GROUP BY clause:-
SELECT *
FROM images i
JOIN users u
ON u.user_id = i.user_id
GROUP BY u.user_id
This would bring back one record per user, but which image it returns is not defined. It could bring back the first image for that user, or the last one, or any other one (and which one it returns could change in the future). Further, there is no actual reason it couldn't return values from different rows for each of the columns on the images table (unlikely, but nothing specified to stop this happening).
Note that basic SQL standards specify (with a small exception) that any non aggregate field brought back in a SELECT statement must be in the GROUP BY clause. MySQL used to not enforce this restriction by default, but recently this changed and it is enforced by default. As such by default this query would no longer work as it is returning all the fields from the images and users tables while only specifying the user_id from the users table in the GROUP BY clause.
What you should do is define which image you want for each user. Assuming the first image (and that the images table uses an auto increment primary key called id):-
SELECT u.*,
i.*
FROM users u
LEFT OUTER JOIN
(
SELECT user_id
MIN(id) AS first_image_id
FROM images
GROUP BY user_id
) sub0
ON u.user_id = sub0.user_id
LEFT OUTER JOIN images i
ON sub0.user_id = i.user_id AND sub0.first_image_id = i.id
This uses a sub query to get the first image id for each user. Then that is joined to the images table to get the other details for that image from the images table.
Note I have used LEFT OUTER JOIN. This is to return a row for a user who doesn't have any images uploaded.
Note it is generally a bad idea to use SELECT *, rather than specifying the columns to be returned. I have left this in place here as I do not know the column names of your tables.

Showing users who liked an item in an item list

This is an issue that I've deemed impractical to implement but I would like to get some feedback to confirm.
I have a product and users database, where users can like products, the like data is stored in a reference table with just pid and uid.
The client request is to show 3 users who have liked every product in the product listing.
The problem is, its not possible to get this data in one query for the product listing,
How I once implemented and subsequently un-implemented it was to perform a request for the users who have liked the products during the loop through the product list.
ie.
foreach($prods as $row):
$likers = $this->model->get_likers($row->id);
endforeach;
That works, but obviously results in not only super slow product listings, and also creates a big strain on the database/cpu.
The final solution that was implemented was to only show the latest user who has liked it (this can be gotten from a join in the products list query) and have a link showing how many people have liked, and upon clicking on it, opens a ajax list of likers.
So my question is, is there actually a technique to show likers on the product list, or is it simply not possible to execute practically? I notice actually for most social media sites, they do not show all likers on the listings, and do employ the 'click to see likers' method. However, they do show comments per items on the listing, and this is actually involves the same problem doesn't it?
Edit: mock up attached on the desired outcome. there would be 30 products per page.
By reading your comment reply to Alex.Ritna ,yes you can get the x no. of results with per group ,using GROUP_CONCAT() and the SUBSTRING_INDEX() it will show the likers seperated by comma or whatever separator you specified in the query (i have used ||).ORDER BY clause can be used in group_concat function.As there is no schema information is available so i assume you have one product table one user table and a junction table that maintains the relation of user and product.In the substring function i have used x=3
SELECT p.*,
COUNT(*) total_likes,
SUBSTRING_INDEX(
GROUP_CONCAT( CONCAT(u.firstname,' ',u.lastname) ORDER BY some_column DESC SEPARATOR '||'),
'||',3) x_no_of_likers
FROM product p
LEFT JOIN junction_table jt ON(p.id=jt.product_id)
INNER JOIN users u ON(u.id=jt.user_id)
GROUP BY p.id
Fiddle
Now at your application level you just have to loop through the products and split the x_no_of_likers by separator you the likers per product
foreach($prods as $row):
$likers=explode('||',$row['x_no_of_likers']);
$total_likes= $row['total_likes'];
foreach($likers as $user):
....
endforeach;
endforeach;
Note there is a default 1024 character limit set on GROUP_CONCAT() but you can also increase it by following the GROUP_CONCAT() manual
Edit from comments This is another way how to get n results per group, from this you can get all the fields from your user table i have used some variables to get the rank for product group ,used subquery for junction_table to get the rank and in outer select i have filtered records with this rank using HAVING jt.user_rank <=3 so it will give three users records per product ,i have also used subquery for products (SELECT * FROM product LIMIT 30 ) so the first 30 groups will have 3 results for each,for below query limit cannot be used at the end so i have used in the subquery
SELECT p.id,p.title,u.firstname,u.lastname,u.thumbnail,jt.user_rank
FROM
(SELECT * FROM `product` LIMIT 30 ) p
LEFT JOIN
( SELECT j.*,
#current_rank:= CASE WHEN #current_rank = product_id THEN #user_rank:=#user_rank +1 ELSE #user_rank:=1 END user_rank,
#current_rank:=product_id
FROM `junction_table` j ,
(SELECT #user_rank:=0,#current_rank:=0) r
ORDER BY product_id
) jt ON(jt.product_id = p.id)
LEFT JOIN `users` u ON (jt.`user_id` = u.`id`)
HAVING jt.user_rank <=3
ORDER BY p.id
Fiddle n results per group
You should be able to get a list of all users that have liked all products with this sql.
select uid,
count(pid) as liked_products
from product_user
group by uid
having liked_products = (select count(1) from products);
But as data grows this query gets slow. Better then to maintain a table with like counts that is maintained through a trigger or separately. On every like/dislike the counter is updated. This makes it easy to show the number of likes for each product. Then if the actual users that liked that product is wanted do a separate call (on user interaction) that fetches the specific likes for one product). Don't do this for all products on a page until actually requested.
I am assuming the size of both these tables is non-trivially large. You should create a new table (say LastThreeLikes), where the columns would be pid,uid_1,uid_2 and uid_3, indexed by pid. Also, add a column to your product table called numLikes.
For each "like" that you enter into your reference table, create a trigger that also populates this LastThreeLikes table if the numLikes is less than 3. You can choose to randomly update one of the values anyway if you want to show new users once in a while.
While displaying a product, simply fetch the uids from this table and display them back.
Note that you also need to maintain a trigger for the "Unlike" action (if there is any) to re-populate the LastThreeLikes table with a new user id.
Problem
The problem is the volume of data. From the point of view that you need two integer value as a answer you should forget about building a heavy query from your n<->n relations table.
Solution
Generates a storable representation using the file_put_contents() with append option each time a user likes a product. I don't have enough room to write the class in here.
public function export($file);
3D array format
array[product][line][user]
Example:
$likes[1293][1][456]=1;
$likes[82][2][656]=1;
$likes[65][3][456]=1;
.
.
.
Number of users who like this particular product:
$number_users_like_this_product = count($likes[$idProduct]);
All idUser who like this particular product:
$users_like_this_product = count($likes[$idProduct][$n]);
All likes
$all_likes = count($likes);
Deleting a like
This loop will unset the only line where $idProduct and $IdUser you want. Since all the variables are unsigned integer it is very fast.
for($n=1, $n <= count($likes[$idProduct]), $n++)
{
unset($likes[$idProduct][$n][$idUser]);
}
Conclusion
Get all likes will be easy as:
include('likes.php');
P.S If you want to give a try i will be glad to optimize my stuff and share it. I've created the class in 2012.

Include NULL as 0 in COUNT SQL Query

I know for a fact this has been asked a few times before, but none of the answered questions relating to this seem to work or are far too confusing for me..
I should probably explain.
I'm trying to create an AJAX script to run to order some results by the number of 'Likes' it has.
My current code is this:
SELECT COUNT(*) AS total, likes.palette_id, palette.*
FROM likes LEFT JOIN palette ON likes.palette_id = palette.palette_id
GROUP BY likes.palette_id
ORDER BY total DESC
Which works fine, however it doesn't list the results with 0 likes for obvious reasons, they don't exist in the table.
I've attached images of the current tables:
Likes table:
http://imgur.com/EGeR3On
Palette table:
http://imgur.com/fKZmSve
There are no results in the likes table until the user clicks 'Like'. It is then that the database gets updated and the palette_id and user_id are inserted.
I'm trying to count how many times *palette_id* occurs in the likes table but also display 0 for all palettes that don't appear in the likes table.
Is this possible? If so, can someone help me out at all?
Thank you
It might not be the exact MySQL syntax (I'm used to SQL Server), but should be pretty straight forward to translate if needed.
SELECT p.*, IFNULL(l.total, 0) AS total
FROM palette p
LEFT JOIN (
SELECT palette_id, COUNT(*) AS total
FROM likes
GROUP BY palette_id
) l
ON l.palette_id = p.palette_id
ORDER BY total
Try this:
SELECT COUNT(likes.palette_id) AS total, palette.palette_id, palette.*
FROM palette LEFT JOIN likes ON likes.palette_id = palette.palette_id
GROUP BY palette.palette_id
ORDER BY total DESC
EDIT:
In regards to the discussion about listing columns that are not in the GROUP BY, there's a good explanation in this MySql documentation page.
MySQL extends the use of GROUP BY so that the select list can refer
to nonaggregated columns not named in the GROUP BY clause. This means
that the preceding query is legal in MySQL. You can use this feature
to get better performance by avoiding unnecessary column sorting and
grouping. However, this is useful primarily when all values in each
nonaggregated column not named in the GROUP BY are the same for each
group. The server is free to choose any value from each group, so
unless they are the same, the values chosen are indeterminate.
In this example, the palette information not added to the GROUP BY will be the same for each group because we are grouping by palette_id so there won't be any issue using palette.*
Your join is written backwards. It should be palette LEFT JOIN likes, because you want all rows in palette and rows in likes, if they exist. The "all rows in palette" will get you a palette_id for the entries there without any matching "likes."

With out using DISTINCT / GROUP BY how to fetch unique row of data from mysql5 table?

I have one image table and image view status table.
In each click of image I am tracking image view time. status. date time.. So in image view status table have more than one entry for one image.
I need to check whether one user already viewed one image or not.
So I write one query like this..
select A.title, if(B.view_status='completed','completed','notviewed') from images A left join imgstatus_view B on (A.imgId=B.ImgId and A.fileId=B.fileID) Where userId=1 and imgId=121
If I view one image three times its fetching 3 records. I need only one record to check whether user viewed the image or not.
So with out using DISTINCT or GROUP BY How can I fetch unique row of data.
Please help me.
you can use ORDER BY and LIMIT clause. to get most recent record you can use below query
select A.title, if(B.view_status='completed','completed','notviewed') from images A left join imgstatus_view B on (A.imgId=B.ImgId and A.fileId=B.fileID) Where userId=1 and imgId=121 ORDER BY datetimefield DESC LIMIT 0,1
to get old record
select A.title, if(B.view_status='completed','completed','notviewed') from images A left join imgstatus_view B on (A.imgId=B.ImgId and A.fileId=B.fileID) Where userId=1 and imgId=121 ORDER BY datetimefield LIMIT 0,1
if you dont have datetime field remove ORDER BY clause which will give most recent modified record

how to display table records who has one record when another table who is joined has many records

this site is like an ecommerce site and i have two tables that i'm stuck on.
tbl_products
---------------
phash
product
price
desc
maxcount
tbl_product_images
-------------------
phash
thumb
large
tbl_products stores the products description
tbl_product_images stores the path of the thumb and large image of eac product.
phash is an md5 of the products name its what i use rather than the id or product name when matching things together.
what i'm having throuble with is say tbl_products has one record for a product and tbl_product_images
in relation to that product has 5 rows for its images.
how would i run the query?
$sql = "select
tbl_products.phash
tbl_products.product
tbl_products.price
tbl_products.desc
tbl_products.maxcount
tbl_product_images.phash
tbl_product_images.thumb
tbl_product_images.large
from tbl_products
inner join tbl_product_images
on tbl_products.phash = tbl_products_images.phash";
this will display 5 rows of records since tbl_product_images has 5 records.
the way i output is my traditional way
$query = $db->query("$sql");
while($row = $db->fetch($query))
{
....
}
i am really unsure how i'm going to do this, if you need more explanation to what i'm trying to explain please let me know. thanks
The simplest way is to limit the number of results coming from the images table - though that's only appropriate if you don't care which image is returned for each product:
select
tbl_products.phash
tbl_products.product
tbl_products.price
tbl_products.desc
tbl_products.maxcount
tbl_product_images.phash
tbl_product_images.thumb
tbl_product_images.large
from tbl_products
inner join (SELECT * FROM tbl_product_images LIMIT 1) images
on tbl_products.phash = images.phash
You can add an ORDER BY [col] DESC to that LIMITed query if there's some other column in the images table (like a date) you could use to determine which image appears.
(Note: If there were some column in tbl_product_images that determined which image to return, you could either put that in a WHERE clause on the query, or include it in the join criteria, like: ON tbl_products.phash = tbl_product_images.phash AND tbl_product_images.color = 'Blue')
Further, if you are trying to get a product with all of its images, but only want one row in the resultset, it may be possible to "pivot" the set of images into columns. However, this is tricky if you can have an arbitrary (or simply large) number of images per product. You may be better off just returning multiple rows per product, and handling the results in your application code...

Categories