Get last values from joined table in MySQL [duplicate] - mysql

This question already has an answer here:
How to get the last row in the table using group by with Order by DESC?
(1 answer)
Closed 2 months ago.
For a Home-Automation Dashboard I am trying to display the latest values for different devices in different rooms.
My tables look like this
Table: devices
DeviceID
Floor
Room
Measurement
More Information
1
0
WC
Room-Temp
...
2
0
WC
Desired Room-Temp
...
3
0
WC
rel. Humidity
...
...
....
...
....
...
15
1
Kid
Room-Temp
...
16
1
Kid
Desired Room-Temp
...
17
1
Kid
rel. Humidity
...
Apart from that I have a table "messdaten" (=measurements) where I store the collected data (DeviceID+Timestamp is unique).
DeviceID
Timestamp
Value
3
2022-07-28 13:00:03
63
15
2022-07-28 12:58:37
21.3
17
2022-07-28 12:58:37
60
1
2022-07-28 12:55:00
20.8
...
...
...
What I would like to have now is a table in this form where I find all the latest data from each device for one specific room (here for example "WC").
DeviceID
Floor
Room
Measurement
Timestamp
Value
1
0
WC
Room-Temp
2022-07-28 12:55:00
20.8
2
0
WC
Desired Room-Temp
2022-07-28 12:53:00
20.5
3
0
WC
rel. Humidity
2022-07-28 13:00:03
63
My closest approach so far is:
SELECT devices.DeviceID, Floor, Room, Measurement, max(messdaten.Timestamp), messdaten.Value
FROM devices LEFT JOIN messdaten ON messdaten.DeviceID = devices.DeviceID
WHERE Room = (SELECT Room FROM devices WHERE DeviceID = 1)
GROUP BY devices.DeviceID
ORDER BY `devices`.`Measurement` ASC;
Unfortunately using "max" in Timestamp only applies on the column 'Timestamp' so that I get the latest timestamp but not the corresponding value.

On MySQL 8+, we can use ROW_NUMBER() here:
WITH cte AS (
SELECT d.DeviceID, d.Floor, d.Room, d.Measurement, m.Timestamp, m.Value,
ROW_NUMBER() OVER (PARTITION BY d.DeviceID ORDER BY m.Timestamp DESC) rn
FROM devices d
LEFT JOIN messdaten m ON m.DeviceID = d.DeviceID
WHERE Room IN (SELECT Room FROM devices WHERE DeviceID = 1)
)
SELECT DeviceID, Floor, Room, Measurement, Timestamp, Value
FROM cte
WHERE rn = 1
ORDER BY Measurement;

Related

MySQL join and group based on date ranges

I have table A
uid dt val_A
10 04/09/2012 34
10 08/09/2012 35
10 10/09/2012 36
100 04/09/2012 40
100 08/09/2012 41
and table B
uid date val_B
10 04/09/2012 1
10 05/09/2012 1
10 06/09/2012 2
10 07/09/2012 2
10 08/09/2012 1
100 07/09/2012 1
100 07/09/2012 3
I want to join them to get table C. I want to join them on uid. Furthermore I want to have a new column val_C which holds the average of val_B where date in B is greater or equal than the corresponding row-value dt in A AND less than the next higher dt value for this uid in table A. It means I want to aggregate the values in B based on date ranges defined in A. The joined table should look like this:
uid dt val_A val_C
10 04/09/2012 34 1.5
10 08/09/2012 35 1
10 10/09/2012 36 0
100 04/09/2012 40 2
100 08/09/2012 41 0
How can this be achieved?
//EDIT
How could a more generalized solution look like where all dates in B2 which are greater than the greatest date in A are being joined & aggregated to the greatest date in A. B2:
uid date val_B
10 04/09/2012 1
10 05/09/2012 1
10 06/09/2012 2
10 07/09/2012 2
10 08/09/2012 1
100 07/09/2012 1
100 07/09/2012 3
100 10/09/2012 4
100 11/09/2012 2
Desired output C2:
uid dt val_A val_C
10 04/09/2012 34 1.5
10 08/09/2012 35 1
10 10/09/2012 36 0
100 04/09/2012 40 2
100 08/09/2012 41 3
If you're on MySQL v8+ that supports LEAD() function, then you can try this:
WITH cte AS (
SELECT uid, dt, val_A,
IFNULL(LEAD(dt) OVER (PARTITION BY uid ORDER BY uid, dt),dt) dtRg
FROM tableA)
SELECT cte.uid, cte.dt, cte.val_A,
AVG(val_B) AS val_C
FROM cte
LEFT JOIN tableB tb1
ON cte.uid=tb1.uid
AND tb1.dt >= cte.dt
AND tb1.dt < cte.dtRg
GROUP BY cte.uid, cte.dt, cte.val_A
The query in common table expression (cte):
SELECT uid, dt, val_A,
IFNULL(LEAD(dt) OVER (PARTITION BY uid ORDER BY uid, dt),dt) dtRg
FROM tableA
will give you a result like this:
As you can see, the dtRg column is generated using LEAD() function which takes the next row dt value according to the ORDER BY. Read more about LEAD() here.
After that, join the cte with tableB on matching uid and where tableB.dt is the same or bigger than the existing tableA.dt - which is now as cte.dt, but lower than cte.dtRg - which is the next date in tableA that was generated by LEAD(). And finally adding AVG(val_B) AS val_C
Demo fiddle
On older MySQL version, you can try this:
SELECT tA.uid, tA.dt, tA.val_A,
AVG(val_B) AS val_C
FROM
(SELECT uid, dt, val_A,
(SELECT dt FROM tableA ta1
WHERE ta1.uid=ta2.uid
AND ta1.dt > ta2.dt LIMIT 1) AS dtRg
FROM tableA ta2) tA
LEFT JOIN tableB tB
ON tA.uid=tB.uid
AND tB.dt >= tA.dt
AND tB.dt < tA.dtRg
GROUP BY tA.uid, tA.dt, tA.val_A;
The difference are as following:
Instead of using LEAD(), it uses correlated subquery in SELECT to get the next dt value of next row in the same uid.
Instead of common table expression, it uses a derived table.
Fiddle for MySQL v5.7 version

Get original RANK() value based on row create date

Using MariaDB and trying to see if I can get pull original rankings for each row of a table based on the create date.
For example, imagine a scores table that has different scores for different users and categories (lower score is better in this case)
id
leaderboardId
userId
score
submittedAt ↓
rankAtSubmit
9
15
555
50.5
2022-01-20 01:00:00
2
8
15
999
58.0
2022-01-19 01:00:00
3
7
15
999
59.1
2022-01-15 01:00:00
3
6
15
123
49.0
2022-01-12 01:00:00
1
5
15
222
51.0
2022-01-10 01:00:00
1
4
14
222
87.0
2022-01-09 01:00:00
1
5
15
555
51.0
2022-01-04 01:00:00
1
The "rankAtSubmit" column is what I'm trying to generate here if possible.
I want to take the best/smallest score of each user+leaderboard and determine what the rank of that score was when it was submitted.
My attempt at this failed because in MySQL you cannot reference outer level columns more than 1 level deep in a subquery resulting in an error trying to reference t.submittedAt in the following query:
SELECT *, (
SELECT ranking FROM (
SELECT id, RANK() OVER (PARTITION BY leaderboardId ORDER BY score ASC) ranking
FROM scores x
WHERE x.submittedAt <= t.submittedAt
GROUP BY userId, leaderboardId
) ranks
WHERE ranks.id = t.id
) rankAtSubmit
FROM scores t
Instead of using RANK(), I was able to accomplish this by with a single subquery that counts the number of users that have a score that is lower than and submitted before the given score.
SELECT id, userId, score, leaderboardId, submittedAt,
(
SELECT COUNT(DISTINCT userId) + 1
FROM scores t2
WHERE t2.userId = t.userId AND
t2.leaderboardId = t.leaderboardId AND
t2.score < t.score AND
t2.submittedAt <= t.submittedAt
) AS rankAtSubmit
FROM scores t
What I understand from your question is you want to know the minimum and maximum rank of each user.
Here is the code
SELECT userId, leaderboardId, score, min(rankAtSubmit),max(rankAtSubmit)
FROM scores
group BY userId,
leaderboardId,
scorescode here

Need help to figure out sorting sql query

I'm trying to get the total number of levels gained or lost from this sort of table:
id name level timestamp
1 Rex 15 10:25
2 Rex 15 10:26
3 Rex 15 10:27
4 Rex 14 10:28
5 Rex 13 10:29
6 Rex 13 10:30
7 Rex 13 10:31
8 Rex 13 10:29
9 Xer 44 10:30
10 Xer 44 10:31
11 Xer 45 10:32
12 Xer 45 10:33
13 Xer 45 10:34
Currently I'm running
SELECT id, name, level, timestamp, MAX(level) - MIN(level) AS gained
FROM log
GROUP BY name
But the problem with this query is that both gained and lost levels will count as gained. It would be perfect if I could get a negative int in the gained column if the user has lost levels
The output I want from the data above is:
id name level timestamp gained
8 Rex 13 10:29 -2
13 Xer 45 10:34 1
If you need to respect the timeline, then try something like this:
SELECT MAX(id) id, name,
( SELECT level FROM log l0 WHERE l.name = l0.name ORDER BY timestamp DESC LIMIT 1 ) level,
MAX(timestamp) timestamp,
-- last entry for the name
( SELECT level FROM log l1 WHERE l.name = l1.name ORDER BY timestamp DESC LIMIT 1 ) -
-- first entry for the name
( SELECT level FROM log l2 WHERE l.name = l2.name ORDER BY timestamp ASC LIMIT 1 ) gained
FROM log l
GROUP BY name
I used LAG in as subquery to get the changes and then summed those changes in an outer sub-query. To get the last row I uses yet another query to find the max time for each name. Maybe not the most efficient query but it works
SELECT l.id, l.name, l.level, l.timestamp, sg.gain
FROM log l
JOIN (SELECT name, SUM(gain) gain
FROM (SELECT name, level - COALESCE(LAG(level) OVER w, level) as gain
FROM log
WINDOW w AS (PARTITION BY name ORDER BY timestamp)) as g
GROUP BY name) as sg ON sg.name = l.name
JOIN (SELECT name, MAX(time) max_t
FROM log
GROUP BY name) mt ON mt.name = l.name AND mt.max_t = l.time

get 1st and 2nd highest vlaue rows in case of similar values

I have a table with the columns : id, status, value.
id status value
-- ------ -----
1 10 100
2 10 100
3 10 60
4 11 20
5 11 15
6 12 100
7 12 50
8 12 50
I would like to get the id and value of the first and second highest valued rows, from each status group. My table should have the following columns:
status, id of the first highest value, first highest value, id of second highest value, second highest value.
I should get:
status 1stID 1stValue 2ndID 2ndValue
------ ----- -------- ----- --------
10 1/2 100 2/1 100
11 4 20 5 15
12 6 100 7/8 50
I tried all kinds of solutions, but I couldn't find a solution for same-value 1st s (two rows with the same value, which happened to be the highest in that status group) or same-value seconds.
For example, in case of two rows sharing the highest value in their status group, this not-so-elegant query will return two rows with the same status, different 1sts and same 2nd:
SELECT 2nds.status, 1sts.id AS "1stID",1sts.value AS "1stValue",
2nds.id AS "2ndID",2nds.value AS "2ndValue"
FROM
(SELECT v.* FROM
(SELECT status, MAX(value) AS "SecMaxValue" FROM table o
WHERE value < (SELECT MAX(value) FROM table
WHERE status = o.status
GROUP BY status) AS m
INNER JOIN table v
ON v.status = m.status AND v.value = m.SecMaxValue) AS 2nds
INNER JOIN
(SELECT v.* FROM
(SELECT status, MAX(value) AS maxValue FROM table
GROUP BY status) AS m
INNER JOIN table v
ON v.status = m.status AND v.value = m.MaxValue) AS 1sts
ON 1sts.status = 2nds.status ;
This query will give me:
status 1stID 1stValue 2ndID 2ndValue
------ ----- -------- ----- --------
10 1 100 3 60
10 2 100 3 60
11 4 20 5 15
12 6 100 7 50
12 6 100 8 50
To conclude, I would like to find a solution in which:
a. if there are two rows with the highest value the query puts the details one of them in the column of the 1st and the details of other in 2nd (no mather which)
b. if there are two rows with the second highst value it puts the highest in its place and one of the seconds in the second place.
Is there a way to change the query above? someone has a nicer solution?
I came across several 1st and 2nd queries but they had the same problem - for example this solution: Finding the highest n values of each group in MySQL. it does not deliver 1st and 2nd in the same row, but the main problem it provides only one of the firsts.
Thanks
After spent a lot of time, finally I found a solution for above problem. Please try it out:
select 1st.status as Status,
SUBSTRING_INDEX(1st.id,'/',1) as 1stID,
1st.value as 1stValue,
(case when locate('/',1st.id) > 0 then SUBSTRING_INDEX(1st.id,'/',-1)
else 2nd.id
end) as 2ndID,
(case when locate('/',1st.id) > 0 then 1st.value
else 2nd.value
end) as 2ndValue
from
(
(select status, SUBSTRING_INDEX(Group_concat(id separator '/'),'/',2) as id,value
from t1
where (status,value) in (select status,value
from t1
group by status
having max(value))
group by status) 1st
inner join
(select status,id,value
from t1
where (status,value) not in (select status,value
from t1
group by status
having max(value))
group by status,value
order by status,value desc) 2nd
on 1st.status = 2nd.status)
group by 1st.status;
Just replace t1 with your tablename and it should work like a charm.
Click here for Updated Demo
If you have any doubt(s), feel free to ask.
Hope it helps!

Finding the largest price increase/decrease in a MySQL table

I am trying to find a way to get the largest price difference (in a time frame, e.g. 24 hours) in a MySQL table using a source and productId as reference.
Here is a sample product, productId 22.
id price createdAt updatedAt sourceId productId
21 799.00 2017-07-26 19:46:46 2017-07-26 19:46:45 1 22
853 920.00 2017-07-26 06:46:46 2017-07-26 06:46:46 1 22
855 799.00 2017-07-22 16:17:11 2017-07-22 16:17:11 2 22
851 770.00 2017-07-21 16:17:11 2017-07-21 16:17:11 1 22
856 799.00 2017-07-20 16:17:11 2017-07-20 16:17:11 2 22
852 599.00 2017-07-19 16:17:11 2017-07-19 16:17:11 1 22
857 810.00 2017-07-18 16:17:11 2017-07-18 16:17:11 2 22
858 799.00 2017-07-17 16:17:11 2017-07-17 16:17:11 2 22
In the example above for productId 22 I am sorting by createdAt, so in this scenario I'd take id 21 and substract it from id 853, this would give -121, meaning the product went down 121 dollars.
In the full data it's a mush up of prices, sourceIds and productIds. The goal here is to make a result look like this:
id createdAt sourceId productId adjustment
21 2017-07-26 19:46:46 1 22 -121
22 2017-07-26 16:46:46 2 22 201
23 2017-07-26 15:46:46 6 24 -20
Above is kind of how I am trying to get the data to look, so I'll know of the price difference of each product of each source. Then I can control the data, such as ordering by adjustment and seeing which source + product had the largest decrease or increase in a time frame.
I've tried doing a ton of sub-queries, I've probably put in a hundred examples that I've modified from Google. I can piece together parts of this, such as only getting products that have recieved a change of any kind from the past 24 hours. I've tried to merge the last two rows of each product Id, then do a math, and list all the products. It's been 2 days of trying to build this query, is it just best for me to not use queries for everything and do it on my backend?
I've even went to a support site like hackhands and they couldn't figure it out. I've exhausted all of my ideas.
This query breaks down the problem:
1) Getting the records corresponding to start_at time of the window for each product in order to get the baseline price.
2) Gets the the records for the max price for each product in the time frame.
3) Gets the records for the min price for each product in the time frame.
4) Combines 1 and 2 and 3 to form a single record per product and shows the info and the difference between base line price and the highest and lowest in the time frame.
If you only need the bigger of the two you can add and extra layer of select wrapping this query and user GREATER(a,b) to keep one diff or the other.
select BOWPRICE.product_id, BOWPRICE.created_at, BOWPRICE.price,
MAXPRICE.max_price_upd_time, MAXPRICE.max_price, ABS((BOWPRICE.price - MAXPRICE.max_price)) max_price_diff,
MINPRICE.min_price_upd_time, MINPRICE.min_price, ABS((BOWPRICE.price - MINPRICE.min_price)) min_price_diff
from
(
select mainA.product_id, mainA.created_at, mainA.price from SOTEST mainA
where id in (
select id
from SOTEST N
where created_at = (
select min(N1.created_at)
from SOTEST N1
where N1.created_at >= '2017-07-26 00:00:00'
and N1.product_id = N.product_id
)
group by mainT.product_id
)
) BOWPRICE,
(
select mainB.product_id, mainB.updated_at max_price_upd_time, mainB.price max_price from SOTEST mainB
where id in(
select id from SOTEST M
where M.price = (
select max(M1.price)
from SOTEST M1
where M1.created_at >= '2017-07-26 00:00:00'
and M1.created_at < '2017-07-27 00:00:00'
and M1.product_id = M.product_id
group by product_id LIMIT 1
)
)
) MAXPRICE,
(
select mainC.product_id, mainC.updated_at min_price_upd_time, mainC.price min_price from SOTEST mainC
where id in(
select id from SOTEST Q
where Q.price = (
select min(Q1.price)
from SOTEST Q1
where Q1.created_at >= '2017-07-26 00:00:00'
and Q1.created_at < '2017-07-27 00:00:00'
and Q1.product_id = Q.product_id
group by product_id LIMIT 1
)
)
) MINPRICE
where BOWPRICE.product_id = MAXPRICE.product_id
and BOWPRICE.product_id = MINPRICE.product_id