MySQL query to find start and end of process in a given date range. There can be multiple processes in the given date range. The query I've written gives the start and end correctly if there is only one process. If there are 2 processes, it gives the start date-time stamp of the 1st process and the end date-time stamp of the second process. How can I get the start time and end time stamp of both processes (the 1st process and 2nd process)?
Table contains two columns
date_time, datetime
value, bit
Table 1 sample data
date_time value
2020-02-19 00:10:00 0
2020-02-19 00:11:00 0
2020-02-19 00:12:00 1
2020-02-19 00:13:00 1
2020-02-19 00:14:00 1
2020-02-19 00:15:00 0
2020-02-19 00:16:00 0
2020-02-19 00:17:00 1
2020-02-19 00:18:00 1
2020-02-19 00:19:00 1
2020-02-19 00:20:00 0
Sample output
Rowno date_time value
3. 2020-02-19 00:12:00 1
6. 2020-02-19 00:15:00 0
8. 2020-02-19 00:17:00 1
11. 2020-02-19 00:20:00 0
Note: When process begins value is 1 else it is 0. To identiify start time we have to get the first row with value 1 and previous value 0. Similarly for end time we have to identify last row with value 1 and next row value 0.
Query:
-- Identify 1st row with value 1
SET #row_number = 0, #result = 0;
select #result := (a.num - 1) as prev_rec, a.num, a.date_time, a.value from (
SELECT (#row_number:=#row_number + 1) AS num, date_time, value
FROM table1
where date_time >= '2020-02-19 00:00:00' and date_time <= '2020-02-25 23:59:00') as a
where a.value = 1
order by a.date_time limit 1;
-- Check if value for previous rec is 0 to identify start time
SET #row_number = 0;
select a.num, a.date_time, a.value from (
SELECT (#row_number:=#row_number + 1) AS num, date_time, value
FROM table1
where date_time >= '2020-02-19 00:00:00' and date_time <= '2020-02-25 23:59:00') as a
where a.num = #result
order by a.date_time limit 1;
Similarly I look for end time stamp
-- Identify last row with value 1
SET #row_number = 0, #result = 0;
select #result := (a.num + 1) as next_rec, a.num, a.date_time, a.value from (
SELECT (#row_number:=#row_number + 1) AS num, date_time, value
FROM table1
where date_time >= '2020-02-19 00:00:00' and date_time <= '2020-02-23 23:59:59') as a
where a.value = 1
order by a.date_time desc limit 1;
-- Check if value for next rec is 0 to identify end time
SET #row_number = 0;
select a.num, a.date_time, a.value from (
SELECT (#row_number:=#row_number + 1) AS num, date_time, value
FROM table1
where date_time >= '2020-02-19 00:00:00' and date_time <= '2020-02-23 23:59:59') as a
where a.num = #result
order by a.date_time limit 1;
Can I get a way to get a solution?
Please forgive me if I have left out anything relevant. I'm asking a question for the first time here.
To do this in mysql 8 or mariadb 10.2 or above you simply use the lag window function:
select date_time, value from (
select date_time, value, lag(value) over (order by date_time) previous_value
from mysterytable
) mysterytable_with_lag
where value != previous_value
order by date_time
(optionally add or value and previous_value is null to the where clause if you want to include the earliest row when its value is 1).
For earlier versions, you can emulate the lag function with variables:
select date_time, value from (
select date_time, value, previous_value
from (select #previous_value := null) initvars
cross join (
select date_time, #previous_value previous_value, #previous_value := value value
from mysterytable
order by date_time
) mysterytable_post_initvars
) mysterytable_with_lag
where value != previous_value
order by date_time
Related
Is ther any clear way of doing the following in one SQL (PL) code? :)
What I have in the tables is the following:
CONTACT_TBL (HEADER)
CONTRACT_ID
BEGIN_DATE
END_DATE
TOT_AMOUNT
123
13-MAY-16
12-MAY-34
100
456
13-JAN-14
12-DEC-25
300
789
13-SEP-14
12-OCT-34
700
CONTRACT_ACTIVTY (DETAIL)
CONTRACT_ID
SEQNUM
END_DATE
AMOUNT
COMMENTS
123
1
12-MAY-19
25
25 - Initial Expiration
123
2
12-MAY-24
25
25
123
3
12-MAY-29
25
25
123
4
12-MAY-34
25
25 - End of Contract
What we need is to populate for each row:
BEGIN_DATE
For SEQNUM #1 - BEGIN_DATE is always CONTACT_TBL.BEGIN_DATE (in this case 13-MAY-15, a 3 year span)
For SEQNUM #2 to N - BEGIN_DATE is 1 day more than previous row's END_DATE (in this case 12-MAY-19 + 1 = 13-MAY-19, all 5 year spans)
Mark the row 'Active' if the SYSDATE is between BEGIN_DATE and END_DATE (in this case it's Row #2)
CONTRACT_ID
SEQNUM
BEGIN_DATE
END_DATE
AMOUNT
STATUS
123
1
13-MAY-16
12-MAY-19
25
123
2
13-MAY-19
12-MAY-24
25
Active
123
3
13-MAY-24
12-MAY-29
25
123
4
13-MAY-29
12-MAY-34
25
N.B. This answer assumes the db is Oracle
This can be done by using LAG to find the date from the previous row.
LAG can accept 3 parameters, the first of which is the column whose value you want to find from the previous row, the second is how many rows to go back each time (default is 1), and the third is what to display if there is no previous row found.
In your case, you want to default to the begin_date from the contact_tbl if there is no previous row to the first row in the contract_activity table, so you need to join the two tables as well, like so:
WITH contact_tbl AS (SELECT 123 contract_id, to_date('13/05/2016', 'dd/mm/yyyy') begin_date, to_date('12/05/2034', 'dd/mm/yyyy') end_date, 100 tot_amount FROM dual UNION ALL
SELECT 456 contract_id, to_date('13/05/2016', 'dd/mm/yyyy') begin_date, to_date('12/05/2034', 'dd/mm/yyyy') end_date, 100 tot_amount FROM dual UNION ALL
SELECT 789 contract_id, to_date('13/05/2016', 'dd/mm/yyyy') begin_date, to_date('12/05/2034', 'dd/mm/yyyy') end_date, 100 tot_amount FROM dual),
contract_activity AS (SELECT 123 contract_id, 1 seqnum, to_date('12/05/2019', 'dd/mm/yyyy') end_date, 25 amount, '25 - Initial Expiration' comments FROM dual UNION ALL
SELECT 123 contract_id, 2 seqnum, to_date('12/05/2024', 'dd/mm/yyyy') end_date, 25 amount, '25' comments FROM dual UNION ALL
SELECT 123 contract_id, 3 seqnum, to_date('12/05/2029', 'dd/mm/yyyy') end_date, 25 amount, '25' comments FROM dual UNION ALL
SELECT 123 contract_id, 4 seqnum, to_date('12/05/2034', 'dd/mm/yyyy') end_date, 25 amount, '25 - End of Contract' comments FROM dual),
-- end of creating subqueries to mimic your tables
contract_details AS (SELECT ca.contract_id,
ca.seqnum,
LAG(ca.end_date + 1, 1, c.begin_date) OVER (PARTITION BY ca.contract_id ORDER BY seqnum) begin_date,
ca.end_date,
ca.amount
FROM contact_tbl c
INNER JOIN contract_activity ca ON c.contract_id = ca.contract_id)
SELECT contract_id,
seqnum,
begin_date,
end_date,
amount,
CASE WHEN TRUNC(SYSDATE) BETWEEN begin_date AND end_date THEN 'Active' END status
FROM contract_details;
CONTRACT_ID SEQNUM BEGIN_DATE END_DATE AMOUNT STATUS
----------- ---------- ----------- ----------- ---------- ------
123 1 13/05/2016 12/05/2019 25
123 2 13/05/2019 12/05/2024 25 Active
123 3 13/05/2024 12/05/2029 25
123 4 13/05/2029 12/05/2034 25
Your status column is a simple case expression, but since it refers to the calculated begin_date, rather than repeating the calculation, I did the begin_date calculation in one subquery, and then referenced that in the outer query.
Note how I used TRUNC(sysdate) instead of just sysdate in the case expression for the status - DATEs in Oracle have a time component (which defaults to midnight if you don't specify the time), so if you ran the query any time between 12/05/2024 00:00:01 and 12/05/2024 23:59:59, your query wouldn't return a status of Active for the second row if you used sysdate. You need to explicitly truncate sysdate to midnight in order for the comparison to work.
Hope below snippet serve the purpose.
DECLARE
CURSOR cur_contracts IS
SELECT
*
FROM
CONTACT_TBL
ORDER BY
CONTRACT_ID;
CURSOR cur_contracts_act(contact_id NUMBER) IS
SELECT
*
FROM
CONTRACT_ACTIVTY
ORDER BY
CONTRACT_ID,
SEQNUM;
l_ca_begin_date DATE;
l_ca_status VARCHAR2(10);
l_prev_rec_end_date DATE;
BEGIN
FOR i IN cur_contracts
LOOP
contract_activity_seq := 0;
FOR j IN cur_contracts_act (i.CONTRACT_ID)
LOOP
contract_activity_seq := contract_activity_seq + 1;
IF (contract_activity_seq = 1) THEN
l_ca_begin_date := i.begin_date;
ELSE
l_ca_begin_date := l_prev_rec_end_date + 1;
END IF;
IF SYSDATE BETWEEN l_ca_begin_date AND j.end_date THEN
l_ca_status := 'Active';
ELSE
l_ca_status := NULL;
END IF;
l_prev_rec_end_date := j.end_date;
UPDATE CONTRACT_ACTIVTY
SET BEGIN_DATE = l_ca_begin_date,
status = l_ca_status
WHERE CURRENT OF cur_contracts_act;
END LOOP;
END LOOP;
COMMIT;
EXCEPTION
WHEN OTHERS THEN raise_application_error(
-20001,
'An error was encountered - ' || SQLCODE || ' -ERROR- ' || SQLERRM
);
END;
/
I have the following table structure that I query on.
ID Date Before value After value
1 2014-04-25 Win Loss
1 2014-04-30 Loss Win
1 2014-08-18 Win Loss
1 2014-08-27 Loss Remise
1 2014-09-05 Remise Loss
2 2014-05-25 Win Remise
2 2014-06-07 Remise Win
2 2014-06-20 Win Loss
As output of my select statement I want the following result:
ID Start_Date End_Date After value
1 start 2014-04-25 Win
1 2014-04-26 2014-04-30 Loss
1 2014-05-01 2014-08-18 Win
1 2014-08-19 2014-08-27 Loss
1 2014-08-28 2014-09-05 Remise
1 2014-09-06 current Loss
2 start 2014-05-25 Win
2 2014-05-26 2014-06-07 Remise
2 2014-06-08 2014-06-20 Win
2 2014-06-21 current Loss
Of course the table has 1000's of records where it needs to go right. I tried with ranking and joining, but no luck yet. If there is a next row I somehow need to retrieve this value as start date +1.
http://sqlfiddle.com/#!2/520b13/2
This approach uses MySql inline variables to preserve the last ID and last date for comparison to the next record.
SELECT
t.id,
IF( #LastID = t.id, #LastDate, 'start ' ) as StartDate,
IF( NOT t.date = t2.date, t.date, 'current ' ) as EndDate,
t.after_value,
#LastID := t.id as saveForNextLineID,
#LastDate := t.date + INTERVAL 1 day as saveForNextDate
from
the_table t
left join ( select id, max( date ) as date
from the_table
group by id ) t2
ON t.id = t2.id,
(select #LastID := 0, #LastDate := 'start ' ) sqlvars
order by
t.id,
t.date
SQL Fiddle sample
Reading between the lines somewhat but you might be able to use UNION to manually add a start and end date eg:
select * from mytable
union
select DISTINCT ID,'2000-01-01' as `Date`,`Before value`,``After value` from mytable
union
select DISTINCT ID,NOW() as `Date`,`Before value`,``After value` from mytable
Try Popping this in a sub-select and run your grouping code on that.
Something like:
select t1.id,
(select max(date) + interval '1' day
from the_table t2
where t2.date < t1.date
and t2.id = t1.id) as start_date,
t1.date as end_date,
t1.after_value
from the_table t1
order by t1.id, t1.date;
SQLFiddle: http://sqlfiddle.com/#!2/8905a/3
I've a table and i want that data is interval by 4 or, when i'm using modulo the record is not that i expected, PFB `
SELECT (DATE_FORMAT(subscribed_from, '%Y-%m')) AS date_ FROM subscription
WHERE operator = 'tim'
AND DATE_FORMAT(subscribed_from, '%Y-%m-%d') BETWEEN '2013-01-01' AND '2014-12-31'
GROUP BY (DATE_FORMAT(subscribed_from, '%Y-%m'));
it will show record like this
2013-01
2013-02
2013-03
2013-04
2013-05
2013-06
2013-07
2013-08
2013-09
i want take only data interval by 4, this below is record that i expected.
2013-01
2013-05
2013-09
2014-02
and also for interval by 2, this below record is that i expected
2013-01
2013-03
2013-05
2013-07
2013-09
if i using modulo % 2 it will start from 2013-01 and jump by 2, but the problem if the where range i want to start from 2013-02, 02 it self not showing on the result. so if the where clause the month start from 2 it will given the interval such as 2,4,6,8,10,12
SELECT date_, SUM(the_metric_you_want_to_aggregate)
FROM (
SELECT 4*FLOOR(
(DATE_FORMAT(subscribed_from, '%Y%m') - 201301)
/4) AS date_,
the_metric_you_want_to_aggregate
FROM subscription
WHERE operator = 'tim'
AND subscribed_from BETWEEN 20130101000000 AND 201412315959
) AS ilv
GROUP BY date_
(where 201301 is the year/month start of the range you are selecting by - assuming that is the reference for the 4-month aggregation)
Note that enclosing column references in functions (...DATE_FORMAT(subscribed_from, '%Y-%m-%d') BETWEEN...) prevents the use of indexes.
You have to use variables. Here is sample for interval by 4.
SET #row_number:=0;
SELECT date_ from (
SELECT (DATE_FORMAT(subscribed_from, '%Y-%m')) AS date_,#row_number:=#row_number+1 FROM subscription
WHERE operator = 'tim' AND DATE_FORMAT(subscribed_from, '%Y-%m-%d') BETWEEN '2013-01-01' AND '2014-12-31'
GROUP BY (DATE_FORMAT(subscribed_from, '%Y-%m'))
) as tbl where #row_number % 4=0;
let says i'm using this method to generate the intevals, but i want the start number is from my input, let says it start from 4 and if the condition put %4 should be the output is 4, 8 ,12 ....
enter code here
SET #row:=0;
SELECT *
FROM (
SELECT
#row := #row +1 AS rownum
FROM (
SELECT #row) r, subscription
) ranked
WHERE rownum %4 = 1
I have the following table structure that I query on.
ID Date Before value After value
1 2014-04-25 Win Loss
1 2014-04-30 Loss Win
1 2014-08-18 Win Loss
1 2014-08-27 Loss Remise
1 2014-09-05 Remise Loss
2 2014-05-25 Win Remise
2 2014-06-07 Remise Win
2 2014-06-20 Win Loss
As output of my select statement I want the following result:
ID Start_Date End_Date After value
1 start 2014-04-25 Win
1 2014-04-26 2014-04-30 Loss
1 2014-05-01 2014-08-18 Win
1 2014-08-19 2014-08-27 Loss
1 2014-08-28 2014-09-05 Remise
1 2014-09-06 current Loss
2 start 2014-05-25 Win
2 2014-05-26 2014-06-07 Remise
2 2014-06-08 2014-06-20 Win
2 2014-06-21 current Loss
Of course the table has 1000's of records where it needs to go right. I tried with ranking and joining, but no luck yet. If there is a next row I somehow need to retrieve this value as start date +1.
http://sqlfiddle.com/#!2/520b13/2
This approach uses MySql inline variables to preserve the last ID and last date for comparison to the next record.
SELECT
t.id,
IF( #LastID = t.id, #LastDate, 'start ' ) as StartDate,
IF( NOT t.date = t2.date, t.date, 'current ' ) as EndDate,
t.after_value,
#LastID := t.id as saveForNextLineID,
#LastDate := t.date + INTERVAL 1 day as saveForNextDate
from
the_table t
left join ( select id, max( date ) as date
from the_table
group by id ) t2
ON t.id = t2.id,
(select #LastID := 0, #LastDate := 'start ' ) sqlvars
order by
t.id,
t.date
SQL Fiddle sample
Reading between the lines somewhat but you might be able to use UNION to manually add a start and end date eg:
select * from mytable
union
select DISTINCT ID,'2000-01-01' as `Date`,`Before value`,``After value` from mytable
union
select DISTINCT ID,NOW() as `Date`,`Before value`,``After value` from mytable
Try Popping this in a sub-select and run your grouping code on that.
Something like:
select t1.id,
(select max(date) + interval '1' day
from the_table t2
where t2.date < t1.date
and t2.id = t1.id) as start_date,
t1.date as end_date,
t1.after_value
from the_table t1
order by t1.id, t1.date;
SQLFiddle: http://sqlfiddle.com/#!2/8905a/3
I have a table with the following data.
serial date flag
1 1 aug 1
2 2 aug 1
3 3 aug 0
4 4 aug 0
5 5 aug 1
6 6 aug 1
7 7 aug 1
What I need is this result set:
from to flag
1-aug 2-aug 1
3-Aug 4-Aug 0
5-Aug 7-Aug 1
When I group on flag all 1 values are combined. Any suggestions?
an old question, i know, but this may still help someone...
to solve this, i have assumed that the date will be sequential. i have then joined the data, first with 'yesterday's' data and then with 'tomorrow's' data and taken those rows where the flag value has changed - this marks the start & end dates of a given series. to match the start dates with the end dates, i've ordered each series, assigned a counter and joined on this counter.
here's the code:
SET #cnt1 := 0;
SET #cnt2 := 0;
select firstdate, lastdate, startDates.flag from
(
select #cnt2 := #cnt2 + 1 as i, table1.date as firstdate, table1.flag from table1
left join (
select DATE_ADD(date, INTERVAL 1 DAY) as dateplus1, date, flag from table1
) plus1 on table1.date = plus1.dateplus1
where table1.flag <> IFNull(plus1.flag,-1)
order by table1.date
) startDates
inner join
(
select #cnt1 := #cnt1 + 1 as i, table1.date as lastdate, table1.flag from table1
left join (
select DATE_ADD(date, INTERVAL -1 DAY) as dateminus1, date, flag from table1
) minus1 on table1.date = minus1.dateminus1
where table1.flag <> IFNull(minus1.flag,-1)
order by table1.date
) endDates on startDates.i = endDates.i
;
sql fiddle here: http://sqlfiddle.com/#!2/25594/1/2