Sum values on periods

  • Hello,

    I need to sum float values, which are different for different periods of time. I have following table: store, product, from_date, to_date, stock

    Example data:

    1; 1; 2014-02-01; 2014-02-05; 10

    1; 1; 2014-02-03; 2014-02-28; 5

    1; 1; 2014-02-15; null; 20

    2; 1; 2014-01-01; null; 20

    Expected result:

    1; 1; 2014-02-01; 2014-02-02; 10

    1; 1; 2014-02-03; 2014-02-05; 15

    1; 1; 2014-02-06; 2014-02-14; 5

    1; 1; 2014-02-15; 2014-02-28; 25

    1; 1; 2014-03-01; null; 20

    2; 1; 2014-01-01; null; 20

    Any ideas how do achieve such result not using cursor? This is the only thing which came to my mind, but it is very slow 🙁

    Thanks in advance

    Edit:

    I forgot to mention that there can be gaps in the periods and rows with the same dates for particular store and product. Here is script to create table and insert sample data:

    CREATE TABLE dates(

    rn int Identity(1,1),

    DateCreated datetime default GETDATE(),

    store int NOT NULL,

    product varchar(20) NOT NULL,

    FROM_DATE datetime not NULL,

    TO_DATE datetime NULL,

    stock float not null default 0

    )

    go

    insert into dates (store, product, from_date, to_date, stock) values (1, '1', convert(datetime, '20140201', 112), convert(datetime, '20140205', 112), 10);

    insert into dates (store, product, from_date, to_date, stock) values (1, '1', convert(datetime, '20140203', 112), convert(datetime, '20140228', 112), 5);

    insert into dates (store, product, from_date, to_date, stock) values (1, '1', convert(datetime, '20140215', 112), null, 20);

    insert into dates (store, product, from_date, to_date, stock) values (2, '1', convert(datetime, '20140101', 112), null, 20);

    insert into dates (store, product, from_date, to_date, stock) values (3, '1', convert(datetime, '20140101', 112), null, 30);

    insert into dates (store, product, from_date, to_date, stock) values (3, '1', convert(datetime, '20140101', 112), null, 30);

    insert into dates (store, product, from_date, to_date, stock) values (4, '1', convert(datetime, '20140101', 112), convert(datetime, '20140131', 112), 50);

    insert into dates (store, product, from_date, to_date, stock) values (4, '1', convert(datetime, '20140215', 112), convert(datetime, '20140228', 112), 50);

  • What constitutes a period of time? Day/Month/year/etc?

    ______________________________________________________________________________Never argue with an idiot; Theyll drag you down to their level and beat you with experience

  • I cannot see from your example, what kind of logic splits set of dates:

    FROM 2014-02-01 TO 2014-02-05

    FROM 2014-02-03 TO 2014-02-28

    FROM 2014-02-15

    to

    FROM 2014-02-01 TO 2014-02-02

    FROM 2014-02-03 TO 2014-02-05

    FROM 2014-02-06 TO 2014-02-14

    FROM 2014-02-15 TO 2014-02-28

    FROM 2014-03-01

    Why "FROM 2014-02-01 TO 2014-02-05" split to

    "FROM 2014-02-01 TO 2014-02-02" and "FROM 2014-02-03 TO 2014-02-05"

    but not to

    "FROM 2014-02-01 TO 2014-02-03" and "FROM 2014-02-04 TO 2014-02-05"

    ?

    _____________________________________________
    "The only true wisdom is in knowing you know nothing"
    "O skol'ko nam otkrytiy chudnyh prevnosit microsofta duh!":-D
    (So many miracle inventions provided by MS to us...)

    How to post your question to get the best and quick help[/url]

  • +1 Eugene, we need to know how to slice the dates in order to provide the sums...

    Without having any idea whatsoever, this might get the OP started ;WITH CTE AS (

    SELECT

    Stock, Product,

    FROM_DATE,

    TO_DATE ,

    DATEDIFF(d, FROM_DATE, ISNULL(TO_DATE, GETDATE())) Period

    FROM #Dates

    )

    SELECT SUM(Stock) [StockSum], Period

    FROM CTE

    GROUP BY Period

    ORDER BY Period

    ______________________________________________________________________________Never argue with an idiot; Theyll drag you down to their level and beat you with experience

  • I don't know if I understand your question correctly, but period in my case is time between FROM_DATE and TO_DATE.

    I'm not interested in time difference, just dates. However period can be "0" when FROM_DATE = TO_DATE, which means this stock is only for 1 day in store or can be "infinite" when TO_DATE is null. It means we don't know when to stop selling this product.

    I hope this answers your question.

    Edit:

    I need to split (2014-02-01, 2014-02-05) into (2014-02-01, 2014-02-02) and (2014-02-03, 2014-02-05) because on 2014-02-03 stock increases +5 pieces

    FROM_DATETO_DATESTOCK

    2014-02-012014-02-0210

    2014-02-032014-02-0515

    2014-02-062014-02-145

    2014-02-152014-02-2825

    2014-03-01null20

  • How do you define that it goes from 10 to 15 and then back to 5?

    It looks like a running total problem. Before 2012, those problems have several solutions, the fastest is known as Quirky Update[/url]. Some people won't trust it and a well constructed cursor would be the best option for you.

    If we can find out how to partition correctly your data, then the rest is easy. It might help if you post your current code.

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Luis Cazares (2/20/2014)


    How do you define that it goes from 10 to 15 and then back to 5?

    Result stock for partucular date is calculated:

    select ISNULL(SUM(stock), 0) from dates WITH (NOLOCK)

    where store = @store and product = @product

    and from_date <= @date and to_date >= @date

    So 2014-02-02 there will be rows:

    (2014-02-01, 2014-02-05); 10

    But next day 2014-02-03 there will be rows:

    (2014-02-01, 2014-02-05); 10

    (2014-02-03, 2014-02-28); 5

    which gives 15 in total.

    I don't know how to describe it in other way. Please let me know if it is clear now.

    My code now is a little bit complicated and long :blush: but works (slow - I have more than 14.000.000 records in dates table)

    1. Update TO_DATE from null to 2199-12-31

    2. Create "calendar table"

    3. Loop through all stores and products and calculate stock

    update dates set to_date = CONVERT(datetime, '21991231', 112) where to_date is null

    insert into #dates_cal(store, product, CAL_DATE)

    select distinct * from (

    select store, product, from_date from dates

    union

    select store, product, to_date from dates

    ) x

    DECLARE @store int, @prev_store int

    DECLARE @product varchar(20), @prev_prod varchar(20)

    DECLARE @from_date datetime, @to_date datetime, @date datetime

    DECLARE @Stock float, @prev_stock float, @next_stock float

    DECLARE dates_cursor CURSOR FAST_FORWARD READ_ONLY FOR

    SELECT c.*

    FROM #dates_cal c WITH (NOLOCK)

    order by store, product, cal_date

    OPEN dates_cursor

    FETCH NEXT FROM dates_cursor INTO @store, @product, @date

    Set @prev_store = -1

    Set @prev_prod = '#'

    WHILE @@FETCH_STATUS = 0

    BEGIN

    if (@store <> @prev_store or @product <> @prev_prod) begin

    Set @from_date = @date

    Set @prev_stock = null

    end

    Set @Stock = 0

    select @Stock = ISNULL(SUM(stock), 0) from dates WITH (NOLOCK)

    where store = @store and product = @product

    and from_date <= @date and to_date >= @date

    if (ISNULL(@stock, 0) <> ISNULL(@prev_stock, 0) and @prev_stock is not null) begin

    insert into dates_out (store, product, FROM_DATE, TO_DATE, stock)

    values (@store, @product, @from_date, CASE WHEN (@date-1) = CONVERT(datetime, '21991231', 112) THEN null ELSE (@date-1) END, @prev_stock)

    Set @from_date = @date

    end

    Set @prev_stock = @Stock

    Set @next_stock = 0

    select @next_stock = ISNULL(SUM(stock), 0) from dates WITH (NOLOCK)

    where store = @store and product = @product

    and from_date <= (@date+1) and to_date >= (@date+1)

    if (ISNULL(@stock, 0) <> ISNULL(@next_stock, 0)) begin

    insert into dates_out (store, product, FROM_DATE, TO_DATE, stock)

    values (@store, @product, @from_date, CASE WHEN (@date) = CONVERT(datetime, '21991231', 112) THEN null ELSE (@date) END, @Stock)

    Set @from_date = @date + 1

    Set @prev_stock = @next_stock

    end

    Set @prev_store = @store

    Set @prev_prod = @product

    FETCH NEXT FROM dates_cursor INTO @store, @product, @date

    END

    CLOSE dates_cursor

    DEALLOCATE dates_cursor

  • ...

    I need to split (2014-02-01, 2014-02-05) into (2014-02-01, 2014-02-02) and (2014-02-03, 2014-02-05) because on 2014-02-03 stock increases +5 pieces

    FROM_DATETO_DATESTOCK

    2014-02-012014-02-0210

    2014-02-032014-02-0515

    2014-02-062014-02-145

    2014-02-152014-02-2825

    2014-03-01null20

    What in your "sample data " indicates that the stock changed on the 2014-02-03?

    We can see in your expected results only...

    Don't you have some other table which contains some sort of daily transactions, so that where your "split" day is coming from?

    _____________________________________________
    "The only true wisdom is in knowing you know nothing"
    "O skol'ko nam otkrytiy chudnyh prevnosit microsofta duh!":-D
    (So many miracle inventions provided by MS to us...)

    How to post your question to get the best and quick help[/url]

  • This might help others to complete the requirement (and maybe improve it). It's missing the gaps problem (shown on store 4).

    If the gaps aren't a problem, then this should do it. If you have any questions, feel free to ask and document the code as needed.

    CREATE TABLE #dates(

    rn int Identity(1,1),

    DateCreated datetime default GETDATE(),

    store int NOT NULL,

    product varchar(20) NOT NULL,

    FROM_DATE datetime not NULL,

    TO_DATE datetime NULL,

    stock float not null default 0

    )

    go

    insert into #dates (store, product, from_date, to_date, stock) values

    (1, '1', convert(datetime, '20140201', 112), convert(datetime, '20140205', 112), 10)

    ,(1, '1', convert(datetime, '20140203', 112), convert(datetime, '20140228', 112), 5)

    ,(1, '1', convert(datetime, '20140215', 112), null, 20)

    ,(2, '1', convert(datetime, '20140101', 112), null, 20)

    ,(3, '1', convert(datetime, '20140101', 112), null, 30)

    ,(3, '1', convert(datetime, '20140101', 112), null, 30)

    ,(4, '1', convert(datetime, '20140101', 112), convert(datetime, '20140131', 112), 50)

    ,(4, '1', convert(datetime, '20140215', 112), convert(datetime, '20140228', 112), 50);

    WITH E1(N) AS(

    SELECT N FROM( VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))E(N)

    ),

    E2(N) AS(

    SELECT a.N FROM E1 a, E1 b

    ),

    E4(N) AS(

    SELECT a.N FROM E2 a, E2 b

    ),

    Calendar AS(

    SELECT TOP( SELECT DATEDIFF( DAY, MIN(FROM_DATE), MAX(TO_DATE)) + 1 FROM #dates)

    DATEADD( DD, ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1, (SELECT MIN(FROM_DATE) FROM #dates)) caldate

    FROM E4

    ),

    Dates AS(

    SELECT store, product, caldate, SUM( stock) stock

    FROM Calendar t

    JOIN #dates d ON t.caldate BETWEEN d.FROM_DATE AND ISNULL( d.TO_DATE, '9999-01-01')

    GROUP BY store, product, caldate

    )

    SELECT store, product, MIN( caldate) FROM_DATE, MAX(caldate) TO_DATE, stock

    FROM Dates

    GROUP BY store, product, stock

    ORDER BY store, product, FROM_DATE

    SELECT *

    FROM #dates

    ORDER BY store, product, FROM_DATE

    DROP TABLE #dates

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Eugene Elutin (2/20/2014)


    What in your "sample data " indicates that the stock changed on the 2014-02-03?

    We can see in your expected results only...

    Don't you have some other table which contains some sort of daily transactions, so that where your "split" day is coming from?

    System works as follows - there are many factors which calculate how many pieces of product should be in the store for given period of time. There are many factors (let's call them F1, F2 and F3) and when they are calculated stock with given start and end dates is inserted into "dates" table shown above. Total stock of product in store changes in time and logistics should know those changes to properly order goods. So total stock is sum of calculated factors (let's consider product '1' in store 1):

    F1: 2014/02/01 - 2014/02/05; stock = 10

    F2: 2014/02/03 - 2014/02/28; stock = 5

    F3: 2014/02/15 - 9999/12/31; stock = 20

    Now my job is to find intersection of such periods and calculate total stock for them (sum all values) like in the picture from attachment.

    Luis Cazares (2/20/2014)


    This might help others to complete the requirement (and maybe improve it). It's missing the gaps problem (shown on store 4).

    If the gaps aren't a problem, then this should do it. If you have any questions, feel free to ask and document the code as needed.

    Thank you for your suggestion. As soon as I arrive my office, I will try your code. Maybe then I'll have some questions.

  • Here's the final solution with the gaps problem solved (Hoping you won't have dates that go over 25 years in the future).

    WITH E1(N) AS(

    SELECT N FROM( VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))E(N) --10 rows

    ),

    E2(N) AS(

    SELECT a.N FROM E1 a, E1 b -- 10 * 10 = 100 rows

    ),

    E4(N) AS(

    SELECT a.N FROM E2 a, E2 b -- 100 * 100 = 10,000 rows

    ),

    Calendar AS(

    SELECT --TOP( SELECT DATEDIFF( DAY, MIN(FROM_DATE), MAX(TO_DATE)) + 1 FROM #dates)

    DATEADD( DD, ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1, (SELECT MIN(FROM_DATE) FROM #dates)) caldate

    FROM E4 --We create 10,000 dates starting from the first TO_DATE

    ),

    Dates AS(

    SELECT store,

    product,

    caldate,

    SUM( stock) stock, -- joins stocks that have days which overlap

    DATEADD(dd, - ROW_NUMBER() OVER ( PARTITION BY store, product ORDER BY caldate), caldate) AS DateGroup --creates a field to group islands of contiguous dates

    FROM Calendar t

    JOIN #dates d ON t.caldate BETWEEN d.FROM_DATE AND ISNULL( d.TO_DATE, '9999-01-01') --you might consider updating all of your nulls to avoid this function

    GROUP BY store, product, caldate

    )

    SELECT store,

    product,

    MIN( caldate) FROM_DATE, --first date of group

    CASE WHEN MAX(caldate) < DATEADD( YEAR, 25, GETDATE())

    THEN MAX(caldate) END TO_DATE, -- last date of group or null if it's the maximum

    stock

    FROM Dates

    GROUP BY store, product, stock, DateGroup

    ORDER BY store, product, FROM_DATE

    Reference for the gaps solution: http://www.sqlservercentral.com/articles/T-SQL/71550/

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Very creative Luis, kudos!

    ______________________________________________________________________________Never argue with an idiot; Theyll drag you down to their level and beat you with experience

  • MyDoggieJessie (2/20/2014)


    Very creative Luis, kudos!

    Thank you! It became clear after realizing it wasn't a running total.

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Luis Cazares (2/20/2014)


    Here's the final solution with the gaps problem solved (Hoping you won't have dates that go over 25 years in the future).(...)

    Thank you for your solution and sorry for such late response. I'm testing it right, but I have faced problem with TEMPDB size (I have more than 60GB of free space):

    Msg 1101, Level 17, State 10, Line 1

    Could not allocate a new page for database 'TEMPDB' because of insufficient disk space in filegroup 'DEFAULT'. Create the necessary space by dropping objects in the filegroup, adding additional files to the filegroup, or setting autogrowth on for existing files in the filegroup.

    It runs for 30 minutes and then throws this error. Is there any way to eliminate it?

    Also I would be very happy if you could explain your solution a little bit - I'm not so good in SQL programming :blush:

  • I updated my post to include some comments on it. I hope it's easier to understand that way. Try running it one part at a time to understand what it's doing.

    You could change the CTE calendar to a physical table. I'm not sure if it would help but you could try. Verify the maximum date for the calendar when validating TO_DATE in the last select.

    If you have more questions, I'll try to explain further.

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2

Viewing 15 posts - 1 through 15 (of 15 total)

You must be logged in to reply to this topic. Login to reply