Different ORDER BY direction for MySql query results - mysql

I am trying to do some ordering on a mysql query that I can't figure out.
id | status | created_at
------------------------
1 | open | 1348778070
2 | closed | 1348711241
3 | open | 1348839204
4 | closed | 1348738073
5 | banned | 1348238422
How do I order the above table so that the 'open' records are first, in ASC order; and then the non-open records are second in DESC order? In another word, have a dynamic second level ordering direction based on some condition?
I have tried a UNION of two SELECT queries with ordering within them, which doesn't work because UNION by default produces an unordered set of rows.
Also I've tried a pseudo column that subtracts the created_at timestamp from a large number, for the closed status records, so I can just ORDER BY ASC to get the result as per below...
SELECT table.*, (table.created_at) as tmp_order FROM table
WHERE table.status = 'open'
UNION
SELECT table.*, (999999999 - table.created_at) as tmp_order FROM table
WHERE table.status = 'closed'
ORDER BY tmp_order ASC
This works but I feel there has to be a better way. Ideally a solution would not include a random big number as above

UPDATED
SELECT *
FROM tmp_order
ORDER BY FIELD(status, 'open') DESC,
CASE
WHEN status = 'open'
THEN created_at
ELSE (999999999 - created_at)
END
or
SELECT *
FROM tmp_order
ORDER BY FIELD(status, 'open') DESC,
CASE
WHEN status = 'open'
THEN created_at END,
CASE
WHEN status <> 'open'
THEN created_at END DESC
Output:
| ID | STATUS | CREATED_AT |
----------------------------
| 1 | open | 1348778070 |
| 3 | open | 1348839204 |
| 4 | closed | 1348738073 |
| 2 | closed | 1348711241 |
| 5 | banned | 1348238422 |
Here is SQLFiddle demo.

Try:
SELECT id, status,
if (status = 'open', created_at, 999999999 - created_at) as tmp_order
FROM table
ORDER BY status, tmp_order

This is how I would solve it:
SELECT
id, status, created_at
FROM
yourtable
ORDER BY
status DESC,
CASE WHEN status='open' THEN created_at END,
CASE WHEN status='closed' THEN created_at END DESC

In your case, you can probably do it in one query, but the general solution, which works for any two different and unrelated. orderings is to use two unioned subqueries each with their own ordering;
SELECT * FROM (
SELECT *
FROM table
WHERE table.status = 'open'
ORDER BY created_at DESC) x
UNION ALL
SELECT * FROM (
SELECT *
FROM table
WHERE table.status = 'closed'
ORDER BY created_at) y

Related

Get last 3 rows from SQL table without duplicates of a row

Lets say we have a table that looks like this:
+---------------+----------------+-------------------+
| ID | random_string | time |
+---------------+----------------+-------------------+
| 2 | K2K3KD9AJ |2022-07-21 20:41:15|
| 1 | SJQJ8JD0W |2022-07-17 23:46:13|
| 1 | JSDOAJD8 |2022-07-11 02:52:21|
| 3 | KPWJOFPSS |2022-07-11 02:51:57|
| 1 | DA8HWD8HHD |2022-07-11 02:51:49|
------------------------------------------------------
I want to select the last 3 entries into the table, however they must all have separate ID's.
Expected Result:
+---------------+----------------+-------------------+
| ID | random_string | time |
+---------------+----------------+-------------------+
| 2 | K2K3KD9AJ |2022-07-21 20:41:15|
| 1 | SJQJ8JD0W |2022-07-17 23:46:13|
| 3 | KPWJOFPSS |2022-07-11 02:51:57|
------------------------------------------------------
I have already tried:
SELECT DISTINCT id FROM table ORDER BY time DESC LIMIT 3;
And:
SELECT MIN(id) as id FROM table GROUP BY time DESC LIMIT 3;
If you're not on MySQL 8, then I have two suggestions.
Using EXISTS:
SELECT m1.ID,
m1.random_string,
m1.time
FROM mytable m1
WHERE EXISTS
(SELECT ID
FROM mytable AS m2
GROUP BY ID
HAVING m1.ID=m2.ID
AND m1.time= MAX(time)
)
Using JOIN:
SELECT m1.ID,
m1.random_string,
m1.time
FROM mytable m1
JOIN
(SELECT ID, MAX(time) AS mxtime
FROM mytable
GROUP BY ID) AS m2
ON m1.ID=m2.ID
AND m1.time=m2.mxtime
I've not test in large data so don't know which will perform better (speed) however this should return the same result:
Here's a fiddle
Of course, this is considering that there will be no duplicate of exact same ID and time value; which seems to be very unlikely but still it's possible.
Using MySql 8 an easy solution is to assign a row number using a window:
select Id, random_string, time
from (
select *, Row_Number() over(partition by id order by time desc) rn
from t
)t
where rn = 1
order by time desc
limit 3;
See Demo

GROUP_CONCAT ORDER BY With LIMIT Unknown column 'p.id' in 'where clause' [duplicate]

This question already has answers here:
GROUP_CONCAT with limit
(7 answers)
Closed 9 months ago.
Here is my table:
+----+---------+------------+----------+
| id | message | projectID | noteType |
+----+---------+------------+----------+
| 1 | 1 | 125 | update |
| 2 | 2 | 125 | update |
| 3 | 3 | 125 | update |
| 4 | 4 | 125 | update |
| 5 | 5 | 125 | update |
| 6 | 6 | 125 | update |
My query using the suggestion below:
SELECT `p`.`id`, `proName`, `p`.`proType`, `p`.`priority`,
`p`.`busSegment`, `p`.`portfolio`, `p`.`description`,
(SELECT group_concat('<li>', `message`, '</li>') AS temp FROM (SELECT
projectID, message FROM notes where projectID = p.id AND noteType =
'update' ORDER BY id DESC LIMIT 3) three_messages GROUP BY projectID) as
updates
FROM `projects` as `p`
WHERE `p`.`id` = 125
Error:
Error Code: 1054. Unknown column 'p.id' in 'where clause'
All records are returned. For some reason, the LIMIT 3 is not working.
The suggestion query work by it self.
SELECT group_concat('<li>', `message`, '</li>') AS updates
FROM (
SELECT projectID, message
FROM notes where projectID = 125 AND noteType = 'update'
ORDER BY id DESC
LIMIT 3
) three_messages
GROUP BY projectID;
+---------------------+
| updates |
+---------------------+
| 6,5,4 |
+---------------------+
I'm guessing you have more than project id = 125 in your real query and for each one you want 3 results. in that case you may need to do some kind of ranking. I am doing it here with the row_number() function.
here is the fiddle https://www.db-fiddle.com/f/uqSzoth466RX86c5dCkfDR/0
with t as(select a.*,
row_number() over
(partition by project_id order by message desc) as rn
from mytable a
where note_type = 'update')
SELECT project_id,
group_concat('<li>', `message`, '</li>' ORDER BY id DESC) AS updates
from t where rn <=3 group by project_id;
LIMIT applies to the rows after grouping.
You can use it in a subquery and group the results of that subquery:
SELECT group_concat('<li>', `message`, '</li>' ORDER BY id DESC) AS updates
FROM (
SELECT id, projectID, message
FROM notes where projectID = 125 AND noteType = 'update'
ORDER BY id DESC
LIMIT 3
) three_messages
GROUP BY projectID
It does look to me like you want:
group_concat('<li>', `message`, '</li>' ORDER BY id DESC SEPARATOR '')
though, to not put commas between the list items.
I also think it is a bad idea to use id for ordering; if you want newest notes, use a timestamp. Imagine if your database was hacked and you had to restore from a backup. After the restore, you start your system again and notes get added. Then you go through your application logs and recover some number of notes that were lost; if you are imputing order to ids, you have to increase all the ids added after the restore to make room for the recovered ones. It's much better just to be able to insert with a timestamp.

Calculate unique items seen by users via sql

I need help to resolve the next case.
The data which users want to see is accessible by pagination requests and later these requests are stored in the database in the next form:
+----+---------+-------+--------+
| id | user id | first | amount |
+----+---------+-------+--------+
| 1 | 1 | 0 | 5 |
| 2 | 1 | 10 | 10 |
| 3 | 1 | 10 | 5 |
| 4 | 1 | 15 | 10 |
| 5 | 2 | 0 | 10 |
| 6 | 2 | 0 | 5 |
| 7 | 2 | 10 | 5 |
+----+---------+-------+--------+
The table is ordered by user id asc, first asc, amount desc.
The task is to write the SQL statement which calculate what total unique amount of data the user has seen.
For the first user total amount must be 20, since the request with id=1 returned first 5 items, with id=2 returned another 10 items. Request with id=3 returns data already 'seen' by request with id=2. Request with id=4 intersects with id=2, but still returns 5 'unseen' pieces of data.
For the second user total amount must be 15.
As a result of SQL statement, I should get the next output:
+---------+-------+
| user id | total |
+---------+-------+
| 1 | 20 |
+---------+-------+
| 2 | 15 |
+---------+-------+
I am using MySQL 5.7, so window functions are not available for me. I stuck with this task for a day already and still cannot get the desired output. If it is not possible with this setup, I will end up calculating the results in the application code. I would appreciate any suggestions or help with resolving this task, thank you!
This is a type of gaps and islands problem. In this case, use a cumulative max to determine if one request intersects with a previous request. If not, that is the beginning of an "island" of adjacent requests. A cumulative sum of the beginnings assigns an "island", then an aggregation counts each island.
So, the islands look like this:
select userid, min(first), max(first + amount) as last
from (select t.*,
sum(case when prev_last >= first then 0 else 1 end) over
(partition by userid order by first) as grp
from (select t.*,
max(first + amount) over (partition by userid order by first range between unbounded preceding and 1 preceding) as prev_last
from t
) t
) t
group by userid, grp;
You then want this summed by userid, so that is one more level of aggregation:
with islands as (
select userid, min(first) as first, max(first + amount) as last
from (select t.*,
sum(case when prev_last >= first then 0 else 1 end) over
(partition by userid order by first) as grp
from (select t.*,
max(first + amount) over (partition by userid order by first range between unbounded preceding and 1 preceding) as prev_last
from t
) t
) t
group by userid, grp
)
select userid, sum(last - first) as total
from islands
group by userid;
Here is a db<>fiddle.
This logic is similar to Gordon's, but runs on older releases of MySQL, too.
select userid
-- overall length minus gaps
,max(maxlast)-min(minfirst) + sum(gaplen) as total
from
(
select userid
,prevlast
,min(first) as minfirst -- first of group
,max(last) as maxlast -- last of group
-- if there was a gap, calculate length of gap
,min(case when prevlast < first then prevlast - first else 0 end) as gaplen
from
(
select t.*
,first + amount as last -- last value in range
,( -- maximum end of all previous rows
select max(first + amount)
from t as t2
where t2.userid = t.userid
and t2.first < t.first
) as prevlast
from t
) as dt
group by userid, prevlast
) as dt
group by userid
order by userid
See fiddle

How to deduplicate mysql rows but keep max view

I have MySQL rows like this
id | title | desc | view
1 | i'm a title | i'm a desc | 0
2 | i'm a title | i'm a desc | 0
3 | i'm a title | i'm a desc | 5
4 | i'm a title | i'm a desc | 0
5 | i'm a title | i'm a desc | 0
6 | i'm a title | i'm a desc | 3
8 | i'm a title | i'm a desc | 0
And i would like to keep only
3 | i'm a title | i'm a desc | 5
because this record as the max view and others are duplicates
If your data is not too big, you can use delete like this:
delete t from yourtable t join
(select title, `desc`, max(view) as maxview
from yourtable t
group by title, `desc`
) tt
on t.title = tt.title and
t.`desc` = tt.`desc` and
t.view < tt.maxview;
Note: if there are multiple rows with the same maximum number of views, this will keep all of them. Also, desc is a lousy name for a column because it is a SQL (and MySQL) reserved word.
EDIT:
If you have a large amount of data, often it is faster to do the truncate/re-insert approach:
create table temp_t as
select t.*
from yourtable t join
(select title, `desc`, max(view) as maxview
from yourtable t
group by title, `desc`
) tt
on t.title = tt.title and
t.`desc` = tt.`desc` and
t.view = tt.maxview;
truncate table yourtable;
insert into yourtable
select *
from temp_t;
I could not understand what the specific question is. The possible solutions are followed...
1) Use UPDATE instead of INSERT statement in mysql. Just write UPDATE your_table_name SET view=view+1
2) or you can run a cron job if using php to delete duplicate rows having lower value
3) If INSERT is necessary then you should do ON DUPLICATE KEY UPDATE. Refer to the documentation * http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html

Sort data before using GROUP BY?

I have read that grouping happens before ordering, is there any way that I can order first before grouping without having to wrap my whole query around another query just to do this?
Let's say I have this data:
id | user_id | date_recorded
1 | 1 | 2011-11-07
2 | 1 | 2011-11-05
3 | 1 | 2011-11-06
4 | 2 | 2011-11-03
5 | 2 | 2011-11-06
Normally, I'd have to do this query in order to get what I want:
SELECT
*
FROM (
SELECT * FROM table ORDER BY date_recorded DESC
) t1
GROUP BY t1.user_id
But I'm wondering if there's a better solution.
Your question is somewhat unclear but I have a suspicion what you really want is not any GROUP aggregates at all, but rather ordering by date first, then user ID:
SELECT
id,
user_id,
date_recorded
FROM tbl
ORDER BY date_recorded DESC, user_id ASC
Here would be the result. Note reordering by date_recorded from your original example
id | user_id | date_recorded
1 | 1 | 2011-11-07
3 | 1 | 2011-11-06
2 | 1 | 2011-11-05
5 | 2 | 2011-11-06
4 | 2 | 2011-11-03
Update
To retrieve the full latest record per user_id, a JOIN is needed. The subquery (mx) locates the latest date_recorded per user_id, and that result is joined to the full table to retrieve the remaining columns.
SELECT
mx.user_id,
mx.maxdate,
t.id
FROM (
SELECT
user_id,
MAX(date_recorded) AS maxdate
FROM tbl
GROUP BY user_id
) mx JOIN tbl t ON mx.user_id = t.user_id AND mx.date_recorded = t.date_recorded
Iam just using the technique
"Using order clause before group by inserting it in group_concat clause"
SELECT SUBSTRING_INDEX(group_concat(cast(id as char)
ORDER BY date_recorded desc),',',1),
user_id,
SUBSTRING_INDEX(group_concat(cast(`date_recorded` as char)
ORDER BY `date_recorded` desc),',',1)
FROM data
GROUP BY user_id