Merging sql row groups - sql

My applications splits a single row data into different row chunks which are always in sorted order of startdate.
Where rowpart = 0 is the start and rowpart=2 is always the end
rowpart=1 is the middle part,which can be repeated n no of times.
I need to return row in such form like startdate of rowpart=0 and enddate of rowpart=2(if present or else return enddate for rowpart )
Rowpart = 0 is the start of new row chunk
Rowpart = 2 is always the end of the chunk
Chunks can be spread across different dates.
+-----+-------------------------+-------------------------+----------+
| Id | startdate | enddate | rowpart |
+-----+-------------------------+-------------------------+----------+
| 100 | 2016-11-30 00:00:00.000 | 2016-11-30 01:00:00.000 | 0 |
| 100 | 2016-11-30 02:00:00.000 | 2016-11-30 03:00:00.000 | 1 |
| 100 | 2016-11-30 10:00:00.000 | 2016-12-01 00:00:00.000 | 0 |
| 100 | 2016-12-01 02:00:00.000 | 2016-12-01 02:30:00.000 | 1 |
| 100 | 2016-12-01 10:00:00.000 | 2016-12-01 10:30:00.000 | 1 |
| 100 | 2016-12-01 16:00:00.000 | 2016-12-01 16:30:00.000 | 2 |
| 101 | 2016-12-11 10:00:00.000 | 2016-12-11 10:30:00.000 | 0 |
+-----+-------------------------+-------------------------+----------+
So the above table should return:
+-----+-------------------------+-------------------------+
| Id | startdate | enddate |
+-----+-------------------------+-------------------------+
| 100 | 2016-11-30 00:00:00.000 | 2016-11-30 03:00:00.000 |
| 100 | 2016-12-30 10:00:00.000 | 2016-12-01 16:30:00.000 |
| 101 | 2016-12-11 10:00:00.000 | 2016-12-11 10:30:00.000 |
+-----+-------------------------+-------------------------+
Any help would be appreciated

This should work:
;WITH temp
AS
(
SELECT Id, startdate,enddate,rowpart,
--Find out First Record
CASE WHEN rowpart=0
THEN 1
ELSE 0
END AS is_first,
--Find out Last Record, Check if next rowpart is 0 or NULL:
CASE WHEN COALESCE(LEAD(rowpart) OVER (ORDER BY Id, startdate),0) = 0 --Check if next rowpart is 0 or NULL
THEN 1
ELSE 0
END AS is_last
FROM #tab
)
SELECT DISTINCT
Id,
CASE WHEN is_first = 1
THEN startdate
ELSE LAG(startdate) OVER (ORDER BY Id, startdate)
END AS startdate,
CASE WHEN is_last = 1
THEN enddate
ELSE LEAD(enddate) OVER (ORDER BY Id, startdate)
END AS enddate
FROM temp
WHERE is_first = 1 OR is_last = 1
ORDER BY Id, startdate
What i try to do here: Inside the CTE i mark the first and the last record for each sequence. If rowpart=0 --> it's the first record. If the next record is null or the the rowpart of the next record is 0 then we have the last record.
So when querying the CTE we can eliminate the "records in between". What remains are 1 or 2 records per sequence (the first and the last, in some cases this is the same record).
Then we replace startdate with the startdate of the first record of the sequence and enddate with the enddate of the last record of the sequence.
Eliminate duplicate values with DISTINCT and you get the desired output.
This is a dirty piece of SQL, but at least it works ;-)
If you didn't know SQL Servers LEAD and LAGfunction to access previous or following row values check this out: http://blog.sqlauthority.com/2013/09/22/sql-server-how-to-access-the-previous-row-and-next-row-value-in-select-statement/

Looks like a simple Group by is all you need
Try this
select Id,min(startdate),max(enddate)
From yourtable
Group by Id,cast(startdate as date)

Select
Id,
startdate,
enddate
from (
select Id,
startdate,
enddate,ROW_NUMBER()OVER(PARTITION BY CONVERT(DATE,startdate) ORDER BY startdate DESC )RN from #Table1
GROUP BY Id, startdate, enddate)T
WHERE T.RN = 1

Check This. using CTE and Joins :
with CTE as
(
select distinct *,
CASE WHEN COALESCE(LEAD(rowpart) OVER (ORDER BY Id, startdate),0) = 0
THEN 1
ELSE 0
end as RN2
from #table
)
select distinct bb.id,bb.startdate,aa.enddate from
(
select C2.*,ROW_NUMBER()OVER( ORDER BY id, startdate ) RN3
from CTE C2 where RN2= 1
) aa
join
(
select distinct *,
ROW_NUMBER()OVER( ORDER BY id, startdate ) RN3
from CTE c1 where rowpart=0
) bb on aa.RN3=bb.RN3
OutPut :

WITH
your_table_lead AS
(
SELECT
your_table.*,
LAG(rowpart, 1, 2) OVER (PARTITION BY id
ORDER BY startdate) AS last_rowpart,
LEAD(rowpart, 1, 0) OVER (PARTITION BY id
ORDER BY startdate) AS next_rowpart
FROM
your_table
),
filtered_sorted AS
(
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY id
ORDER BY startdate) AS id_seq_num
FROM
your_table_lead
WHERE
rowpart IN (0, 2)
OR next_rowpart = 0
OR last_rowpart = 2
)
SELECT
id,
MIN(startdate),
MAX(enddate)
FROM
filtered_sorted
GROUP BY
id,
id_seq_num - CASE rowpart WHEN 2 THEN 1 ELSE rowpart END
I'm on my phone so apologies for typos, etc.
The first steps just try to filter out everything except the first and last entry of each 'group'. If the rowpart is 0 or 2, the row is included, or if the Next Row's rowpart is 0 then that row is included too (If there isn't a next row, use 0).
Then the 'trick' is to find a way to group up the 'pairs'.
If we have a sequence of 0,2,0,1,0,2,2,0 then what we want is to group them like a,a,b,b,c,c,d,e.
That can be done by turning all the 2's into 1's, the deducting the value from the ROW_NUMBER().
0,2,0,1,0,2,2,0 => 0,1,0,1,0,1,1,0
1,2,3,4,5,6,7,8 - 0,1,0,1,0,1,1,0 => 1,1,3,3,5,5,6,8
So, now we have 5 distinct 'groups', on which we can apply MIN() and MAX().

Related

Flag the date when they return

Story:
For each id , they have a join date to a subscription and when they get rebilled monthly, they have a returning date. The first part of the exercise was to flag consecutive months of returned dates from the join date. Here's an example:
+----+------------+----------------+------+
| id | join_date | returning_date | flag |
+----+------------+----------------+------+
| 1 | 2018-12-01 | 2019-01-01 | 1 |
| 1 | 2018-12-01 | 2019-02-01 | 1 |
| 1 | 2018-12-01 | 2019-03-01 | 1 |
+----+------------+----------------+------+
Objective:
What I would like to add is to flag those who return from a canceled subscription. That flag can be in another column. For example the following results shows that on May 1st 2019 , he returned. This date needs to be flagged:
+----+------------+----------------+------+
| id | join_date | returning_date | flag |
+----+------------+----------------+------+
| 1 | 2018-12-01 | 2019-01-01 | 1 |
| 1 | 2018-12-01 | 2019-02-01 | 1 |
| 1 | 2018-12-01 | 2019-03-01 | 1 |
| 1 | 2018-12-01 | 2019-05-01 | 0 |
| 1 | 2018-12-01 | 2019-06-01 | 0 |
+----+------------+----------------+------+
Fiddle Data:
DROP TABLE IF EXISTS #T1
create table #t1 (id int,join_date date, returning_date date)
insert into #t1 values
(1,'2018-12-01', '2019-01-01'),
(1,'2018-12-01', '2019-02-01'),
(1,'2018-12-01', '2019-03-01'),
(1,'2018-12-01', '2019-05-01'),
(1,'2018-12-01', '2019-06-01'),
(2,'2018-12-01', '2019-02-01'),
(2,'2018-12-01', '2019-03-01'),
(2,'2018-12-01', '2019-05-01'),
(2,'2018-12-01', '2019-06-01'),
(3,'2019-05-01', '2019-06-01'),
(3,'2019-05-01', '2019-08-01'),
(3,'2019-05-01', '2019-10-01')
Current query with flag for consecutive months:
select *
,CASE WHEN DATEDIFF(MONTH,join_date,returning_date) = ROW_NUMBER() OVER (PARTITION BY id ORDER BY returning_date ASC) THEN 1 ELSE 0 END AS flag
from #t1
ORDER BY ID,returning_date
You seem to be asking if there are any gaps since an id first returned (with a given join_date).
If so, that is simply counting. How many months since the first return_date? How many rows? Compare these to see if there are gaps:
select t1.*,
(case when datediff(month, min(returning_date) over (partition by id, join_date order by returning_date), returning_date) <>
row_number() over (partition by id, join_date order by returning_date) - 1
then 0 else 1
end) as flag
from t1;
Here is a db<>fiddle.
since you didn't specify which recurrence of returning as the target to flag, my query flags any non-consecutive date as a return date cause a subscriber could leave and return many times after their join date (the subscriber with [id] 3 technically returned in August and then again in October so that's returning twice but October is marked as LAST instead based on the data set). i also made it easier to read by adding in start date and end date based on the data set in your fiddle.
you can use this query as a temp table, cte, basis, or whatever to continue to query against if you need to manipulate the data further.
select a.*
,case
when a.returning_date = (select min(c.returning_date) from subscription c where c.id = a.id and c.join_date = a.join_date) then 'START'
when a.returning_date = (select max(c.returning_date) from subscription c where c.id = a.id and c.join_date = a.join_date) then 'END'
when b.id is null then 'RETURN'
else 'CONSECUTIVE'
end as SubStatus
from subscription a
left join subscription b on a.id = b.id and a.join_date = b.join_date and DATEADD(month,-1,a.returning_date) = b.returning_date
here is the result set from my query:
id join_date returning_date SubStatus
----------- ---------- -------------- -----------
1 2018-12-01 2019-01-01 START
1 2018-12-01 2019-02-01 CONSECUTIVE
1 2018-12-01 2019-03-01 CONSECUTIVE
1 2018-12-01 2019-05-01 RETURN
1 2018-12-01 2019-06-01 END
2 2018-12-01 2019-02-01 START
2 2018-12-01 2019-03-01 CONSECUTIVE
2 2018-12-01 2019-05-01 RETURN
2 2018-12-01 2019-06-01 END
3 2019-05-01 2019-06-01 START
3 2019-05-01 2019-08-01 RETURN
3 2019-05-01 2019-10-01 END
flag consecutive months
and
renders all future payments
are not phrases that are going to lead to a pretty query. Which is why you had to resort to a while loop. Nevertheless, what you seek is possible, and with work may prove more performant than your while loop for large data. I present my sample code below using cte's, but you may want to use temp tables ore update an originally null 'flag' column on the base table.
In flagNonConsecutive, a flag is applied for any date that is not consecutive with the previous date (as identified using the lag window function) or by the join_date.
This meets the first requirement. Then in minNonConsecutives, you identify the earliest of those flags for each id.
In the main query, any dates after the minimum get the 0 treatment:
with
flagNonConsecutive as (
select *,
nonConsecutive =
case
when datediff(month, join_date, returning_date) = 1 then 1
when datediff(
month,
lag(returning_date) over(
partition by id
order by returning_date
),
returning_date
) = 1 then 1
else 0
end
from #t1
),
minNonConsecutives as (
select id,
minNonConsec = min(returning_date)
from flagNonConsecutive
where nonConsecutive = 0
group by id
)
select fnc.id,
fnc.join_date,
fnc.returning_date,
flag = iif(fnc.returning_date >= mnc.minNonConsec, 0, 1)
from flagNonConsecutive fnc
left join minNonConsecutives mnc on fnc.id = mnc.id;

Get Max And Min dates for consecutive values in T-SQL

I have a log table like below and want to simplfy it by getting min start date and max end date for consecutive Status values for each Id. I tried many window function combinations but no luck.
This is what I have:
This is what want to see:
This is a typical gaps-and-islands problem. You want to aggregate groups of consecutive records that have the same Id and Status.
No need for recursion, here is one way to solve it using window functions:
select
Id,
Status,
min(StartDate) StartDate,
max(EndDate) EndDate
from (
select
t.*,
row_number() over(partition by id order by StartDate) rn1,
row_number() over(partition by id, status order by StartDate) rn2
from mytable t
) t
group by
Id,
Status,
rn1 - rn2
order by Id, min(StartDate)
The query works by ranking records over two different partitions (by Id, and by Id and Status). The difference between the ranks gives you the group each record belongs to. You can run the subquery independently to see what it returns and understand the logic.
Demo on DB Fiddle:
Id | Status | StartDate | EndDate
-: | :----- | :------------------ | :------------------
1 | B | 07/02/2019 00:00:00 | 18/02/2019 00:00:00
1 | C | 18/02/2019 00:00:00 | 10/03/2019 00:00:00
1 | B | 10/03/2019 00:00:00 | 01/04/2019 00:00:00
2 | A | 05/02/2019 00:00:00 | 22/04/2019 00:00:00
2 | D | 22/04/2019 00:00:00 | 05/05/2019 00:00:00
2 | A | 05/05/2019 00:00:00 | 30/06/2019 00:00:00
Try the following query. First order the data by StartDate and generate a sequence (rid). Then you the recursive cte to get the first row (rid=1) for each group (id,status), and recursively get the next row and compare the start/end date.
;WITH cte_r(id,[Status],StartDate,EndDate,rid)
AS
(
SELECT id,[Status],StartDate,EndDate, ROW_NUMBER() OVER(PARTITION BY Id,[Status] ORDER BY StartDate) AS rid
FROM log_table
),
cte_range(id,[Status],StartDate,EndDate,rid)
AS
(
SELECT id,[Status],StartDate,EndDate,rid
FROM cte_r
WHERE rid=1
UNION ALL
SELECT p.id, p.[Status], CASE WHEN c.StartDate<p.EndDate THEN p.StartDate ELSE c.StartDate END AS StartDate, c.EndDate,c.rid
FROM cte_range p
INNER JOIN cte_r c
ON p.id=c.id
AND p.[Status]=c.[Status]
AND p.rid+1=c.rid
)
SELECT id,[Status],StartDate,MAX(EndDate) AS EndDate FROM cte_range GROUP BY id,StartDate ;

SQL Ranking by blocks

Im sure the answer to this is going to end up being really obvious, but i just cant get this bit of sql to work.
I have a table that has 3 columns in:
User | Date | AchievedTarget
----------------------------------------
1 | 2018-01-01 | 1
1 | 2018-02-01 | 0
1 | 2018-03-01 | 1
1 | 2018-04-01 | 1
1 | 2018-05-01 | 0
I want to add a ranking as follows based on the AchievedTarget column, is this possible with the data in the table above to create the ranking in the table below:
User | Date | AchievedTarget | Rank
----------------------------------------
1 | 2018-01-01 | 1 | 1
1 | 2018-02-01 | 0 | 1
1 | 2018-03-01 | 1 | 1
1 | 2018-04-01 | 1 | 2
1 | 2018-05-01 | 0 | 1
This is a guess, based on that this is actually a gaps and island question. if so, this does result in the second dataset the OP has provided:
CREATE TABLE dbo.TestTable ([User] tinyint, --Avoid using keywords for column names
[date] date, --Avoid using datatypes for column names
AchievedTarget bit);
GO
INSERT INTO dbo.TestTable ([User],[date],AchievedTarget)
VALUES (1,'20180101',1),
(1,'20180201',0),
(1,'20180301',1),
(1,'20180401',1),
(1,'20180501',0);
GO
WITH Grps AS(
SELECT [User],[date],AchievedTarget,
ROW_NUMBER() OVER (ORDER BY [date]) -
ROW_NUMBER() OVER (PARTITION BY AchievedTarget ORDER BY [date]) AS Grp
FROM dbo.TestTable)
SELECT [User],[date],AchievedTarget,
ROW_NUMBER() OVER (PARTITION BY AchievedTarget, Grp ORDER BY [date]) AS [Rank] --Avoid using keywords for column names
FROM Grps
ORDER BY [date]
GO
DROP TABLE dbo.TestTable;
Other method:
with tmp as (
select row_number() over(order by date) ID, *
from dbo.TestTable
)
select f1.*, NbBefore + 1
from tmp f1
outer apply
(
select top 1 f2.ID IDLimit from tmp f2 where f2.ID<f1.ID and f2.AchievedTarget<>f1.AchievedTarget
order by f2.ID desc
) f3
outer apply
(
select count(*) NbBefore from tmp f4 where f4.ID<f1.ID and f4.ID> f3.IDLimit
) f5

Grouping rows if their dates overlap, and ranking them

My situation is I have a table of transactions, with start and end dates. The problem is that often times these transaction dates overlap with each other, and I want to group these scenarios together.
For example in the case below, transaction #1 is the "root" transaction, while #2-#4 are overlapping with #1 and/or with each other. However, transaction #5 is not overlapping with anything, hence it is a new "root" transaction.
+----------------+-----------+-----------+----------------------------------+
| Transaction ID | StartDate | EndDate | |
+----------------+-----------+-----------+----------------------------------+
| 1 | 1/1/2017 | 1/3/2017 | root transaction |
| 2 | 1/2/2017 | 1/6/2017 | overlaps with #1 |
| 3 | 1/5/2017 | 1/10/2017 | overlaps with #2 |
| 4 | 1/3/2017 | 1/13/2017 | overlaps with #2 and #3 |
| 5 | 1/15/2017 | 1/20/2017 | no overlap, new root transaction |
+----------------+-----------+-----------+----------------------------------+
Below is how I want the output to look. I want to
Identify the root transaction (column 4)
Rank the transactions in a chain by EndDate, so that the root is always = 1
+----------------+-----------+-----------+------------------+------+
| Transaction ID | Start | End | Root Transaction | Rank |
+----------------+-----------+-----------+------------------+------+
| 1 | 1/1/2017 | 1/3/2017 | 1 | 1 |
| 2 | 1/2/2017 | 1/6/2017 | 1 | 2 |
| 3 | 1/5/2017 | 1/10/2017 | 1 | 3 |
| 4 | 1/3/2017 | 1/13/2017 | 1 | 4 |
| 5 | 1/15/2017 | 1/20/2017 | 5 | 1 |
+----------------+-----------+-----------+------------------+------+
How would I go about this in SQL?
Here is one method using an OUTER APPLY
Declare #YourTable table ([Transaction ID] int,StartDate date,EndDate date)
Insert Into #YourTable values
(1,'1/1/2017','1/3/2017'),
(2,'1/2/2017','1/6/2017'),
(3,'1/5/2017','1/10/2017'),
(4,'1/3/2017','1/13/2017'),
(5,'1/15/2017','1/20/2017')
Select [Transaction ID]
,[Start] = StartDate
,[End] = EndDate
,[Root Transaction]=Grp
,[Rank] = Row_Number() over (Partition By Grp Order by [Transaction ID])
From (
Select A.*
,Grp = max(Flag*[Transaction ID]) over (Order By [Transaction ID])
From (
Select A.*,Flag = IsNull(B.Flg,1)
From #YourTable A
Outer Apply (
Select Top 1 Flg=0
From #YourTable
Where (StartDate between A.StartDate and A.EndDate
or EndDate between A.StartDate and A.EndDate )
and [Transaction ID]<A.[Transaction ID]
) B
) A
) A
Returns
EDIT - Some Commentary
In the OUTER APPLY, Flag will be set to 1 or 0. 1 Indicates a New Group. 0 Indicates that the record will overlap with an existing range
Then the next query "up", We use the window function to apply a Grp Code (Flag*Trans ID). Remember a new group is 1 and existing is 0.
Now the window function will take max of this product, as it traverses the Transactions.
The final query is just to apply the Rank using the window function partition by the Grp, Order by Trans ID
If it helps with the visualization:
The 1st sub-query (outer apply) genererates
The 2nd sub-query generates
This is an example of "gaps-and-islands". For your data, you can determine the "island"s by determining where each starts -- that is, where a record does not overlap with the previous record. You can then get the rank using row_number().
So, here is a method:
select t.*,
min(transactionId) over (partition by island) as start,
row_number() over (partition by island order by endDate) as rnk
from (select t.*,
sum(startIslandFlag) over (order by startDate) as island
from (select t.*,
(case when not exists (select 1
from t t2
where t2.startdate < t.startdate and
t2.enddate >= t.startdate
)
then 1 else 0
end) as startIslandFlag
from t
) t
) t;
Notes:
In the event that the lowest transaction id is not the "root", then a tweak may be needed to the code to get the transaction id with the minimum start date.
If there are duplicate start dates in the code, a tweak may be needed with the cumulative sums (using an explicit range window).
Identify the root transactions:
with roots as (
select *
from tran as t1
where not exists (
select 1
from tran as t2
where t2.Transaction_ID < t1.Transaction_ID
and (
t1.StartDate between t2.StartDate and t2.EndDate
or
t1.EndDate between t2.StartDate and t2.EndDate
)
)
)
Create a two root system to capture all the overlaps in between them
select t.Transaction_ID,
t.StartDate as [Start],
t.EndDate as [End],
r1.Transaction_ID as Root_Transaction,
row_number() over (partition by r1.Transaction_ID order by t.EndDate) as [Rank]
from roots as r1
inner join roots as r2
on r2.Transaction_ID > r1.Transaction_ID
inner join tran as t
on t.Transaction_ID >= r1.Transaction_ID
and t.Transaction_ID < r2.Transaction_ID
where not exists ( --this "not exists" makes sure r1 and r2 are consequetive roots
select 1
from roots as r3
where r3.Transaction_ID > r1.Transaction_ID
and r3.Transaction_ID < r2.Transaction_ID
)

Remove duplicate rows query result except for one in Microsoft SQL Server?

How would I delete all duplicate month from a Microsoft SQL Server Table?
For example, with the following syntax I just created:
SELECT * FROM Cash WHERE Id = '2' AND TransactionDate between '2014/07/01' AND '2015/02/28'
and the query result is:
+----+-------------------------+
|Id | TransactionDate |
+----+-------------------------+
| 2 | 2014-07-22 00:00:00.000 |
| 2 | 2014-08-09 00:00:00.000 |
| 2 | 2014-08-25 00:00:00.000 |
| 2 | 2014-08-29 00:00:00.000 |
| 2 | 2015-01-27 00:00:00.000 |
| 2 | 2015-01-28 00:00:00.000 |
+----+-------------------------+
How would I remove duplicates month which is only return any 1 value for any 1 month each, like this result:
+----+-------------------------+
|Id | TransactionDate |
+----+-------------------------+
| 2 | 2014-07-22 00:00:00.000 |
| 2 | 2014-08-09 00:00:00.000 |
| 2 | 2015-01-27 00:00:00.000 |
+----+-------------------------+
You can do it with the help of ROW_NUMBER.
This will tell you which are the rows you are going to keep
SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28'
You can delete the other rows with a CTE.
with myCTE (id,transactionDate, firstTrans) AS (
SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28'
)
delete from myCTE where firstTrans <> 1
Will only keep one transaction for each month of each year.
EDIT:
filter by the row_number and will only return the rows you want
select id, transactionDate from (SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28') where firstTrans = 1
When you run this query you will get the highest Id for each month in each year.
SELECT MAX(<IdColumn>) AS Id, YEAR(<DateColumn>) AS YE, MONTH(<DateColumn>) AS MO FROM <YourTable>
GROUP BY YEAR(<DateColumn>), MONTH(<DateColumn>)
If needed, for example, you can late delete rows that their Id is not in this query.
Select only the first row per month
SELECT *
FROM Cash c
WHERE c.Id = '2'
AND c.TransactionDate between '2014/07/01' AND '2015/02/28'
AND NOT EXISTS ( SELECT 'a'
FROM Cash c2
WHERE c2.Id = c.Id
AND YEAR(c2.TransactionDate) * 100 + MONTH(c2.TransactionDate) = YEAR(c.TransactionDate) * 100 + MONTH(c.TransactionDate)
AND c2.TransactionDate < c.TransactionDate
)