I\'ve seen a lot of solutions to identify records where date ranges overlap, and still other examples of merging overlapping ranges.
However I am interested in results
CREATE TABLE #TMP
(
CustomerID INT,
ProductID VARCHAR(12),
Eff_Dt DATE,
End_Dt DATE
);
--
INSERT INTO #TMP
VALUES
-- Customer 1000: Expecting results to show 2 rows: "1/1 - 1/5"; "1/10 - 1/15"
(1000, 'PRODUCT_A', N'2013-01-01T00:00:00', N'2013-01-31T00:00:00'),
(1000, 'PRODUCT_B', N'2013-01-01T00:00:00', N'2013-01-05T00:00:00'),
(1000, 'PRODUCT_B', N'2013-01-10T00:00:00', N'2013-01-15T00:00:00'),
(1000, 'PRODUCT_C', N'2013-01-01T00:00:00', N'2013-01-31T00:00:00'),
-- Customer 2000: Expecting results to show 1 row: "1/19 - 1/31"
(2000, 'PRODUCT_A', N'2013-01-01T00:00:00', N'2013-01-31T00:00:00'),
(2000, 'PRODUCT_B', N'2013-01-01T00:00:00', N'2013-01-31T00:00:00'),
(2000, 'PRODUCT_C', N'2013-01-19T00:00:00', N'2013-01-31T00:00:00'),
-- Customer 3000: Expecting results to show NO rows (or nulls)
(3000, 'PRODUCT_A', N'2013-01-01T00:00:00', N'2013-01-10T00:00:00'),
(3000, 'PRODUCT_A', N'2013-01-16T00:00:00', N'2013-01-31T00:00:00'),
(3000, 'PRODUCT_B', N'2013-01-01T00:00:00', N'2013-01-12T00:00:00'),
(3000, 'PRODUCT_C', N'2013-01-15T00:00:00', N'2013-01-31T00:00:00'),
-- Customer 4000: Expecting results to show 1 row: "1/15 - 1/23"
(4000, 'PRODUCT_A', N'2013-01-15T00:00:00', N'2013-01-31T00:00:00'),
(4000, 'PRODUCT_B', N'2013-01-01T00:00:00', N'2013-01-31T00:00:00'),
(4000, 'PRODUCT_C', N'2013-01-01T00:00:00', N'2013-01-23T00:00:00'),
-- Customer 5000: Expecting results to show 0 rows
(5000, 'PRODUCT_A', N'2013-01-17T00:00:00', N'2013-01-31T00:00:00'),
(5000, 'PRODUCT_B', N'2013-01-01T00:00:00', N'2013-01-10T00:00:00'),
(5000, 'PRODUCT_C', N'2013-01-07T00:00:00', N'2013-01-19T00:00:00'),
-- Customer 6000: Expecting results to show 3 rows: "1/11 - 1/12"; "1/17 - 1/22"; "1/26 - 1/27"
(6000, 'PRODUCT_A', N'2013-01-01T00:00:00', N'2013-01-04T00:00:00'),
(6000, 'PRODUCT_A', N'2013-01-09T00:00:00', N'2013-01-12T00:00:00'),
(6000, 'PRODUCT_A', N'2013-01-17T00:00:00', N'2013-01-22T00:00:00'),
(6000, 'PRODUCT_A', N'2013-01-26T00:00:00', N'2013-01-31T00:00:00'),
(6000, 'PRODUCT_B', N'2013-01-04T00:00:00', N'2013-01-28T00:00:00'),
(6000, 'PRODUCT_C', N'2013-01-11T00:00:00', N'2013-01-27T00:00:00');
--
--
;
WITH MIN_MAX
AS (SELECT MIN(Eff_Dt) S,
MAX(End_Dt) E
FROM #TMP),
ALL_DATES
AS (SELECT MIN_MAX.S DT
FROM MIN_MAX
UNION ALL
SELECT DATEADD(DAY, 1, ALL_DATES.DT)
FROM ALL_DATES
WHERE ALL_DATES.DT < (SELECT MIN_MAX.E FROM MIN_MAX)),
GROUPED
AS (SELECT Q.CustomerID,
Q.ProductID,
Q.DT,
CASE WHEN MAX(TX.CustomerID) IS NULL THEN 0 ELSE 1 END YES
FROM
(
SELECT *FROM ALL_DATES
CROSS JOIN
(SELECT DISTINCT CustomerID, ProductID FROM #TMP) AS AQ
) AS Q
LEFT JOIN #TMP AS TX
ON TX.CustomerID = Q.CustomerID
AND TX.ProductID = Q.ProductID
AND Q.DT
BETWEEN TX.Eff_Dt AND TX.End_Dt
GROUP BY Q.CustomerID,
Q.ProductID,
Q.DT),
BuildFlags
AS (SELECT G.CustomerID,
G.DT,
ROW_NUMBER() OVER (PARTITION BY G.CustomerID ORDER BY G.DT) RN,
CASE
WHEN WQ.tot =
(
SELECT COUNT(DISTINCT g2.ProductID)
FROM GROUPED AS g2
WHERE g2.CustomerID = G.CustomerID
AND g2.DT = G.DT
AND g2.YES = 1
) THEN 1
ELSE 0
END FLAG
FROM GROUPED AS G
CROSS APPLY
(
SELECT COUNT(DISTINCT E9.ProductID) tot
FROM #TMP AS E9
WHERE E9.CustomerID = G.CustomerID
) AS WQ ),
AddRanks
AS (SELECT *,
BuildFlags.RN - ROW_NUMBER() OVER (PARTITION BY BuildFlags.CustomerID,
BuildFlags.FLAG
ORDER BY BuildFlags.DT
) groupRank
FROM BuildFlags)
SELECT AddRanks.CustomerID,
MIN(AddRanks.DT) AS StartDate,
MAX(AddRanks.DT) AS EndDate
FROM AddRanks
WHERE AddRanks.FLAG = 1
GROUP BY AddRanks.CustomerID,
AddRanks.groupRank
ORDER BY AddRanks.CustomerID,
MIN(AddRanks.DT)
OPTION (MAXRECURSION 0);
You can Find the Execution at DB Fiddle
Try:
select ab.CustomerID,
case when ab_Eff_Dt > c.Eff_Dt then ab_Eff_Dt else c.Eff_Dt end abc_Eff_Dt,
case when ab_End_Dt < c.End_Dt then ab_End_Dt else c.End_Dt end abc_End_Dt
from
(select a.CustomerID,
case when a.Eff_Dt > b.Eff_Dt then a.Eff_Dt else b.Eff_Dt end ab_Eff_Dt,
case when a.End_Dt < b.End_Dt then a.End_Dt else b.End_Dt end ab_End_Dt
from #tmp a
join #tmp b
on a.CustomerID = b.CustomerID and a.Eff_Dt < b.End_Dt and b.Eff_Dt < a.End_Dt
where a.ProductID = 'PRODUCT_A' and b.ProductID = 'PRODUCT_B') ab
join #tmp c
on ab.CustomerID = c.CustomerID and ab_Eff_Dt < c.End_Dt and c.Eff_Dt < ab_End_Dt
where c.ProductID = 'PRODUCT_C'
(SQLFiddle here)
Here's the answer:
select t.customerid, t.eff_dt, count(distinct t2.productId),
MIN(t2.end_dt) as end_dt
from #tmp t join
#tmp t2
on t.CustomerID = t2.CustomerID and
t.Eff_Dt between t2.Eff_Dt and t2.End_Dt
group by t.CustomerID, t.eff_dt
having count(distinct t2.productId) = 3
This is using a self-join to count the number of different products on each eff_dt
. You want three distinct products, so that is what the having
clause is doing.
There are three distinct products until one of them ends. That would be the first end_dt
after the effective date -- which is calculated by the min(end_dt)
.