sqlsql-server

Select next working date and update values in table


I have table in SQL Server with these columns:

CREATE TABLE temp
(
    T_DATE DATE, 
    T_TYPE VARCHAR(20), 
    T_AMT INT
);

Below is the sample data:

INSERT INTO TEMP VALUES('2020-10-19','M',230.23);
INSERT INTO TEMP VALUES('2020-10-19','P',1.23);
INSERT INTO TEMP VALUES('2020-10-20','M',130.23);
INSERT INTO TEMP VALUES('2020-10-20','P',2.23);
INSERT INTO TEMP VALUES('2020-10-21','M',23.23);
INSERT INTO TEMP VALUES('2020-10-21','S',10.23);
INSERT INTO TEMP VALUES('2020-10-22','M',220.23);
INSERT INTO TEMP VALUES('2020-10-22','T',0.23);
INSERT INTO TEMP VALUES('2020-10-23','M',830.23);
INSERT INTO TEMP VALUES('2020-10-23','P',30.23);
INSERT INTO TEMP VALUES('2020-10-26','M',230.23);
INSERT INTO TEMP VALUES('2020-10-26','P',10.23);
INSERT INTO TEMP VALUES('2020-10-27','M',230.23);
INSERT INTO TEMP VALUES('2020-10-27','S',13.23);

This is the output for SELECT * FROM TEMP;:

T_DATE      T_TYPE  T_AMT
-------------------------------
2020-10-19    M     230
2020-10-19    P       1
2020-10-20    M     130
2020-10-20    P       2
2020-10-21    M      23
2020-10-21    S      10
2020-10-22    M     220
2020-10-22    T       0
2020-10-23    M     830
2020-10-23    P      30
2020-10-26    M     230
2020-10-26    P      10
2020-10-27    M     230
2020-10-27    S      13

Here the requirement is, need to update the T_AMT column, below will be the logic.

Example:

Like this:

    2020-10-19  M   230
    2020-10-19  P   1
    2020-10-20  M   230(PREVIOUS DATE T_TYPE VALUE (M)) + 2 (CURRENT DATE T_TYPE VALUE(P)) = 232 
    2020-10-20  P   2

similarly for T_TYPE values as S, but in case of S it should minus instead of plus.

If T_TYPE is neither P or S then nothing should do.

This logic should follow for all rows for T_TYPE = M for each date.....

Going further

The model above is a simplification of the real model to focus on a thread of values;
in reality the table includes multiple, distinct sets of data, with 2 additional columns T_ANO and T_CP that serve as a distinction between sets.

Although to simplify the problem a solution based only on the model above is sufficient,
I would appreciate an answer that, in addition, takes into account this separation (modifying each set independently of the others).

A full dataset with those columns is available in this db<>fiddle


Solution

  • If we think about which rows will change, we will deduce that every M row will be computed solely from the P rows + the first M row. For example, M 2020-10-22 will be computed as M 2020-10-21 + P 2020-10-22; but M 2020-10-21 itself will have been computed as M 2020-10-20 + P 2020-10-21, with M 2020-10-20 itself depending on M 2020-10-19 (which is the first in line so we stop here).
    Thus M 2020-10-22 = M 2020-10-19 + P 2020-10-20 + P 2020-10-21 + P 2020-10-22:
    new M values will be a windowed sum over P and S values + the first M value.

    -- In our new values tables, we shall not omit P and S, because they contribute to the total…
    with newm as
    (
      select
        t_date,
        t_type,
        start_amt + sum(case t_type when 'P' then t_amt when 'S' then - t_amt end) over (order by t_date, t_type desc) t_amt -- order by t_type desc will make P and S appear before the M they should update.
      from
      (
        select top 1 t_date start_date, t_amt start_amt
        from temp where t_type = 'M' order by t_date
      ) firstm
      join temp on temp.t_date > start_date -- The first M does not change, and its P and S don't count. 
    )
    update temp
    set temp.t_amt = newm.t_amt
    from temp join newm on newm.t_type = temp.t_type and newm.t_date = temp.t_date
    where temp.t_type = 'M'; -- … but in the end update only M rows. 
    
    T_DATE T_TYPE T_AMT
    2020-10-19 P 1
    2020-10-19 M 230
    2020-10-20 P 2
    2020-10-20 M 232
    2020-10-21 S 10
    2020-10-21 M 222
    2020-10-22 T 0
    2020-10-22 M 222
    2020-10-23 P 30
    2020-10-23 M 252
    2020-10-26 P 10
    2020-10-26 M 262
    2020-10-27 S 13
    2020-10-27 M 249

    (see it in a fiddle)

    Multi-set variant

    With the addition of T_ANO and T_CP to your question, we have to adapt to make the "set identifier" correctly passed through the chain.
    The query then becomes (with /*>*/ and /*<*/ marking the additions, compared to the first solution):

    with newm as
    (
      select
        /*>*/temp.t_ano, temp.t_cp,/*<*/
        t_date, t_type,
        start_amt + sum(case t_type when 'P' then t_amt when 'S' then - t_amt end) over (/*>*/partition by temp.t_ano, temp.t_cp/*<*/ order by t_date, t_type desc) t_amt -- order by t_type desc will make P and S appear before the M they should update.
      from
      (
        /*>*/
        -- To get the top 1 per t_ano and t_cp, we could either
        -- - continue with top 1 but in a correlated subquery
        -- - group by t_ano and t_cp, min(t_date), and get the corresponding t_amt in a correlated subquery (possibly with top 1)
        -- - distinct of a window over (partition by t_ano, t_cp order by t_date)
        --   we'll choose it here:
        select distinct
          t_ano, t_cp,
          first_value(t_date) over (partition by t_ano, t_cp order by t_date) start_date, -- = min(t_date) over (partition by without order by), but let's keep first_value to get it symetric with t_amt.
          first_value(t_amt) over (partition by t_ano, t_cp order by t_date) start_amt
        from temp where t_type = 'M'
        /*<*/
      ) firstm
      join temp on temp.t_date > start_date -- The first M does not change, and its P and S don't count.
      /*>*/and temp.t_ano = firstm.t_ano and temp.t_cp = firstm.t_cp/*<*/
    )
    update temp
    set temp.t_amt = newm.t_amt
    from temp join newm on newm.t_type = temp.t_type and newm.t_date = temp.t_date
    /*>*/and newm.t_ano = temp.t_ano and newm.t_cp = temp.t_cp/*<*/
    where temp.t_type = 'M';
    

    This runs on your data in a new fiddle.