Hi. Thank you for your time and expertise.
Please take a look at fully defined problem below and supplied test code. Give it your consideration and see if you see set based solution to this challenge. Expected results are produced by given function.
Thank you again.
Vladimir
SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS ON GO USE tempdb go BEGIN TRANSACTION -- routing is a sequence of operations that a particular manufacturing part travels through CREATE TABLE dbo.Routing ( rotID int NOT NULL -- Routing ID ,ortSequence smallint NOT NULL -- operation sequence ,pltID int -- plant ID in which this operation is performed ,SchedOffset decimal(9,2) NOT NULL -- fraction of a day that requires to process operation --,pltSequence smallint NOT NULL -- operation sequence ,PRIMARY KEY (rotID, ortSequence) ) -- data for two routings. A part can travel through multiple plants INSERT dbo.Routing --OUTPUT inserted.* SELECT rotID ,ortSequence + 1 ,pltID ,SchedOffset FROM ( VALUES (239,0,155,0.2) ,(239,1,155,0.3) ,(239,2,155,0.3) ,(239,3,155,0.3) ,(239,4,155,0.5) -- next is the last operation in prior plant ,(239,5,157,0.4) ,(239,6,157,0.2) ,(239,7,157,1) ,(239,8,157,0.2) ,(239,9,157,1) ,(239,10,157,1) ,(239,11,157,0.5) ,(239,12,156,0.3) ,(241,0,155,0.5) ,(241,1,155,5) ,(241,2,155,0.2) ,(241,3,155,0.3) ,(241,4,155,0.5) ,(241,5,156,0.3) ,(241,6,156,0.4) ,(241,7,156,0.2) ,(241,8,156,0.1) ) r (rotID, ortSequence, pltID, SchedOffset) --SELECT * FROM dbo.Routing ORDER BY rotID, ortSequence -- Each plant has independent calendar/schedule, with marked working(Mfg) days CREATE TABLE dbo.PlantCalendar ( pltID int NOT NULL ,ptcDate date NOT NULL -- date ,ptcMfgDay bit NOT NULL -- determines if given plant is working ,PRIMARY KEY(pltID, ptcDate) ) INSERT dbo.PlantCalendar --OUTPUT inserted.* VALUES -- plant 155 calendar (155, '2013-11-01', 1) ,(155, '2013-11-02', 1) ,(155, '2013-11-03', 0) ,(155, '2013-11-04', 1) ,(155, '2013-11-05', 1) ,(155, '2013-11-06', 1) ,(155, '2013-11-07', 1) ,(155, '2013-11-08', 1) ,(155, '2013-11-09', 0) ,(155, '2013-11-10', 0) ,(155, '2013-11-11', 0) -- long weekend ,(155, '2013-11-12', 1) ,(155, '2013-11-13', 1) ,(155, '2013-11-14', 1) ,(155, '2013-11-15', 1) -- plant 156 calendar ,(156, '2013-11-01', 1) ,(156, '2013-11-02', 1) ,(156, '2013-11-03', 0) ,(156, '2013-11-04', 1) ,(156, '2013-11-05', 1) ,(156, '2013-11-06', 1) ,(156, '2013-11-07', 1) ,(156, '2013-11-08', 1) ,(156, '2013-11-09', 0) ,(156, '2013-11-10', 0) ,(156, '2013-11-11', 1) ,(156, '2013-11-12', 1) ,(156, '2013-11-13', 1) ,(156, '2013-11-14', 1) ,(156, '2013-11-15', 1) -- plant 157 calendar ,(157, '2013-11-01', 1) ,(157, '2013-11-02', 1) ,(157, '2013-11-03', 0) ,(157, '2013-11-04', 1) ,(157, '2013-11-05', 1) ,(157, '2013-11-06', 1) ,(157, '2013-11-07', 1) ,(157, '2013-11-08', 1) ,(157, '2013-11-09', 1) ,(157, '2013-11-10', 0) ,(157, '2013-11-11', 1) ,(157, '2013-11-12', 1) ,(157, '2013-11-13', 0) ,(157, '2013-11-14', 0) ,(157, '2013-11-15', 1) --SELECT * FROM dbo.PlantCalendar /* Processing is on SQL Server 2012 Definition: Given an input date, I need to find operation start/due dates. In other words, backschedule it. A part is due on this input day after the last operation has passed Dates must be only Mfg(manufacturing) dates, when ptcMfgDay = 1. There are no gaps in dates. Each day is either Mfg or not. My present coding: I created a function and recursive query that accomplishes this. However, this must perform well on large data sets, for 1,000s of parts. Therefore, I am looking for record set based solution, non-recursive solution, hoping it will be faster. I spent enough time trying... but maybe you can see a way. */ GO CREATE FUNCTION dbo.fnAPP_getPlantMfgDateDESC ( @pltID int ,@Date date ,@Offset smallint ) RETURNS TABLE WITH SCHEMABINDING AS RETURN ( SELECT pc.ptcDate FROM dbo.PlantCalendar pc WHERE pc.ptcDate <= @Date AND pc.pltID = @pltID AND pc.ptcMfgDay = 1 ORDER BY pc.ptcDate DESC OFFSET CASE WHEN @Offset < 0 THEN 0 ELSE @Offset END ROWS -- offset cannot be negative FETCH NEXT 1 ROWS ONLY ) GO CREATE FUNCTION dbo.fnAPP_getOperationBackschedule ( @rotID int ,@ReqDueDate date ) RETURNS TABLE WITH SCHEMABINDING AS RETURN ( WITH rbs AS -- recursive backscheduling ( SELECT i.rotID --,i.oprID ,i.ortSequence ,i.pltID ,i.SchedOffset -- operation base schedule offset ,i.SchedOffset as SchedOffsetRT -- running total of operation schedule offset by plant ,offset.SchedOffsetCRT -- ceileing of running total of operation schedule offset by plant ,CAST(NULL as decimal(9,2)) as COOffset -- Carryover offset ,pc.ptcDate as StartDate -- operation start date ,rpdd.ptcDate as DueDate -- operation due date ,rpdd.ptcDate as PlantDueDate -- routing plant start date ,rpdd.ptcDate as rotDueDate -- routing due date --,i.pdtID FROM dbo.Routing i -- compute ceiling value so that running total offset always ends up in full days CROSS APPLY(SELECT CEILING(i.SchedOffset) as SchedOffsetCRT) offset -- requested due date may not be calendar mfg date, need to look it up in plant calendar and possibly find earlier mfg date <= requested due date OUTER APPLY dbo.fnAPP_getPlantMfgDateDESC (i.pltID, @ReqDueDate, 0) rpdd -- when SchedOffsetCRT = 1, this means all work can be done within one calendar mfg date, i.e. start = due date, i.e. sql function offset value = 0 OUTER APPLY dbo.fnAPP_getPlantMfgDateDESC (i.pltID, rpdd.ptcDate, offset.SchedOffsetCRT - 1) pc WHERE i.ortSequence = ( -- start from the last operation in input data set SELECT MAX(im.ortSequence) FROM dbo.Routing im WHERE im.rotID = i.rotID ) AND i.rotID = @rotID UNION ALL SELECT i.rotID --,i.oprID ,i.ortSequence ,i.pltID ,i.SchedOffset ,offset_rt.SchedOffsetRT ,offset.SchedOffsetCRT ,co_offset.COOffset ,pc.ptcDate as StartDate ,CASE i.pltID WHEN rbs.pltID THEN rbs.StartDate ELSE pdd.PlantDueDate END as DueDate -- when plant changes, the last operation's due date is plant's due date, not previous operation's start date ,pdd.PlantDueDate ,NULL as rotDueDate --,i.pdtID FROM dbo.Routing i JOIN rbs -- join to the previous operation in a routing; rbs set is all operation values from prior recursive iteration ON rbs.rotID = i.rotID AND rbs.ortSequence - CAST(1 as smallint) = i.ortSequence -- walk back towards starting sequence, i.e. 1; needs casting otherwise 1 is treated as int and this causes implicit column value conversion, failing to seek effectively CROSS APPLY(SELECT CASE WHEN i.pltID != rbs.pltID THEN CAST(rbs.SchedOffsetRT - FLOOR(rbs.SchedOffsetRT) as decimal(9,2)) END as COOffset) as co_offset -- query the next valid mfg day to use when plant is changed, which becomes new PlantDueDate OUTER APPLY dbo.fnAPP_getPlantMfgDateDESC (i.pltID, rbs.StartDate, CASE WHEN rbs.SchedOffsetRT = 0 OR co_offset.COOffset > 0 THEN 0 ELSE 1 END) pdd_next -- when plant changes, reset operation offset running total, i.e partition by it and accumulate current offsets for new plant; use day remainder and carry over into a changed plant if start date of the last operation in a prior plant is also mfg day in new plant CROSS APPLY(SELECT CAST(CASE i.pltID WHEN rbs.pltID THEN rbs.SchedOffsetRT ELSE CASE rbs.StartDate WHEN pdd_next.ptcDate THEN co_offset.COOffset ELSE 0 END END + i.SchedOffset as decimal(9,2)) as SchedOffsetRT) offset_rt -- compute ceiling value so that running total offset always ends up in full days CROSS APPLY(SELECT CEILING(offset_rt.SchedOffsetRT) as SchedOffsetCRT) offset -- when plant changes, preserve due date so it can be a date peg to offset back dates within a given plant. If the current plant can handle prior plant remaining load on the same mfg day, then use this date else use next date as new plant peg. CROSS APPLY(SELECT CASE i.pltID WHEN rbs.pltID THEN rbs.PlantDueDate ELSE pdd_next.ptcDate END as PlantDueDate) as pdd -- get operation start mfg date using current plant's calendar OUTER APPLY dbo.fnAPP_getPlantMfgDateDESC (i.pltID, pdd.PlantDueDate, offset.SchedOffsetCRT - 1) pc ) SELECT i.rotID --,i.oprID ,i.ortSequence ,i.pltID ,i.SchedOffset ,i.SchedOffsetRT ,i.SchedOffsetCRT ,i.COOffset ,i.StartDate ,i.DueDate ,i.PlantDueDate ,CASE i.ortSequence WHEN 1 THEN i.StartDate END as rotStartDate ,i.rotDueDate --,i.pdtID FROM rbs i ) GO -- The above function can now be used to backschedule operation dates, deriving start/due dates for each operation. -- I have tried to come up with non-recursive solution but failed so far. DECLARE @ReqDueDate date = '2013-11-15' SELECT * FROM dbo.fnAPP_getOperationBackschedule (239, @ReqDueDate) ORDER BY ortSequence ASC --SELECT * FROM dbo.fnAPP_getOperationBackschedule (241, @ReqDueDate) ORDER BY ortSequence ASC ------------------------------------------------- -- Anyone sees a better way than recursive CTE? ------------------------------------------------- GO /* Below, I am attempting to create set based code with no recursion but I cannot see how I can apply "running total" to dates. However, it may be possible to create a solution. Then I saw optimization and tried to iterate by plant at a time but that stood no chance performance wise to simple iteration over operation sequence, as additional aggrregates for windowing functions drowned it. It is clear that within a plant a function call will produce the correct dates but how do I pass returned the last ptcDate for the first plant into the same function call for the second plant(or next) in descending sequence? */ SELECT * ,LEAD(rot.SchedOffsetSum, 1)OVER(PARTITION BY rot.rotID ORDER BY rot.ortSequence ASC) - CAST(FLOOR(LEAD(rot.SchedOffsetSum, 1)OVER(PARTITION BY rot.rotID ORDER BY rot.ortSequence ASC)) as decimal(9,2)) as COOffset FROM ( SELECT rot.rotID ,rot.ortSequence ,rot.pltID --, DENSE_RANK()OVER(PARTITION BY rot.rotID ORDER BY rot.ortSequence DESC) --- rot.ortSequence as x ,rot.SchedOffset ,SUM(rot.SchedOffset)OVER(PARTITION BY rot.rotID, rot.pltID) as SchedOffsetSum ,SUM(rot.SchedOffset)OVER(PARTITION BY rot.rotID, rot.pltID ORDER BY rot.ortSequence DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as SchedOffsetRT ,CEILING(SUM(rot.SchedOffset)OVER(PARTITION BY rot.rotID, rot.pltID ORDER BY rot.ortSequence DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) as SchedOffsetCRT --,NULLIF(LEAD(rot.pltID, 1)OVER(PARTITION BY rot.rotID ORDER BY rot.ortSequence ASC), rot.pltID) as pltID_np ,DENSE_RANK()OVER(PARTITION BY rot.rotID ORDER BY ps.pSequence DESC) as pltSequence --,ps.* FROM dbo.Routing rot CROSS APPLY ( SELECT np.ortSequence as pSequence FROM dbo.Routing np WHERE np.rotID = rot.rotID AND np.pltID = rot.pltID ORDER BY np.ortSequence OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ) as ps --CROSS APPLY --( -- SELECT -- MAX(np.ortSequence) as ortSequence -- FROM dbo.Routing np -- WHERE np.rotID = rot.rotID -- AND np.pltID = rot.pltID --) as ps WHERE rot.rotID = 239 ) rot ORDER BY rot.ortSequence
ROLLBACK
Vladimir Moldovanenko