Select only last record of distinct values [duplicate] - mysql

This question already has answers here:
SQL select only rows with max value on a column [duplicate]
(27 answers)
Closed 2 years ago.
I have a table like that
| Symbol | Value | created_at |
|:-----------|------------:|:------------:|
| A | A1 | 01/01/1970 |
| A | A2 | 01/01/2020 |
| B | B1 | 01/01/1970 |
| B | B2 | 01/01/2020 |
| C | C1 | 01/01/1970 |
| C | C2 | 01/01/2020 |
I need to query only the last record ( sorted by created_at ) of each symbol in the table
Expected output is this :
| Symbol | Value | created_at |
|:-----------|------------:|:------------:|
| A | A2 | 01/01/2020 |
| B | B2 | 01/01/2020 |
| C | C2 | 01/01/2020 |
I have no idea how I can achieve that, do you have some suggestions? Thanks you !

One option is to filter with a subquery:
select t.*
from mytable t
where t.created_at = (
select max(t1.created_at) from mytable t1 where t1.symbol = t.symbol
)
This query would take advantage of an index on (symbol, created_at).
If you are running MySQL 8.0, you can also use row_number():
select t.*
from (
select t.*, row_number() over(partition by symbol order by created_at desc) rn
from mytable
) t
where rn = 1

with t as
(
select *, row_number() over(PARTITION BY Symbol ORDER BY created_at DESC) as rn
from your_table
)
select * from t
where rn = 1

You can use windowing functions (something like the following should work although I haven't tested it):
select *, row_number() over(partition by symbol order by created_at desc) as rownum where rownum = 1

Related

How do I use row_number() with partitioning and without ordering?

My table looks like :
table_1
| Id | Num |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
I want a row_number next to 'num' column, but as soon as the num changes it's value, the row_number resets.
I want my table to look like:
| Id | Num | row_num |
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
| 5 | 1 | 1 |
| 6 | 2 | 1 |
| 7 | 2 | 2 |
One way to get your desired output is to use lag and a conditional sum to flag when the number changes, then you can use row_number and partition by this flag:
with lagNum as (
select id, num, Lag(num) over(order by id) as v
from t
), changed as (
select id, num,
Sum(case when num = v then 0 else 1 end) over(order by id rows unbounded preceding) as v
from lagNum
)
select id, num, row_number() over(partition by v order by id) as row_num
from changed
Example Fiddle
This does require at least MySql 8 which added support for window functions
This is a type of gaps-and-islands problem. For this version, the simplest solution is probably to identify the islands using the difference of row numbers:
select t.*,
row_number() over (partition by seqnum - seqnum_2 order by id) as row_num
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by num order by id) as seqnum_2
from table_1 t
) t;
If you run the subquery, you will see how the difference identifies the "adjacent" values of num.
Note: If (as in your example) the ids are sequential with no gaps, you can simplify this to:
select t.*,
row_number() over (partition by id - seqnum_2 order by id) as row_num
from (select t.*,
row_number() over (partition by num order by id) as seqnum_2
from table_1 t
) t;

How query which employee has been full-time employeed

I have the following MySQL table:
+----+---------+----------------+------------+
| id | user_id | employment_type| date |
+----+---------+----------------+------------+
| 1 | 9 | full-time | 2013-01-01 |
| 2 | 9 | half-time | 2013-05-10 |
| 3 | 9 | full-time | 2013-12-01 |
| 4 | 248 | intern | 2015-01-01 |
| 5 | 248 | full-time | 2018-10-10 |
| 6 | 58 | half-time | 2020-10-10 |
| 7 | 248 | NULL | 2021-01-01 |
+----+---------+----------------+------------+
I want to query, for example, which employees were full-time employed on 2014-01-01.
Which SQL query I need to pass to get the correct result?
In this case, the result will be an employee with user_id=9;
Is this table properly structured to be possible to get such a result?
If your version of MySql is 8.0+ you can do it with FIRST_VALUE() window function:
SELECT DISTINCT user_id
FROM (
SELECT user_id,
FIRST_VALUE(employment_type) OVER (PARTITION BY user_id ORDER BY date DESC) last_type
FROM tablename
WHERE date <= '2014-01-01'
) t
WHERE last_type = 'full-time'
For previous versions of MySql you can do it with NOT EXISTS:
SELECT t1.user_id
FROM tablename t1
WHERE t1.date <= '2014-01-01' AND t1.employment_type = 'full-time'
AND NOT EXISTS (
SELECT 1
FROM tablename t2
WHERE t2.user_id = t1.user_id AND t2.date BETWEEN t1.date AND '2014-01-01'
AND COALESCE(t2.employment_type, '') <> t1.employment_type
)
See the demo.
Results:
| user_id |
| ------- |
| 9 |
You want the most recent record on or before that date. I would use row_number():
select t.*
from (select t.*,
row_number() over (partition by user_id order by date desc) as seqnum
from t
where date <= '2014-01-01'
) t
where seqnum = 1 and employment_type = 'full_time';
A fun method that just uses group by is:
select t.user_id
from t
where t.date <= '2014-01-01'
group by t.user_id
having max(date) = max(case when employment_type = 'full_time' then date end);
This checks that the maximum date -- before the cutoff -- is the same as the maximum date for 'full-time'.

MySql group by and order by different column

I have a table and I wanted to group by one column and get all values with order by date and time column.
**My table**
-------------------------------
id | name | created_at
===+======+===========
1 | a | 2020-11-18 04:33:55
2 | b | 2020-11-14 10:17:28
3 | c | 2020-11-12 20:26:00
4 | a | 2020-11-11 18:35:24
5 | c | 2020-11-10 10:55:04
**Result**
-------------------------------
id | name | created_at
===+======+===========
1 | a | 2020-11-18 04:33:55
2 | b | 2020-11-14 10:17:28
3 | c | 2020-11-12 20:26:00
In the older version of Mysql(V. 5.7.32) the below query is working fine.
SELECT * FROM `my_table` GROUP BY name ORDER by created_at DESC
But in the new version of mysql(V. 8.0.22) the below code is not working.
Please anyone have a solution for it.
WITH
cte AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY name ORDER BY created_at DESC) rn
FROM my_table )
SELECT *
FROM cte
WHERE rn = 1;

Duplicate and get the last item from the mysql table

table 1 t1
+----+----------+
| id | name |
+----+----------+
| 1 | free |
| 2 | basic |
| 3 | advanced |
+----+----------+
table 2 t2
+----+-------+------+
| id | t1_fk | cost |
+----+-------+------+
| 1 | 2 | 1650 |
| 3 | 3 | 2000 |
| 4 | 2 | 550 |
+----+-------+------+
I want to get the output of t2 table but without duplicates. I was able to get this using GROUP BY function. Also i need the last item on the duplicate (i got stuck here).
Here's what i tried and it didn't work.
SELECT id cost FROM t2 GROUP BY t1_fk ORDER BY MAX(id) DESC
any help
On MySQL 8+, we can use ROW_NUMBER here:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY t1_fk ORDER BY id DESC) rn
FROM t2
)
SELECT id, t1_fk, cost
FROM cte
WHERE rn = 1;
On earlier versions of MySQL, one canonical way to handle this would be to use a join to a subquery which finds the max id value for each t1_fk:
SELECT a.id, a.t1_fk, a.cost
FROM t2 a
INNER JOIN
(
SELECT t1_fk, MAX(id) AS max_id
FROM t2
GROUP BY t1_fk
) b
ON a.t1_fk = b.t1_fk AND a.id = b.max_id;

How to track previous row status count

I want to calculate count of order status changes within different states.
My Orderstatus table:
| id |ordr_id| status |
|----|-------|------------|
| 1 | 1 | pending |
| 2 | 1 | processing |
| 3 | 1 | complete |
| 4 | 2 | pending |
| 5 | 2 | cancelled |
| 6 | 3 | processing |
| 7 | 3 | complete |
| 8 | 4 | pending |
| 9 | 4 | processing |
Output I want:
| state | count |
|----------------------|-------|
| pending->processing | 2 |
| processing->complete | 2 |
| pending->cancelled | 1 |
Currently I'm fetching the results by SELECT order_id,GROUP_CONCAT(status) as track FROM table group by order_id and then process the data in php to get the output. But is that possible in query itself ?
Use lag():
select prev_status, status, count(*)
from (select t.*,
lag(status) over (partition by order_id order by status) as prev_status
from t
) t
group by prev_status, status;
LAG() is available in MySQL starting with version 8.
Note that you can filter out the first status for each order by putting where prev_status is not null in the outer query.
Your version is not quite correct, because it does not enforce the ordering. It should be:
SELECT order_id,
GROUP_CONCAT(status ORDER BY id) as track
EDIT:
In earlier versions of MySQL, you can use a correlated subquery:
select prev_status, status, count(*)
from (select t.*,
(select t2.status
from t t2
where t2.order_id = t.order_id and t2.id < t.id
order by t2.id desc
limit 1
) as prev_status
from t
) t
group by prev_status, status;
If id column ensure the sequence of records, you can use self join to achieve your requirement as below-
SELECT A.Status +'>'+ B.Status, COUNT(*)
FROM OrderStatus A
INNER JOIN OrderStatus B
ON A.id = B.id -1
WHERE B.Status IS NOT NULL
GROUP BY A.Status +'>'+ B.Status
With a join of the 3 status change types to the grouping of the table that you already did:
select c.changetype, count(*) counter
from (
select 'pending->processing' changetype union all
select 'processing->complete' union all
select 'pending->cancelled'
) c inner join (
select
group_concat(status order by id separator '->') changestatus
from tablename
group by ordr_id
) t on concat('->', t.changestatus, '->') like concat('%->', changetype, '->%')
group by c.changetype
See the demo.
Results:
> changetype | counter
> :------------------- | ------:
> pending->cancelled | 1
> pending->processing | 2
> processing->complete | 2
...or just a simple join...
SELECT CONCAT(a.status,'->',b.status) action
, COUNT(*) total
FROM my_table a
JOIN my_table b
ON b.ordr_id = a.ordr_id
AND b.id = a.id + 1
GROUP
BY action;
+----------------------+-------+
| action | total |
+----------------------+-------+
| pending->cancelled | 1 |
| pending->processing | 2 |
| processing->complete | 2 |
+----------------------+-------+
Note that this relies on the fact that ids are contiguous.