How to count number of months in T-SQL - sql-server

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.

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

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.

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;

Related

SQL Server add a column and populate multiple rows based on conditions across multiple columns

I have a table that looks like this:
Timestamp CPID Con Context Type Value
2018-01-01 03:11 1 2 6 8 0
2018-01-01 03:11 1 2 3 8 0
2018-01-01 03:11 1 2 3 3 100
2018-01-01 03:15 2 1 6 8 16
2018-01-01 03:15 2 1 3 8 15
2018-01-01 03:15 2 1 3 3 200
I want to add a column called new_column, and populate it with 1s when Value=0 when Context=6. I want to consider Timestamp, CPID and Con as a group, so that when for a given group has Context=6, the other rows in that group are also assigned 1 in new_column. The result would look like this:
Timestamp CPID Con Context Type Value new_column
2018-01-01 03:11 1 2 6 8 0 1
2018-01-01 03:11 1 2 3 8 0 1
2018-01-01 03:11 1 2 3 3 100 1
2018-01-01 03:15 2 1 6 8 16 0
2018-01-01 03:15 2 1 3 8 15 0
2018-01-01 03:15 2 1 3 3 200 0
Notes: the row orders are not always the same, so I can't just fill down 2 rows every time; I also cannot directly ALTER Table because it is read only.
I'm still new to SQL so struggling with this one.
You can create a view by using exists :
select t.*,
(case when exists (select 1
from table t1
where t1.Timestamp = t.Timestamp and t1.CPID = t.CPID and
t1.Con = t.Con and (t1.Context = 6 or t1.Value = 0)
)
then 1 else 0
end) as new_column
from table t;

Get output of multiple Counts in one query

Could you help me how to write a query for the following issue:
There are two tables:
Table persons:
P_id Name BirthDate
1 N1 2016-08-02
2 N2 2015-05-02
3 N3 2013-06-01
4 N4 2014-01-09
Table visited:(p_id is foreign key to table persons)
Id. Visitor_id. P_id. Visit_date
1 10 1 2017-03-05
2 11 2 2017-01-01
3 10 2 2017-02-03
4 12 3 2016-05-07
5 11 4 2016-04-09
6 10 1 2017-04-09
We are going to get the count of visited by each Visitor and also count of visited distinct person on filter on for those person who their age are under 1, between 1 and 2, between 2 and 3 at date of visit_date by each visitor_id.
The results should be like :
Under_one Bet_one_two Bet_two_three
Visitor_id VisitedCount/PersonCount VisitedCount/PersonCount VisitedCount/PersonCount
10 2 1 1 1 0 0
11 0 0 1 1 1 1
12 0 0 0 0 1 1
Between 1 and 2 means the result of subtracting visited_date and birthdate (for example : the result of 2013/03/05 - 2011/06/07) is between 1 and 2 years.
I don't know if I can give you the output laid out exactly as you have specified, but this
SELECT
visited.Visitor_id,
visited.P_id,
Int(([Visit_date]-[BirthDate])/365) AS Age,
Count(persons.P_id) AS NumVisits
FROM persons INNER JOIN visited ON persons.P_id = visited.P_id
GROUP BY
visited.Visitor_id,
visited.P_id,
Int((-[BirthDate]+[Visit_date])/365);
returns
Visitor_id P_id Age NumVisits
10 1 0 2
10 2 1 1
11 2 1 1
11 4 2 1
12 3 2 1

Select data from previous rows with condition

My data looks like this
col1; col2; col3; col4; sort; desc; levels
1 1 1 1 10 ab null
1 1 1 2 20 c 1
1 1 1 3 30 d null
1 1 1 4 40 e null
1 1 1 5 50 fg 1
1 1 1 6 60 hij 1
1 1 1 7 70 k 1
The desired output looks like this
col1; col2; col3; col4; desc; addDesc
1 1 1 1 ab null
1 1 1 2 c ab
1 1 1 3 d null
1 1 1 4 e null
1 1 1 5 fg e
1 1 1 6 hij e
1 1 1 7 k e
The logic is whenever the column LEVELS is not null, take the value from previous DESC where LEVELS is null.
I hope you guys understand this!
Thank you all in advance!
You can use OUTER APPLY:
SELECT t1.col1,
t1.col2,
t1.col3,
t1.col4,
t1.[desc],
CASE
WHEN t1.levels IS NOT NULL THEN t2.[desc]
END [addDesc]
FROM dbo.YourTable t1
OUTER APPLY (SELECT TOP 1 [desc]
FROM dbo.YourTable
WHERE levels IS NULL
AND sort < t1.sort
ORDER BY sort DESC) t2;

SQL Server SELECT Rank() and Grouping of data

Please help me with this specific query.
Here's my sample query
with cte as
(
select entryID,LogDateTime,logtype[logtype],batch,
rank() over (partition by logType
order by logdatetime) rnk
from Emp_TimeLog
where cast(LogDateTime as date) = '2016-05-17'
) select entryID,LogDateTime,logType,batch, rnk
from cte order by LogDateTime
It resulted like this
2016-05-17 11:57:44.000 1 1 1
2016-05-17 11:57:53.000 5 1 1
2016-05-17 11:57:58.000 6 1 1
2016-05-17 11:58:10.000 2 1 1
2016-05-17 11:58:18.000 1 2 2
2016-05-17 11:58:25.000 3 1 1
2016-05-17 11:58:32.000 4 1 1
2016-05-17 11:58:42.000 5 2 2
2016-05-17 11:58:49.000 6 2 2
2016-05-17 11:58:55.000 2 2 2
But what I want is like this.
2016-05-17 11:57:44.000 1 1 1
2016-05-17 11:57:53.000 5 1 1
2016-05-17 11:57:58.000 6 1 1
2016-05-17 11:58:10.000 2 1 1
2016-05-17 11:58:18.000 1 2 2
2016-05-17 11:58:25.000 3 2 2
2016-05-17 11:58:32.000 4 2 2
2016-05-17 11:58:42.000 5 2 2
2016-05-17 11:58:49.000 6 2 2
2016-05-17 11:58:55.000 2 2 2
The last 2 columns are the Batch and Rank,
while the 3rd column is the logType.
it should be group by batch of logType wherein LogType group is 1,2,3,4,5,6.
so, for every 1 group it is another batch and I need to place it on last 2 columns.
Please help with this, on how I can achieve this.
Thanks in advance.
You just need to fix the order in the rank... as:
rank() over (partition by logType
order by batch) rnk

How to split rows recursively

I've following table:
Id CreationDate FromEntryNo ToEntryNo
1 2013-01-01 1 4
2 2013-01-03 5 8
3 2013-01-05 9 11
...
I want to split this into multiple rows to have a list with all consecutive EntryNo, something like this:
Id CreationDate FromEntryNo ToEntryNo EntryNo
1 2013-01-01 1 4 1
1 2013-01-01 1 4 2
1 2013-01-01 1 4 3
1 2013-01-01 1 4 4
2 2013-01-03 5 8 5
2 2013-01-03 5 8 6
2 2013-01-03 5 8 7
2 2013-01-03 5 8 8
3 2013-01-05 9 11 9
3 2013-01-05 9 11 10
3 2013-01-05 9 11 11
...
My first attempt is CTE with recursion, but it doesn't work:
with cte as
(select gr.Id, gr.CreationDate, gr.FromEntryNo, gr.ToEntryNo, gr.FromEntryNo as [EntryNo]
from dbo.[Register] gr
union all
select No, CreationDate, FromEntryNo, ToEntryNo, EntryNo + 1 from cte where EntryNo <= ToEntryNo
)
select Id, CreationDate, FromEntryNo, ToEntryNo, EntryNo from cte
<
Any idea how to do this using one SQL query?
with cte as
(select gr.Id, gr.CreationDate, gr.FromEntryNo, gr.ToEntryNo,
gr.FromEntryNo as [EntryNo]
from dbo.[Register] gr
union all
select Id, CreationDate, FromEntryNo,
ToEntryNo, EntryNo + 1
from cte where EntryNo < ToEntryNo
)
select Id, CreationDate, FromEntryNo, ToEntryNo, EntryNo
from cte
ORDER BY Id,EntryNo

Resources