multiple LEFT JOIN using LIMIT on one of the joins - mysql

I looked around for answers to this question but all the ones I tried simply didn't work. The other answer suggestions all threw errors for me. Maybe it's because I'm using using MariaDB ?.
SELECT * FROM 'view_winners'
I need top 3 in column 'class'
table view_winners is multiple left joins and I could not figure out how to limit 3 of the left join on table allClasses.
view_winners is:
`$view = "view_winners";
$db->query("DROP $view");
$db->query("CREATE VIEW $view AS
SELECT *
FROM thw22 evnt
LEFT JOIN allUsers usr
ON usr.user_id = evnt.e_owner_id
LEFT JOIN hw_vehicles veh
ON veh.vehicle_id = evnt.e_vehicle_id
LEFT JOIN hw_m_vehicle_class mcls
ON mcls.v_class_id = evnt.e_class_id
LEFT JOIN allClasses cls
ON mcls.cvm_id = cls.class_id
LEFT JOIN hw_v_scores sco
ON sco.v_score_id = evnt.e_score_id
WHERE (cls.class_name <> '' OR cls.class_name IS NOT NULL)
AND (sco.total <> '' OR sco.total IS NOT NULL)
ORDER BY cls.vehicle_type ASC, cls.class_name ASC, sco.total DESC
");`
It's probably best if I could LIMIT 3 on LEFT JOIN allClasses but I can't figure that out. So I figured I would loop through the result and unset rows over 3 in class in PHP. But again I could not figure out how to compare rows as looping through.
I need help with the LIMIT 3 on the JOIN or how to compare the results unsetting rows.
entry
class
score
786
sally
99
234
sally
90
456
bob
45
621
joe
90
964
joe
80
548
joe
66
346
joe
22
900
frank
89
700
frank
86
800
frank
72
123
frank
70
860
frank
50
333
frank
45
Desired results:
entry
class
score
786
sally
99
234
sally
90
456
bob
45
621
joe
90
964
joe
80
548
joe
66
900
frank
89
700
frank
86
800
frank
72

Might this answer help
And to clarify, it appears that for example, you want AT MOST, 3 entries per class (person name per sample data). If one class has only a single entry, get it. However, if someone else has 8 classes you want only the first 3 based on some pre-determined priority ordering, such as top 3 scores.
In your case, the OVER is partitioned by the "class", and the order by will be the score DESC (descending). So having the view give you this extra computed column (per class), you can then filter WHERE finalColumnNameYouAssign <= 3

I just answered similar question yesterday. See How to limit SQL query with JOIN
Here's my solution based on 2 linked tables, users and history for each user. There is also another solution there depending on your MySQL version.
SELECT *
FROM
`users` AS u
LEFT JOIN `history` AS h ON u.id = h.user_id
WHERE
FIND_IN_SET(h.id, (SELECT `list` FROM
(SELECT user_id, SUBSTRING_INDEX(GROUP_CONCAT(id SEPARATOR ','), ',', 3) AS `list` FROM
(SELECT h.user_id, h.id
FROM
`users` AS u
LEFT JOIN `history` AS h ON u.id = h.user_id
) AS `a`
GROUP BY user_id
) AS `b`
WHERE b.user_id = u.id
) )

Instead of having so many joins and confusing myself badly, I made a couple different views that I will need anyway for other statistics. Then by simply ordering by class ASC, score DESC I have a very simple master list of all classes in order then scores highest to lowest (along with all the other joined data). After that I can compare each row and limit 3 as follows:
SELECT * FROM
(SELECT *,
#rn := IF(#prev = class_name,
#rn + 1, 1)
AS rn,
#prev := class_name
FROM view_allScores
JOIN (SELECT #prev := NULL, #rn := 0)
AS vars
) AS T1
WHERE T1.rn <= 3
The confusing thing was that I was trying to add a LIMIT to a join and that kept confusing me.

Related

compare mysql numeric values group_concat of two columns with join

I have 3 tables
1.users
user_id nationality
1 Egyptian
2 Palestinian
3 French
centers
id center_name
1 q
12 y
5 x
23 z
centers_users
student_id center_id
1 12
2 5
3 5
1 23
2 12
what I expect
Nationality center_name count_of_users_from this country
Egyptian y,z 10
Palestinian x,y 33
French x,q 7
I have tried many mysql queries but I cannot get the result I want
Final query I execute:
SELECT * from (SELECT (LENGTH(GROUP_CONCAT(DISTINCT user_id))-ENGTH(REPLACE(GROUP_CONCAT(DISTINCT user_id), ',', ''))) as ss,GROUP_CONCAT( DISTINCT user_id) ,nationality from user where user_id in(SELECT student_id FROM `centers_users`) GROUP by nationality)a
But only get the count with nationality.
When I Join with centers gives me redundancy because I cannot put "ON" condition with
group_concat
How can I implement it?
Thanks..
I think you want to join the tables and aggregate:
select u.nationality,
group_concat(distinct c.center_name) as center_names,
count(distinct user_id) as users_from_this_country
from users u join
user_centers uc
on u.user_id = uc.student_id join
centers c
on c.center_id = uc.center_id
group by u.nationality;
You may be able to use count(*) for users_from_this_country. It depends on how you want to count a user who is in multiple centers in the same country.

Alternative Using Having in mysql

I am trying to get the records where avg is greater than 81, I noticed I can't use a simple where avg(score) > 80
But using a Having statement is problematic as well as it does not consider where the individual records average is greater than 80, but it considers the group average. Is there an alternative?
In general, if we want to return aggregates (SUM,AVG) and also return detail that makes up the aggregate, we typically use two SELECT
As a rudimentary example, consider a table of "test_score"
test_id student_id score
------- ---------- -----
101 6 90
101 7 71
101 8 88
222 6 93
222 7 78
222 8 81
We can calculate the average score for each test, with a SELECT ... GROUP BY query.
SELECT r.test_id AS test_id
, AVG(r.score) AS avg_score
, MAX(r.score) AS high_score
FROM test_score r
GROUP
BY r.test_id
We expect that to return a resultset like this:
test_id avg_score
------- ---------
101 83
222 84
We can use that query as an inline view i.e. we wrap it in parens and reference it like a table in the FROM clause of another SELECT.
As a demonstration, to return student scores that were better (or equal to) average for each test:
SELECT s.test_id
, s.avg_score
, t.student_id
, t.score
FROM ( -- inline view to get average score for each test_id
SELECT r.test_id AS test_id
, AVG(r.score) AS avg_score
FROM test_score r
GROUP
BY r.test_id
) s
LEFT
JOIN test_score t
ON t.test_id = s.test_id
AND t.score >= s.avg_score
ORDER
BY t.test_id
, s.score DESC
And we'd expect that to return something like:
test_id avg_score student_id score
------- --------- ---------- -----
101 83 6 90
101 83 8 88
222 84 6 93
The first two columns, returned from the inline view, are the result of the aggregate (AVG). The last two columns are detail rows, matched to the rows from the aggregate result.
To summarize the main point here:
To return aggregates along with details, we typically need two SELECT.
One SELECT to get the aggregates (with a GROUP BY if the aggregates are "per" each something or other)
Another SELECT to get the details and a match to the aggregate.
If the average score being computed in your query is already correct, you are just having trouble filtering by it, just wrap it in parens and select from it
select * from (
SELECT Count(entry_id) AS Filled,
q.question AS Questions,
AVG(ag.score) AS TOTAL
FROM entry e
LEFT JOIN entry_answer ea
ON ea.entry_id= e.entry
LEFT JOIN question q
ON q.question_id = ea.question_id
LEFT JOIN question_group qg
ON ea.question_parent_id = qg.question_parent_id
LEFT JOIN answer_group ag
ON ag.question_id = qg.question_parent_id
JOIN sent_list using (sent_list_id)
WHERE
entry_group_id = 2427
AND ag.score >= 0
AND ea.rated_answer_id = ag.rated_answer_id
AND sent_id = 6156
AND e.entry_date BETWEEN '2018-01-01' AND '2019-12-31'
group by ea.question_id
) results where total >= 81

MySQL Create Ordinal

I'm trying to create a temp table or view that I can use for charting league members changing handicaps over the season. This requires creating a pivot table.
Even before that, however, I need to create an ordinal column to represent the nth match the person played that season. I can't use date_played since players can play on all different dates, and I'd like each player's 2nd match to line up vertically, and their third to do so as well, etc.
I used code from the answer to this question which seemed like it should work. Here's a copy of the relevant section:
SELECT books.*,
if( #libId = libraryId,
#var_record := #var_record + 1,
if(#var_record := 1 and #libId := libraryId, #var_record, #var_record)
) AS Ordinal
FROM books
JOIN (SELECT #var_record := 0, #libId := 0) tmp
ORDER BY libraryId;
`lwljhb_lwl_matches` # One record for each match played
id
date_played
other fields about the match
id date_played
1 2017-08-23
2 2017-08-29
3 2017-09-26
4 2017-08-24
5 2017-09-02
6 2017-09-21
7 2017-08-24
8 2017-08-31
9 2017-09-05
10 2017-09-15
11 2017-09-17
`lwljhb_users`
id
display_name
id display_name
1 Alan
2 Bill
3 Dave
`lwljhb_lwl_players` # One record per player per match
id
match_id # Foreign key to matches.id
player_id # Foreign key to users.id
end_hcp
other fields about the player's performance in the linked match
id match_id player_id end_hcp
1 1 1 720
2 2 1 692
3 3 1 694
4 4 2 865
5 5 2 868
6 6 2 842
7 7 3 363
8 8 3 339
9 9 3 332
10 10 3 348
11 11 3 374
Normally there would be two records in PLAYERS for each MATCH record, but I didn't add them here. The fact that the id and match_id are the same in every record of PLAYERS is artificial because this isn't real data.
Before I used this snippet, my code looked like this:
SELECT u.display_name,
m.date_played,
p.end_hcp
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
and generated data that looks like this:
display_name date_played end_hcp
Alan 2017-08-23 720
Alan 2017-08-29 692
Alan 2017-09-26 694
Bill 2017-08-24 865
Bill 2017-09-02 868
Bill 2017-09-21 842
Dave 2017-08-24 363
Dave 2017-08-31 339
Dave 2017-09-05 332
Dave 2017-09-15 348
Dave 2017-09-17 374
At any time during the season there will be different numbers of matches played by each of the players, but by the end of the season, they will have all played the same number of matches.
I tried to incorporate Alin Stoian's code into mine like so, changing a variable name and field names as I thought appropriate.
SET #var_record = 1;
SELECT u.display_name,
m.date_played,
p.end_hcp,
if( #player = u.display_name,
#var_record := #var_record + 1,
if(#var_record := 1 and #player := u.display_name, #var_record, #var_record)
) AS Ordinal
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
JOIN (SELECT #var_record := 0, #player := 0) tmp
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
I was hoping for a new column with the ordinals, but the new column is all zeros. Before I move on to trying the pivot table, I have to get these ordinals, so I hope someone here can show me my mistake.
Try this:
SELECT u.display_name,
m.date_played,
p.end_hcp,
#var_record := if ( #player = u.display_name,
#var_record + 1,
if(#player := u.display_name, 1, 1)
) AS Ordinal
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
JOIN (SELECT #var_record := 0, #player := 0) tmp
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
Demo
OK, it turns out that I wasn't getting all 1s in my Ordinal column after all, just in the 1st 25 records. Since it was obviously wrong I didn't look further.
The sample data I provided is in physical order as well as logical order, but MY ACTUAL data is not. That was the only difference I could think of, so I investigated further and found that in the 1st 1,000 records of output I got 7 records with 2 as the ordinal, not coincidentally where the m.ids were consecutive.
I created a view to put the data in order like in the sample and the JOINed that to the rest of the tables but still got bad data for the Ordinal.
When I swapped out the view for a temporary table it worked. It turns out that an ORDER BY in a view will be ignored if there's an ORDER by joining to it.
Bill Karwin pointed this out to me in a different question (46892912).

Writing a LIMIT subquery within my SQL query

So I have the following query which fetches active competitions for an organisation, but also aims to fetch the user that is in the lead - for each competition fetched.
The query currently works in that it fetches the competitions, however it currently fetches all the users and I would like to LIMIT 1 on the users fetched, using the SUM(activity_weight) you can see below.
The results come out like this (removed some results to make it easy to see) and in my case, I only want to fetch John and Sally, as they are the leaders of the competitions.
competitionId compName start_date end_date name totalPoints
------------------------------------------------------------
123 First Comp 13-09-09 13-10-09 John 100
123 First Comp 13-09-09 13-10-09 Bob 50
431 Second Comp 13-05-04 13-10-05 Sally 500
431 Second Comp 13-05-04 13-10-05 Jessica 50
I understand that I must use some form of subquery to use the LIMIT, but having a problem nailing the syntax of it.
Any help is much appreciated! THANK YOU
SELECT c.competitionId, c.name, c.start_date, c.end_date, a.userid, u.name,
u.profilePic ,
SUM(activity_weight) AS totalPoints
FROM activity_entries a INNER JOIN users1 u ON u.id = a.userid
INNER JOIN competitions c ON c.competitionId = a.competitionId
WHERE c.organisationId = '$organisation' AND c.start_date < now() AND c.end_date > now()
GROUP BY a.userid, c.competitionId ORDER BY c.id DESC, totalPoints DESC
Try this query
select * from
(select
#rn:=if(#prv=competitionId , #rn+1, 1) as rId,
#prv:=competitionId as competitionId ,
totalPoints,
your_other_columns
from (select * from ...)subquery
join
(select #prv:=0, #rn:=0)tmp
order by
competitionId , totalPoints desc) a
-- only top 2 ordered by points for every competition
where rid<=2
output:
rID competitionId compName start_date end_date name totalPoints
------------------------------------------------------------
1 123 First Comp 13-09-09 13-10-09 John 100
2 123 First Comp 13-09-09 13-10-09 Bob 50
1 431 Second Comp 13-05-04 13-10-05 Sally 500
2 431 Second Comp 13-05-04 13-10-05 Jessica 50
change the last part to where rid<=1 to select top 1

MySQL Selecting wrong column value in Group By query

Here's a real noobish MySQL query problem I'm having.
I have a high score table in a game I'm writing. The high score DB records a name, level, and score achieved. There are many near duplicates in the db. For example:
Name | Level | Score | Timestamp (key)
Bob 2 41 | 1234567.890
Bob 3 15 | 1234568.890
Bob 3 20 | 1234569.890
Joe 2 40 | 1234561.890
Bob 3 21 | 1234562.890
Bob 3 21 | 1234563.890
I want to return a "highest level achieved" high score list, with an output similar to:
Name | Level | Score
Bob 3 21
Joe 2 40
The SQL Query I currently use is:
SELECT *, MAX(level) as level
FROM highscores
GROUP BY name
ORDER BY level DESC, score DESC
LIMIT 5
However this doesn't quite work. The "Score" field output always seems to be randomly pulled from the group, instead of taking the corresponding score for the highest level achieved. Eg:
Name | Level | Score
Bob 3 41
Joe 2 40
Bob never got 41 points on level 3! How can I fix this?
You'll need to use a subquery to pull the score out.
select distinct
name,
max(level) as level,
(select max(score) from highscores h2
where h2.name = h1.name and h2.level = h1.level) as score
from highscores h1
group by name
order by level desc, score desc
Cheers,
Eric
It irks me that I didn't take the time to explain why this is the case when I posted the answer, so here goes:
When you pull back everything (*), and then the max level, what you'll get is each record sequentially, plus a column with the max level on it. Note that you're not grouping by score (which would have given you Bob 2 41, and Bob 3 21--two records for our friend Bob).
So, how the heck do we fix this? You need to do a subquery to additionally filter your results, which is what that (select max(score)...) is. Now, for each row that reads Bob, you will get his max level (3), and his max score at that level (21). But, this still gives us however many rows Bob has (e.g.-if he has 5 rows, you'll get 5 rows of Bob 3 21). To limit this to only the top score, we need to use a DISTINCT clause in the select statement to only return unique rows.
UPDATE: Correct SQL (can't comment on le dorfier's post):
SELECT h1.Name, h1.Level, MAX(h1.Score)
FROM highscores h1
LEFT OUTER JOIN highscores h2 ON h1.name = h2.name AND h1.level < h2.level
LEFT OUTER JOIN highscores h3 ON h1.name = h3.name AND h2.level = h3.level AND h1.score < h3.score
WHERE h2.Name IS NULL AND h3.Name IS NULL
GROUP BY h1.Name, h1.Level
This is efficient.
SELECT h1.Name, h1.Level, h1.Score
FROM highscores h1
LEFT JOIN highscores h2 ON h1.name = h2.name AND h1.level < h2.level
LEFT JOIN highscores h3 ON h1.name = h3.name AND h1.level = h3.level AND h1.score < h3.score
WHERE h2.id IS NULL AND h3.id IS NULL
You're looking for the level/score for which there is no higher level for that user, and no higher score that that level.
Interesting problem. Here's another solution:
SELECT hs.name, hs.level, MAX(score) AS score
FROM highscores hs
INNER JOIN (
SELECT name, MAX(level) AS level FROM highscores GROUP BY name
) hl ON hl.name = hs.name AND hl.level = hs.level
GROUP BY hs.name, hs.level;
Personally, I find this the easiest to understand, and my hunch is that it will be relatively efficient for the database to execute.
I like the above query best, but just for kicks... I find the following one amusing in a kludgey sort of way. Assuming score can't exceed 99999...
SELECT name, level, score
FROM highscores hs
INNER JOIN (
SELECT name, MAX(level * 100000 + score) AS hfactor
FROM highscores GROUP BY name
) hf ON hf.hfactor = hs.level * 100000 + hs.score AND hf.name = hs.name;