Case Statement for sum of multiple rows - sql-server

So I am working on creating a payroll import file for our accounting department and I am having trouble calculating overtime properly in my report.
Here is my query
declare #week1start date
declare #week1end date
declare #week2start date
declare #week2end date
--Week 1 Start and End
set #week1start = CAST('10/15/2018' as date)
set #week1end = CAST('10/21/2018' as date)
--Week 2 Start and End
set #week2start = CAST('10/22/2018' as date)
set #week2end = CAST('10/28/2018' as date)
--Week 1 Hours Worked
(select Employee.EMPLOYEE_NUM as [Employee Num],Employee.NAME as [Employee Name],Time_Clk.StoreNumber as [Store],
[Week 1 Hours Worked]= case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then 40
else (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2))))
end,
[Week 1 OT Hours] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) -40
else 0
end,
0 as [Week 2 Hours Worked],
0 as [Week 2 OT Hours]
from Time_Clk
join Employee on Time_Clk.EMPLOYEE_ID = Employee.EMPLOYEE_NUM
where CAST(out_time as DATE) >= #week1start and CAST(out_time as date) <= #week1end and Time_Clk.OUT_TYPE = 1
group by Employee.EMPLOYEE_NUM,Employee.NAME,Time_Clk.StoreNumber)
union all
--Week 2 hours worked
(select Employee.EMPLOYEE_NUM as [Employee Num],Employee.NAME as [Employee Name],Time_Clk.StoreNumber as [Store],
0 as [Week 1 Hours Worked],
0 as [Week 1 OT Hours],
[Week 2 Hours Worked] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then 40
else (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2))))
end,
[Week 2 OT Hours] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) -40
else 0
end
from Time_Clk
join Employee on Time_Clk.EMPLOYEE_ID = Employee.EMPLOYEE_NUM
where CAST(out_time as DATE) >= #week2start and CAST(out_time as date) <= #week2end and Time_Clk.OUT_TYPE = 1
group by Employee.EMPLOYEE_NUM,Employee.NAME,Time_Clk.StoreNumber)
order by [Employee Num]
So most of my output is fine but here is a select output for a particular user
EmpNum EmpName Store Week 1 Hrs Week 1 OT Week 2 Hours Week 2 OT
1 Name 1 0.00 0.00 40.00 0.88
1 Name 1 39.20 0.00 0.00 0.00
1 Name 2 5.23 0.00 0.00 0.00
So in the problem that I am facing is that Week 1 technically has overtime hours because they worked more than 40 hours that week, but at two different locations. Ideally the result would look like
EmpNum EmpName Store Week 1 Hrs Week 1 OT Week 2 Hours Week 2 OT
1 Name 1 0.00 0.00 40.00 0.88
1 Name 1 39.20 0.00 0.00 0.00
1 Name 2 0.80 4.43 0.00 0.00
I considered doing a case statement but to determine that the person had a total over 40 hours between all 3 lines in week 1, but I dont believe I can do that with the way the data is.
Hopefully all of the formatting comes through ok, I am new to posting on these forums.

Try using only start and end date for the total period all weeks.
If given start date is always period begin date you can compute week number and group hours by resulting week number. You can use datepart or just mod 7 (%) if you use some fiscal calendar.
Once you have that in a vertical form you can do some pivoting magic to get the result you are looking for.
maybe something like this...
if object_id('employee') is not null drop table employee
if object_id('timeclock') is not null drop table timeclock
Go
CREATE TABLE employee (employeeid int identity(1,1), name varchar(50)) ;
GO
CREATE TABLE timeclock (id int identity(1,1), employeeid int, IN_TIME datetime, OUT_TIME datetime);
Go
insert into employee
(name)
values (CHAR(ABS(CHECKSUM(NEWID()))%26+65))
GO 5
;WITH [days] AS
(
SELECT 1 AS [day]
UNION ALL
SELECT [day]+1
FROM [days]
WHERE [day] < 20
)
INSERT INTO timeclock
Select employeeid, GetDate()+[day], Dateadd(hh,8, GETDATE()+[day])
from employee
CROSS APPLY [days]
declare #start datetime = '11/12/2018';
declare #end datetime = #start + 15
;with aggregatedata
as
(
select
employee.employeeid
, employee.name
,weeknum = ROW_NUMBER() over (partition by employee.employeeid order by employee.employeeid, DATEPART(week, In_time) )
,[hours] = sum(DATEDIFF(Minute,IN_TIME,OUT_TIME))
from employee
inner join timeclock on employee.employeeid = timeclock.employeeid
WHERE
in_time > #start and OUT_TIME < #end
group by employee.employeeid, employee.name, DATEPART(week, In_time)
)
select
employeeid, [name], [1], [2], [3], [4]
from aggregatedata
PIVOT
(
sum([hours])
for weeknum in ([1], [2], [3], [4])
) as pvt

If you remove Time_Clk.StoreNumber from your SELECT and from the GROUP BY, then you would get the results you are after. If accounting is using this to determine what accounts to charge to, then naturally you have to keep it in. In that case, you may want to use a window function.
sum(<your statement>) over (partition by EmpNum)
But this would place the value in both store 1 and store 2 row.
Sample data would really make this easier.

Related

Calculate time between startdate and enddate and subtracting days that have no worktime

My goal is to check if an email is answered within 24 hours during workdays. de definition of a workday is if there is time registered in another table. this because we sometimes work on a Saturday or a Sunday or to exclude holidays. I made a view from that table that gives a 1 if the date has worktime or a 0 if there is no worktime registered.
DateWorked
HasWorked
2021-04-01 00:00:00.000
1
2021-04-02 00:00:00.000
1
2021-04-03 00:00:00.000
1
2021-04-04 00:00:00.000
0
2021-04-05 00:00:00.000
1
So for example a few situations:
1. MailIncoming: 2021-04-01 16:30:00, MailAnswering: 2021-04-02 14:00:00
This one is easy, I don't have to subtract anything and the mail is answered within 24 hours.
2. MailIncoming: 2021-04-01 09:30:00, MailAnswering: 2021-04-03 14:00:00
This one is also easy, I don't have to subtract anything and the mail is not answered within 24 hours.
3. MailIncoming: 2021-04-03 12:30:00, MailAnswering: 2021-04-05 10:00:00
There is 1 day where no one has worked, so I need to subtract 1 whole day from the total time, and in that case the email is answered within 24 hours during workdays.
4. MailIncoming: 2021-04-04 11:00:00, MailAnswering: 2021-04-05 18:00:00
The remaining 13 hours from 04 do not count toward the '24 hours during workdays' so the email is answered within 24 during workdays.
Also, there can be multiple dates with zero after each other.
So the outcome I'm looking for is:
MailIncoming
MailAnswering
TotalTime
TotalTimeWithoutDaysNotWorked
2021-04-04 11:00:00.000
2021-04-05 18:00:00.000
31
18
How can I calculate this last column? Or am I approaching this in the wrong way?
The query needs a way to generate calculated dates between MailIncoming and MailAnswering so there can be a LEFT JOIN (or INNER JOIN) to the WorkingDay table. In this case the query uses dbo.fnTally which is known to be a fast and efficient way to generate rows.
tables
drop table if exists #WorkingDay;
go
create table #WorkingDay(
DateWorked Date,
HasNotWorked int);
drop table if exists #MailIncoming;
go
create table #MailIncoming(
MailIncoming DateTime,
MailAnswering DateTime);
insert into #WorkingDay values
('2021-04-01', 0),
('2021-04-02', 0),
('2021-04-03', 0),
('2021-04-04', 1),
('2021-04-05', 0),
('2021-04-06', 0);
insert into #MailIncoming values
('2021-04-01 16:30:00', '2021-04-02 14:00:00'),
('2021-04-01 09:30:00', '2021-04-03 14:00:00'),
('2021-04-03 12:30:00', '2021-04-05 10:00:00'),
('2021-04-04 11:00:00', '2021-04-05 18:00:00');
dbo.fnTally
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Jeff Moden Script on SSC: https://www.sqlservercentral.com/scripts/create-a-tally-function-fntally
**********************************************************************************************************************/
(#ZeroOrOne BIT, #MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
H2(N) AS ( SELECT 1
FROM (VALUES
(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
)V(N)) --16^2 or 256 rows
, H4(N) AS (SELECT 1 FROM H2 a, H2 b) --16^4 or 65,536 rows
, H8(N) AS (SELECT 1 FROM H4 a, H4 b) --16^8 or 4,294,967,296 rows
SELECT N = 0 WHERE #ZeroOrOne = 0 UNION ALL
SELECT TOP(#MaxN)
N = ROW_NUMBER() OVER (ORDER BY N)
FROM H8
;
query
select mi.MailIncoming, mi.MailAnswering,
avg(datediff(hour, MailIncoming, MailAnswering)) hrs_to_ans,
sum(case when w.HasNotWorked=1 and
v.calc_dt > mi_dt.inc_dt and
v.calc_dt < mi_dt.ans_dt
then -24
when w.HasNotWorked=1
then datediff(hour, dateadd(day, 1, mi_dt.inc_dt), mi.MailIncoming)
else 0 end) hrs_to_sub
from #MailIncoming mi
cross apply (values (cast(MailIncoming as date),
cast(MailAnswering as date))) mi_dt(inc_dt, ans_dt)
cross apply dbo.fnTally(0, datediff(day, mi.MailIncoming, mi.MailAnswering)) fn
cross apply (values (dateadd(day, fn.n, mi_dt.inc_dt))) v(calc_dt)
left join #WorkingDay w on v.calc_dt=w.DateWorked
group by mi.MailIncoming, mi.MailAnswering
order by mi.MailIncoming;
MailIncoming MailAnswering hrs_to_ans hrs_to_sub
2021-04-01 09:30:00.000 2021-04-03 14:00:00.000 53 0
2021-04-01 16:30:00.000 2021-04-02 14:00:00.000 22 0
2021-04-03 12:30:00.000 2021-04-05 10:00:00.000 46 -24
2021-04-04 11:00:00.000 2021-04-05 18:00:00.000 31 -13
I suggest you to use a column HasNotWorked, so the tables are
create table WorkingDay(DateWorked Date, HasNotWorked int);
create table MailIncoming(MailIncoming DateTime, MailAnswering DateTime);
and the rows
insert into WorkingDay values('2021-04-01', 0);
insert into WorkingDay values('2021-04-02', 0);
insert into WorkingDay values('2021-04-03', 0);
insert into WorkingDay values('2021-04-04', 1);
insert into WorkingDay values('2021-04-05', 0);
insert into WorkingDay values('2021-04-06', 0);
insert into MailIncoming values('2021-04-04 11:00:00.000', '2021-04-06 18:00:00.000');
I want calculate the start date. If is in working day, we must consider the hour of the mail, else the first working day with
case when
(select HasNotWorked from WorkingDay where DateWorked = convert(date, MailIncoming)) = 1 then
(select min(DateWorked) from WorkingDay where DateWorked > MailIncoming and HasNotWorked = 0)
else MailIncoming end as startDate
and discard the day that are not working day
((select sum(HasNotWorked) from WorkingDay where DateWorked between convert(date, startDate)
and convert(date, MailAnswering)
) * 24) as numNotWorkingDay
so the query could be
select startDate, MailAnswering, MailIncoming, hour, numNotWorkingDay, hour - numNotWorkingDay hourWitoutWorkingDay
from (
select
MailAnswering, startDate, MailIncoming,
DateDiff("hh", startDate, MailAnswering) hour,
((select sum(HasNotWorked) from WorkingDay where DateWorked between convert(date, startDate)
and convert(date, MailAnswering)
) * 24) as numNotWorkingDay
from (
select *,
case when
(select HasNotWorked from WorkingDay where DateWorked = convert(date, MailIncoming)) = 1 then
(select min(DateWorked) from WorkingDay where DateWorked > MailIncoming and HasNotWorked = 0)
else MailIncoming end as startDate
from MailIncoming) as startCalc
) as calcTable;
sqlfiddle

How to insert "empty" row extracting a month list?

I've this sp, which return a list of data, for each "month" (i.e. each row is a month). Somethings like that:
SELECT
*,
(CAST(t1.NumActivities AS DECIMAL) / t1.NumVisits) * 100 AS PercAccepted,
(CAST(t1.Accepted AS DECIMAL) / t1.Estimated) * 100 AS PercValue
FROM
(SELECT
MONTH(DateVisit) AS Month,
COUNT(*) AS NumVisits,
SUM(CASE WHEN DateActivity is not null THEN 1 ELSE 0 END) AS NumActivities,
SUM(Estimate) AS Estimated,
SUM(CASE WHEN DateActivity is not null THEN Estimate ELSE 0 END) AS Accepted
FROM [dbo].[Activities]
WHERE
DateVisit IS NOT NULL
AND (#year IS NULL OR YEAR(DateVisit) = #year)
AND (#clinicID IS NULL OR ClinicID = #clinicID)
GROUP BY MONTH(DateVisit)) t1
This is a result:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
8 1 1 3000.00 3000.00 100.00000000000 100.00000000000000
But as you can see, I could "miss" some Month (for example, here April "4" is missed).
Is it possible to insert, for the missing month/row, an empty (0) record? Such as:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
4 0 0 0 0 0 0
...
Here is a example with sample data:
CREATE TABLE #Report
(
Id INT,
Name nvarchar(max),
Percentage float
)
INSERT INTO #Report VALUES (1,'ONE',2.01)
INSERT INTO #Report VALUES (2,'TWO',3.01)
INSERT INTO #Report VALUES (5,'Five',5.01)
;WITH months(Month) AS
(
SELECT 1
UNION ALL
SELECT Month+1
FROM months
WHERE Month < 12
)
SELECT *
INTO #AllMonthsNumber
from months;
Your select query:
The left join will gives you the NULL for other months so just use ISNULL('ColumnName','String_to_replace')
\/\/\/\/
SELECT Month, ISNULL(Name,0), ISNULL(Percentage,0)
FROM AllMonthsNumber A
LEFT JOIN #Report B
ON A.Month = B.Id
EDIT:
Yes you can do it without creating AllMonthNumber Table:
You can use master..spt_values (found here) system table which contains the numbers so just with some where condition.
SELECT Number as Month, ISNULL(B.Name,0), ISNULL(Percentage,0)
FROM master..spt_values A
LEFT JOIN #Report B ON A.Number = B.Id
WHERE Type = 'P' AND number BETWEEN 1 AND 12

Averages for 8-hour shifts in one row, grouped by days

I have a SQL Server table (tbl) with 2 columns: ts (timestamp) and val (value). I want to make a selection that gives back four columns: first is the day, the second is the average of the values from 10pm the preceding day till 6am, the third row is the average of the values from 6am till 2pm, and the fourth row contains the average of the stored values from 2pm till 10pm. So averages for 8-hour periods in a day which instead of midnight starts at the previous day at 10pm.
This is the query I have so far: http://sqlfiddle.com/#!3/41334/2
I have the average for the whole 24-hour periods (from 10pm), but now I'm stuck. I was thinking that I could make 3 selections for the 8 hours periods and then join them on the day, but I don't know how or if at all I'm on the right track. Please help.
The result I would like to get using my example data:
DAY | AVG_NITE | AVG_MORN | AVG_AFTN
2014.12.07 | 3.75 | 5.6667 | 4.5714
2014.12.08 | 4.6 | 5.6 | 5.4
2014.12.09 | 5.5 | (null) | (null)
the code below produces desired ouput. it uses CTEs but you can change them to subqueries or Views.
WITH tbl2 AS (
SELECT DATEADD(hour, 2, ts) AS ts2
,val
FROM tbl
)
, tbl_hours AS (
SELECT convert(varchar, ts2,102) AS [day]
,ROUND(DATEDIFF(hour, cast(ts2 AS DATE), ts2)/8,0) AS period
,val
FROM tbl2
)
SELECT
[day]
,AVG( case when period = 0 then val else null end) AS [avg_nite]
,AVG( case when period = 1 then val else null end) AS [avg_morn]
,AVG( case when period = 2 then val else null end) AS [avg_aftn]
FROM tbl_hours
GROUP BY [day]
select convert(varchar, dateadd(hour,2,ts), 102) as day,
avg(val) as avg_fullday
,avg(case when dateadd(day, -1, datepart(hour, ts)) in (22,23)
or DATEPART(hour, ts) in (0,1,2,3,4,5)
then (val) end)'Nite'
,avg(case when DATEPART(hour, ts) in( 6,7,8,9,10,11,12,13)
then (val) end) 'Morn'
,avg(case when DATEPART(hour, ts) in (14,15,16,17,18,19,20,21) then (val) end) 'Aftn'
from #tbl
group by convert(varchar, dateadd(hour,2,ts), 102);
drop table #tbl

Updating a % of cells in a Column

I have 4 million rows in my table with a blank column called Cancelled Bookings divided into 3 years 2010, 2011 and 2012
Booking_Skey BookingNumber ArrivalDate DepartureDate BookingDate CancelledDate BookingValue PitchType_Skey Site_Skey
124532 B00124532 2010-12-31 2011-01-02 2010-12-31 NULL 10.00 7 2
What I need to do is create a code where I can change the % of cancelations for the year I want to update:
So for 2010 I need the following
--Cancelled Bookings--
8% of the total bookings are cancelled in the Year 2010, the cancellation date can be equal too or less than the Arrival Date and equal to or greater than the Booking Date
20% of the 8% are cancelled on the same day as the Arrival Date
20% of the 8% are cancelled the day before the Arrival Date
20% of the 8% are cancelled 7 days prior to the Arrival Date
The rest of the cancellations are randomised between 1 and 90 days
.
USE Occupancy
SELECT ArrivalDate,
DATEADD(day,
CASE WHEN Rand(CHECKSUM(NEWID())) BETWEEN 0 and 0.92 THEN NULL ELSE
CASE WHEN Rand(CHECKSUM(NEWID())) BETWEEN 0.92 and 0.94 THEN 0 ELSE
CASE WHEN Rand(CHECKSUM(NEWID())) BETWEEN 0.94 and 0.96 THEN -1 ELSE
CASE WHEN Rand(CHECKSUM(NEWID())) BETWEEN 0.96 and 0.98 THEN -7 ELSE
Round(Rand(CHECKSUM(NEWID())) * -90,0) END END END END, ArrivalDate) AS DaystoReduce
FROM Bookings
WHERE DATEPART(Year,ArrivalDate) = '2010' and CancelledDate BETWEEN ArrivalDate AND DepartureDate
Can you help?
Thanks
Wayne
How about something like the following? Its not very pretty so will leave that as an exercise for you, but basically its along the lines of calculating the number of each type and then apply the rules against the row number...
declare #shareCancelled float, #shareSameDay float, #sharePrevDay float, #shareSevenDays float
select #shareCancelled = 0.08, #shareSameDay = 0.20, #sharePrevDay = 0.20, #shareSevenDays = 0.20
declare #count int, #cancelled int, #sameDay int, #prevDay int, #sevenDays int
select #count = COUNT(*) from Bookings WHERE DATEPART(Year,ArrivalDate) = '2010' and CancelledDate BETWEEN ArrivalDate AND DepartureDate
select #cancelled = #count * #shareCancelled,
#sameDay = #count * #shareCancelled * #shareSameDay,
#prevDay = #count * #shareCancelled * #sharePrevDay,
#sevenDays = #count * #shareCancelled * #shareSevenDays
select ArrivalDate,
DATEADD(day,
CASE WHEN a.RowNum <= #sameDay THEN 0
WHEN a.RowNum <= #sameDay + #prevDay THEN -1
WHEN a.RowNum <= #sameDay + #prevDay + #sevenDays THEN -7
WHEN a.RowNum <= #cancelled THEN -(ABS(CAST(NEWID() AS binary(6)) %90) + 1)
ELSE NULL END
, GETDATE()) as DaystoReduce
from (
select *, ROW_NUMBER() OVER(ORDER BY NEWID()) as RowNum from Bookings WHERE DATEPART(Year,ArrivalDate) = '2010' and CancelledDate BETWEEN ArrivalDate AND DepartureDate
) as a

Apply week number to dates for whole weeks only

The time is: (m/d/yyyy) => 2009/01/04
Using this command using datepart(wk,'20090104') I can get the week number (for any given date).
So :
SELECT datepart(wk,'20090101') //1
SELECT datepart(wk,'20090102') //1
SELECT datepart(wk,'20090103') //1
SELECT datepart(wk,'20090104') //2
So far so good.
The problem :
Those 3 first dates are not part of a full week, so I can't put them in a fixed 52-week chart.
Our company needs to see information about each whole week in the 52 weeks of a year. (Each year has 52 whole weeks).
So 20090101 doesn't belong to the first week of 2009 !
It belongs to the previous year (which is irrelevant to my question)
So I need a UDF (I've been searching a lot, and ISOWEEK is not answering my needs) which by a given datetime, will give me the Week Number (week = whole week, so partial weeks aren't considered).
Example :
calcweekNum ('20090101') //52 ...from the last year
calcweekNum ('20090102') //52 ...from the last year
calcweekNum ('20090103') //52 ...from the last year
calcweekNum ('20090104') //1
..
..
calcweekNum ('20090110') //1
calcweekNum ('20090111') //2
calcweekNum ('20090112') //2
...
Here's a different approach. All you need to supply is the year:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
x AS
(
SELECT md = MIN(d) FROM n
WHERE DATEPART(WEEKDAY, d) = 1 -- assuming DATEFIRST is Sunday
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, x.md)-1)/7) + 1
FROM n CROSS JOIN x
WHERE n.d >= x.md
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY [date];
Results:
date week
---------- ----
2009-01-04 1
2009-01-05 1
2009-01-06 1
2009-01-07 1
2009-01-08 1
2009-01-09 1
2009-01-10 1
2009-01-11 2
2009-01-12 2
...
2009-12-25 51
2009-12-26 51
2009-12-27 52
2009-12-28 52
2009-12-29 52
2009-12-30 52
2009-12-31 52
Note that week 52 won't necessarily be a full week, and that in some cases (e.g. 2012), the last day or two of the year might fall in week 53, so they're excluded.
An alternative approach is to repeat the MIN expression twice:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, (SELECT MIN(d)
FROM n WHERE DATEPART(WEEKDAY, d) = 1))-1)/7) + 1
FROM n
WHERE n.d >= (SELECT md = MIN(d) FROM n WHERE DATEPART(WEEKDAY, d) = 1)
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY d;
Here's a function for you to calculate it on the fly:
CREATE FUNCTION dbo.WholeWeekFromDate (
#Date datetime
)
RETURNS tinyint
AS BEGIN
RETURN (
SELECT DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1
FROM (SELECT DateAdd(Day, (DateDiff(Day, 0, #Date) + 1) / 7 * 7, 0)) X (CalcDate)
);
END;
I don't recommend you use it, as it may perform badly due to being called once for every row. If you absolutely must have a function to use in real queries, then convert it to an inline function returning a single column and row, and use it as so:
SELECT
OtherColumns,
(SELECT WeekNumber FROM dbo.WholeWeekFromDate(DateColumn)) WeekNumber
FROM
YourTable;
This will allow it to be "inlined" in the execution plan and perform significantly better.
But even better, as others have suggested, is to use a BusinessDate table. Here's a head start on creating one for you:
CREATE TABLE dbo.BusinessDate (
BusinessDate date NOT NULL CONSTRAINT PK_BusinessDate PRIMARY KEY CLUSTERED,
WholeWeekYear smallint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekYear_Valid
CHECK (WholeWeekYear BETWEEN 1900 AND 9999),
WholeWeekNumber tinyint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekNumber_Valid
CHECK (WholeWeekNumber BETWEEN 1 AND 53),
Holiday bit CONSTRAINT DF_BusinessDate_Holiday DEFAULT (0),
Weekend bit CONSTRAINT DF_BusinessDate_Weekend DEFAULT (0),
BusinessDay AS
(Convert(bit, CASE WHEN Holiday = 0 AND Weekend = 0 THEN 1 ELSE 0 END)) PERSISTED
);
And I'll even populate it from 1900-01-01 through 2617-09-22 (Is that enough for the projected life of your product? And it's only 7.8MB so don't fret over size):
WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
B (N) AS (SELECT 1 FROM A F, A A, A L, A C, A O, A N),
C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B),
Dates AS (
SELECT
N,
DateAdd(Day, N, '18991231') Dte,
DateAdd(Day, N / 7 * 7, '19000101') CalcDate
FROM C
)
INSERT dbo.BusinessDate
SELECT
Dte,
Year(CalcDate),
DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1,
0,
(N + 6) % 7 / 5 -- calculate weekends
FROM Dates; -- 3-7 seconds or so on my VM server
Then join to the table on the date, and use the WholeWeekNumber column for your output. You might also consider adding a WeekNumberYear because it's going to be a tad difficult to figure out that the 52 of 2009-01-01 really belongs to 2008 without this... a strange data point in there for sure if you don't (laugh).
Example table contents:
BusinessDate WholeWeekYear WholeWeekNumber Holiday Weekend BusinessDay
------------ ------------- --------------- ------- ------- -----------
1/1/2009 2008 52 0 0 1
1/2/2009 2008 52 0 0 1
1/3/2009 2008 52 0 1 0
1/4/2009 2009 1 0 1 0
1/5/2009 2009 1 0 0 1
1/6/2009 2009 1 0 0 1
1/7/2009 2009 1 0 0 1
1/8/2009 2009 1 0 0 1
1/9/2009 2009 1 0 0 1
1/10/2009 2009 1 0 1 0
1/11/2009 2009 2 0 1 0
If you really don't want to use this as a general business date calculation table, you can drop the last 3 columns, otherwise, update the Holiday column to 1 for company holidays.
Note: if you actually make the above table, and your access to it most often uses JOIN or WHERE conditions on a different column than BusinessDate, then make the primary key nonclustered and add a clustered index starting with the alternate column.
Some of the above scripts require SQL 2005 or higher.
It would be relatively easy to setup a custom calendar table with one row for each date of the year in it, and then have other fields that will allow you to rollup however you want. I do this when I have clients using varying calendars, i.e. fiscal years, and it makes the query logic very simple.
Then you just join date-to-date and get the week-number that you want.
date | reporting year | reporting week
-----------|----------------|---------------
2009-01-01 | 2008 | 52
2009-01-02 | 2008 | 52
2009-01-03 | 2008 | 52
2009-01-04 | 2009 | 01
2009-01-05 | 2009 | 01
etc.
and then to use it ( for example to get total sales rollup by your custom weeks, didn't validated my sql):
select reporting_year, reporting_month, sum(sales)
from sales
inner join custom_date_table cdt on cdt.sysdate = sales.sysdate
group by reporting_year, reporting_month
where report_year=2009
DECLARE #StartDate DATE;
SET #StartDate = '20120101';
WITH Calendar AS (
SELECT #StartDate AS DateValue
,DATEPART(DW, #StartDate) AS DayOfWeek
,CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END AS WeekNumber
UNION ALL
SELECT DATEADD(d, 1, DateValue)
,DATEPART(DW, DATEADD(d, 1, DateValue)) AS DayOfWeek
,CASE WHEN DayOfWeek = 7 THEN WeekNumber + 1 ELSE WeekNumber END
FROM Calendar
WHERE DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
)
SELECT DateValue, WeekNumber
FROM Calendar
WHERE WeekNumber BETWEEN 1 AND 52
AND DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
OPTION (MAXRECURSION 0);
Don't use a UDF, use a calendar table instead, then you can define week numbers exactly as your company requires and simply query them from the table, which will be much easier and possibly much faster than using a UDF.
A calendar table has numerous uses in SQL (search this site or Google) so you should probably create it anyway.
There is no good answer for this.
A year is NOT 52 weeks long.
It is 52 weeks and one day in normal years, and 52 weeks and two days in leap years.

Resources