Let's imagine I have a databases with two tables, Users and Posts. The first table contains a row for each user, the second table a row for each post that users have written. If I want to display a post count on the users' profiles, which of these two strategies work the best:
Every time a user creates a post I UPDATE the Users table, +1 a field PostCount;
When someone visits the profile I simply run a select statement to get a count of post, for example SELECT COUNT(post_id) FROM Posts WHERE id_user = 100;
In the first case I have to UPDATE a table very often, which it could be bad as I believe a table gets locked when doing the update; in the second case I have to run a count every time the user visits a profile. Which poison is the less bitter? Is there any other way?
I would say that it depends on how many times you will display PostCount, especially for a huge amount of Users. If you are going to display it for 1000+ users on a page that will be called a lot of times, then the first solution should be the best. But you need to do transactions to be sure both tables Posts and Users are updated when adding a new post.
Otherwise, the second solution should be enough, but you should use LEFT OUTER JOIN so that you would get both information from Users and Posts table in only one query. Eg:
SELECT *
FROM Users u
LEFT OUTER JOIN (
SELECT user_id , COUNT(*) AS posts_count
FROM Posts
GROUP BY user_id
) p ON p.user_id = u.id
WHERE u.id = :searched_id
(And anyway you should use a Cache system so that you don't have to do the same SQL query for a same page if shown to several users.)
Related
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.
I'm having some problems retrieving data from two tables and then listing them. I'd like to list the user's feed posts and their likes activity all in one.
Feeds - Table for users posts
Likes - Table for users likes (So when a use likes a post, a record is added to likes (Table likes contains data which contains the feeds ID of the post liked)
What I'm attempting to make: List BOTH feeds and user's Like activity in an ACTIVITY WALL.
So it should output like (ordered by timestamp desc):
"THIS IS A POST by user A"
Shows that user C liked user B's post
"THIS IS A POST by user B"
"THIS IS A POST by user L"
Shows that user A liked user F's post
"THIS IS A POST by user F"
-and it goes on-
My current SQL:
SELECT * FROM feeds,likes WHERE feeds.deleted!=0 or likes.deleted!=0 ORDER BY feeds.timestamp, likes.timestamp
However, my problem is I have no idea how to link both tables, since the IDs in my 'feeds' differ from those in 'likes'
To combine the two sets, you can use a UNION ALL set operator.
Something like this:
SELECT f.timestamp AS `timestamp`
, 'feed' AS `src`
, f.feed_id AS `id`
, f.feed_content AS `content`
FROM feeds f
WHERE f.deleted!=0
UNION ALL
SELECT l.timestamp AS `timestamp`
, 'like' AS `src`
, l.like_id AS `id`
, l.note AS `content`
FROM likes l
WHERE l.deleted!=0
ORDER BY 1 DESC
Note the the queries (on either side of the UNION ALL operator) need to match, in terms of the number of columns returned, and the datatype of each column.
To accommodate differences, such as extra columns returned from one table, but not from the other, you can add literal expressions in place of the "missing" columns.
The return of the extra src column is one way we can use to distinguish which query a row was returned by. It's not mandatory to return such a column, but it's something I often find useful. (The src column could be removed from each query, if it's not useful for your use case.)
Note that it's also possible to combine the results from more than two queries in this way, we'd just add another UNION ALL and another query.
The column names in the combined resultset are determined from the first query. The column names and aliases in the second query are ignored.
The ORDER BY applies to the entire set, and follows the last select.
Query should be linked via postID
F=feeds table, L=likes table, U1=usertable linked to owned feeds, U2=usertable linked to likes table
SELECT F.postTitle+' posted by '+ U1.username,'liked by'+U2.username
FROM likes L
LEFT JOIN feeds F on (F.postID=L.postID)
LEFT JOIN users U1 on (U1.userID=F.userID)
LEFT JOIN users U2 on (U2.userID=L.userID)
ORDER BY L.date,L.postID DESC
When you write SELECT * FROM feeds,likes... you are implicitly CROSS JOINing both tables. The engine will combine every record in each table with every record in the other. That is far from what you want.
I don't think you should be "linking" both tables, either. What you need, roughly speaking, is to get every post and every like, and then order that big set according to timestamps.
It sounds more like a UNION between two queries, and an ORDER BY applied to the whole UNION. UNIONs are never easy on the eye, by the way...
The thing with UNIONs is that both sub-queries need to return the same amount of columns. Not knowing exactly which columns you have, I'll show you one possible solution:
SELECT activity, timestamp FROM (
( SELECT CONCAT(u.name,' posted ',f.content) as activity, timestamp
FROM user u
JOIN feed f on (f.user_id=u.id)
WHERE f.deleted!=0
) UNION
( SELECT CONCAT(u.name, ' liked a post by ',u2.name) as activity, timestamp
FROM user u
JOIN likes l on (l.user_id=u.id)
JOIN feed f on (l.feed_id=f.id)
JOIN user u2 on (f.user_id=u2.id)
WHERE l.deleted!=0
)
) as whole_result
ORDER by timestamp desc
You should, of course, modify this to match your structure.
Hope this helps!
I think, it's better to use 3rd table, say, "actions", and insert to it real actions. Then just select rows from this table, joined to "posts" & "users" table.
When user posts articles, o likes an article, insert corresponding row to "actions" table.
actions table:
|id|action_name|user_id|post_id| date |
1 posted 3 3 5/7/2014
2 liked 5 3 5/7/2014
3 liked 4 3 6/7/2014
4 posted 5 6 7/7/2014
5 liked 3 6 7/7/2014
SELECT user_name a, post_title b, action_name c FROM actions c LEFT JOIN users a ON a.id=c.user_id LEFT JOIN posts b ON b.id = c.post_id ORDER BY c.date DESC LIMIT 10
Then, in loop, choose how to display this data, according to "action_name".
In such way you can expand your wall for other activities, +use indexes for better database performance.
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.
I'm building a site that requires sharing with either group(s) or individual user(s). I know for a fact that google does not use mysql, but i was wondering how i could replicate such feature on my site. On g+, one can:
Share a post with the "public" (everyone can see it).
Share a post with "all circles" (everyone in your circles can see it).
Share a post with both circles and individual users. E.g. post = "my first post" and is shared with family,friends, user 1(Joey tribbiani), user 2 (Ross geller) etc.
Conditions:
If a post is shared with a circle and a new user is added to the circle, then (s)he should be able to see all the previous posts shared with that circle.
If a user is removed from a circle. (s)he cannot see posts shared with that circle except posts (s)he has commented on.
Currently my database tables look like this.
Circle_category
Cat_id
Cat_name
user_id
Posts
post_id
user_id
post
is_public
all_circle
Post_to_circle
entry_id
post_id
cat_id
Post_to_user
entry_id
post_id
user_id
Post a user in family circle(which is in Circle_category with cat_id of 1 ) can see
They can see posts that are public.
They can see posts shared with all circles.
They can see posts shared with family circle.
They can see posts shared with them (Individual user).
SQL
SELECT p.* FROM posts p
JOIN Post_to_circle pc
ON p.post_id = pc.post_id
JOIN Post_to_user pu
ON p.post_id = pu.post_id
WHERE p.is_public = 1
OR all_circle = 1
OR pc.cat_id = $cat_id
OR pu.user_id = $user_id
Quetions:
Firstly, I've been able to get posts from case 1(see all public post), case 2 (Posts shared with all circles) but the other 2 cases do not work. I thought about it and saw that the main problem is that i specified the where clause to get posts where p.is_public = 1 which means it neglets rows where p.is_public = 0. How do i update the query so it shows posts covering all four cases and also covers the conditions we talked about at the beginning.
Secondly, is there a better way to structure my tables? i'm not sure i'm doing it the right way.
From a quick read trough, all i can say is:
you are using a join statement instead of a left join statement.
using join means:
keep all rows from the table used in from-clause that validate true for the condition specified in that join clause.
since you are using 2 statements, the first join throws away all the records that dont have the needed join, the second join throws away all the records that dont have the needed join in the second one, but it only uses records that matched the first join.
you should use left join instead. this keeps all rows from the first table. all rows that didnt have a match, get the values NULL for the columns specified in the joined table(s)
simple example:
users table:
user_id
name
user_posts
post_id
user_id
content
created
related queries:
select *
from users u
JOIN user_posts up on up.user_id = u.user_id and up.created > date_sub(curdate(), interval 1 day)
this will use all users and make match with each post that was created less then a day ago by that user.
if the user didnt have a post in the last day, he will NOT be in the resultset.
change this example to
select *
from users u
LEFT JOIN user_posts up on up.user_id = u.user_id and up.created > date_sub(curdate(), interval 1 day)
this will use all users and make a match with each post that was created less then a day ago by that user
if the user hasn't posted in the last day, he will STILL be in the resultset, but all the columns from the posts table will be NULL
the where filters all the rows you have left after the joins. (mysql will use where clauses before joining, if they can speed up the query).
altering your query:
make sure the clauses in where statement are wrapped between () for all the different cases. ALSO this is NOT the complete answer, as there is info missing (example user tables, circle relation tables, friend relations)
also the all_circles option confuses me, so it's missing from the query, but this should get you on the right track
SELECT p.* FROM posts p
left JOIN Post_to_circle pc
ON p.post_id = pc.post_id and /* define statement for valid circles for user you're trying to get the posts for */
left JOIN Post_to_user pu
ON p.post_id = pu.post_id and /* define statement for valid friends for user you're trying to get the posts for */
WHERE
/* 1 day old */
p.created > date_sub(curdate(), interval 1 day)
AND (
/* is public */
p.is_public = 1 OR
/* or to friends */
pu.id is not null OR
/* or to circles */
pc.id is not null
)
Also, i'm suspecting you'll need 2 subqueries, which is not the best thing to do, and my advise would be to find all correct ids for the friends, and all ids for the valid circles and then using an IN clause in each join statement (part thats in comment)
Let me set up the situation first.
I have a "users" table with X fields, the fields dont really matter for my question except for "visibility". Visibility is a tinyint and the values mean the following (0 = visible to all, 1 = visible to friends only, and 2 = invisible).
I also have a friends table (id, user_id, target_user_id). user_id is friends with target_user_id. Easy enough so far right?
Here is where it gets sticky. Im writing a PHP API and my class method looks kinda like this:
public function getUsers($requester, $page, $num) {}
the $requester is the user id of the person requesting the users
the $page is the pagination page number
the $num is the number of items per page
What I want to do in SQL is get $num users from the users table if their visibility field is = 0 or 1. If the visibility flag is 1 however, I need to make sure the user id and the $requester are friends in the friends table and only return that user if they are friends.
I thought about using PHP to filter the visibility after I get my results back but the pagination (limit) will be screwed up if I ask for 5 records for example and one or more user has visibility set to 1 and are not friends with the requester. This pretty much has to be done entirely thru sql.
Possible??
Try creating a temp table 'temp' with same structure as users.
select * into temp From users where visibility=0 or visibility=1;
Select * from temp, friends where (temp.visibility=0) or (temp.user_id = friends.target_user_id);
Don't forget to empty the temp table.
I haven't tried the second query yest, let me know what output you got.
select * from users, friends where (users.visibility=0) or (users.visibility=1 and users.user_id = friends.target_user_id);
I think you can use LEFT JOIN for this.. something like
"SELECT *
FROM users t1
LEFT JOIN friends t2 ON t2.user_id=t1.id AND t1.visibility=1
INNER JOIN user t3 ON t3.id=t2.target_user_id
WHERE t1.visibility=0 OR t1.visibility=1
LIMIT ".($page*$num).",".$num