Flesym
Flesym

Reputation: 573

How to count number of months in T-SQL

I've got a problem in SQL Server.

"Whate'er is well conceived is clearly said, And the words to say it flow with ease", Nicolas Boileau-Despreaux

Well, I don't think I'll be able to make it clear but I'll try ! And I'd like to apologize for my bad english !

I've got this table :

id  ind lvl result  date
1   1   a   3   2017-01-31
2   1   a   3   2017-02-28
3   1   a   1   2017-03-31
4   1   a   1   2017-04-30
5   1   a   1   2017-05-31
6   1   b   1   2017-01-31
7   1   b   3   2017-02-28
8   1   b   3   2017-03-31
9   1   b   1   2017-04-30
10  1   b   1   2017-05-31
11  2   a   3   2017-01-31
12  2   a   1   2017-02-28
13  2   a   3   2017-03-31
14  2   a   1   2017-04-30
15  2   a   3   2017-05-31

I'd like to count the number of month the combo {ind, lvl} remain in the result 1 before re-initializing the number of month to 0 if the result is not 1.

Clearly, I need to get something like that :

id  ind lvl result  date    BadResultRemainsFor%Months
1   1   a   3   2017-01-31  0
2   1   a   3   2017-02-28  0
3   1   a   1   2017-03-31  1
4   1   a   1   2017-04-30  2
5   1   a   1   2017-05-31  3
6   1   b   1   2017-01-31  1
7   1   b   3   2017-02-28  0
8   1   b   3   2017-03-31  0
9   1   b   1   2017-04-30  1
10  1   b   1   2017-05-31  2
11  2   a   3   2017-01-31  0
12  2   a   1   2017-02-28  1
13  2   a   3   2017-03-31  0
14  2   a   1   2017-04-30  1
15  2   a   3   2017-05-31  0

So that if I was looking for the number of months the result was 1 for the date 2017-05-31 with the id 1 and the lvl a, I know it's been 3 months.

Upvotes: 1

Views: 143

Answers (3)

Gurwinder Singh
Gurwinder Singh

Reputation: 39527

Assuming the dates are continously increasing in month, you can use window function like so:

select 
  t.id, ind, lvl, result, dat,
  case when result = 1 then row_number() over (partition by grp order by id) else 0 end x
from (
select t.*, 
  dense_rank() over (order by e, result) grp
from (
select 
  t.*,
  row_number() over (order by id) - row_number() over (partition by ind, lvl, result order by id) e
from your_table t
order by id) t ) t;

Upvotes: 1

Nolan Shang
Nolan Shang

Reputation: 2328

Assume all the date the the end day of month:

    ;WITH tb(id,ind,lvl,result,date) AS(
        select 1,1,'a',3,'2017-01-31' UNION
        select 2,1,'a',3,'2017-02-28' UNION
        select 3,1,'a',1,'2017-03-31' UNION
        select 4,1,'a',1,'2017-04-30' UNION
        select 5,1,'a',1,'2017-05-31' UNION
        select 6,1,'b',1,'2017-01-31' UNION
        select 7,1,'b',3,'2017-02-28' UNION
        select 8,1,'b',3,'2017-03-31' UNION
        select 9,1,'b',1,'2017-04-30' UNION
        select 10,1,'b',1,'2017-05-31' UNION
        select 11,2,'a',3,'2017-01-31' UNION
        select 12,2,'a',1,'2017-02-28' UNION
        select 13,2,'a',3,'2017-03-31' UNION
        select 14,2,'a',1,'2017-04-30' UNION
        select 15,2,'a',3,'2017-05-31' 
    )
    SELECT t.id,t.ind,t.lvl,t.result,t.date
    ,CASE WHEN t.isMatched=1 THEN ROW_NUMBER()OVER(PARTITION BY t.ind,t.lvl,t.id-t.rn ORDER BY t.id) ELSE 0 END
    FROM (
        SELECT t1.*,c.MonthDiff,CASE WHEN c.MonthDiff=t1.result THEN 1 ELSE 0 END  AS isMatched
               ,CASE WHEN c.MonthDiff=t1.result THEN ROW_NUMBER()OVER(PARTITION BY t1.ind,t1.lvl,CASE WHEN c.MonthDiff=t1.result THEN 1 ELSE 0 END ORDER BY t1.id) ELSE null END AS rn
        FROM tb AS t1
        LEFT JOIN tb AS t2 ON t1.ind=t2.ind AND t1.lvl=t2.lvl AND t2.id=t1.id-1
        CROSS APPLY(VALUES(ISNULL(DATEDIFF(MONTH,t2.date,t1.date),1))) c(MonthDiff)

    ) AS t
    ORDER BY t.id
id          ind         lvl  result      date       
----------- ----------- ---- ----------- ---------- --------------------
1           1           a    3           2017-01-31 0
2           1           a    3           2017-02-28 0
3           1           a    1           2017-03-31 1
4           1           a    1           2017-04-30 2
5           1           a    1           2017-05-31 3
6           1           b    1           2017-01-31 1
7           1           b    3           2017-02-28 0
8           1           b    3           2017-03-31 0
9           1           b    1           2017-04-30 1
10          1           b    1           2017-05-31 2
11          2           a    3           2017-01-31 0
12          2           a    1           2017-02-28 1
13          2           a    3           2017-03-31 0
14          2           a    1           2017-04-30 1
15          2           a    3           2017-05-31 0

Upvotes: 2

Damien_The_Unbeliever
Damien_The_Unbeliever

Reputation: 239824

By slightly tweaking your input data and slightly tweaking how we define the requirement, it becomes quite simple to produce the expected results.

First, we tweak your date values so that the only thing that varies is the month and year - the days are all the same. I've chosen to do that my adding 1 day to each value1. The fact that this produces results which are one month advanced doesn't matter here, since all values are similarly transformed, and so the monthly relationships stay the same.

Then, we introduce a numbers table - here, I've assumed a small fixed table is adequate. If it doesn't fit your needs, you can easily locate examples online for creating a large fixed numbers table that you can use for this query.

And, finally, we recast the problem statement. Instead of trying to count months, we instead ask "what's the smallest number of months, greater of equal to zero, that I need to go back from the current row, to locate a row with a non-1 result?". And so, we produce this query:

declare @t table (id int not null,ind int not null,lvl varchar(13) not null,
result int not null,date date not null)
insert into @t(id,ind,lvl,result,date) values
(1 ,1,'a',3,'20170131'),    (2 ,1,'a',3,'20170228'),    (3 ,1,'a',1,'20170331'),
(4 ,1,'a',1,'20170430'),    (5 ,1,'a',1,'20170531'),    (6 ,1,'b',1,'20170131'),
(7 ,1,'b',3,'20170228'),    (8 ,1,'b',3,'20170331'),    (9 ,1,'b',1,'20170430'),
(10,1,'b',1,'20170531'),    (11,2,'a',3,'20170131'),    (12,2,'a',1,'20170228'),
(13,2,'a',3,'20170331'),    (14,2,'a',1,'20170430'),    (15,2,'a',3,'20170531')

;With Tweaked as (
    select
        *,
        DATEADD(day,1,date) as dp1d
    from
        @t
), Numbers(n) 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
)
select
    id,     ind,        lvl,        result,     date,
    COALESCE(
        (select MIN(n) from Numbers n1
        inner join Tweaked t2
        on
            t2.ind = t1.ind and
            t2.lvl = t1.lvl and
            t2.dp1d = DATEADD(month,-n,t1.dp1d)
        where
            t2.result != 1
        ),
        1) as [BadResultRemainsFor%Months]
from
    Tweaked t1

The COALESCE is just there to deal with the edge case, such as for your 1,b data, where there is no previous row with a non-1 result.

Results:

id          ind         lvl           result      date       BadResultRemainsFor%Months
----------- ----------- ------------- ----------- ---------- --------------------------
1           1           a             3           2017-01-31 0
2           1           a             3           2017-02-28 0
3           1           a             1           2017-03-31 1
4           1           a             1           2017-04-30 2
5           1           a             1           2017-05-31 3
6           1           b             1           2017-01-31 1
7           1           b             3           2017-02-28 0
8           1           b             3           2017-03-31 0
9           1           b             1           2017-04-30 1
10          1           b             1           2017-05-31 2
11          2           a             3           2017-01-31 0
12          2           a             1           2017-02-28 1
13          2           a             3           2017-03-31 0
14          2           a             1           2017-04-30 1
15          2           a             3           2017-05-31 0

1An alternative way to perform the adjustment is to use a DATEADD/DATEDIFF pair to perform a "floor" operation against the dates:

DATEADD(month,DATEDIFF(month,0,date),0) as dp1d

Which resets all of the date values to be the first of their own month rather than the following month. This may fell more "natural" to you, or you may already have such values available in your original data.

Upvotes: 2

Related Questions