Limit results on an GROUP_CONCAT() or INNER JOIN - mysql

I've perused extensively the other threads talking about limits on group_concat() and inner joins but haven't found my answer, so I guess I'll go ahead and ask it:
I'm developing an existing photo community site. I want to retrieve members who have their birthday on a given day (today) and then retrieve each member's 5 most highly rated photos. But I also only want the 10 "most favorite" birthday members (ie with the highest favorite count). Here's what I have:
SELECT users.user_id, users.user_name,
GROUP_CONCAT(CONVERT(photos.photo_id,char(32))
ORDER BY photos.average_rate) as photo_ids
FROM users
INNER JOIN photos ON photos.user_id=users.user_id
WHERE users.day_of_birth = DATE_FORMAT('2012-04-17', '%m-%d')
AND users.photo_count>0
GROUP BY users.user_id
ORDER BY users.favorite_count DESC, users.photo_count DESC LIMIT 0,10
This does what I want, EXCEPT that I cannot limit the amount of photo_ids to 5. This is a problem since the output will be sent as JSON to the app, and some members have uploaded upwards of 20,000 photos already, leading to an unacceptably long output string. The only "solution" that seems to work for me is setting the sever variable group_concat_max_len to something reasonable that will hold at least 5 ids, but this is very hacky and unreliable. Is there any way to return exactly 5 photo_ids per user with a single query? Or will I need to do a loop in my PHP?
I don't necessarily need the photo_ids in a comma-separated value, I can also ditch the group_concat() approach entirely and do an inner join if that is more feasible. But even there I'm not aware of a way to limit the results to 5.

These advanced ones are what makes me love MySQL :)
SELECT user_id, user_name,
GROUP_CONCAT(CONVERT(photo_id, char(32)) ORDER BY photos.average_rate) as photo_ids
FROM ( SELECT user_id, user_name, photo_id, favorite_count, photo_count,
(case when #user_id = user_id then #rownum := #rownum + 1 else CONCAT(#rownum := 1, #user_id := user_id) end) AS dummy_val
FROM ( SELECT users.user_id, users.user_name, users.favorite_count, users.photo_count, photos.photo_id
FROM users
INNER JOIN photos
ON photos.user_id=users.user_id
WHERE users.day_of_birth = DATE_FORMAT('2012-04-17', '%m-%d')
AND users.photo_count > 0
ORDER BY users.id ASC, photos.average_rate ASC
) AS h,
( #rownum := NULL,
#user_id := NULL
) AS vars
HAVING rownum <= 5) AS h2
GROUP BY user_id
ORDER BY favorite_count DESC, photo_count DESC LIMIT 0, 10
Basicly I get all rows, and throw away all photos which are 6 or higher in calculated rownum.

SELECT u.user_id
, u.user_name
, GROUP_CONCAT(p.photo_id ORDER BY p.average_rate) AS photo_ids
FROM
( SELECT user_id
, user_name
, favorite_count
, photo_count
FROM users
WHERE day_of_birth = DATE_FORMAT('2012-04-17', '%m-%d')
AND photo_count > 0
ORDER BY favorite_count DESC
, photo_count DESC
LIMIT 10
) AS u
INNER JOIN
photos AS p
ON p.user_id = u10.user_id
AND p.average_rate >=
( SELECT pp.average_rate
FROM photos AS pp
WHERE pp.user_id = u10.user_id
ORDER BY pp.average_rate DESC
LIMIT 1 OFFSET 4
)
GROUP BY u.user_id
ORDER BY u.favorite_count DESC
, u.photo_count DESC

Related

mysql - join table row based on id with the highest column value in the table - multiple joins and conditions

I have searched on SO prior to asking and have tried things I found - I have more involved due to multiple joins and conditions and cannot get the correct results.
SQL Fiddle here with basic data entered.
The query below does not give the results I want, but gives an idea of what I am looking to achieve. I want to return 1 result per computer_id where time.capture_timestamp is between a specific start/end value and is the highest value in the table for that computer_id including that row's other column values. I have tried a few different things I found here on SO involving MAX() and subqueries, but can't seem to get what I am looking for.
SELECT
computers.computer_name,
users.username,
time.title,
time.capture_timestamp
FROM computers
INNER JOIN time
ON time.computer_id = computers.computer_id AND time.capture_timestamp >= 0 AND time.capture_timestamp <= 9999999999
INNER JOIN users
ON users.user_id = time.user_id
GROUP BY computers.computer_id
ORDER BY time.capture_timestamp DESC
The fiddle as is will return :
computer_name username title capture_timestamp
computer1 user1 some title 1595524341
computer2 user3 some title3 1595524331
while the result I would like is actually :
computer_name username title capture_timestamp
computer1 user2 some title2 1595524351
computer2 user3 some title3 1595524331
... based on the example values in the fiddle. Yes, the start/end time values include 'everything' in this example, but in use there would actually be a timestamp range provided.
Using ROW_NUMBER:
WITH cte AS (
SELECT c.computer_name, u.username, t.title, t.capture_timestamp,
ROW_NUMBER() OVER (PARTITION BY c.computer_id
ORDER BY t.capture_timestamp DESC) rn
FROM computers c
INNER JOIN time t ON t.computer_id = c.computer_id
INNER JOIN users u ON u.user_id = t.user_id
WHERE t.capture_timestamp BETWEEN 0 AND 9999999999
)
SELECT computer_name, username, title, capture_timestamp
FROM cte
WHERE rn = 1;
You can add a correlated sub query to get the desired results
select
computers.computer_name,
users.username,
t.title,
t.capture_timestamp
from computers
inner join time t
on t.computer_id = computers.computer_id
and t.capture_timestamp >= 0 and t.capture_timestamp <= 9999999999
inner join users
on users.user_id = t.user_id
where t.capture_timestamp =(
select max(capture_timestamp)
from time
where capture_timestamp >= 0 and capture_timestamp <= 9999999999
and t.computer_id = computer_id
)
order by t.capture_timestamp desc
DEMO

Select last date and COUNT rows in a JOIN jquery?

I want to select the last inserted date and at the same time I want to select the user-name and count how many times the user-profile is visited.
So I am using this query
SELECT v.visitor_date, i.info_name, count(DISTINCT v.visitor_date) AS counted
FROM profile_visitors v
INNER JOIN profile_info i ON i.info_userId = v.visitor_accountId
ORDER BY v.visitor_date DESC
LIMIT 1
The result of the fiddle is wrong and SHOULD be
2015-07-28 11:05:16 - Testname - 5
Anyone knows what is wrong with the query?
http://sqlfiddle.com/#!9/2814c/1
DISTINCT does NOT give you the first or last record of any group, in fact you cannot guarantee which record DISTINCT will display within a group (nor does this matter by the way). So select MAX visitor date.
Try below query
SELECT MAX( v.visitor_date ) , i.info_name, COUNT( DISTINCT v.visitor_date ) AS counted FROM profile_visitors v INNER JOIN profile_info i ON i.info_userId = v.visitor_accountId ORDER BY v.visitor_date DESC LIMIT 1
You can try it:
SELECT v.visitor_date,
i.info_name,
COUNT(*) AS counted
FROM profile_visitors v
INNER JOIN profile_info i ON i.info_userId = v.visitor_accountId
GROUP BY v.visitor_accountId
ORDER BY v.visitor_date DESC
LIMIT 1

better query to get user monthly rank?

I have two tables:
user_score_post -> fields: id, post_id, user_id, score_date -> with about 3m rows
post -> field: id, user_id, body -> with about 10k rows...
and this is my query for retrieving monthly rank of a user with dedicated user_id and score according to the number of likes that his posts have:
SELECT COUNT(*) FROM
(SELECT COUNT(l.id) AS likes
FROM user_score_post l
JOIN post p ON p.id = l.post_id
AND score_date >= :time // last month
GROUP BY p.user_id) AS score
WHERE score.likes > :score // user current score
but it takes 2.4 seconds to execute. Is it normal despite using proper indexes and a powerful dedicated server?
what is the best alternative for this query? and what is the best indexing composition?
It would seem the following returns the score for each user. Perhaps also score_date should use bewteen month start and end.
select p.user_id, count(*) from user_post_score l
join post p on (p.user_id=l.user_id)
where score_date > :time
group by p.user_id
Sometimes in MySQL it is faster to use a correlated subquery rather than a join with aggregation. You might try:
SELECT COUNT(*)
FROM (SELECT (SELECT COUNT(*) AS likes
FROM user_score_post l
WHERE p.id = l.post_id AND score_date >= :time
) as likes
FROM post p
) score
WHERE score.likes > :score // user current score
Then, the index that you want is user_score_post(post_id, score_date). This might help.

MySQL ORDER BY multiple column ASC and DESC

I have 2 MYSQL tables, users and scores. Detail:
users table:
scores table:
My intention is get 20 users list that have point field sort DESC (descending) combine avg_time field sort ASC (ascending). I use the query:
SELECT users.username, scores.point, scores.avg_time
FROM scores, users
WHERE scores.user_id = users.id
GROUP BY users.username
ORDER BY scores.point DESC, scores.avg_time
LIMIT 0, 20
The result is:
The result is wrong because the first line is exactly point = 100 and avg_time = 60.
My desired result is:
username point avg_time
demo123 100 60
demo123456 100 100
demo 90 120
I tried many times with different queries but the result is still wrong. Could you give me some solutions?
Ok, I THINK I understand what you want now, and let me clarify to confirm before the query. You want 1 record for each user. For each user, you want their BEST POINTS score record. Of the best points per user, you want the one with the best average time. Once you have all users "best" values, you want the final results sorted with best points first... Almost like ranking of a competition.
So now the query. If the above statement is accurate, you need to start with getting the best point/average time per person and assigning a "Rank" to that entry. This is easily done using MySQL # variables. Then, just include a HAVING clause to only keep those records ranked 1 for each person. Finally apply the order by of best points and shortest average time.
select
U.UserName,
PreSortedPerUser.Point,
PreSortedPerUser.Avg_Time,
#UserRank := if( #lastUserID = PreSortedPerUser.User_ID, #UserRank +1, 1 ) FinalRank,
#lastUserID := PreSortedPerUser.User_ID
from
( select
S.user_id,
S.point,
S.avg_time
from
Scores S
order by
S.user_id,
S.point DESC,
S.Avg_Time ) PreSortedPerUser
JOIN Users U
on PreSortedPerUser.user_ID = U.ID,
( select #lastUserID := 0,
#UserRank := 0 ) sqlvars
having
FinalRank = 1
order by
Point Desc,
Avg_Time
Results as handled by SQLFiddle
Note, due to the inline #variables needed to get the answer, there are the two extra columns at the end of each row. These are just "left-over" and can be ignored in any actual output presentation you are trying to do... OR, you can wrap the entire thing above one more level to just get the few columns you want like
select
PQ.UserName,
PQ.Point,
PQ.Avg_Time
from
( entire query above pasted here ) as PQ
i think u miss understand about table relation..
users : scores = 1 : *
just join is not a solution.
is this your intention?
SELECT users.username, avg(scores.point), avg(scores.avg_time)
FROM scores, users
WHERE scores.user_id = users.id
GROUP BY users.username
ORDER BY avg(scores.point) DESC, avg(scores.avg_time)
LIMIT 0, 20
(this query to get each users average point and average avg_time by desc point, asc )avg_time
if you want to get each scores ranking? use left outer join
SELECT users.username, scores.point, scores.avg_time
FROM scores left outer join users on scores.user_id = users.id
ORDER BY scores.point DESC, scores.avg_time
LIMIT 0, 20
#DRapp is a genius. I never understood how he coded his SQL,so I tried coding it in my own understanding.
SELECT
f.username,
f.point,
f.avg_time
FROM
(
SELECT
userscores.username,
userscores.point,
userscores.avg_time
FROM
(
SELECT
users.username,
scores.point,
scores.avg_time
FROM
scores
JOIN users
ON scores.user_id = users.id
ORDER BY scores.point DESC
) userscores
ORDER BY
point DESC,
avg_time
) f
GROUP BY f.username
ORDER BY point DESC
It yields the same result by using GROUP BY instead of the user #variables.
group by default order by pk id,so the result
username point avg_time
demo123 100 90 ---> id = 4
demo123456 100 100 ---> id = 7
demo 90 120 ---> id = 1

querying for user's ranking in one-to-many tables

I am trying to write a query to find the score rank of a user's games. I need it to take in a user id and then return that user's relative ranking to other user's scores. There is a user and a game table. The game table has a userId field with a one-to-many relationship.
Sample table:
users:
id freebee
1 10
2 13
games:
userId score
1 15
1 20
2 10
1 15
passing $id 1 into this function should return the value 1, as user 1 currently has the highest score. Likewise, user 2 would return 2.
Currently this is what I have:
SELECT outerU.id, (
SELECT COUNT( * )
FROM users userI, games gameI
WHERE userI.id = gameI.userId
AND userO.id = gameO.userId
AND (
userI.freebee + SUM(gameI.score)
) >= ( userO.freebee + SUM(gameO.score) )
) AS rank
FROM users userO,
games gameO
WHERE id = $id
Which is giving me an "invalid use of group function" error. Any ideas?
SELECT u.id,total_score,
( SELECT COUNT(*) FROM
(SELECT u1.id, (IFNULL(u1.freebee,0)+ IFNULL(SUM(score),0)) as total_score
FROM users u1
LEFT JOIN games g ON (g.userId = u1.id)
GROUP BY u1.id
)x1
WHERE x1.total_score > x.total_score
)+1 as rank,
( SELECT COUNT(DISTINCT total_score) FROM
(SELECT u1.id, (IFNULL(u1.freebee,0)+ IFNULL(SUM(score),0)) as total_score
FROM users u1
LEFT JOIN games g ON (g.userId_Id = u1.id)
GROUP BY u1.id
)x1
WHERE x1.total_score > x.total_score
)+1 as dns_rank
FROM users u
LEFT JOIN
( SELECT u1.id, (IFNULL(u1.freebee,0)+ IFNULL(SUM(score),0)) as total_score
FROM users u1
LEFT JOIN games g ON (g.userId = u1.id)
GROUP BY u1.id
)x ON (x.id = u.id)
rank - (normal rank - e.g. - 1,2,2,4,5), dns_rank - dense rank (1,2,2,3,4). Column total_score - just for debugging...
The query does not like the reference of an outer table in the Sum function SUM(gameO.score) in the correlated subquery. Second, stop using the comma format for joins. Instead use the ANSI syntax of JOIN. For example, in your outer query did you really mean to use a cross join? That is how you wrote and how I represented it in the solution below but I doubt that is what you want.
EDIT
I've adjusted my query given your new information.
Select U.id, U.freebee, GameRanks.Score, GameRanks.Rank
From users As U
Join (
Select G.userid, G.score
, (
Select Count(*)
From Games As G2
Where G2.userid = G.userid
And G2.Score > G.Score
) + 1 As Rank
From Games As G
) As GameRanks
On GameRanks.userid = U.id
Where U.id =1
I'm not a MySQL person, but I believe that the usual way to do ranking in it is using a variable within your SQL statement. Something like the below (untested):
SELECT
SQ.user_id,
#rank:=#rank + 1 AS rank
FROM
(
SELECT
U.user_id,
U.freebee + SUM(COALESCE(G.score, 0)) AS total_score
FROM
Users U
LEFT OUTER JOIN Games G ON
G.user_id = U.user_id
) SQ
ORDER BY
SQ.total_score DESC
You could use that as a subquery to get the rank for a single user, although performance-wise that might not be the best route.
Here is "simplified" version for calculating a rank based only on "games" table. For calculating rank for a specific game only you need to add additional joins.
SELECT COUNT(*) + 1 AS rank
FROM (SELECT userid,
SUM(score) AS total
FROM games
GROUP BY userid
ORDER BY total DESC) AS gamescore
WHERE gamescore.total > (SELECT SUM(score)
FROM games
WHERE userid = 1)
It's based on the idea that ranking == number of players with bigger score + 1
Check this out:
http://rpbouman.blogspot.com/2009/09/mysql-another-ranking-trick.html