Reputation: 120917
I have a Payment
table that looks a little like this:
Id (int identity)
CustomerId (int)
PaymentDate (SmallDateTime)
Now I want to write a query that will find those customers that have made three payments within a period of three months. Given the following data:
Id CustomerId PaymentDate (YYYY-MM-DD)
------------------------------------------
1 1 2010-01-01
2 1 2010-02-01
3 1 2010-03-01
4 1 2010-06-01
5 2 2010-04-01
6 2 2010-05-01
7 2 2010-06-01
8 2 2010-07-01
I would like to produce the following result:
CustomerId LastPaymentDateInPeriod
-------------------------------------
1 2010-03-01
2 2010-07-01
Where LastPaymentDateInPeriod
is the PaymentDate
with the highest value within a three-month period. If there is more than one three-month period for a given customer it would have to return the highest value from the most recent period (this is what I tried to illustrate for customer 2 in the above example). Note that three payments on three consecutive days would also meet the criteria. The payments just have to fall within a three-month period.
I know how to do this with a cursor and a lot of smaller queries but this is slow (and, I've come to understand, should only be a last resort). So do any of you SqlServer geniuses know how to do this with a query?
Thanks in advance.
Upvotes: 3
Views: 402
Reputation: 7990
This should do the job:
select
CustomerID,
max(LastPaymentDateInPeriod) as LastPaymentDateInPeriod
from
(
select
LastPaymentInPeriod.CustomerID,
LastPaymentInPeriod.PaymentDate as LastPaymentDateInPeriod
from Payment LastPaymentInPeriod
inner join Payment RelatedPayment on
LastPaymentInPeriod.CustomerID = RelatedPayment.CustomerID and
LastPaymentInPeriod.PaymentDate > RelatedPayment.PaymentDate and
datediff(m, RelatedPayment.PaymentDate, LastPaymentInPeriod.PaymentDate) < 3
group by
LastPaymentInPeriod.CustomerID,
LastPaymentInPeriod.PaymentDate
having
count(*) > 1
) as PaymentPeriods
group by
CustomerID
update: I've tested this now and it seems to work for @Martin's data
update2: If it's a requirement that Jan 31 and Apr 1 should be considered as less than 3 months apart then the DATEDIFF function call can be replaced with something like this:
create function fn_monthspan
(
@startdate datetime,
@enddate datetime
)
returns int
as
begin
return datediff(m, @startdate, @enddate) - case when datepart(d, @startdate) > datepart(d, @enddate) then 1 else 0 end
end
Upvotes: 5
Reputation: 1514
I thought of:
select customerId,max(PaymentDate) from payment where customerId in
(select case when count(*)<3 then null else customerId end as customerId from payment
where paymentdate>dateadd(month,-3,getdate()) group by customerId)
group by customerId;
Upvotes: 0
Reputation: 48016
This gives you all three payments within a 3 month span.
;
WITH CustomerPayments AS
(
SELECT 1 Id, 1 CustomerId, Convert (DateTime, '2010-01-01') PaymentDate
UNION SELECT 2, 1, '2010-02-01'
UNION SELECT 3, 1, '2010-03-01'
UNION SELECT 4, 1, '2010-06-01'
UNION SELECT 5, 2, '2010-04-01'
UNION SELECT 6, 2, '2010-05-01'
UNION SELECT 7, 2, '2010-06-01'
UNION SELECT 8, 2, '2010-07-01'
UNION SELECT 9, 3, '2010-07-01'
UNION SELECT 10, 3, '2010-07-01'
),
FirstPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
Group by CustomerId
)
),
SecondPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
WHERE ID NOT IN
(
SELECT ID
from FirstPayment
)
Group by CustomerId
)
),
ThirdPayment AS
(
SELECT Id, CustomerId, PaymentDate
FROM CustomerPayments
where Id IN
(
SELECT Min (Id) Id
FROM CustomerPayments
WHERE ID NOT IN
(
SELECT ID
from FirstPayment
UNION
SELECT ID
from SecondPayment
)
Group by CustomerId
)
)
SELECT *
FROM
FirstPayment FP
Left JOIN SecondPayment SP
ON FP.CustomerId = SP.CustomerId
Left JOIN ThirdPayment TP
ON SP.CustomerId = TP.CustomerId
WHERE 1=1
AND SP.PaymentDate IS NOT NULL
AND TP.PaymentDate IS NOT NULL
AND ABS (DATEDIFF (mm, SP.PaymentDate, TP.PaymentDate)) <3
Upvotes: 1
Reputation: 452988
Bit of a rushed job as I'm off out.
declare @T TABLE
(
Id int,
CustomerId int,
PaymentDate SmallDateTime
)
insert into @T
SELECT 1, 1,'2010-01-01' UNION ALL
SELECT 2, 1,'2010-02-01' UNION ALL
SELECT 3, 1,'2010-03-01' UNION ALL
SELECT 4, 1,'2010-06-01' UNION ALL
SELECT 5, 2,'2010-04-01' UNION ALL
SELECT 6, 2,'2010-05-01' UNION ALL
SELECT 7, 2,'2010-06-01' UNION ALL
SELECT 8, 2,'2010-07-01'
;with CTE1 AS
(
SELECT Id, CustomerId, PaymentDate, ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY PaymentDate) RN
FROM @T
), CTE2 AS
(
SELECT C1.Id, C1.CustomerId, MAX(C2.PaymentDate) AS LastPaymentDateInPeriod
FROM CTE1 C1 LEFT JOIN CTE1 C2 ON C1.CustomerId = C2.CustomerId AND C2.RN BETWEEN C1.RN AND C1.RN + 2 and C2.PaymentDate <=DATEADD(MONTH,3,C1.PaymentDate)
GROUP BY C1.Id, C1.CustomerId
HAVING COUNT(*)=3
)
SELECT CustomerId, MAX(LastPaymentDateInPeriod) LastPaymentDateInPeriod
FROM CTE2
GROUP BY CustomerId
Upvotes: 1