Я не могу дать никаких обещаний по поводу производительности, но я верю, что это даст правильный ответ каждый раз.
create table UserLogin ( [User] nvarchar(50),
Login_Time datetime2 )
insert into UserLogin ( [User], Login_Time ) values
( 'Bart', '2018-12-31T12:13:14.888' ),
( 'Mary', '2019-01-06T09:10:11.876' ),
( 'Fred', '2018-12-31T21:22:23.456' ),
( 'Fred', '2019-01-01T07:08:09.101' ),
( 'Fred', '2018-09-17T13:14:15.616' ),
( 'Wilma', '2018-09-23T11:12:13.141' ),
( 'Betty', '2018-11-11T14:41:14.444' )
;with
-- Using DATEPART(WEEK,... has a problem. When called on two dates
-- in the same week, it can give different results.
-- For example, 31-Dec-2018 and 01-Jan-2019 are in the same week,
-- but DATEPART(WEEK,... returns 53 and 1.
-- To solve this, we'll use ISO_WEEK instead.
-- ISO defines week 1 of a year as the seven days starting on a Monday
-- that contain the first Thursday of the year.
-- This means that week 1 can begin in the previous year.
-- Calling DATEPART(ISO_WEEK,... on 31-Dec-2018 and 01-Jan-2019 both return 1.
U as
( select [User],
Login_Time,
DatePart(ISO_WEEK,Login_Time)as [Week],
Year(Login_Time)as [Year]
from UserLogin ),
-- For both 31-Dec-2018 and 01-Jan-2019, we want the
-- Year to be 2019 and the Week to be 1.
-- So when we have ISO week 1, and a date near the end of the year,
-- we want to use the following year.
V as
( select [User],
[Week],
case when [Week]=1 and Month(Login_Time)=12
then [Year]+1
else [Year]
end as [Year]
from U ),
-- Count the distinct users in each time period
A as
( select [Year],
[Week],
count(distinct [User])as No_Users
from V
group by [Year],[Week] ),
-- This is January 1st of year 0001, the earliest date that SQL Server
-- can handle. We'll be using it shortly.
Z as
( select cast('0001-01-01T00:00:00.000'as datetime2)as DayZero ),
-- To get the first day of the year,
-- add that many years, subtract 1, to 01-Jan-0001.
F as
( select No_Users,
DATEADD(year,[Year]-1,DayZero)as FirstDayOfYear,[Year],
[Week]
from A, Z ),
-- What day of the week is the first day of the year?
-- By counting the days from 01-Jan-0001 we bypass
-- any confusion caused by different values of @@DATEFIRST,
-- or negative numbers. Divide by 7 and keep the remainder.
-- The result is 0 for Monday, 1 for Tuesday and so on.
D as
( select No_Users,
FirstDayOfYear,
DATEDIFF(day,DayZero,FirstDayOfYear)%7 as DayOfWeekOfFirstDay,
[Year],
[Week]
from Z, F ),
-- Now we work out the date of the Monday of the week 1.
-- When the first day of the year is a Monday (0),
-- we just keep that day.
-- When it is Tuesday (1), we subtract 1 day.
-- For Wednesday(2), we subtract 2 days.
-- And for Thursday (3), we subtract 3 days.
-- If the first day of the year is Friday (4), we need to add 3 days.
-- For Saturday (5) we add 2 days,
-- and for Sunday (6) we add 1 day.
-- The expression ( 10 - X ) % 7 - 3
-- gives us the number of days to add.
T as
( select No_Users,
DayOfWeekOfFirstDay,
DATEADD(day,(10-DayOfWeekOfFirstDay)%7-3,FirstDayOfYear)
as MondayWeekOne,
[Year],
[Week] from D ),
-- That gives us the Monday of the first week of the year.
-- Now we need to find the Monday of the week we're looking for.
W as
( select No_Users,
DATEADD(day,7*([Week]-1),MondayWeekOne)as MondayOfWeek,
[Year],
[Week] from T ),
-- Given the Monday that begins that week,
-- find the Sunday that ends that week.
S as
( select No_Users,
MondayOfWeek,
DATEADD(day,6,MondayOfWeek)as SundayOfWeek,
[Year],
[Week] from W )
-- Format and sort to taste.
select [Year],
[Week],
'W ' + format(MondayOfWeek, 'dd/MM')
+ ' - '
+ format(SundayOfWeek, 'dd/MM') as WeekRange,
No_Users
from S
order by [Year],[Week]
Результат:
Year Week WeekRange No_Users
----------- ----------- --------------- --------
2018 38 W 17/09 - 23/09 2
2018 45 W 05/11 - 11/11 1
2019 1 W 31/12 - 06/01 3