i've tried some other topics for this but couldn't get answers that actually worked for me.
I have a activities table with some values ( in mysql)
| id| user_id | elevation | distance |
|---|------------|--------------------|----------|
| 1 | 1 | 220 | 5000 |
| 2 | 1 | 300 | 7000 |
| 3 | 2 | 520 | 2000 |
| 4 | 2 | 120 | 3500 |
I need to sum distance and elevation until distance sum up to certain value, per user_id.
Example, sum until 5000 is reached:
User 1 - distance 5000 - elevation 220
User 2 - distance 5500 - elevation 640
I found many solutions but none with group_by. How i do this in mysql?
Update : I used that query but now i'm with another problem. The join always use the insert order, and not a datetime field i want.
SELECT
t.*
FROM
(
SELECT
t.*,
(
#d := #d + DISTANCE
) AS running_distance
FROM
(
SELECT
t.*,
c.meta
FROM
inscricao i
INNER JOIN categorias c ON
i.categoria_id = c.id
LEFT JOIN(
select
t.data_inicio,t.usuario_id,t.aplicativo,t.data_fim,t.distance,t.tempo_decorrido,t.ritmo_cardiaco,t.velocidade_media,t.type,t.ganho_de_altimetria
from
corridas t
order by
data_inicio asc
) t ON
t.usuario_id = i.usuario_id
AND t.data_inicio >= i.inicio
AND t.data_fim <= i.fim
WHERE
i.desafio_id = 29
AND(
i.usuario_id = 5354
)
ORDER BY
data_inicio asc
-- usuario_id
) t
join (
SELECT
#u :=- 1,
#d := 0
) params
ORDER BY
data_inicio asc
) t
WHERE
(
running_distance >= meta * 1000
AND running_distance - DISTANCE < meta * 1000
)
OR(
running_distance <= meta * 1000
)
order by
data_inicio desc
So if a older activity is inserted after, the sum gets wrong. Someone knows how to handle it?
You can use variables to get the cumulative sum . . . then some simple filtering logic:
select t.*
from (select t.*,
(#d := if(#u = user_id, #d + distance,
if(#u := user_id, distance, distance)
)
) as running_distance -- pun intended ??
from (select t.*
from t
order by user_id, id
) t cross join
(select #u := -1, #d := 0) params
) t
where running_distance >= 5000 and
running_distance - distance < 5000;
Notes:
The more recent versions of MySQL are finicky about variable assignment and order by. The innermost subquery is not needed in earlier versions of MySQL.
MySQL does not guarantee the order of evaluation of expressions in a select. Hence, all variable assignments are in a single expression.
If distance can be negative, then a user may have more than one row in the result set.
This is not an aggregation query.
Related
I have this table called my_users
my_id | name | raffle_tickets
1 | Bob | 3
2 | Sam | 59
3 | Bill | 0
4 | Jane | 10
5 | Mike | 12
As you can see Sam has 59 tickets so he has the highest chance of winning.
Chance of winning:
Sam = 59/74
Bob = 3/74
Jane = 10/74
Bill = 0/74
Mike = 12/74
PS: 74 is the number of total tickets in the table (just so you know I didn't randomly pick 74)
Based on this, how can I randomly pick a winner, but ensure those who have more raffles tickets have a higher chance of being randomly picked? Then the winner which is picked, has 1 ticket deducted from their total tickets
UPDATE my_users
SET raffle_tickets = raffle_tickets - 1
WHERE my_id = --- Then I get stuck here...
Server version: 5.7.30
For MySQL 8+
WITH
cte1 AS ( SELECT name, SUM(raffle_tickets) OVER (ORDER BY my_id) cum_sum
FROM my_users ),
cte2 AS ( SELECT SUM(raffle_tickets) * RAND() random_sum
FROM my_users )
SELECT name
FROM cte1
CROSS JOIN cte2
WHERE cum_sum >= random_sum
ORDER BY cum_sum LIMIT 1;
For 5+
SELECT cte1.name
FROM ( SELECT t2.my_id id, t2.name, SUM(t1.raffle_tickets) cum_sum
FROM my_users t1
JOIN my_users t2 ON t1.my_id <= t2.my_id
WHERE t1.raffle_tickets > 0
GROUP BY t2.my_id, t2.name ) cte1
CROSS JOIN ( SELECT RAND() * SUM(raffle_tickets) random_sum
FROM my_users ) cte2
WHERE cte1.cum_sum >= cte2.random_sum
ORDER BY cte1.cum_sum LIMIT 1;
fiddle
You want a weighted pull from a random sample. For this purpose, variables are probably the most efficient solution:
select u.*
from (select u.*, (#t := #t + raffle_tickets) as running_tickets
from my_users u cross join
(select #t := 0, #r := rand()) params
where raffle_tickets > 0
) u
where #r >= (running_tickets - raffle_tickets) / #t and
#r < (running_tickets / #t);
What this does is calculate the running sum of tickets and then divide by the number of tickets to get a number between 0 and 1. For example this might produce:
my_id name raffle_tickets running_tickets running_tickets / #t
1 Bob 3 3 0.03571428571428571
2 Sam 59 62 0.7380952380952381
4 Jane 10 72 0.8571428571428571
5 Mike 12 84 1
The ordering of the original rows doesn't matter -- which is why there is no ordering in the subquery.
The ratio is then used with rand() to select a particular row.
Note that in the outer query, #t is the total number of tickets.
Here is a db<>fiddle.
Let's say I have table (numbers) with one column:
n
---
4
5
67
23
7
89
and I want to get median (only even list of numbers). I thought it would be easy so I wrote:
SELECT SUM(ord.n)/2
FROM (
SELECT n
FROM numbers
ORDER BY n ASC
LIMIT 2 OFFSET (SELECT COUNT(n)/2-1 FROM numbers)
) AS ord
but of course it throw me syntax error. I guess I can't insert subquery into offset but I want to know what I have to do to get expected result? I know there are different ways to write query to get median but I need to know is there any possibility to insert 'variable' into offset instead of placing some number?
I think you are looking for the query below this should work from MySQL version 5.1 and up
SELECT
AVG(filter.n)
FROM (
SELECT
*
, (#position := #position + 1) AS init_position
FROM
t
CROSS JOIN (
SELECT
#position := 0
, #max := (SELECT COUNT(t.n) FROM t)
, #median_mode := (CASE WHEN ((#max % 2) = 0) THEN 'even' ELSE 'odd' END)
) AS init_user_param
ORDER BY
t.n ASC
) AS filter
WHERE
CASE
WHEN #median_mode = 'even'
THEN filter.init_position BETWEEN (#max / 2) AND ((#max / 2) + 1)
WHEN #median_mode = 'odd'
THEN filter.init_position = ((#max + 1) / 2)
END
Result
| AVG(filter.n) |
| ------------- |
| 15 |
see demo
Result when 89 is out the list.
| AVG(filter.n) |
| ------------- |
| 7 |
see demo
You can use ROW_NUMBER() window function:
WITH cte AS (SELECT COUNT(*) counter FROM numbers)
SELECT AVG(n) median
FROM (
SELECT
row_number() over (order by n) ordinal,
n
FROM numbers
) t
WHERE (SELECT counter FROM cte) IN (2 * ordinal, 2 * (ordinal - 1))
See the demo.
Result:
| median |
| ------ |
| 15 |
The (probably) most similar solution to your algorithm is to use two queries. First get the offset. Then insert it into your query. The SQL-only way would be to use a prepared statement:
set #offset = (SELECT COUNT(n)/2-1 FROM numbers);
set #sql = "
SELECT SUM(ord.n)/2
FROM (
SELECT n
FROM numbers
ORDER BY n ASC
LIMIT 2 OFFSET ?
) AS ord
";
prepare stmt from #sql;
execute stmt using #offset;
db-fiddle
I am trying to update position for my player in present in table.
This table consists of name , id, points and position.
Default value of points is 0 then position will be Unranked.
If two users have same points then there positions will be same.
Demo table
id | name | points | position
1 | a | 0 | Unranked
2 | b | 120 | 2
3 | c | 130 | 3
4 | d | 120 | 1
Required result should be
id | name | points | position
1 | a | 0 | Unranked
2 | b | 120 | 2
3 | c | 130 | 1
4 | d | 120 | 2
Query will be like for unranked update mytable set position = 'Unranked' Where points = 0
How will i use points and position set query ?
There's no need to hold the computed column position in the table. The following works for all versions :
create table tab ( id int, name varchar(1), points int );
insert into tab values
(1,'a', 0),
(2,'b',120),
(3,'c',130),
(4,'d',120);
select t.id, t.name, t.points,
( case when points = 0 then 'Unranked' else t.rnk end ) as position
from
(
select t1.*,
#rnk := if(#pnt = points,#rnk,#rnk + 1) rnk,
#pnt := points
from tab t1
cross join (select #rnk := 0, #pnt := 0 ) t2
order by points desc
) t
order by t.id;
id name points position
-- ---- ------ --------
1 a 0 Unranked
2 b 120 2
3 c 130 1
4 d 120 2
If you want to hold the column position in your table, then you can use the following update statement by binding through primary column id :
update tab tt
set position = ( select
( case when points = 0 then 'Unranked' else t.rnk end ) as position
from
(
select t1.*,
#rnk := if(#pnt = points,#rnk,#rnk + 1) rnk,
#pnt := points
from tab t1
cross join (select #rnk := 0, #pnt := 0 ) t2
order by points desc
) t
where t.id = tt.id );
Rextester Demo
This is a pain. You can get the results you want with a subquery, but that doesn't quite work in an update clause. In a select, you can do:
select t.*,
(select 1 + count(*)
from t t2
where t2.points > 0 and t2.points > t.points
) as rank
from t;
You can now incorporate this into an update:
update t join
(select t.*,
(select 1 + count(*)
from t t2
where t2.points > 0 and t2.points > t.points
) as new_position
from t;
) tt
on t.id = tt.id
set t.position = tt.new_position
where t.points > 0;
If your version of MySQl (MySQL 8.x) supports window function then following is possible:
SELECT name,
RANK() OVER (
ORDER BY points DESC
) position
FROM mytable
where points != 0
Selected data can be then joined for the update like in the answer from Gordon Linoff.
Let's say I have the following table:
user_id | category_id | points
-------------------------------
1 | 1 | 4
2 | 1 | 2
2 | 1 | 5
1 | 2 | 3
2 | 2 | 2
1 | 3 | 1
2 | 3 | 4
1 | 3 | 8
Could someone please help me to construct a query to return user's rank per category - something like this:
user_id | category_id | total_points | rank
-------------------------------------------
1 | 1 | 4 | 2
1 | 2 | 3 | 1
1 | 3 | 9 | 1
2 | 1 | 7 | 1
2 | 2 | 2 | 2
2 | 3 | 4 | 2
First, you need to get the total points per category. Then you need to enumerate them. In MySQL this is most easily done with variables:
SELECT user_id, category_id, points,
(#rn := if(#cat = category_id, #rn + 1,
if(#cat := category_id, 1, 1)
)
) as rank
FROM (SELECT u.user_id, u.category_id, SUM(u.points) as points
FROM users u
GROUP BY u.user_id, u.category_id
) g cross join
(SELEct #user := -1, #cat := -1, #rn := 0) vars
ORDER BY category_id, points desc;
You want to get the SUM of points for each unique category_id:
SELECT u.user_id, u.category_id, SUM(u.points)
FROM users AS u
GROUP BY uc.category_id
MySQL doesn't have analytic functions like other databases (Oracle, SQL Server) which would be very convenient for returning a result like this.
The first three columns are straightforward, just GROUP BY user_id, category_id and a SUM(points).
Getting the rank column returned is a bit more of a problem. Aside from doing that on the client, if you need to do that in the SQL statement, you could make use of MySQL user-defined variables.
SELECT #rank := IF(#prev_category = r.category_id, #rank+1, 1) AS rank
, #prev_category := r.category_id AS category_id
, r.user_id
, r.total_points
FROM (SELECT #prev_category := NULL, #rank := 1) i
CROSS
JOIN ( SELECT s.category_id, s.user_id, SUM(s.points) AS total_points
FROM users s
GROUP BY s.category_id, s.user_id
ORDER BY s.category_id, total_points DESC
) r
ORDER BY r.category_id, r.total_points DESC, r.user_id DESC
The purpose of the inline view aliased as i is to initialize user defined variables. The inline view aliased as r returns the total_points for each (user_id, category_id).
The "trick" is to compare the category_id value of the previous row with the value of the current row; if they match, we increment the rank by 1. If it's a "new" category, we reset the rank to 1. Note this only works if the rows are ordered by category, and then by total_points descending, so we need the ORDER BY clause. Also note that the order of the expressions in the SELECT list is important; we need to do the comparison of the previous value BEFORE it's overwritten with the current value, so the assignment to #prev_category must follow the conditional test.
Also note that if two users have the same total_points in a category, they will get distinct values for rank... the query above doesn't give the same rank for a tie. (The query could be modified to do that as well, but we'd also need to preserve total_points from the previous row, so we can compare to the current row.
Also note that this syntax is specific to MySQL, and that this is behavior is not guaranteed.
If you need the columns in the particular sequence and/or the rows in a particular order (to get the exact resultset specified), we'd need to wrap the query above as an inline view.
SELECT t.user_id
, t.category_id
, t.total_points
, t.rank
FROM (
SELECT #rank := IF(#prev_category = r.category_id, #rank+1, 1) AS rank
, #prev_category := r.category_id AS category_id
, r.user_id
, r.total_points
FROM (SELECT #prev_categor := NULL, #rank := 1) i
CROSS
JOIN ( SELECT s.category_id, s.user_id, SUM(s.points) AS total_points
FROM users s
GROUP BY s.category_id, s.user_id
ORDER BY s.category_id, total_points DESC
) r
ORDER BY r.category_id, r.total_points DESC, r.user_id DESC
) t
ORDER BY t.user_id, t.category_id
NOTE: I've not setup a SQL Fiddle demonstration. I've given an example query which has only been desk checked.
Suppose a table, tableX, like this:
| date | hours |
| 2014-07-02 | 10 |
| 2014-07-03 | 10 |
| 2014-07-07 | 20 |
| 2014-07-08 | 40 |
The dates are 'workdays' -- that is, no weekends or holidays.
I want to find the increase in hours between consecutive workdays, like this:
| date | hours |
| 2014-07-03 | 0 |
| 2014-07-07 | 10 |
| 2014-07-08 | 20 |
The challenge is dealing with the gaps. If there were no gaps, something like
SELECT t1.date1 AS 'first day', t2.date1 AS 'second day', (t2.hours - t1.hours)
FROM tableX t1
LEFT JOIN tableX t2 ON t2.date1 = DATE_add(t1.date1, INTERVAL 1 DAY)
ORDER BY t2.date1;
would get it done, but that doesn't work in this case as there is a gap between 2014-07-03 and 2014-07-07.
Just use a correlated subquery instead. You have two fields, so you can do this with two correlated subqueries, or a correlated subquery with a join back to the table. Here is the first version:
SELECT t1.date1 as `first day`,
(select t2.date1
from tableX t2
where t2.date1 > t.date1
order by t2.date asc
limit 1
) as `next day`,
(select t2.hours
from tableX t2
where t2.date1 > t.date1
order by t2.date asc
limit 1
) - t.hours
FROM tableX t
ORDER BY t.date1;
Another alternative is to rank the data by date and then subtract the hours of the previous workday's date from the hours of the current workday's date.
SELECT
ranked_t1.date1 date,
ranked_t1.hours - ranked_t2.hours hours
FROM
(
SELECT t.*,
#rownum := #rownum + 1 AS rank
FROM (SELECT * FROM tableX ORDER BY date1) t,
(SELECT #rownum := 0) r
) ranked_t1
INNER JOIN
(
SELECT t.*,
#rownum2 := #rownum2 + 1 AS rank
FROM (SELECT * FROM tableX ORDER BY date1) t,
(SELECT #rownum2 := 0) r
) ranked_t2
ON ranked_t2.rank = ranked_t1.rank - 1;
SQL Fiddle demo
Note:
Obviously an index on tableX.date1 would speed up the query.
Instead of a correlated subquery, a join is used in the above query.
Reference:
Mysql rank function on SO
Unfortunately, MySQL doesn't (yet) have analytic functions which would allow you to access the "previous row" or the "next row" of the data stream. However, you can duplicate it with this:
select h2.LogDate, h2.Hours - h1.Hours as Added_Hours
from Hours h1
left join Hours h2
on h2.LogDate =(
select Min( LogDate )
from Hours
where LogDate > h1.LogDate )
where h2.LogDate is not null;
Check it out here. Note the index on the date field. If that field is not indexed, this query will take forever.