subtract multiple dates within one id - mysql

I'm trying to accomplish something in mySQL, which is difficult due to the lack of a "lag" function and because of all the examples I've read regarding date differences, since in all those examples there's always one ID to one date. In here, I am trying to do the date differences within an ID, and when the date difference is over 60, then returning an indicator of 1, else 0.
Not sure what the best way is to go about this. Would it be some combination of using row_number() with the date? The snag is doing this within multiple IDs, as a lot of things I read don't cover that. Any direction would be helpful.
Thanks!
ID | Service Date | Date Difference | Indicator
1 | 1/22/2016 | 0 | 1
1 | 3/26/2016 | 64 | 1
1 | 5/25/2016 | 60 | 0
1 | 9/15/2016 | 113 | 1
2 | 8/1/2016 | 0 | 1
3 | 1/26/2016 | 0 | 1
3 | 3/9/2016 | 43 | 0
3 | 4/30/2016 | 52 | 0
4 | 8/9/2016 | 0 | 1
5 | 11/19/2016 | 0 | 1
6 | 10/14/2016 | 0 | 1
7 | 1/31/2016 | 0 | 1
7 | 8/11/2016 | 193 | 1

You can use variables, but this is tricky. For this to work reliably, all the variables need to be assigned in a single expression:
select t.*, datediff(prev_date, date) as diff,
(case when datediff(prev_date, date) < 60 then 0 else 1 end) as indicator
from (select t.*,
(case when #id = id
then (case when (#prev := #d) = NULL then 'never' -- intentional
when (#d := date) = NULL then 'never' -- intentional
else #prev
end)
when (#d := date) = NULL then 'never' -- intentional
else NULL
end) as prev_date
from t cross join
(select #id := -1, #d := '') params
order by id, date
) t

create view id_and_date as
select id, service_date from your table;
create view id_and_date_and_prior as
select
a.id, a.service_date,
coalesce(
(select max(b.service_date) from id_and_date b
where b.id = a.id and b.service_date < a.service_date),
a.service_date)
as prior_date
from id_and_date a
select a.id, a.service_date, a.prior_date
date_diff(a.service_date, a.prior_date) as diff,
case when date_diff(a.service_date, a.prior_date) > 60
then 1 else 0 end
as indicator
from id_and_date_and_prior a

Posting to simplify and correct the function calls from the answer provided by #tpdi. Please accept/upvote their answer, as this was pretty much copied from it.
Changes:
date_diff to DATEDIFF
removed create view calls in favor of subquery of t
assigned variable to the diff value
indicator of 0 on initial value as 0 diff to 1
replaced when case in favor of IF
SELECT
c.id,
c.service_date,
#diff := DATEDIFF(c.service_date, c.prior_date) AS diff,
IF(#diff = 0 || #diff > 60, 1, 0) AS indicator
FROM (
SELECT
a.id,
a.service_date,
COALESCE(
(SELECT MAX(b.service_date)
FROM t AS b
WHERE b.id = a.id
AND b.service_date < a.service_date),
a.service_date
) AS prior_date
FROM t AS a
) AS c;
Will result in:
| id | service_date | diff | indicator |
| 1 | 2016-01-22 | 0 | 1 |
| 1 | 2016-03-26 | 64 | 1 |
| 1 | 2016-05-25 | 60 | 0 |
| 1 | 2016-09-15 | 113 | 1 |
| 2 | 2016-08-01 | 0 | 1 |
| 3 | 2016-01-26 | 0 | 1 |
| 3 | 2016-03-09 | 43 | 0 |
| 3 | 2016-04-30 | 52 | 0 |
| 4 | 2016-08-09 | 0 | 1 |
| 5 | 2016-11-19 | 0 | 1 |
| 6 | 2016-10-14 | 0 | 1 |
| 7 | 2016-01-31 | 0 | 1 |
| 7 | 2016-08-11 | 193 | 1 |

Related

Get last balance sign change in (My)SQL

I have a Transaction table that records every amount added to or subtracted from the balance of a Customer, with the new balance:
+----+------------+------------+--------+---------+
| id | customerId | timestamp | amount | balance |
+----+------------+------------+--------+---------+
| 1 | 1 | 1000000001 | 10 | 10 |
| 2 | 1 | 1000000002 | -20 | -10 |
| 3 | 1 | 1000000003 | -10 | -20 |
| 4 | 2 | 1000000004 | -5 | -5 |
| 5 | 2 | 1000000005 | -5 | -10 |
| 6 | 2 | 1000000006 | 10 | 0 |
| 7 | 3 | 1000000007 | -5 | -5 |
| 8 | 3 | 1000000008 | 10 | 5 |
| 9 | 3 | 1000000009 | 10 | 15 |
| 10 | 4 | 1000000010 | 5 | 5 |
+----+------------+------------+--------+---------+
The Customer table stores the current balance, and looks like:
+----+---------+
| id | balance |
+----+---------+
| 1 | -20 |
| 2 | 0 |
| 3 | 15 |
| 4 | 5 |
+----+---------+
I would like to add a balanceSignSince column, that would store the timestamp at which the balance sign last changed. Transitioning to and from positive, negative, or zero counts as a balance change.
After the update, based on the above data, the Customer table should contain:
+----+---------+------------------+
| id | balance | balanceSignSince |
+----+---------+------------------+
| 1 | -20 | 1000000002 |
| 2 | 0 | 1000000006 |
| 3 | 15 | 1000000008 |
| 4 | 5 | 1000000010 |
+----+---------+------------------+
How can I write a SQL query that updates every Customer with the last time the balance sign changed, based on the Transaction table?
I suspect I can't do this without a quite complex stored procedure, but am curious to see if any clever ideas come up.
This uses a simulated rank() function.
select customerId, min(tstamp) from
(
select tstamp,
if (#cust = customerId and sign(#bal) = sign(balance), #rn := #rn,
if (#cust = customerId and sign(#bal) <> sign(balance), #rn := #rn + 1, #rn := 0)) as rn,
#cust := customerId as customerId, #bal := balance as balance
from
(select #rn := 0) x,
(select id, #cust := customerId as customerId, tstamp, amount, #bal := balance as balance
from trans order by customerId, tstamp desc) y
) z
where rn = 0
group by customerId;
Check it: http://rextester.com/XJVKK61181
This script returns a table like this:
+------------+----+------------+---------+
| tstamp | rn | customerId | balance |
+------------+----+------------+---------+
| 1000000003 | 0 | 1 | -20 |
| 1000000002 | 0 | 1 | -10 |
| 1000000001 | 1 | 1 | 10 |
| 1000000006 | 0 | 2 | 0 |
| 1000000005 | 2 | 2 | -10 |
| 1000000004 | 2 | 2 | -5 |
| 1000000009 | 0 | 3 | 15 |
| 1000000008 | 2 | 3 | 5 |
| 1000000007 | 3 | 3 | -5 |
| 1000000010 | 0 | 4 | 5 |
+------------+----+------------+---------+
Then selecting min(timestamp) of files where rn = 0:
+------------+-------------+
| customerId | min(tstamp) |
+------------+-------------+
| 1 | 1000000002 |
+------------+-------------+
| 2 | 1000000006 |
+------------+-------------+
| 3 | 1000000009 |
+------------+-------------+
| 4 | 1000000010 |
+------------+-------------+
Updated answer with the restriction that this needs to work on the existing data
The following query should work for most cases, there is still an issue with customers having only a single transaction or no sign change. As this is a one time update, I would run the query below and then do a simple update for all users not having a timestamp set, for them it's going to be the timestamp of the first transaction:
# Find the smallest timestamp, e.g. the
# transaction which changed the signum.
SELECT
p.customerId as customerId,
MIN(t.timestamp) as balanceSignSince
FROM
transaction as t,
(
# find the latest timestamp having
# a different sign for each user.
# Here is the issue with users having
# only a single transaction or no sign
# changes.
SELECT
u.customerId as customerId,
MAX(t.timestamp) as balanceSignSince
FROM
transaction as t,
customer as c,
(
# find the timestamp of the very last
# transaction for every user.
SELECT
t.customerId as customerId,
MAX(t.timestamp) as lastTransaction
FROM
transaction as t
GROUP BY
t.customerId
) as u
WHERE
u.customerId = c.id
AND u.customerId = t.customerId
AND SIGN(c.balance) <> SIGN(t.balance)
GROUP BY
u.customerId
) as p
WHERE
p.customerId = t.customerId
AND p.balanceSignSince < t.timestamp
GROUP BY
p.customerId;
Fiddle: http://sqlfiddle.com/#!9/bd0760/13
Original Answer
This should work to get the timestamp of a sign change:
SELECT
c.id as id,
MAX(t.timestamp) as balanceSignSince
FROM
transaction as t,
customer as c
WHERE
t.customerId = c.id
AND SIGN(t.balance) <> SIGN(c.balance)
This needs to be executed before the customer table is updated with the new balance. If you have a trigger on transation:insert you should probably put the above into the query updating the customer table.

Create 2 columns based on value in existing column

I have the following table. I would like to add 2 new columns with a select query that will show the total based on the flag type.
Table:
tt | company | count | flag
--------------------------------------------
123 | adeco | 5 | 1
123 | mic | 4 | 2
333 | manpower | 88 | 2
444 | linar | 2 | 2
555 | dlank | 3 | 1
Desired:
tt | company | total | flag | total_flag1 | total_flag2
-------------------------------------------------------------------
123 | adeco | 5 | 1 | 5 | 0
123 | mic | 4 | 2 | 0 | 4
333 | manpower | 88 | 2 | 0 | 88
444 | linar | 2 | 2 | 0 | 2
555 | dlank | 3 | 1 | 3 | 0
By your desired result, you should use case when or if syntax to to this:
select
yourtable.*,
case when flag = 1 then `count` else 0 end as total_flag1,
case when flag = 2 then `count` else 0 end as total_flag2
from yourtable
Or
select
yourtable.*,
if(flag = 1, `count`, 0) as total_flag1,
if(flag = 2, `count`, 0) as total_flag2
from yourtable
I think you can do what you want using correlated subqueries or join:
select t.*, tsum.total_flag1, tsum.total_flag2
from t join
(select t.tt,
sum(case when flag = 1 then total else 0 end) as total_flag1,
sum(case when flag = 2 then total else 0 end) as total_flag2
from t
group by t.tt
) tsum
on t.tt = tsum.tt;

SQL Query Conditional accumulation

it is possible to display accumulated data, resetting the count based on a condition?
I would like to create a script to accumulate if there is value 1 in cell number, but if another value the count should be restarted. Something like what is displayed in the column cumulative_with_condition.
+----+------------+--------+
| id | release | number |
+----+------------+--------+
| 1 | 2016-07-08 | 4 |
| 2 | 2016-07-09 | 1 |
| 3 | 2016-07-10 | 1 |
| 4 | 2016-07-12 | 2 |
| 5 | 2016-07-13 | 1 |
| 6 | 2016-07-14 | 1 |
| 7 | 2016-07-15 | 1 |
| 8 | 2016-07-16 | 2-3 |
| 9 | 2016-07-17 | 3 |
| 10 | 2016-07-18 | 1 |
+----+------------+--------+
select * from version where id > 1 and id < 9;
+----+------------+--------+---------------------------+
| id | release | number | cumulative_with_condition |
+----+------------+--------+---------------------------+
| 2 | 2016-07-09 | 1 | 1 |
| 3 | 2016-07-10 | 1 | 2 |
| 4 | 2016-07-12 | 2 | 0 |
| 5 | 2016-07-13 | 1 | 1 |
| 6 | 2016-07-14 | 1 | 2 |
| 7 | 2016-07-15 | 1 | 3 |
| 8 | 2016-07-16 | 2-3 | 0 |
+----+------------+--------+---------------------------+
You want something like row_number() (not exactly, but like that). You can do that using variables:
select t.*,
(#rn := if(number = 1, #rn + 1,
if(#n := number, 0, 0)
)
) as cumulative_with_condition
from t cross join
(select #n := '', #rn := 0) params
order by t.id;
As an alternative to using user variables, as demonstrated by Gordon Linoff, in this case it's also possible to self-join, group and count:
SELECT t.id, t.release, t.number, COUNT(version.id) AS cumulative_with_condition
FROM version RIGHT JOIN (
SELECT highs.*, MAX(lows.id) min
FROM version lows RIGHT JOIN version highs ON lows.id <= highs.id
WHERE lows.number <> '1'
GROUP BY highs.id
) t ON version.id > t.min AND version.id <= t.id
WHERE t.id > 1 AND t.id < 9
GROUP BY t.id
See it on sqlfiddle.
But, frankly, neither approach is particularly elegant—as I commented previously, you're probably best off implementing this within your application code.

Update innerquery result

I have a query and a result as follows.
In the database NULL and 0 represent the same meaning.
Now I want a counter based on Null+0 or 1
Eg:in the following example I want the result like this:
IsVirtual Category counter
NULL+0 3 343+8 = (351 is Total)
Query
select * from
(
Select IsVirtual, Category, count(*) as counter
from [Hardware]
group by IsVirtual, Category
) innercat
Output
+-----------+----------+---------+
| IsVirtual | Category | counter |
+-----------+----------+---------+
| NULL | 3 | 343 |
| 0 | 3 | 8 |
| 1 | 2 | 1 |
| 0 | 1 | 1 |
| NULL | 6 | 119 |
| 0 | 4 | 1 |
| NULL | 1 | 70 |
| 0 | 5 | 9 |
| NULL | 4 | 54 |
| 0 | 2 | 2 |
| NULL | 5 | 41 |
| NULL | 2 | 112 |
| 1 | 1 | 5 |
+-----------+----------+---------+
I think you want this :
SELECT COALESCE(IsVirtual, 0) as [IsVirtual],
Category,
Count(*) as [Counter]
FROM yourtable
GROUP BY COALESCE(IsVirtual, 0),Category
This will give you expected result without using subquery.
try with this
select * from (
Select CASE ISNULL(IsVirtual,0)
WHEN 0 Then 'NULL + 0'
ELSE IsVirtual
END AS IsVirtual, Category, count(*) as counter from [Hardware] group by ISNULL(IsVirtual,0), Category
)innercat
You can also do the same thing by using MAX function
This might help you.
SELECT
max(IsVirtual) as IsVirtual,
Category,
Count(*) as Counter
FROM
yourtable
GROUP BY
Category

Group by historical date for each date in the provided range

Am stuck with this for days. wanna group a sql result set by historical dates. so want a result to be grouped by date from each date in the date specified range all the way back in time. Here is my sql so far, but it groups the result by date instead of historical date.
Please help!
SELECT ledger.transdate,
sum(case when transcodes.dtcr = 'C' then ledger.amount else 0 end) Credit,
sum(case when transcodes.dtcr = 'D' then ledger.amount else 0 end) Debit,
sum(case when transcodes.dtcr = 'C' then ledger.amount else 0 end) -
sum(case when transcodes.dtcr = 'D' then ledger.amount else 0 end) Balance
FROM
LEDGER
INNER JOIN TRANSCODES ON (LEDGER.TRANSCODE = TRANSCODES.TRANSCODE)
where ledger.transdate >= '2013-02-28' and ledger.transdate <= '2013-03-01'
group by ledger.transdate
Maybe consider this example...
SELECT * FROM ints;
+---+
| i |
+---+
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
+---+
SELECT x.i, SUM(y.i) running FROM ints x JOIN ints y ON y.i <= x.i GROUP BY i;
+---+---------+
| i | running |
+---+---------+
| 0 | 0 |
| 1 | 1 |
| 2 | 3 |
| 3 | 6 |
| 4 | 10 |
| 5 | 15 |
| 6 | 21 |
| 7 | 28 |
| 8 | 36 |
| 9 | 45 |
+---+---------+
If I understood you correctly, and you want to group by date-only part of transdate field, you can use Date() function, i.e. ... group by Date(transdate)
Your answer can be found here:
http://www.codeproject.com/Articles/300785/Calculating-simple-running-totals-in-SQL-Server