I need help with a query involving a review system set up with the following two tables.
reviews
-------
id date user_id item_id rating review
1 02-2012 40 456 3 'I like it'
2 03-2012 22 342 1 'I don't like it'
3 04-2012 45 548 0 'I hate it'
reviews_thumbs
--------------
review_id user_id like
1 22 1
1 45 -1
2 40 -1
3 22 1
The "reviews_thumbs" table exists to keep track of upvotes and downvotes for the reviews, so that reviews can be rated by quality. In the 'like' column, a 1 is an upvote and a -1 is a downvote. (The rating column in the reviews table is a star system, unrelated.)
When loading reviews, I need to join the reviews_thumbs table in such a way that I know the following details (for each individual review as they are returned):
1. The total number of upvotes
2. The total number of downvotes
3. Whether the current active user has upvoted or downvoted the review
I have accomplished this using the following query, which isn't sitting right with me:
SELECT `reviews`.*,
COUNT(upVoteTable.`user_id`) AS upVotes,
COUNT(downVoteTable.`user_id`) AS downVotes,
COUNT(userUpTable.`user_id`) AS userUp,
COUNT(userDownTable.`user_id`) as userDown
FROM `reviews`
LEFT JOIN `reviews_thumbs` AS upVoteTable
ON upVoteTable.`review_id` = `reviews`.`id`
AND upVoteTable.`like` = 1
LEFT JOIN `reviews_thumbs` AS downVoteTable
ON downVoteTable.`review_id` = `reviews`.`id`
AND downVoteTable.`like` = -1
LEFT JOIN `reviews_thumbs` AS userUpTable
ON userUpTable.`review_id` = `reviews`.`id`
AND userUpTable.`like` = 1
AND userUpTable.`user_id` = :userid
LEFT JOIN `reviews_thumbs` AS userDownTable
ON userDownTable.`review_id` = `reviews`.`id`
AND userDownTable.`like` = -1
AND userDownTable.`user_id` = :userid
WHERE `item_id`=:itemid
GROUP BY `reviews`.`id`
ORDER BY `date` DESC
(And binding the appropriate :userid and :itemid.)
So this query works perfectly and accomplishes what I need it to. But that is a lot of joining, and I'm almost positive there must be a better way to do this, but I can't seem to figure anything out.
Could someone please point me in the right direction on how to accomplish this in a cleaner way?
What I've Tried:
I've tried doing a GROUP_CONCAT, to list a string that contains all the user ids and likes, and to then run a regex to find the user's id to see if they've voted on the review, but this also feels really unclean.
Thank you in advance for any help you may provide.
You could modify reviews_thumbs to look more like this:
reviews_thumbs
--------------
review_id user_id upvote downvote
1 22 1 0
1 45 0 1
2 40 0 1
3 22 1 0
This would effectively store duplicate information, but that's okay when you have a good purpose. You really have 2 things you want to know, and this gives you a quick sum on 2 columns (and a quick subtraction on those results) to get exactly what you are looking for. This cuts you down to querying the reviews_thumbs table to 2x, once for the totals, and once for the users specific action.
Not sure if that improves your query performance, but you could try to do the counting in a sub-select
SELECT `reviews`.*,
(SELECT count(*) FROM reviews_thumbs t WHERE t.review_id =reviews.id AND t.like = 1) AS upVotes
...
FROM reviews
...
Related
I'm building a small messaging system for my app, the primary idea is to have 2 tables.
Table1 messages
Id,sender_id,title,body,file,parent_id
Here is where messages are stored, decoupled from whom will receive it to allow for multiple recipients.
Parent I'd link to parent message if its a reply, and file is a blob to store single file attached to message
Table 2 message_users
Id,thread_id,user_id,is_read,stared,deleted
Link parent thread to target users,
Now for a single user to get count of unread messages I can do
Select count(*) from message_users where user_id = 1 and is_read is null
To get count of all messages in his inbox I can do
Select count(*) from message_users where user_id = 1;
Question is how to combine both in single optimized query ?
So you're trying to achieve something that will total rows that meet one condition and the total rows that meet an extra condition:
|---------|---------|
| total | unread |
|---------|---------|
| 20 | 12 |
|---------|---------|
As such will need something with a form along the lines of:
SELECT A total, B unread FROM message_users WHERE user_id=1
A is fairly straightforward, you already more-or-less have it: COUNT(Id).
B is marginally more complicated and might take the form SUM( IF(is_read IS NULL,1,0) ) -- add 1 each time is_read is not null; the condition will depend on your database specifics.
Or B might look like: COUNT(CASE WHEN is_read IS NULL THEN 1 END) unread -- this is saying 'when is_read is null, count another 1'; the condition will depend on your database specifics.
In total:
SELECT COUNT(Id) total, COUNT(CASE WHEN is_read IS NULL THEN 1 END) unread FROM message_users WHERE user_id=1
Or:
SELECT COUNT(Id) total, SUM( IF(is_read IS NULL,1,0) ) unread FROM message_users WHERE user_id=1
In terms of optimised, I'm not aware of a query that can necessarily go quicker than this. (Would love to know of it if it does exist!) There may be ways to speed things up if you have a problem with performance:
Examine your indexes: use the built in tools EXPLAIN and some reading around etc.
Use caches and/or pre-compute the value and store it elsewhere -- e.g. have a field unread_messages against user and grab this value directly. Obviously there will need to be some on-write invalidation, or indeed some service running to keep these values up to date. There are many ways of achieving this, tools in MySQL, hand roll your own etc etc.
In short, start optimising from a requirement and some real data. (My query takes 0.8s, I need the results in 0.1s and they need to be consistent 100% of the time -- how can I achieve this?) Then you can tweak and experiment with the SQL, hardware that the server runs on (maybe?), caching/pre-calculate at different points etc.
In MySQL, when you count a field, it only counts non null occurrences of that field, so you should be able to do something like this:
SELECT COUNT(user_id), COUNT(user_id) - COUNT(is_read) AS unread
FROM message_users
WHERE user_id = 1;
Untested, but it should point you in the right direction.
You can use sum with CASE WHEN clause. If is_read is null then +1 is added to the sum, else +0.
SELECT count(*),
SUM(CASE WHEN is_read IS NULL THEN 1 ELSE 0 END) AS count_unread
FROM message_users WHERE user_id = 1;
Im having a brain fart as to how I would do this.
I need to select only the latest entry in a group of same id entries
I have records in an appointment table.
lead_id app_id
4 42
3 43
1 44
2 45
2 46 (want this one)
1 48
3 49 (this one)
4 50 (this one)
1 51 (this one)
The results I require are app_id 46,49,50,51
Only the latest entries in the appointment table, based on duplicate lead_id identifiers.
Here is the query you're looking for:
SELECT A.lead_id
,MAX(A.app_id) AS [last_app_id]
FROM appointment A
GROUP BY A.lead_id
If you want to have every columns corresponding to these expected rows:
SELECT A.*
FROM appointment A
INNER JOIN (SELECT A2.lead_id
,MAX(A2.app_id) AS [last_app_id]
FROM appointment A2
GROUP BY A2.lead_id) M ON M.lead_id = A.lead_id
AND M.last_app_id = A.app_id
ORDER BY A.lead_id
Here i simply use the previous query for a jointure in order to get only the desired rows.
Hope this will help you.
The accepted answer by George Garchagudashvili is not a good answer, because it has group by with unaggregated columns in the select. Select * with group by is simply something that should not be allowed in SQL -- and it isn't in almost all databases. Happily, the default version of the more recent versions of MySQL also rejects this syntax.
An efficient solution is:
select a.*
from appointment a
where a.app_id = (select max(a2.app_id)
from appointment a2
where a2.lead_id = a.lead_id
);
With an index on appointment(lead_id, app_id), this should be as fast or faster than George's query.
I think this is much more optimal and efficient way of doing it (sorting next grouping):
SELECT * FROM (
SELECT * FROM appointment
ORDER BY lead_id, app_id DESC
) AS ord
GROUP BY lead_id
this will be useful when you need all other fields too from the table without complicated queries
Result:
lead_id app_id
1 51
2 46
3 49
4 50
I'm trying to build a really simple online functionality system. One of the queries that this system must handle is the returning of a scoreboard (all of the players scores, ordered and ranked). I know basic sql queries, but i'm completely lost when it comes to sub queries and variables within queries etc.
The table only has three columns. game, user_id, score. The table will let people upload and download scores for any game. I need to work out how to create a query that returns only the users from the game being queried, orders the players by score, then ranks them so duplicate scores will have the same rank. Here's a brief example of the desired outcome:
TABLE
user game score
fred A 100
bill A 78
john A 78
dave B 71
terry B 60
jean B 60
tom A 60
nick A 57
DESIRED OUTPUT
user score rank
fred 100 1
bill 78 2
john 78 2
tom 60 4
nick 57 5
CURRENT OUTPUT ** TAKES INTO ACCOUNT THE GAMES IT SHOULD IGNORE
user score rank
fred 100 1
bill 78 2
john 78 2
tom 60 5
nick 57 8
This is currently the query that works the best:
SELECT #rank:=#rank+1 AS ranking, user_id, score
FROM score_table , (SELECT #rank:=1) AS i
WHERE game='A'
ORDER BY score DESC
But the rank seems to take into account other games, which ruins the rankings. Other queries i've found have ranked correctly but not eliminated the other games (again, taking the other games scores into account when ranking.
Again, the above is an example I tweaked, as I have no idea how to use the # variables, sub queries etc.
Many thanks,
Dan.
To show the same rank for same score you can use case and additional variable for checking same rank
SELECT ranking,user,score FROM
(SELECT
#rank:=case when score =#s then #rank else #rank+1 end AS ranking,
#s:=score,
user, score,game
FROM tablename , (SELECT #rank:=0,#s:=0) AS i
ORDER BY score DESC
) new_alias
WHERE game='A'
Demo
Edit from comments
Updated Demo
I am developing a small gaming website for college fest where users attend few contests and based on their ranks in result table, points are updated in their user table. Then the result table is truncated for the next event. The schemas are as follows:
user
-------------------------------------------------------------
user_id | name | college | points |
-------------------------------------------------------------
result
---------------------------
user_id | score
---------------------------
Now, the first 3 students are given 100 points, next 15 given 50 points and others are given 10 points each.
Now, I am having problem in developing queries because I don't know how many users will attempt the contest, so I have to append that many ? in the query. Secondly, I also need to put ) at the end.
My queries are like
$query_top3=update user set points =points+100 where id in(?,?,?);
$query_next5=update user set points = points +50 where id in(?,?,?,?,?);
$query_others=update user set points=points+50 where id in (?,?...........,?);
How can I prepare those queries dynamically? Or, is there any better approach?
EDIT
Though its similar to this question,but in my scenario I have 3 different dynamic queries.
If I understand correctly your requirements you can rank results and update users table (adding points) all in one query
UPDATE users u JOIN
(
SELECT user_id,
(
SELECT 1 + COUNT(*)
FROM result
WHERE score >= r.score
AND user_id <> r.user_id
) rank
FROM result r
) q
ON u.user_id = q.user_id
SET points = points +
CASE
WHEN q.rank BETWEEN 1 AND 3 THEN 100
WHEN q.rank BETWEEN 4 AND 18 THEN 50
ELSE 10
END;
It totally dynamic based on the contents in of result table. You no longer need to deal with each user_id individually.
Here is SQLFiddle demo
I have two tables as follows:
I have a RatingsTable that contains a ratingname and a bit whether it is a positive or negative rating:
RatingsTable
----------------------
ratingname ispositive
----------------------
Good 1
Bad 0
Fun 1
Boring 0
And I have a FeedbackTable that contains feedback on things: the person rating, the rating and the thing rated. The feedback can be determined if it's a positive or negative rating based on the RatingsTable.
FeedbackTable
---------------------------------
username thing ratingname
---------------------------------
Jim Chicken Good
Jim Steak Bad
Ted Waterskiing Fun
Ted Hiking Fun
Nancy Hiking Boring
I am trying to write an efficient MySQL query for the following:
On a page, I want to display the the top 'things' that have the highest proportion of positive ratings. I want to be sure that the items from the feedback table are unique...meaning, that if Jim has rated Chicken Good 20 times...it should only be counted once. At some point I will want to require a minimum number of ratings (at least 10) to be counted for this page as well. I'll want to to do the same for highest proportional negative ratings, but I am sure I can tweak the one for positive accordingly.
To get the "things" in order of proportion of good ratings you can use this query:
SELECT thing, SUM(ispositive) / COUNT(*) AS proportion_positive
FROM (SELECT DISTINCT username, thing, ratingname FROM FeedbackTable) T1
JOIN RatingsTable T2
ON T1.ratingname = T2.ratingname
GROUP BY thing
ORDER BY proportion_positive DESC
For your example data it returns this:
thing proportion_positive
Chicken 1.0000
Waterskiing 1.0000
Hiking 0.5000
Steak 0.0000
To require at least 10 votes before displaying a thing in the results add this line after the GROUP BY:
HAVING COUNT(*) >= 10
To get the proportion of negative ratings change SUM(ispositive) to SUM(NOT ispositive).
Note: it might be better to add a unique constraint to your voting table instead of selecting only the disctinct values.
SELECT *
FROM `feedback`
LEFT JOIN `ratings` ON `feedback`.`rating` = `rating`.`label`
ORDER BY `rating`.`value` DESC
GROUP BY `feedback`.`username`
LIMIT 10
The summary: join the ratings to your feedback table, but group by the username so you only get one username per result.