I have a table like this, where every status change for every character is saved.
table characters_changes
+----+--------+---------+-------+----------------------+
| id | rank | job | money | datetime |
+----+--------+---------+-------+----------------------+
| 1 | 2 | tailor | 25 | 2018-06-01 12:30:15 |
| 1 | 3 | NULL | 5 | 2018-06-02 10:50:19 |
| 1 | 2 | NULL | -5 | 2018-06-03 18:44:35 |
| 1 | NULL | tinker | 10 | 2018-06-04 04:10:12 |
| 1 | 3 | NULL | NULL | 2018-06-05 17:31:00 |
| 2 | 1 | spy | 7 | 2018-06-01 12:30:15 |
| 2 | 2 | NULL | NULL | 2018-06-02 10:50:19 |
| 2 | NULL | no job | 7 | 2018-06-03 17:31:00 |
| 3 | 3 | soldier | 12 | 2018-06-01 12:30:15 |
| 3 | 1 | NULL | -11 | 2018-06-02 10:50:19 |
+----+--------+---------+-------+----------------------+
NULL means that there was no change at corresponding attribute.
rank and job changes mean replace one with another,
while money change means adding and subtracting the sum (if not NULL). There is guaranteed at least one row per character without any NULL-s.
So I need to get a table where I could show current status of every character at the end.
With their last rank, last job and resulting sum of money. A table like this one.
table characters_status
+----+--------+---------+-------+
| id | rank | job | money |
+----+--------+---------+-------+
| 1 | 3 | tinker | 35 |
| 2 | 2 | no job | 14 |
| 3 | 1 | soldier | 1 |
+----+--------+---------+-------+
What even worse, table characters_changes is a temporary table.
datetime in it comes from another table of events.
So as it is temporary, I can only query it once. But there can be any number of characters and most likely going to be more columns like rank and job.
The whole system is needed to provide the possibility to get statuses for all characters at any given datetime by ignoring all changes after that. But that part is easy for me, so I left it out of scope of my question.
This should work, I think:
SELECT id
, CASE WHEN INSTR(ranks, '|') = 0 THEN ranks ELSE LEFT(ranks, INSTR(ranks, '|')-1) END AS rank
, CASE WHEN INSTR(jobs, '|') = 0 THEN jobs ELSE LEFT(jobs, INSTR(jobs, '|')-1) END AS job
, monies
FROM
(
SELECT id
, GROUP_CONCAT(rank ORDER BY datetime DESC SEPARATOR '|') AS ranks
, GROUP_CONCAT(job ORDER BY datetime DESC SEPARATOR '|') AS jobs
, SUM(money) AS monies
FROM characters_changes
GROUP BY id
) AS lists
;
Technically, you can do it without a subquery, but I broke it down this way for clarity. The alternative would be expressions like this:
, CASE COUNT(rank) WHEN 0 THEN NULL WHEN 1 THEN GROUP_CONCAT(rank ORDER BY datetime DESC SEPARATOR '|') ELSE LEFT(GROUP_CONCAT(rank ORDER BY datetime DESC SEPARATOR '|'), INSTR(GROUP_CONCAT(rank ORDER BY datetime DESC SEPARATOR '|'), '|')-1) END AS rank
Related
This question already has answers here:
SQL select only rows with max value on a column [duplicate]
(27 answers)
Closed 3 years ago.
I'm struggling to do something in SQL which I'm sure must be simple, but I can't figure it out. I want the MAX() value of a group, but I also want the value of another column in the same row as the max value. Here is an example table definition:
mysql> desc Sales;
+---------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| StoreID | int(11) | YES | | NULL | |
| Day | int(11) | YES | | NULL | |
| Amount | int(11) | YES | | NULL | |
+---------+---------+------+-----+---------+-------+
3 rows in set (0.00 sec)
And here is some data for it:
mysql> SELECT * FROM Sales;
+---------+------+--------+
| StoreID | Day | Amount |
+---------+------+--------+
| 1 | 1 | 44 |
| 1 | 2 | 31 |
| 1 | 3 | 91 |
| 2 | 1 | 93 |
| 2 | 2 | 32 |
| 2 | 3 | 41 |
| 3 | 1 | 48 |
| 3 | 2 | 95 |
| 3 | 3 | 12 |
+---------+------+--------+
9 rows in set (0.00 sec)
What I want to know is, what Day had the most sales (Amount) for each StoreID.
Now I know I can do this:
SELECT StoreID, MAX(Amount) FROM Sales GROUP BY StoreID;
+---------+-------------+
| StoreID | MAX(Amount) |
+---------+-------------+
| 1 | 91 |
| 2 | 93 |
| 3 | 95 |
+---------+-------------+
3 rows in set (0.00 sec)
That tells me the max amount of each store, but really what I'm after is the day that it occured. But I can't add Day back in to the query because it's not in the group by, and I don't think I really want to group by that value do I?
I'm not sure where to go from here.
In short, the results I want should look like this:
+---------+------+--------+
| 1 | 3 | 91 |
| 2 | 1 | 93 |
| 3 | 2 | 95 |
+---------+------+--------+
You want to filter. Here is one simple method using a correlated subquery:
select s.*
from s
where s.sales = (select max(s2.sales)
from sales s2
where s2.storeId = s.storeId
);
If your data is on the large side, you will want an index on sales(storeId, sales).
For the maximum amounts per store there won't exist a higher amount for the same store.
SELECT *
FROM Sales s
WHERE NOT EXISTS (
SELECT 1
FROM Sales s2
WHERE s2.StoreID = s.StoreID
AND s2.Amount > s.Amount
)
ORDER BY Amount ASC, StoreID ASC;
Test here
Typically you can just join the aggregating query back to get the rest of the row data...
SELECT s.*
FROM Sales AS s
INNER JOIN (
SELECT StoreID, MAX(Amount) AS MaxAmount
FROM Sales
GROUP BY StoreID
) AS m ON s.StoreID = m.StoredID AND s.Amount = m.MaxAmount
;
If there are multiple Sales with the MaxAmount for the same StoreID, the query will return all of them, not just one of them.
game table:
| id | group_id | user_id | last_update | bonus |
| 1 | 4 | 1 | 2017-01-22 00:06:10 | 0 |
| 2 | 4 | 1 | 2017-01-12 00:11:34 | 300 |
| 3 | 4 | 1 | 2017-01-02 00:30:44 | -111 |
| 3 | 4 | 1 | 2017-02-02 00:21:44 | 4330 |
| 3 | 4 | 6 | 2017-01-02 01:02:27 | 30 |
| 3 | 4 | 6 | 2017-01-07 11:22:37 | 40 |
| 3 | 4 | 6 | 2017-03-04 11:22:37 | 0 |
I want to calculate bonus of the current date minus the bonus of the first day of the current month for every user of a given group.
The wanted output:
| user_id | january (last day bonus - first day bonus) |
| 5 | 1400 |
| 19 | 1377 |
| 1 | 806 |
| 14 | 140 |
| 50 | 14 |
Currently, I'm getting bonuses of the given month (1 query), calculating the difference between the last and first ones. I have 4000 users, so I'm performing 4000 queries to do what I want and it's too slow.
Is it possible to do that with only mysql?
Perhaps the "most correct" way would be to use variables or complex subqueries. However, another way that should work is to use group_concat()/substring_index():
select user_id, date_format(last_update, '%Y-%m') as yyyymm,
substring_index(group_concat(bonus order by last_update desc), ',', 1) as last_bonus,
substring_index(group_concat(bonus order by last_update asc), ',', 1) as first_bonus,
(substring_index(group_concat(bonus order by last_update desc), ',', 1) -
substring_index(group_concat(bonus order by last_update asc), ',', 1)
) as bonus_diff
from t
group by user_id, yyyymm;
Note that this converts the bonus to a string -- and then back again to a number for the calculation. That is why I might call this "quick-and-dirty" or a "hack". However, it should work and the conversions are safe because the values start out as numbers.
Second, group_concat() has a default limit of 1024 bytes. That should not be a problem for these aggregations -- unless you have hundreds of rows for a user within a month.
It was very tricky to figure out what to title this question, so if anyone has any ideas for improvements feel free to edit :-).
Here's the deal. I have a MySQL table that includes a bunch of donations, and there's a date for each donation. I also have a years_active column. I need to run a query that will SET the years active for every row to the difference (in years) from the first date to the last date for each unique user.
So this is my starting table:
------------------------------------------------------------
| user_id | donation | date | years_active |
------------------------------------------------------------
| 1 | $10 | 2002-01-01 | null |
| 1 | $15 | 2005-01-01 | null |
| 1 | $20 | 2009-01-01 | null |
| 2 | $10 | 2003-01-01 | null |
| 2 | $5 | 2006-01-01 | null |
| 3 | $15 | 2001-01-01 | null |
------------------------------------------------------------
And this is the table I'd like to achieve:
------------------------------------------------------------
| user_id | donation | date | years_active |
------------------------------------------------------------
| 1 | $10 | 2002-01-01 | 8 |
| 1 | $15 | 2005-01-01 | 8 |
| 1 | $20 | 2009-01-01 | 8 |
| 2 | $10 | 2003-01-01 | 4 |
| 2 | $5 | 2006-01-01 | 4 |
| 3 | $15 | 2001-01-01 | 1 |
------------------------------------------------------------
I know that it's far from ideal to be storing the years_active redundantly in multiple rows like this. Unfortunately this table is for data visualizations and with my software I have absolutely no ability to restructure the data altogether; the years_active MUST be in every row.
In my research it seems like I would use subqueries to get the MIN value for each user id and the MAX value for each unique user id, and then do a DATEDIFF on those, and set the result to the column. But I don't really understand how I would run all these queries over and over again for every unique user.
Can someone point me in the right direction? Is this possible?
SELECT t1.user_id, t1.donation, t1.date, t2.years_active
FROM yourTable t1
INNER JOIN
(
SELECT user_id, MAX(YEAR(date)) - MIN(YEAR(date)) + 1 AS years_active
FROM yourTable
GROUP BY user_id
) t2
ON t1.user_id = t2.user_id
Follow the link below for a running demo:
SQLFiddle
Update:
Here is an UPDATE statement which will assign the years_active column the correct values:
UPDATE yourTable t1
INNER JOIN
(
SELECT user_id, MAX(YEAR(date)) - MIN(YEAR(date)) + 1 AS years_active
FROM yourTable
GROUP BY user_id
) t2
ON t1.user_id = t2.user_id
SET t1.years_active = t2.years_active
I have table of orders. Each customer (identified by the email field) has his own orders. I need to give a different sequence of order numbers for each customer. Here is example:
----------------------------
| email | number |
----------------------------
| test#com.com | 1 |
----------------------------
| example#com.com | 1 |
----------------------------
| test#com.com | 2 |
----------------------------
| test#com.com | 3 |
----------------------------
| client#aaa.com | 1 |
----------------------------
| example#com.com | 2 |
----------------------------
Is possible to do that in a simple way with mysql?
If you want update data in this table after an insert, first of all you need a primary key, a simple auto-increment column does the job.
After that you can try to elaborate various script to fill the number column, but as you can see from other answer, they are not so "simple way".
I suggest to assign the order number in the insert statement, obtaining the order number with this "simpler" query.
select coalesce(max(`number`), 0)+1
from orders
where email='test1#test.com'
If you want do everything in a single insert (better for performance and to avoid concurrency problems)
insert into orders (email, `number`, other_field)
select email, coalesce(max(`number`), 0) + 1 as number, 'note...' as other_field
from orders where email = 'test1#test.com';
To be more confident about not assign at the same customer two orders with the same number, I strongly suggest to add an unique constraint to the columns (email,number)
create a column order_number
SELECT #i:=1000;
UPDATE yourTable SET order_number = #i:=#i+1;
This will keep incrementing the column value in order_number column and will start right after 1000, you can change the value or even you can even use the primary key as the order number since it is unique all the time
I think one more need column for this type of out put.
Example
+------+------+
| i | j |
+------+------+
| 1 | 11 |
| 1 | 12 |
| 1 | 13 |
| 2 | 21 |
| 2 | 22 |
| 2 | 23 |
| 3 | 31 |
| 3 | 32 |
| 3 | 33 |
| 4 | 14 |
+------+------+
You can get this result:
+------+------+------------+
| i | j | row_number |
+------+------+------------+
| 1 | 11 | 1 |
| 1 | 12 | 2 |
| 1 | 13 | 3 |
| 2 | 21 | 1 |
| 2 | 22 | 2 |
| 2 | 23 | 3 |
| 3 | 31 | 1 |
| 3 | 32 | 2 |
| 3 | 33 | 3 |
| 4 | 14 | 1 |
+------+------+------------+
By running this query, which doesn't need any variable defined:
SELECT a.i, a.j, count(*) as row_number FROM test a
JOIN test b ON a.i = b.i AND a.j >= b.j
GROUP BY a.i, a.j
Hope that helps!
You can add number using SELECT statement without adding any columns in table orders.
try this:
SELECT email,
(CASE email
WHEN #email
THEN #rownumber := #rownumber + 1
ELSE #rownumber := 1 AND #email:= email END) as number
FROM orders
JOIN (SELECT #rownumber:=0, #email:='') AS t
I have a MySQL table which contains a number of products. What I want to do is sort the table by one particular column (most of the values begin with numbers for example: 1st, 2nd), etc. However, since some records do not have a value for this column, when I try to sort, the table automatically puts empty rows FIRST.
I am looking for a way to sort the row ASCENDING, but only insert blank records at the end of the sorted records, if that makes sense?
Any help would be most gratefully received!
select * from table
order by if(field = '' or field is null,1,0),field
This is one of the most effective method
ASC Order
SELECT * FROM user ORDER BY name IS NULL, name ASC
Expected Result:
+----+--------+------------+
| id | name | date_login |
+----+--------+------------+
| 3 | david | 2016-12-24 |
| 2 | john | NULL |
| 4 | zayne | 2017-03-02 |
| 1 | NULL | 2017-03-12 |
DESC Order
SELECT * FROM user ORDER BY name IS NULL, name DESC
Expected Result:
+----+--------+------------+
| id | name | date_login |
+----+--------+------------+
| 4 | zayne | 2017-03-02 |
| 2 | john | NULL |
| 3 | david | 2016-12-24 |
| 1 | NULL | 2017-03-12 |