MySQL: Range based on rows in external table - mysql

I am using MySQL to solve this problem. I need to give points to a user based on the total time spent by him on a question. I have calculated the time spent by the user. Let's say it is in user_time table.
user_id question_id time_spent
1 1 7
1 2 50
2 1 11
My points are range based:
[0-10) seconds: 100 points,
[10-20) seconds: 300 points,
[20-30) seconds: 500 points,
[30, inf): 1000 points
Exact 10 seconds will fetch me 300 points. Though, the changes of an exact number would be low given that I am computing from the system clock difference.
This information is currently scored in an external table points_table
time_spent points
0 100
10 300
20 500
30 1000
I need a query which finds out which range the seconds belong to and give me that result.
user_id question_id points
1 1 100
1 2 1000
2 1 300
I tried thinking of different type of joins but couldn't think of one which will answer this specific requirement.

I think the easiest approach is a correlated subquery. Something like this:
select ut.*,
(select pt.points
from points_table pt
where pt.time_spent <= ut.time_spent
order by pt.time_spent desc
limit 1
) as points
from user_time ut

Try this:
SELECT ut.user_id, ut.time_spent, A.points
FROM user_time ut
INNER JOIN (SELECT p1.time_spent AS time_spent1,
p2.time_spent AS time_spent2,
p1.points
FROM points_table p1
INNER JOIN points_table p2 ON p1.time_spent < p2.time_spent
GROUP BY p1.time_spent
) AS A ON ut.time_spent BETWEEN A.time_spent1 AND A.time_spent2

For another take on this, you could achieve the same result without having the points table:
SELECT *,
CASE
WHEN time_spent >= 30 THEN 1000
WHEN time_spent >= 20 THEN 500
WHEN time_spent >= 10 THEN 300
ELSE 100
END 'Points'
FROM user_time;

Related

MySQL Query to get the first user that sums X sales

I'm looking for help with a query.
I have a users table and a sales table. Sales are stored with their date, amount and quantity.
I need to get the first user that reaches an X sum of amount or Y sum of sales.
So I would need to take the sale date in account in order to know which user gets it first.
Can anyone give me a hand with this query or should I use another approach?
I'm currenlty using MySQL 8.0.32.
Here are some sample tables and an example of what is needed.
Users
id
username
1
user1
2
user2
Sales
id
amount
quantity
date
user_id
1
1000
2
2023-01-05
1
2
1500
3
2023-01-05
2
3
500
1
2023-01-07
1
4
1500
3
2023-01-10
1
5
500
1
2023-01-12
1
6
2500
5
2023-01-12
2
7
1000
2
2023-01-15
1
In this case, lets assume that the goal is the first user to sum >=4000. So user2 would be the winner as it reaches to 4000 before user1 even when at the end user1 sums 4500.
I would expect to get the lists of users untill the date that the first user get to the goal ie:
user_id
sum_amount
date
2
4000
2023-01-12
1
3500
2023-01-12
That would be super, but I could handle getting the first user got to the goal.
Thanks.
For MySql 8.0+ you can use SUM() window function to calculate the running total an sort the rows first by the rows that are equal or greater than 4000 and then the date:
SELECT user_id
FROM sales
ORDER BY SUM(amount) OVER(PARTITION BY user_id ORDER BY date) >= 4000 DESC,
date
LIMIT 1;
For previous versions of MySql use a correlated subquery that emulates the functionality of SUM() window function:
SELECT s1.user_id
FROM sales s1
ORDER BY (SELECT SUM(s2.amount)
FROM sales s2
WHERE s2.user_id = s1.user_id AND s2.date <= s1.date
) >= 4000 DESC,
s1.date
LIMIT 1;
See the demo.

MySQL Select records exceeding the cumulative total

Given I have following table
Id
FileSizeMB
1
100
2
100
3
100
4
100
5
100
6
100
I want to select oldest records exceeding a cumulative value, in this case say 500.
So something like this
Id
Cumulative_FileSizeMB
6
100
5
200
4
300
3
400
2
500
1
600
I want to select only records with id 2 and 1 as they are >= 500.
Goal is to delete them.
Thanks
For anyone with same problem.
I have reached this solution using Mysql window functions,
and also there is no need to declare a variable for cumulative total
SELECT * from (
SELECT
id,
FileSizeMB,
SUM(FileSizeMB) OVER (ORDER BY id DESC) AS TotalFileSizeMB
FROM table
) as t1
WHERE TotalFileSizeMB > 500

MySQL: Greatest n per group with joins and conditions

Table Structure
I have a table similar to the following:
venues
The following table describes a list of businesses
id name
50 Nando's
60 KFC
rewards
The table describes a number of rewards, the venue it corresponds to and the number of points needed to redeem the reward.
id venue_id name points
1 50 5% off 10
2 50 10% off 20
3 50 11% off 30
4 50 15% off 40
5 50 20% off 50
6 50 30% off 50
7 60 30% off 70
8 60 60% off 100
9 60 65% off 120
10 60 70% off 130
11 60 80% off 140
points_data
The table describes the number of points remaining a user has for each venue.
venue_id points_remaining
50 30
60 90
Note that this query is actually computed within SQL like so:
select * from (
select venue_id, (total_points - points_redeemed) as points_remaining
from (
select venue_id, sum(total_points) as total_points, sum(points_redeemed) as points_redeemed
from (
(
select venue_id, sum(points) as total_points, 0 as points_redeemed
from check_ins
group by venue_id
)
UNION
(
select venue_id, 0 as total_points, sum(points) as points_redeemed
from reward_redemptions rr
join rewards r on rr.reward_id = r.id
group by venue_id
)
) a
group by venue_id
) b
GROUP BY venue_id
) points_data
but for this question you can probably just ignore that massive query and assume the table is just called points_data.
Desired Output
I want to get a single query that gets:
The top 2 rewards the user is eligible for each venue
The lowest 2 rewards the user is not yet eligible for for each venue
So for the above data, the output would be:
id venue_id name points
2 50 10% off 20
3 50 11% off 30
4 50 15% off 40
5 50 20% off 50
7 60 30% off 70
8 60 60% off 100
9 60 65% off 120
What I got so far
The best solution I found so far is first getting the points_data, and then using code (i.e. PHP) to dynamically write the following:
(
select * from rewards
where venue_id = 50
and points > 30
ORDER BY points desc
LIMIT 2
)
union all
(
select * from rewards
where venue_id = 50
and points <= 30
ORDER BY points desc
LIMIT 2
)
UNION ALL
(
select * from rewards
where venue_id = 60
and points <= 90
ORDER BY points desc
LIMIT 2
)
UNION ALL
(
select * from rewards
where venue_id = 60
and points > 90
ORDER BY points desc
LIMIT 2
)
ORDER BY venue_id, points asc;
However, I feel the query can get a bit too long and in-efficient. For example, if a user has points in 400 venues, that is 800 sub-queries.
I tried also doing a join like so, but can't really get better than:
select * from points_data
INNER JOIN rewards on rewards.venue_id = points_data.venue_id
where points > points_remaining;
which is far from what I want.
Correlated subqueries counting the number of higher or lower rewards to determine the top or bottom entries are one way.
SELECT r1.*
FROM rewards r1
INNER JOIN points_data pd1
ON pd1.venue_id = r1.venue_id
WHERE r1.points <= pd1.points_remaining
AND (SELECT count(*)
FROM rewards r2
WHERE r2.venue_id = r1.venue_id
AND r2.points <= pd1.points_remaining
AND (r2.points > r1.points
OR r2.points = r1.points
AND r2.id > r1.id)) < 2
OR r1.points > pd1.points_remaining
AND (SELECT count(*)
FROM rewards r2
WHERE r2.venue_id = r1.venue_id
AND r2.points > pd1.points_remaining
AND (r2.points < r1.points
OR r2.points = r1.points
AND r2.id < r1.id)) < 2
ORDER BY r1.venue_id,
r1.points;
SQL Fiddle
Since MySQL 8.0 a solution using the row_number() window function would be an alternative. But I suppose you are on a lower version.
SELECT x.id,
x.venue_id,
x.name,
x.points
FROM (SELECT r.id,
r.venue_id,
r.name,
r.points,
pd.points_remaining,
row_number() OVER (PARTITION BY r.venue_id,
r.points <= pd.points_remaining
ORDER BY r.points DESC) rntop,
row_number() OVER (PARTITION BY r.venue_id,
r.points > pd.points_remaining
ORDER BY r.points ASC) rnbottom
FROM rewards r
INNER JOIN points_data pd
ON pd.venue_id = r.venue_id) x
WHERE x.points <= x.points_remaining
AND x.rntop <= 2
OR x.points > x.points_remaining
AND x.rnbottom <= 2
ORDER BY x.venue_id,
x.points;
db<>fiddle
The tricky part is here to partition the set also into the subset where the points of the user are enough to redeem the reward and the one where the points aren't enough, per venue. But as in MySQL logical expressions evaluate to 0 or 1 (in non Boolean context), the respective expressions can be used for that.

MySql Select - row subtract previous row

I'm trying to extract stats from DB.
Table's structure is:
UpdatedId product_name revenue
980 Product1 1000
975 Product1 950
973 Product1 900
970 Product1 800
965 Product21 1200
So revenue = previous revenue + new revenue.
In order to make graphs, the goal is to get the output for Product1 like this
UpdateId Difference
980 50
975 50
973 100
970 0
I tried this query but MySQL gets stuck :)
select a.product_name, a.revenue, b.revenue, b.revenue- a.revenue as difference from updated_stats a, updated_stats b where a.product_name=b.product_name and b.revenue= (select min(revenue) from updated_stats where product_name=a.product_name and revenue > a.revenue and product_name= 'Product1')
Could you please tell me, how it should be queried? Thanks.
I would do this with a correlated subquery:
select u.*,
(select u.revenue - u2.revenue
from updated_stats u2
where u2.product_name = u.product_name and
u2.updatedid < u.updatedid
order by u2.updatedid desc
limit 1
) as diff
from updated_stats u;
Note: This returns NULL instead of 0 for 970. That actually makes more sense to me. But you can use COALESCE() or a similar function to turn it into a 0.
If updated_stats is even moderately sized, you will want an index on updated_status(product_name, updated_id, revenue). This index covers the subquery.

How can I have a column with sum of smaller IDs in MySQL?

Assume I have a table like this:
id pay
-- ---
1 10
2 20
3 30
4 40
5 50
6 60
I want to create a view from table above with this result:
id pay paid_before
-- --- -------------
1 10 0
2 20 10
3 30 30
4 40 60
5 50 100
6 60 150
which "paid_before" is sum of pay rows that have smaller id.
How could I do this job?
This accomplishes what you want.
SELECT p1.id,p1.pay, sum(p2.pay) as Paid_Before FROM PAYMENTS P1 LEFT JOIN
PAYMENTS P2 ON p1.id > p2.id
GROUP BY p1.id, p1.pay
See this sql fiddle
In MySQL, this is most efficiently done with variables:
select p.id, p.pay, (#p := #p + p.pay) - p.pay as PaidBefore
from payments p cross join
(select #p := 0) vars
order by id;
Although this is not standard SQL (which I usually prefer), that is okay. The standard SQL solution is to use cumulative sum:
select p.id, p.pay, sum(p.pay) over (order by p.id) - p.pay as PaidBefore
from payments p;
Many databases support this syntax, but not MySQL.
The SQL Fiddle (courtesy of Atilla) is here.