MySql Preferential Sorting Using FIELD - mysql

Student table :
----------------------
id | name
______________________
1 | Name 1
2 | Name 2
Course Table :
id | student_id | ctype | level
__________________________________
1 | 2 | beginner | complete
2 | 2 | advanced | current
3 | 1 | beginner | current
4 | 2 | intermed | skipped
From the above two table i am trying to get the latest user records based on the level from course table . the level should be matched such that it checks for current, complete and skipped in the same order so if the user has a level of current for any course type it should be fetched else check the level complete...
i am using the following query .
SELECT `sc`.`student_id`,
`s`.`name`,
`sc`.`id` as `course_id`,
`sc`.`ctype`,
`sc`.`level`,
FROM `course` `sc`
LEFT JOIN `students` `s` ON `s`.`id` = `sc`.`student_id`
WHERE sc.id = (SELECT ssc.id FROM course ssc WHERE ssc.student_id = sc.student_id
ORDER BY FIELD(`ssc`.`level`,"current","complete","skipped") DESC LIMIT 1,1)
GROUP BY `sc`.`student_id`
ORDER BY `sc`.`id` DESC
LIMIT 20
The problem with the above query is it displays only if there is more than one user id matching in course table . so the final output i get is it displays only the student with id 2 and ignore the student with id 1 as there is no more than one .
Result form above query
student_id | name | course_id | ctype | level |
=====================================================
2 | Name 2 | 2 | advanced | current
Expected Result
student_id | name | course_id | ctype | level |
=====================================================
2 | Name 2 | 2 | advanced | current
1 | Name 1 | 3 | beginner | current
NOTE : I have also tried FIELD_IN_SET and IN instead of FIELD im getting the same result

Change LIMIT 1,1 to LIMIT 0,1 or just LIMIT 1.
Unlike most other things in SQL, the offset field in the LIMIT clause is 0-based, not 1-based. So if there's only 1 matching row, LIMIT 1,1 skips over it. And if there are 2 or more matching rows, you're not getting the top match, you're getting the 2nd match.
Also, the ordering should be ASC, not DESC, since you want to prefer the lowest field (current), not the highest.
SELECT `sc`.`student_id`,
`s`.`name`,
`sc`.`id` as `course_id`,
`sc`.`ctype`,
`sc`.`level`
FROM `course` `sc`
LEFT JOIN `students` `s` ON `s`.`id` = `sc`.`student_id`
WHERE sc.id = (
SELECT ssc.id FROM course ssc
WHERE ssc.student_id = sc.student_id
ORDER BY FIELD(`ssc`.`level`,"current","complete","skipped") ASC
LIMIT 0,1)
GROUP BY `sc`.`student_id`
ORDER BY `sc`.`id` DESC
LIMIT 20
DEMO
There's also no need for GROUP BYsc.student_id`. The query is only returning one course ID per student, so there can't be multiple rows for each student.

Related

Getting the most recent row and linking it with another table?

Im trying to get the most recent row of a table
user_quiz:
+--------+-----------+-------------+-------------------+------------+
|quiz_id |userid | module_id |number_of_questions| user_score |
+--------+-----------+-------------+-------------------+-------- ---+
| 1 | 1 | 1 | 5 | 5 |
| 2 | 2 | 2 | 10 | 9 |
| 3 | 1 | 1 | 10 | 9 |
+--------+-----------+-------------+-------------------+------------+
I have used the query:
SELECT * FROM user_quiz WHERE userid = 1 ORDER BY quiz_id DESC LIMIT 1
which correctly retrieves the last row.
However I want to link the module_id with another table:
module:
+---------+------------+
|module_id|module_name |
+---------+------------+
| 1 | Forces |
| 2 | Electricity|
+---------+------------+
And retrieve the module name.
The result of the query will be used to print out the users most recent quiz:
Most recent quiz: Forces - Number of questions: 10 - User Score: 9
Is this possible using just one query?
You just need a JOIN:
SELECT uq.*, m.module_name
FROM user_quiz uq JOIN
modules m
ON uq.module_id = m.module_id
WHERE uq.userid = 1
ORDER BY uq.quiz_id DESC
LIMIT 1;
A more simple query to achieve the same would be
SELECT
user_quiz.quiz_id,
user_quiz.number_of_questions,
user_quiz.user_score,
modules .module_name
FROM user_quiz JOIN modules
ON user_quiz.module_id = modules.module_id
WHERE user_quiz.userid = 1
ORDER BY user_quiz.quiz_id DESC
LIMIT 1
If you want to get the same results for all the users, you could use a bit more sophisticated query
SELECT
user_quiz_virtual_table.userid,
user_quiz_virtual_table.quiz_id,
user_quiz_virtual_table.number_of_questions,
user_quiz_virtual_table.user_score,
modules.module_name
FROM (
SELECT
user_quiz.userid
user_quiz.quiz_id,
user_quiz.module_id
user_quiz.number_of_questions,
user_quiz.user_score
FROM user_quiz
ORDER BY user_quiz.quiz_id DESC
GROUP BY userid
) AS user_quiz_virtual_table
JOIN modules ON user_quiz_virtual_table.module_id = modules.module_id

MySQL how to select first row each group with count

I have a table like this (simplified version):
+------+-------+-----+--------------+-----+
| id | name | age | company.name | ...
+------+-------+-----+--------------------+
| 1 | Adam | 21 | Google | ...
| 3 | Peter | 20 | Apple | ...
| 2 | Bob | 20 | Microsoft | ...
| 9 | Alice | 18 | Google | ...
+------+-------+-----+--------------------+
I need groups data with counting rows by any one column. And I need to get first row in each group. User select which column will be used to group.
If user select column age to group then results:
+------+------------+-------+
| id | group_name | count |
+------+------------+-------+
| 9 | 18 | 1 |
+------+------------+-------+
| 2 | 20 | 2 |
+------+------------+-------+
| 1 | 21 | 1 |
+------+------------+-------+
Column to group may be numeric or string.
Currently I does it by this query:
SELECT id, group_name, users_name, count(id) as count FROM (
SELECT persons.id as id, company.type as group_name, users.name as users_name
FROM persons
LEFT JOIN company on company.id = persons.company_id
LEFT JOIN position on position.id=persons.position_id
...
LEFT JOIN source on source.id=persons.source_id
WHERE ...
ORDER BY if(company.type = '' or company.type is null,1,0) ASC,
company.type ASC, IF(persons.status = '' or persons.status is null,1,0) ASC,
persons.status ASC, persons.id
) t1 GROUP BY group_name
but with new version mysql this SQL stoped works I think that order is ignored in sub-select.
I know that similar topics was wroted, but proposed solutions not working with my query. I have to join many tables, add multiple conditions and use cascade order and then select first row from each group. I will be very happy if solution will be optimised for performace.
---- EDIT ----
Proposed solution:
SQL select only rows with max value on a column
which suggest to use MAX() and GROUP BY not working well. For two reason
If grouped column include string, then query return not first row, but last row in each group.
If my dataset has a cascade order, I can not use MAX in a few columns at the same time.
I created sqlfiddle which include exact example.
http://sqlfiddle.com/#!9/23225d/11/0
-- EXAMPLE 1 - Group by string
-- base query
SELECT persons.*, company.* FROM persons
LEFT JOIN company ON persons.company_id = company.id
ORDER BY company.name ASC, company.id ASC;
-- grouping query
SELECT MAX(persons.id) as id, company.name, count(persons.id) as count
FROM persons
LEFT JOIN company ON persons.company_id = company.id
GROUP BY company.name
ORDER BY company.name ASC, persons.id ASC;
-- The results will be:
-- |ID | NAME | COUNT|
-- |1 | Google | 2 |
-- |3 | Microsoft| 3 |
-- EXAMPLE 2 - Cascade order
-- base query
SELECT persons.*, company.* FROM persons
LEFT JOIN company ON persons.company_id = company.id
ORDER BY company.type ASC, persons.status ASC;
-- grouping query
SELECT MAX(persons.id) as id, company.type, count(persons.id) as count
FROM persons
LEFT JOIN company ON persons.company_id = company.id
GROUP BY company.type
ORDER BY company.type ASC, persons.status ASC;
-- The results will be:
-- |ID | NAME| COUNT|
-- |3 | 1 | 2 |
-- |2 | 2 | 3 |
Just change MAX() to MIN() to get the first row instead of the last row in each group.
To get the extreme values of cascading columns, see SQL : Using GROUP BY and MAX on multiple columns. Use that in the subquery part of the query to get the row containing those extremes, as in SQL select only rows with max value on a column.
So the form of the full query is:
SELECT t1.id, t1.grouped_column, t2.count
FROM yourTable AS t
JOIN (SELECT t3.grouped_column, t3.order_column1, MIN(t4.order_column2) AS order_column2, SUM(t3.count) AS count
FROM (SELECT grouped_column, MIN(order_column1) AS order_column1, COUNT(*) AS count
FROM yourTable
GROUP BY grouped_column) AS t3
JOIN yourTable AS t4
ON t3.grouped_column = t4.grouped_column AND t3.order_column1 = t4.order_column1
GROUP BY t4.grouped_column, t4.order_column1) AS t2
ON t1.grouped_column = t2.grouped_column AND t1.ordered_column1 = t2.order_column1 AND t1.order_column2 = t2.order_column2
Since you want to operate on a join, I suggest you define a view that uses the join. Then you can use that view in place of yourTable in the above query.

Mysql order by top two then id

I want to show first two top voted Posts then others sorted by id
This is table
+----+-------+--------------+--------+
| Id | Name | Post | Votes |
+====+=======+==============+========+
| 1 | John | John's msg | -6 |
| 2 |Joseph |Joseph's msg | 8 |
| 3 | Ivan | Ivan's msg | 3 |
| 4 |Natalie|Natalie's msg | 10 |
+----+-------+--------------+--------+
After query result should be:
+----+-------+--------------+--------+
| Id | Name | Post | Votes |
+====+=======+==============+========+
| 4 |Natalie|Natalie's msg | 10 |
| 2 |Joseph |Joseph's msg | 8 |
-----------------------------------------------
| 1 | John | John's msg | -6 |
| 3 | Ivan | Ivan's msg | 3 |
+----+-------+--------------+--------+
I have 1 solution but i feel like there is better and faster way to do it.
I run 2 queries, one to get top 2, then second to get others:
SELECT * FROM table order by Votes desc LIMIT 2
SELECT * FROM table order by Id desc
And then in PHP i make sure that i show 1st query as it is, and on displaying 2nd query i remove entry's that are in 1st query so they don't double.
Can this be done in single query to select first two top voted, then others?
You would have to use subqueries or union - meaning you have a single outer query, which contains multiple queries inside. I would simply retrieve the IDs from the first query and add a id not in (...) criterion to the where clause of the 2nd query - thus filtering out the posts retrieved in the first query:
SELECT * FROM table WHERE Id NOT IN (...) ORDER BY Id DESC
With union the query would look like as follows:
(SELECT table.*, 1 as o FROM table order by Votes desc LIMIT 2)
UNION
(SELECT table.*, 0 FROM table
WHERE Id NOT IN (SELECT Id FROM table order by Votes desc LIMIT 2))
ORDER BY o DESC, if(o=1,Votes,Id) DESC
As you can see, it wraps 3 queries into one and has a more complicated ordering as well because in union the order of the records retrieved is not guaranteed.
Two simple queries seem to be a lot more efficient to me in this particular case.
There could be different ways to write a query that returns the rows in the order you want. My solution is this:
select
table.*
from
table left join (select id from table order by votes desc limit 2) l
on table.id = l.id
order by
case when l.id is not null then votes end desc,
tp.id
the subquery will return the first two id ordered by votes desc, the join will succeed whenever the row is one of the first two otherwise l.id will be null instead.
The order by will order by number of votes desc whenever the row is the first or the second (=l.id is not null), when l.id is null it will put the rows at the bottom and order by id instead.

Show all grouped results and sort

I have a table, like that one:
| B | 1 |
| C | 2 |
| B | 2 |
| A | 2 |
| C | 3 |
| A | 2 |
I would like to fetch it, but sorted and grouped. That is, I would like it grouped by the letter, but sorted by the highest sum of the group. Also, I want to show all entries within the group:
| C | 3 |
| C | 2 |
| A | 2 |
| A | 2 |
| B | 2 |
| B | 1 |
The order is that way because C has 3 and 2. 3+2=5, which is higher than 2+2=4 for A which in turn is higher than 2+1=3 for B.
I need to show all "grouped" letters because there are other columns that are distinct all of which I need shown.
EDIT:
Thanks for the quick reply. I have the audacity, however, to inquire further.
I have this query:
SELECT * FROM `ip_log` WHERE `IP` IN
(SELECT `IP` FROM `ip_log` GROUP BY `IP` HAVING COUNT(DISTINCT `uid`) > 1)
GROUP BY `uid` ORDER BY `IP`
The letters in the upper description are ip (I need it grouped by the IP addresses) and the numbers are timestamp (I need it sorted by the sum (or just used as the sorting parameter)). Should I create a temporary table and then use the solution below?
select t.Letter, t.Value
from MyTable t
inner join (
select Letter, sum(Value) as ValueSum
from MyTable
group by Letter
) ts on t.Letter = ts.Letter
order by ts.ValueSum desc, t.Letter, t.Value desc
SQL Fiddle Example
If your table's columns are letter and number, the way I would go around to doing this would be the following:
SELECT
letter,
GROUP_CONCAT(number ORDER BY number DESC),
SUM(number) AS total
FROM table
GROUP BY letter
ORDER BY total desc
What you will get, based on your example is the following:
| C | 3,2 | 5
| A | 2,2 | 4
| B | 2,1 | 3
You can then process that data to get the actual information you want/need.
If you still want the data in the format you requested originally, it is not possible with a single query. The reason for that is that you can't sort based on an aggregated data that you are not calculating in the same query (the SUM of the number column). So you will need to make a sub-query to calculate that and feed it back into the original query (disclaimer: untested query):
SELECT
letter,
number
FROM table
JOIN (SELECT ltr, SUM(number) AS total FROM table GROUP BY letter) AS totals
ON table.letter = totals.ltr
ORDER BY totals.total desc, letter desc, number desc

Group by and order with joining tables

I am having a problem sorting results from joining tables that have to be grouped in Mysql.
This is my tables setup.
Owners Table
owner_id | owner_name
1 | Test owner 1
2 | Test owner 2
3 | Test owner 3
Images upload table
image_id | image_name | ownerid | upload_date
1 | image1.jpg | 2 | 04-08-2009
2 | image2.jpg | 1 | 04-08-2009
3 | image3.jpg | 3 | 04-08-2009
4 | image4.jpg | 1 | 04-08-2009
5 | image5.jpg | 3 | 04-08-2009
The owner_id field is auto increment and so is the image_id fields.
What I am trying to do is get the owner_name for the last three uploaded images but not by the same owner. So in that example I would like it to return the following results.
Test owner 3
Test owner 1
Test owner 2
In that example the last owner to upload is Test owner 3 then Test owner 1 then Test owner 2.
I am using the following query but it does not return correct results
$sql = "SELECT u.*, s.* FROM UPLOAD_TBL u, OWNER_TBL s WHERE u.ownerid = s.owner_id
GROUP BY s.owner_id ORDER BY u.image_id DESC LIMIT 0, 3";
Any help setting up this query would be greatly appreciated.
See if you can sort on the aggregated image id:
select s.owner_id, s.owner_name, max(u.imag_id) as last_image_id
from UPLOAD_TBL u
inner join OWNER_TBL s on s.ownerid = u.owner_id
group by s.owner_id, s.owner_name
order by last_image_id desc
limit 3
You should simply group by owner_id and sort by upload_date DESC with LIMIT 3
How about something like:
select
distinct(owner_id), owner_name
from
owner
inner join
images on images.ownerid = owner.ownerid
order by
images.upload_date desc limit 3