Finding longest sequence in SQL - sql-server

If I have a table which contains dates, eg (in year-month-day then time format):
2015-06-22 12:39:11.257
2015-06-22 15:44:46.790
2015-06-22 15:48:50.583
2015-06-23 08:25:50.060
2015-07-01 07:11:37.037
2015-07-07 13:40:11.997
2015-07-08 13:12:08.723
2015-07-08 13:12:13.900
2015-07-08 13:12:16.010
2015-07-10 12:29:59.777
2015-07-13 15:42:49.077
2015-07-13 15:47:48.670
2015-07-13 15:47:51.547
2015-07-14 08:11:53.023
2015-07-14 08:14:21.243
2015-07-14 08:16:49.410
2015-07-14 08:17:11.997
2015-07-14 09:58:28.840
2015-07-14 09:59:34.640
2015-07-15 15:39:39.993
2015-07-17 08:45:20.157
2015-07-24 14:00:00.487
2015-07-24 14:03:53.773
2015-07-24 14:12:41.717
2015-07-24 14:13:33.957
2015-07-24 14:15:40.953
2015-08-25 12:43:03.920
... is there a way (in SQL) that I can find the longest unbroken sequence of days. I just need the total number of days. So in the above, there are entries for 22nd June and 23rd of June, so the sequence there is 2 days. There's also entries for 13th July, 14th July, and 15th July; this is the longest sequence - 3 days. I don't care about the time portion, so an entry just before midnight, then an entry just after would count as 2 days.
So I want some SQL that can look at the table, and return the value 3 for the above.

No need for a cursor or any type of recursion to solve this. You can do this using a gaps and islands technique. This produces the desired output from your sample data.
with SomeDates as
(
select cast('2015-06-22 12:39:11.257' as datetime) as MyDate union all
select '2015-06-22 15:44:46.790' union all
select '2015-06-22 15:48:50.583' union all
select '2015-06-23 08:25:50.060' union all
select '2015-07-01 07:11:37.037' union all
select '2015-07-07 13:40:11.997' union all
select '2015-07-08 13:12:08.723' union all
select '2015-07-08 13:12:13.900' union all
select '2015-07-08 13:12:16.010' union all
select '2015-07-10 12:29:59.777' union all
select '2015-07-13 15:42:49.077' union all
select '2015-07-13 15:47:48.670' union all
select '2015-07-13 15:47:51.547' union all
select '2015-07-14 08:11:53.023' union all
select '2015-07-14 08:14:21.243' union all
select '2015-07-14 08:16:49.410' union all
select '2015-07-14 08:17:11.997' union all
select '2015-07-14 09:58:28.840' union all
select '2015-07-14 09:59:34.640' union all
select '2015-07-15 15:39:39.993' union all
select '2015-07-17 08:45:20.157' union all
select '2015-07-24 14:00:00.487' union all
select '2015-07-24 14:03:53.773' union all
select '2015-07-24 14:12:41.717' union all
select '2015-07-24 14:13:33.957' union all
select '2015-07-24 14:15:40.953' union all
select '2015-08-25 12:43:03.920'
)
, GroupedDates as
(
select cast(MyDate as DATE) as MyDate
, DATEADD(day, - ROW_NUMBER() over (Order by cast(MyDate as DATE)), cast(MyDate as DATE)) as DateGroup
from SomeDates
group by cast(MyDate as DATE)
)
, SortedDates as
(
select DATEDIFF(day, min(MyDate), MAX(MyDate)) + 1 as GroupCount
, min(MyDate) as StartDate
, MAX(MyDate) as EndDate
from GroupedDates
group by DateGroup
)
select top 1 GroupCount
, StartDate
, EndDate
from SortedDates
order by GroupCount desc

The input here is, in fact:
select trunc(date_column,'DD') day
from your_table
group by trunc(date_column,'DD');
From this point I can consider dates as numbers to input more easier the data and your problem is to find longest consecutive sequence.
so, an input table:
create table a(
col integer);
insert into a values (1);
insert into a values (2);
insert into a values (4);
insert into a values (5);
insert into a values (6);
insert into a values (8);
insert into a values (9);
insert into a values (11);
insert into a values (13);
insert into a values (14);
insert into a values (17);
and with this query you will get the longest sequence starting from every line:
with s(col, i) as (
select col, 1 i from a
union all
select a.col, i + 1
from s join a on s.col = a.col+1
)
--select * from s
select col, max(i)
from s
group by col
order by col
;
Result:
col max
1 2
2 1
4 3
5 2
6 1
8 2
9 1
11 1
13 2
14 1
17 1
From this point you can easily select the maximum. Also, for dates you can use dateadd(dd,1,date_column).
The explanation of recursive CTE: For every row I will find (if exists) the next row and increment the column i. The recursion exits when there are no "next" line.
OBS: I believe the code can be improved, but you got the ideea.
SQLFIDDLE
UPDATE To improve performance and keep using recursivity we can start only from numbers that doesn't have a prior consecutive number.
with p as (
select * from (
select col, coalesce(col - (lag(col) over (order by col)),2) as has_prev
from a
) b
where has_prev != 1
),
s(col, i) as (
select col, 1 i from p
union all
select s.col, i + 1
from s join a on s.col+i = a.col
)
--select * from p
select col, max(i)
from s
group by col
order by col
;
SQLFIDDLE2

Related

Group records based on time interval starting from timestamp of first record in each group

Struggling with this; need to group records within a specific time interval starting from the first timestamp (FREEZE_TIME) - but the first record outside the first group is the starting point for the time interval for the next group and so on. Expected result, THAW_COUNT, is the count of all groups for a PARENT_SAMPLE_ID. So for table:
SAMPLE_ID
FREEZE_TIME
PARENT_SAMPLE_ID
1
null
null
2
2015-11-27 10:23:10
1
3
2015-11-27 10:59:23
1
4
2015-11-27 11:05:43
1
5
2015-11-27 12:53:48
1
6
2015-11-27 13:42:25
1
I would like to get a result of:
PARENT_SAMPLE_ID
THAW_COUNT
1
2
So sample_id:s 2,3 and 4 should be in the same group and sample id:s 5 and 6 are in the next group.
I have tried something like:
with SampleList as
(
select PARENT_SAMPLE_ID, FREEZE_TIME,
ROW_NUMBER() OVER (partition by PARENT_SAMPLE_ID order by FREEZE_TIME asc) RN
from
SAMPLE
)
,
FirstSample as
(
select PARENT_SAMPLE_ID, FREEZE_TIME
from SampleList
where RN = 1
)
,
SelectedSample as
(
select
s.PARENT_SAMPLE_ID,
ABS(DATEDIFF(MINUTE, s.FREEZE_TIME, sFirst.FREEZE_TIME))/60 DiffToFirst
from SampleList s
inner join FirstSample sFirst ON s.PARENT_SAMPLE_ID = sFirst.PARENT_SAMPLE_ID
group by s.PARENT_SAMPLE_ID, ABS(DATEDIFF(MINUTE, s.FREEZE_TIME, sFirst.FREEZE_TIME))/60
)
select PARENT_SAMPLE_ID, count(*) THAW_COUNT
from SelectedSample
group by PARENT_SAMPLE_ID
But this will return a THAW_COUNT of 3 as sampleId:s 5 and 6 will be in different groups because the grouping is based on hour intervals from freeze time of sampleId 2 only. How do I get the grouping for group 2 to start from the first record outside the first group (sampleId 5) and so on?
This can be treated as a gaps and islands problem. Using some windows functions to check counts and using LAG to look at the "previous" row we can solve this. If you have multiple values for SAMPLE_ID you will want to add some partitioning.
create table #Something
(
SAMPLE_ID int
, FREEZE_TIME datetime
, PARENT_SAMPLE_ID int
)
insert #Something
select 1, null, null union all
select 2, '2015-11-27 10:23:10', 1 union all
select 3, '2015-11-27 10:59:23', 1 union all
select 4, '2015-11-27 11:05:43', 1 union all
select 5, '2015-11-27 12:53:48', 1 union all
select 6, '2015-11-27 13:42:25', 1;
with MyGroups as
(
select *
, GroupNum = count(IsNewGroup) over (order by FREEZE_TIME rows unbounded preceding)
from
(
select *
, IsNewGroup = case when LAG(FREEZE_TIME, 1, '') over(order by FREEZE_TIME) < dateadd(hour, -1, FREEZE_TIME) then 1 end
from #Something
) x
)
select coalesce(PARENT_SAMPLE_ID, SAMPLE_ID)
, count(distinct GroupNum)
from MyGroups
group by coalesce(PARENT_SAMPLE_ID, SAMPLE_ID)
drop table #Something

Is it possible to use the SQL DATEADD function but exclude dates from a table in the calculation?

Is it possible to use the DATEADD function but exclude dates from a table?
We already have a table with all dates we need to exclude. Basically, I need to add number of days to a date but exclude dates within a table.
Example: Add 5 days to 01/08/2021. Dates 03/08/2021 and 04/08/2021 exist in the exclusion table. So, resultant date should be: 08/08/2021.
Thank you
A bit of a "wonky" solution, but it works. Firstly we use a tally to create a Calendar table of dates, that exclude your dates in the table, then we get the nth row, where n is the number of days to add:
DECLARE #DaysToAdd int = 5,
#StartDate date = '20210801';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --Up to 1,000
Calendar AS(
SELECT DATEADD(DAY,T.I, #StartDate) AS D,
ROW_NUMBER() OVER (ORDER BY T.I) AS I
FROM Tally T
WHERE NOT EXISTS (SELECT 1
FROM dbo.DatesTable DT
WHERE DT.YourDate = DATEADD(DAY,T.I, #StartDate)))
SELECT D
FROM Calendar
WHERE I = #DaysToAdd+1;
A best solution is probably a calendar table.
But if you're willing to traverse through every date, then a recursive CTE can work. It would require tracking the total iterations and another column to substract if any traversed date was in the table. The exit condition uses the total difference.
An example dataset would be:
CREATE TABLE mytable(mydate date); INSERT INTO mytable VALUES ('20210803'), ('20210804');
And an example function run in it's own batch:
ALTER FUNCTION dbo.fn_getDays (#mydate date, #daysadd int)
RETURNS date
AS
BEGIN
DECLARE #newdate date;
WITH CTE(num, diff, mydate) AS (
SELECT 0 AS [num]
,0 AS [diff]
,DATEADD(DAY, 0, #mydate) [mydate]
UNION ALL
SELECT num + 1 AS [num]
,CTE.diff +
CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable)
THEN 0 ELSE 1 END
AS [diff]
,DATEADD(DAY, num+1, #mydate) [mydate]
FROM CTE
WHERE (CTE.diff +
CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable)
THEN 0 ELSE 1 END) <= #daysadd
)
SELECT #newdate = (SELECT MAX(mydate) AS [mydate] FROM CTE);
RETURN #newdate;
END
Running the function:
SELECT dbo.fn_getDays('20210801', 5)
Produces output, which is the MAX(mydate) from the function:
----------
2021-08-08
For reference the MAX(mydate) is taken from this dataset:
n diff mydate
----------- ----------- ----------
0 0 2021-08-01
1 1 2021-08-02
2 1 2021-08-03
3 1 2021-08-04
4 2 2021-08-05
5 3 2021-08-06
6 4 2021-08-07
7 5 2021-08-08
You can use the IN clause.
To perform the test, I used a W3Schools Test DB
SELECT DATE_ADD(BirthDate, INTERVAL 10 DAY) FROM Employees WHERE FirstName NOT IN (Select FirstName FROM Employees WHERE FirstName LIKE 'N%')
This query shows all the birth dates + 10 days except for the only employee with name starting with N (Nancy)

reset window function when the time gap is over one hour

I have a dataset already sorted by a window function in sql:
ROW_NUMBER() OVER (PARTITION BY LOAN_NUMBER, CAST(CREATED_DATE AS DATE) ORDER BY LOAN_NUMBER, CREATED_DATE) AS ROW_IDX
shown as above. I wonder if there's a way that reset the ROW_IDX when the CREATED_DATE has begun to have a value with over one hour gap to the minimum datetime in a specific day.
For example, the row index for row 3 should be 1 because the time gap between 2016-11-03 15:39:16.000 and 2016-11-03 12:44:11.000 is over one hour.And row index of row 4 will be 2.
I've tried several ways to manipulate the datatime column, since the consideration is about 'gap' instead of moments of the day, no rounding methods worked perfectly.
Are mean ,when the gap more than 60 minutes, will restart at 1?
Which version are you use? If it is SQL Server 2012+, you can try this.
The following query is not satisfying, but wish can give you help.
Calculating the diff minutes between continuous two line.
Check the diff minutes whether greater than one hour
Get row number base on the gap time has same situation continuously.
Sorry if I can not describe clear. My english is not well.
;WITH tb(RptDate,ISSUE_ID,ACCOUNT,CREATED_DATE )AS(
select '2017-01-17','35775','76505156','2016-11-03 12:44:11.000' UNION
select '2017-01-17','35793','76505156','2016-11-03 12:51:43.000' UNION
-- select '2017-01-17','35793','76505156','2016-11-03 13:47:43.000' UNION
-- select '2017-01-17','35793','76505156','2016-11-03 14:45:43.000' UNION
select '2017-01-17','36097','76505156','2016-11-03 15:39:16.000' UNION
select '2017-01-17','36132','76505156','2016-11-03 15:52:51.000' UNION
select '2017-01-17','41391','76505156','2016-11-10 10:49:30.000'
)
SELECT *,ROW_NUMBER()OVER(PARTITION BY tt.ACCOUNT,a ORDER BY tt.ACCOUNT, rn) AS ROW_IDX FROM (
SELECT * ,rn-ROW_NUMBER () OVER (PARTITION BY ACCOUNT, CAST(CREATED_DATE AS DATE),n ORDER BY rn) AS a
FROM (
SELECT *, ROW_NUMBER()OVER(PARTITION BY ACCOUNT ORDER BY CREATED_DATE) AS rn
,CASE WHEN DATEDIFF(MINUTE, LAG(CREATED_DATE)OVER(PARTITION BY ACCOUNT ORDER BY CREATED_DATE),tb.CREATED_DATE)>60 THEN 1 ELSE 0 END AS n
,ISNULL(DATEDIFF(MINUTE, LAG(CREATED_DATE)OVER(PARTITION BY ACCOUNT ORDER BY CREATED_DATE),tb.CREATED_DATE),0) AS DiffMin
FROM tb
) AS t
) AS tt
ORDER BY rn
RptDate ISSUE_ID ACCOUNT CREATED_DATE rn n DiffMin a ROW_IDX
---------- -------- -------- ----------------------- -------------------- ----------- ----------- -------------------- --------------------
2017-01-17 35775 76505156 2016-11-03 12:44:11.000 1 0 0 0 1
2017-01-17 35793 76505156 2016-11-03 12:51:43.000 2 0 7 0 2
2017-01-17 36097 76505156 2016-11-03 15:39:16.000 3 1 168 2 1
2017-01-17 36132 76505156 2016-11-03 15:52:51.000 4 0 13 1 1
2017-01-17 41391 76505156 2016-11-10 10:49:30.000 5 1 9777 4 1
It is another script,Do not use the LAG function, Each step has a statement:
;WITH tb(RptDate,ISSUE_ID,ACCOUNT,CREATED_DATE )AS(
select '2017-01-17','35775','76505156','2016-11-03 12:44:11.000' UNION
select '2017-01-17','35793','76505156','2016-11-03 12:51:43.000' UNION
-- select '2017-01-17','35793','76505156','2016-11-03 13:47:43.000' UNION
-- select '2017-01-17','35793','76505156','2016-11-03 14:45:43.000' UNION
select '2017-01-17','36097','76505156','2016-11-03 15:39:16.000' UNION
select '2017-01-17','36132','76505156','2016-11-03 15:52:51.000' UNION
select '2017-01-17','41391','76505156','2016-11-10 10:49:30.000'
),t1 AS(
SELECT *, ROW_NUMBER()OVER(PARTITION BY ACCOUNT ORDER BY CREATED_DATE) AS rn FROM tb
),t2 AS (
SELECT t1.*,CASE WHEN DATEDIFF(MINUTE,tt.CREATED_DATE,t1.CREATED_DATE)>60 THEN 1 ELSE 0 END AS m
,t1.rn-ROW_NUMBER()OVER(PARTITION BY t1.ACCOUNT,CASE WHEN DATEDIFF(MINUTE,tt.CREATED_DATE,t1.CREATED_DATE)>60 THEN 1 ELSE 0 END ORDER BY t1.CREATED_DATE) AS a
FROM t1 LEFT JOIN t1 AS tt ON tt.ACCOUNT=t1.ACCOUNT AND tt.rn=t1.rn-1
),t3 AS(
SELECT *,ROW_NUMBER()OVER(PARTITION BY ACCOUNT,t2.a ORDER BY CREATED_DATE) AS ROW_IDX
FROM t2
)
SELECT * FROM t3
ORDER BY t3.ACCOUNT,t3.CREATED_DATE

datepart function not working in sql azure

I want to split date part like year and assign it to a variable in a stored procedure.
I run that stored procedure in sql azure. it throws error "Reference to database and/or server name in 'MASTER..spt_values' is not supported in this version of SQL Server."
Code:
declare #Year int
SET #Year =DATEPART(YYYY,GETDATE())
create table #SundayDates (Sunday datetime,NextSunday datetime)
INSERT INTO #SundayDates(Sunday,NextSunday)
SELECT max(dates),MAX(DATEADD(DD,+7,dates)) AS last_sunday from
(
SELECT dateadd(day,number-1,DATEADD(year,#year-1900,0)) AS dates
FROM MASTER..spt_values WHERE type='p' and
number between 1 and DATEDIFF(day,DATEADD(year,#year-1900,0),DATEADD(year,#year-1900+1,0))
) as t
WHERE DATENAME(weekday,dates)='sunday' GROUP BY DATEADD(month,datediff(month,0,dates),0)
This query gives the same results (as a result set, rather than inserting into a temp table, but can be easily adapted to do so) and doesn't rely on the spt_values table that the error message tells you isn't allowed:
;With Numbers (Num) as (
select 0 union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all
select 6 union all select 7 union all select 8 union all select 9 union all select 10 union all select 11
), MonthEnds as (
select DATEADD(month,DATEDIFF(year,'20010101',CURRENT_TIMESTAMP)*12 + n.Num,'20010131') as EndDate
from Numbers n
), LastSundays as (
select DATEADD(day,-n.Num,EndDate) as EndDate
from
MonthEnds me
cross join
Numbers n
where
n.Num between 0 and 6 and
DATEPART(weekday,DATEADD(day,-n.Num,EndDate)) = DATEPART(weekday,'20130512')
)
select EndDate,DATEADD(day,7,EndDate) as FollowingSunday
from LastSundays

How do I calculate on-time delivery when SQL tables aren't one to one

I have the following two tables:
DueDates:
DECLARE #DueDates TABLE (
uniqueln varchar(10),
ship_dts smalldatetime,
qty decimal(18,4))
INSERT INTO #DueDates
SELECT '51351621AS','1/1/2013',7
UNION ALL
SELECT '51351621AS','1/7/2013',7
UNION ALL
SELECT '51351621AS','1/14/2013',7
UNION ALL
SELECT '51351621AS','1/21/2013',7
UNION ALL
SELECT '51351621AS','1/28/2013',7
UNION ALL
SELECT 'V4351621AS','1/5/2013',10
UNION ALL
SELECT 'V4351621AS','1/10/2013',10
UNION ALL
SELECT 'V4351621AS','1/15/2013',10
UNION ALL
SELECT 'V4351621AS','1/20/2013',10
UNION ALL
SELECT 'V4351621AS','1/25/2013',10
PlDetail
DECLARE #PlDetail TABLE (
uniqueln varchar(10),
shipdate smalldatetime,
shippedqty decimal(18,4))
INSERT INTO #PlDetail
SELECT '51351621AS','1/1/2013',10
UNION ALL
SELECT '51351621AS','1/7/2013',10
UNION ALL
SELECT '51351621AS','1/14/2013',10
UNION ALL
SELECT '51351621AS','1/21/2013',5
UNION ALL
SELECT 'V4351621AS','1/5/2013',13
UNION ALL
SELECT 'V4351621AS','1/15/2013',9
UNION ALL
SELECT 'V4351621AS','1/25/2013',12
UNION ALL
SELECT 'V4351621AS','1/30/2013',10
UNION ALL
SELECT 'V4351621AS','2/5/2013',6
The shipments in PlDetail can be one to one with the orders in the DueDates table, but often they are not.
I am trying to calculate an on-time delivery for each uniqueln schedule using a FIFO method (I cannot change how the data is stored in the tables). Basically, I want to apply the earliest shipments to the earliest deliveries.
If a shipment qty exceeds the balance in a DueDates record, it should have the balance applied to the next scheduled delivery.
The end result should look something like this:
uniqueln ship_dts qty shipdate shippedqty daysLate
51351621AS 1/1/2013 7 1/1/2013 7 0
51351621AS 1/7/2013 7 1/1/2013 3 -6
51351621AS 1/7/2013 7 1/7/2013 4 0
51351621AS 1/14/2013 7 1/7/2013 6 -7
51351621AS 1/14/2013 7 1/14/2013 1 0
51351621AS 1/21/2013 7 1/14/2013 7 -7
51351621AS 1/28/2013 7 1/14/2013 2 -14
51351621AS 1/28/2013 7 1/21/2013 5 -7
V4351621AS 1/5/2013 10 1/5/2013 10 0
V4351621AS 1/10/2013 10 1/5/2013 3 -5
V4351621AS 1/10/2013 10 1/15/2013 7 5
V4351621AS 1/15/2013 10 1/15/2013 2 0
V4351621AS 1/15/2013 10 1/25/2013 8 10
V4351621AS 1/20/2013 10 1/25/2013 4 5
V4351621AS 1/20/2013 10 1/30/2013 6 10
V4351621AS 1/25/2013 10 1/30/2013 4 5
V4351621AS 1/25/2013 10 2/5/2013 6 11
I know how to group the PlDetail shipments strictly by date so any shipment that falls on or before the next due date is grouped together, but it HAS to factor in the scheduled qty vs the shipped qty.
I don't want to create a cursor and cycle through the shipment records, but I can if this type of join won't work. However, I believe it is possible, but I am not sure how to group or join the tables.
It sounds like SQL Server 2012 has some new methods that will make this easier, but right now I am using SQL SERVER 2008 R2 and have to keep it that way for the near future.
What is the best way to approach this? Is a cursor really the only way?
UPDATE:
This is what I have added so far. The end result is a table showing the from and to qty and ship_dts for each uniqueln
WITH DSeq AS (
SELECT TOP 100 PERCENT
Seq = Row_Number() OVER (partition by uniqueln ORDER BY ship_dts),
D.UNIQUELN,
D.SHIP_DTS,
SchBal = (SELECT TOP 100 PERCENT SUM(B.Qty) FROM #DueDates B WHERE b.SHIP_DTS<= D.SHIP_DTS AND b.UNIQUELN=d.UNIQUELN ORDER BY d.SHIP_DTS)
FROM #DueDates D
GROUP BY UNIQUELN,D.QTY,D.UNIQUELN,D.SHIP_DTS
ORDER BY D.UNIQUELN, D.SHIP_DTS
)
--SELECT * FROM DSeq
, Slices AS (
SELECT
LN = D.UNIQUELN,
FromQty = COALESCE(N.SchBal,0),
ToQty = D.SchBal,
D.SHIP_DTS
FROM
DSeq D
LEFT OUTER JOIN DSeq N
ON D.Seq -1 = N.Seq AND D.UNIQUELN=N.UNIQUELN
)
SELECT * FROM Slices
CURSOR APPROACH:
As has been stated, the best approach might be a cursor. Hopefully someone has a join method that will meet all the needs, but until then we are using a cursor.
In case someone is looking for a solution to this with a cursor approach, the code below is how we have done it. The user selects a date range and it produces the results. Please note that you HAVE to run the cursor for ALL records of the uniqueln even if they shipped prior to the selected date range, otherwise the FIFO application of shipments will be wrong.
DECLARE #startdate smalldatetime, #endDate smalldatetime
DECLARE #OnTime TABLE (Uniqueln varchar(10),DueQty int,dueDate smalldatetime,shipDate smalldatetime,shipQty int DEFAULT 0,daysLate int,balQty int)
DECLARE #uniqln1 varchar(10),#toQty int, #dueDate smalldatetime,#bQty int
DECLARE #uniqln2 varchar(10),#shipQty int, #shipDate smalldatetime
DECLARE ot_cursor CURSOR LOCAL FAST_FORWARD
FOR
SELECT uniqueln,qty,ship_dts,qty
FROM #DueDates
ORDER BY uniqueln,ship_dts
OPEN ot_cursor;
FETCH NEXT FROM ot_cursor INTO #uniqln1,#toQty,#dueDate,#bQty
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE s_cursor CURSOR LOCAL FAST_FORWARD
FOR
SELECT Uniqueln,shippedqty,shipdate
FROM #PlDetail p
WHERE uniqueln = #uniqln1
ORDER BY 3
OPEN s_cursor ;
FETCH NEXT FROM s_cursor INTO #uniqln2,#shipQty,#shipDate
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO #OnTime(Uniqueln,DueQty,dueDate,shipDate,shipQty,daysLate,balQty)
SELECT #uniqln1,#toQty,#dueDate,#shipDate,CASE WHEN #bQty>#shipQty THEN #shipQty ELSE #bQty END,DATEDIFF(d,#dueDate,#shipDate),CASE WHEN #bQty>#shipQty THEN #bQty-#shipQty ELSE 0 END
SET #bQty=#bQty-#shipQty
IF #bQty < 0
BEGIN
SET #shipQty = -#bQty
FETCH NEXT FROM ot_cursor INTO #uniqln1,#toQty,#dueDate,#bQty
END
ELSE
IF #bQty =0
BEGIN
BREAK
END
ELSE
BEGIN
SET #shipQty = #bQty
FETCH NEXT FROM s_cursor INTO #uniqln2,#shipQty,#shipDate
END
END
CLOSE s_cursor
DEALLOCATE s_cursor
FETCH NEXT FROM ot_cursor INTO #uniqln1,#toQty,#dueDate,#bQty
END
CLOSE ot_cursor
DEALLOCATE ot_cursor
SELECT * FROM #OnTime
WHERE shipDate BETWEEN #startdate AND #endDate
ORDER BY Uniqueln,dueDate
Tried putting something together using CTEs and a pidgin cross join between the tables. (don't ask, it was ugly)
Anywho, after a bit of reading, and knowing how large the tables are, I feel pretty safe answering this with... drumroll... use a cursor. It'll be ugly and slow, but the logic will make much more sense on paper. There's a lot to be said about maintainable code...
Update: Going on vacation. Here's what I was playing with.
DECLARE #DueDates TABLE (
uniqueln varchar(10),
ship_dts smalldatetime,
qty decimal(18,4))
INSERT INTO #DueDates
SELECT '51351621AS','1/1/2013',7
UNION ALL
SELECT '51351621AS','1/7/2013',7
UNION ALL
SELECT '51351621AS','1/14/2013',7
UNION ALL
SELECT '51351621AS','1/21/2013',7
UNION ALL
SELECT '51351621AS','1/28/2013',7
UNION ALL
SELECT 'V4351621AS','1/5/2013',10
UNION ALL
SELECT 'V4351621AS','1/10/2013',10
UNION ALL
SELECT 'V4351621AS','1/15/2013',10
UNION ALL
SELECT 'V4351621AS','1/20/2013',10
UNION ALL
SELECT 'V4351621AS','1/25/2013',10
DECLARE #PlDetail TABLE (
uniqueln varchar(10),
shipdate smalldatetime,
shippedqty decimal(18,4))
INSERT INTO #PlDetail
SELECT '51351621AS','1/1/2013',10
UNION ALL
SELECT '51351621AS','1/7/2013',10
UNION ALL
SELECT '51351621AS','1/14/2013',10
UNION ALL
SELECT '51351621AS','1/21/2013',5
UNION ALL
SELECT 'V4351621AS','1/5/2013',13
UNION ALL
SELECT 'V4351621AS','1/15/2013',9
UNION ALL
SELECT 'V4351621AS','1/25/2013',12
UNION ALL
SELECT 'V4351621AS','1/30/2013',10
UNION ALL
SELECT 'V4351621AS','2/5/2013',6
; WITH DueDates AS
(
SELECT b.*
FROM #DueDates a
JOIN #DueDates b
ON a.uniqueln = b.uniqueln
AND b.ship_dts >= a.ship_dts
)
, PlDetail AS
(
SELECT b.*
FROM #PlDetail a
JOIN #PlDetail b
ON a.uniqueln = b.uniqueln
AND b.shipdate >= a.shipdate
)
SELECT a.uniqueln
, SUM(a.qty) AS ordered_running_total
, SUM(b.shippedqty) AS shipped_running_total
, a.ship_dts
, b.shipdate
, SUM(b.shippedqty) - SUM(a.qty) AS leftover_running_total
FROM DueDates a
JOIN PlDetail b
ON a.uniqueln = b.uniqueln
AND a.ship_dts >= b.shipdate
GROUP BY a.uniqueln, a.ship_dts, b.shipdate
HAVING SUM(a.qty) <= SUM(b.shippedqty)
ORDER BY a.uniqueln, a.ship_dts, b.shipdate

Resources