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

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

Related

Get data in groups of "Week of..." when dates may be missing

I have data in a table with dates, and want to count the rows by "Week of" (e.g., "Week of 2017-05-01"), where the result has the week's date (starting on Mondays) and the count of matching rows — even if there are no rows for that week. (This will all be in a date range.)
I can partition things into weeks readily enough by grouping on DATEPART(wk, D) (where D is the date column), but I'm struggling with:
How to get the "Week of" date and fill, and
How to have a row for a week where there are no matching rows in the data
Here's grouping by week:
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
SELECT DATEPART(wk, D) AS [Week Number], COUNT(*) AS [Count]
FROM #temp
GROUP BY DATEPART(wk, D)
ORDER BY DATEPART(wk, D);
Which gives me:
+−−−−−−−−−−−−−+−−−−−−−+
| Week Number | Count |
+−−−−−−−−−−−−−+−−−−−−−+
| 19 | 5 |
| 20 | 19 |
| 22 | 8 |
| 23 | 10 |
| 24 | 5 |
| 26 | 4 |
+−−−−−−−−−−−−−+−−−−−−−+
But ideally I want:
+−−−−−−−−−−−−+−−−−−−−+
| Week | Count |
+−−−−−−−−−−−−+−−−−−−−+
| 2017-05-01 | 5 |
| 2017-05-08 | 19 |
| 2017-05-15 | 0 |
| 2017-05-22 | 8 |
| 2017-05-29 | 10 |
| 2017-06-05 | 5 |
| 2017-06-12 | 0 |
| 2017-06-19 | 4 |
| 2017-06-26 | 0 |
+−−−−−−−−−−−−+−−−−−−−+
How can I do that?
Set up information for testing:
SET DATEFIRST 1;
SET DATEFORMAT ymd;
CREATE TABLE #temp (
D DATETIME
);
GO
INSERT INTO #temp (D)
VALUES -- Week of 2017-05-01 (#19)
('2017-05-01'),('2017-05-01'),('2017-05-01'),
('2017-05-06'),('2017-05-06'),
-- Week of 2017-05-08 (#20) - note no data actually on the 8th
('2017-05-10'),
('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),
('2017-05-12'),('2017-05-12'),('2017-05-12'),('2017-05-12'),
('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),
('2017-05-14'),
-- Week of 2017-05-15 (#21)
-- (note we have no data for this week)
-- Week of 2017-05-22 (#22)
('2017-05-22'),('2017-05-22'),('2017-05-22'),
('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),
-- Week of 2017-05-29 (#23)
('2017-05-29'),('2017-05-29'),('2017-05-29'),
('2017-06-02'),('2017-06-02'),
('2017-06-03'),
('2017-06-04'),('2017-06-04'),('2017-06-04'),('2017-06-04'),
-- Week of 2017-06-05 (#24) - note no data actually on the 5th
('2017-06-08'),('2017-06-08'),('2017-06-08'),
('2017-06-11'),('2017-06-11'),
-- Week of 2017-06-12 (#25)
-- (note we have no data for this week)
-- Week of 2017-06-19 (#26)
('2017-06-19'),('2017-06-19'),('2017-06-19'),
('2017-06-20');
GO
To do this, you have to generate a table or CTE with the Monday dates and their week numbers (as shown in this answer, slightly modified for what we need to do below), then LEFT JOIN or OUTER APPLY that with your data grouped by week, using the week numbers:
SET DATEFORMAT ymd;
SET DATEFIRST 1;
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH Mondays AS (
SELECT #startDate AS D, DATEPART(WK, #startDate) AS W
UNION ALL
SELECT DATEADD(DAY, 7, D), DATEPART(WK, DATEADD(DAY, 7, D))
FROM Mondays m
WHERE DATEADD(DAY, 7, D) < #endDate
)
SELECT LEFT(CONVERT(NVARCHAR(MAX), Mondays.D, 120), 10) AS [Week Of], d.Count
FROM Mondays
OUTER APPLY (
SELECT COUNT(*) AS [Count]
FROM #temp
WHERE DATEPART(WK, D) = W
AND D >= #startDate
AND D < #endDate
) d
ORDER BY Mondays.D;
Two notes on that:
I'm assuming we can ensure that #startDate is a Monday, which is easily done outside the query or could be done with a simple loop in T-SQL if needed (backing up until WEEKPART(WEEKDAY, #startDate) is 1). (Or worst case we could generate all the dates and then filter them with WEEKPART(WEEKDAY, ...).)
I'm assuming the date range is always a year or less; otherwise, we'd have duplicated week numbers. If the date range could be longer than a year, combine the week number with the year everywhere we're just using a week number above (e.g., DATEPART(YEAR, D) * 100 + DATEPART(wk, D)).
You can use this.
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH OrgResult AS ( -- Grouping result with missing week. Answer of the first question
SELECT
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D) [Week] -- Fist Day Of the Week
, COUNT(*) [Count]
FROM #temp
WHERE D BETWEEN #startDate AND #endDate
GROUP BY
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D)
)
, Result AS -- Adds only missing weeks. Answer of the second question
(
SELECT * FROM OrgResult
UNION ALL
SELECT DATEADD( DAY, 7, R.[Week] ), 0 [Count]
FROM Result R
WHERE NOT EXISTS( SELECT * FROM OrgResult O WHERE [Week] = DATEADD( DAY, 7, R.[Week] ) )
AND DATEADD( DAY, 7, R.[Week] ) <= #endDate
)
SELECT * FROM Result
ORDER BY [Week]
Result:
Week Count
----------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0
Here's another approach. I included this as it will generate less reads than the Recursive CTE Solution and will be a lot fast
WITH E(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))x(x)),
iTally(N) AS
(
SELECT TOP (((DATEDIFF(day,#startdate, #endDate))/7)+1)
(ROW_NUMBER() OVER (ORDER BY (SELECT 1))-1)
FROM E a, E b, E c
)
SELECT WeekOf = DATEADD(WEEK,N,#startDate), [count] = COUNT(t.D)
FROM iTally i
LEFT JOIN #temp t ON t.D >= DATEADD(WEEK,N,#startDate) AND t.D < DATEADD(WEEK,N+1,#startDate)
GROUP BY DATEADD(WEEK,N,#startDate)
ORDER BY DATEADD(WEEK,N,#startDate); -- not required
Results:
WeekOf count
---------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0

Count how many days a record has existed in my Table

I have a table with different products that gets registered constantly into different locations. I want to know for how long this product existed since the first scan it had and where it's current location is.
here is an example:
CREATE TABLE Products
(
ProductNR varchar (14),
Location int,
Scanned Datetime
);
Insert Into Products(ProductNR, Location, Scanned)
Values('1111', 1, '2017-03-15 09:30'),
('1111', 2, '2017-03-16 11:35'),
('1111', 3, '2017-03-21 12:37'),
('2222', 1, '2017-03-21 09:50'),
('2222', 5, '2017-03-21 12:58');
Expected output:
-----------------------------------------------------
| ID | Current Location | Age
-----------------------------------------------------
| 1111 | 3 | > 5 days
| 2222 | 5 | < 5 days
-----------------------------------------------------
My current query
select ProductNR,
Location,
Scanned,
case When Scanned >= DATEADD(day, -5, GETDATE()) Then 'Less than 5 days old'
When scanned <= DATEADD(day, -5, GETDATE()) Then 'More than 5 days old' else '0' end AS Age
From Products
I think this Query is completely wrong and it's also showing every record, I spent ages trying to figur out how to write this correctly. I hope someone can help
Try this query:
select p.ProductNR, p.Location, p.Scanned
,case
when p.FirstScanned >= dateadd(day, -5, getdate()) then 'Less than 5 days old'
when p.FirstScanned <= dateadd(day, -5, getdate()) then 'More than 5 days old'
else '0'
end as Age
from
(
select p.ProductNR
,p.Location
,p.Scanned
,min(p.Scanned) over (partition by p.ProductNR) as FirstScanned
,max(p.Scanned) over (partition by p.ProductNR) as LastScanned
from Products p
) p
where p.LastScanned = p.Scanned
Can you try this?
SELECT PRODUCTNR, CASE WHEN DATEDIFF(dd,MIN_DATE, SCANNED) > 5 THEN 'More than 5 days old' eLSE 'Less than 5 days old' END AS LAST_SCANN
FROM (
SELECT PRODUCTNR, LOCATION, SCANNED , ROW_NUMBER() OVER(PARTITION BY PRODUCTNR ORDER BY SCANNED DESC) AS RN
, MIN(SCANNED) OVER(PARTITION BY PRODUCTNR ) AS MIN_DATE
FROM PRODUCTS
) X WHERE RN=1;

Conditional counting based on comparison to previous row sql

Let's start with a sample of the data I'm working with:
Policy No | start date
1 | 2/15/2006
1 | 2/15/2009
1 | 2/15/2012
2 | 3/15/2006
3 | 3/19/2006
3 | 3/19/2012
4 | 3/31/2006
4 | 3/31/2009
I'm trying to write code in SQL Server 2008 that counts a few things. The principle is that the policyholder's earliest start date is when the policy began. Every three years an increase is offered to the client. If they agree to the increase, the start date is refreshed with the same date as the original, three years later. If they decline, nothing is added to the database at all.
I'm trying to not only count the number of times a customer accepted the offer (or increased the start date by three years), but separate it out by first offer or second offer. Taking the original start date and dividing the number of days between now and then by 1095 gets me the total number of offers, so I've gotten that far. What I really want it to do is compare each policy number to the one before it to see if it's the same (it's already ordered by policy number), then count the date change in a new "accepted" column and count the times it didn't change but could have as "declined".
Is this a case where I would need to self-join the table to itself to compare the dates? Or is there an easier way?
are you looking for this :-
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Declare #PolicyWithInc Table
(
RowId Int Identity(1,1) Primary Key
,PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
Insert Into #PolicyWithInc(PolicyNo,StartDate)
Select t.PolicyNo
,t.StartDate
From #Test As t
Select pw.PolicyNo
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) = 3 Then 1 Else 0 End) As DateArrived
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) > 3 Then 1 Else 0 End) As DateNotArrived
,Sum(Case When Isnull(Datediff(Year,t.StartDate,pw.StartDate),0) = 3 Then 1 Else 0 End) As Years3IncrementCount
From #PolicyWithInc As pw
Left Join #PolicyWithInc As t On pw.PolicyNo = t.PolicyNo And pw.RowId = (t.RowId + 1)
Group By pw.PolicyNo
Probably below could help:
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
select
count(*) * 3 TotalOffersMade,
count(Data1.StartDate) FirstOfferAccepted,
count(Data2.StartDate) SecondOfferAccepted,
count(Data3.StartDate) ThirdOfferAccepted,
count(*) - count(Data1.StartDate) FirstOfferDeclined,
count(*) - count(Data2.StartDate) SecondOfferDeclined,
count(*) - count(Data3.StartDate) ThirdOfferDeclined
from
(
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
)Offers
LEFT JOIN
#Test Data1
on Offers.PolicyNo = Data1.PolicyNo and Offers.Offer1 = Data1.StartDate
LEFT JOIN
#Test Data2
on Offers.PolicyNo = Data2.PolicyNo and Offers.Offer2 = Data2.StartDate
LEFT JOIN
#Test Data3
on Offers.PolicyNo = Data3.PolicyNo and Offers.Offer3 = Data3.StartDate

Count number of days in a year with a record

I have a SQL Server table named AgentLog in which I store for each agent his daily number of sales.
+-----------+------------+-------------+
| AgentName | Date | SalesNumber |
+-----------+------------+-------------+
| John | 01.01.2014 | 45 |
| Terry | 01.01.2014 | 30 |
| John | 02.01.2014 | 20 |
| Terry | 02.01.2014 | 15 |
| Terry | 03.01.2014 | 52 |
| Terry | 04.01.2014 | 24 |
| Terry | 05.01.2014 | 12 |
| Terry | 06.01.2014 | 10 |
| Terry | 07.01.2014 | 23 |
| John | 08.01.2014 | 48 |
| Terry | 08.01.2014 | 35 |
| John | 09.01.2014 | 37 |
| Terry | 10.01.2014 | 35 |
+-----------+------------+-------------+
If an agent doesn't work on one particular day, there is no record of his sales on that date.
I want to generate a report(query) on a given date interval (ex: 01.01.2014 - 10.01.2014) that counts on how many days an agent wasn't present for work (ex: John - 6 days), was at work (John - 4 days) and also returns the date interval it wasn't present (ex: John 03.01.2014 - 07.01.2014, 10.01.2014) (there can be multiple intervals).
You need to create a custom table and populate it with a record for each date you want in your range (Feel free to go as far back in the past and forward into the future as you feel you may need.). You could do this in Excel very easily and import it.
Select *
from Custom.DateListTable dlt
left outer join agentlog ag
on dlt.Date = ag.Date
I would approach this by getting the number of dates in the interval, as well as the number of dates the agent was at work, and you then have everything you need.
To get the number of days you can use DATEDIFF:
SELECT DATEDIFF(day, '2014-01-01', '2014-10-01') AS totalDays;
To get the number of days an agent worked, you can use the COUNT(*) aggregate function:
SELECT agentName, COUNT(*) AS daysWorked
FROM myTable
GROUP BY agentName;
Then, you can just add to that query to get the days not worked by subtracting totalDays - daysWorked:
SELECT agentName, COUNT(*) AS daysWorked, (DATEDIFF(day, '2014-01-01', '2014-10-01') - COUNT(*)) AS daysMissed
FROM myTable
GROUP BY agentName;
Here is an SQL Fiddle example.
The only way I can think of to resolve this is to creating a temporary table with only one column (datetime) and save there all the dates from the selected range. You can create an stored procedure that fills that temporary table using a cursor with all the dates from the interval. Then do a LEFT join between your table and the temporary table to look for null values in your table (The days where that person didn't come to work)
Try this...
SET DATEFIRST 1; --Monday
DECLARE #StartDate DATETIME = '2014-01.01',
#EndDate DATETIME = '2014-01.10';
WITH data as (
select 0 as i, DATEADD(DAY, 0, #StartDate) as TheDate
union all
select i + 1, DATEADD(DAY, i + 1, #StartDate) as TheDate
from data
where i < (#EndDate - #StartDate)
)
SELECT a.AgentName,
SUM(CASE WHEN c.Date IS NULL THEN 1 ELSE 0 END) AS Missing,
SUM(CASE WHEN c.Date IS NOT NULL THEN 1 ELSE 0 END) AS Working
FROM Agent a
JOIN data b ON NOT EXISTS(SELECT NULL FROM SpecialDate s WHERE s.date = b.TheDate)
LEFT JOIN AgentLog c ON
c.AgentName = a.AgentName
AND c.Date = b.TheDate
WHERE DATEPART(weekday, b.TheDate) <= 5
GROUP BY a.AgentName
OPTION (MAXRECURSION 10000);
It includes a check for weekends, as well as a reference to "SpecialDate" where a list of non working days can be maintained, and excluded from the check.
Reading your question again, I realise that this will only solve half your problem.
NOTE: The following answer mainly addresses the trickiest part of the question, which is how to obtain "absence from work" intervals.
Given these values as Interval Start - End dates:
DECLARE #IntervalStart DATE = '2013-12-30'
DECLARE #IntervalEnd DATE = '2014-01-10'
the following query gives you the "absence from work" intervals:
SELECT AgentName,
DATEADD(d, 1, t.[Date]) As OffWorkStart,
DATEADD(d, -1, t.NextDate) As OffWorkEnd
FROM (
SELECT AgentName, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As NextDate,
DATEDIFF(DAY, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC)) As NextMinusCurrent
FROM #AgentLog) t
WHERE t.NextMinusCurrent > 1
-- Get marginal beginning interval (in case such an interval exists)
UNION ALL
SELECT AgentName, #IntervalStart AS OffWorkStart, DATEADD(DAY, -1, MIN([Date])) AS OffWorkEnd
FROM #AgentLog
GROUP BY AgentName
HAVING MIN([Date]) > #IntervalStart
-- Get marginal ending interval (in case such an interval exists)
UNION ALL
SELECT AgentName, DATEADD(DAY, 1, MAX([Date])) AS OffWorkStart, #IntervalEnd
FROM #AgentLog
GROUP BY AgentName
HAVING MAX([Date]) < #IntervalEnd
ORDER By AgentName, OffWorkStart
With the input data you supplied, the above query gives you the following output:
AgentName OffWorkStart OffWorkEnd
---------------------------------------
John 2013-12-30 2013-12-31
John 2014-01-03 2014-01-07
John 2014-01-10 2014-01-10
Terry 2013-12-30 2013-12-31
Terry 2014-01-09 2014-01-09
The idea behind the basic part of the query is to employ the following nested query:
SELECT AgentName,
[Date],
LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As NextDate,
DATEDIFF(DAY, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC)) As NextMinusCurrent
FROM #AgentLog
in order to get any existing gaps between the days a certain agent is present for work. A value of NextMinusCurrent > 1 indicates such a gap.
Counting days is trivial once you have the above query in place. E.g. placing the above query in a CTE you can count total number of absence days with sth like:
;WITH cte (
... query goes here
)
SELECT AgentName, SUM(DATEDIFF(DAY, OffWorkStart, OffWorkEnd) + 1) AS AbsenceDays
FROM cte
GROUP By AgentName
P.S. The above query makes use of SQL Server LEAD function, which is available from SQL SERVER 2012 onwards.
SQL Fiddle here
EDIT:
CTEs together with ROW_NUMBER() can be used to simulate LEAD function. The first part of the query becomes:
;WITH cte1 AS (
SELECT AgentName,
[Date],
ROW_NUMBER() OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As rn
FROM #AgentLog
),
cte2 AS (
SELECT cte1.AgentName, cte1.[Date],
cteLead.[Date] AS NextDate,
DATEDIFF(DAY, cte1.[Date], cteLead.[Date]) As NextMinusCurrent
FROM cte1
LEFT OUTER JOIN cte1 AS cteLead
ON (cte1.rn = cteLead.rn - 1) AND (cte1.AgentName = cteLead.AgentName)
)
SELECT AgentName,
DATEADD(d, 1, cte2.[Date]) As OffWorkStart,
DATEADD(d, -1, cte2.NextDate) As OffWorkEnd
FROM cte2
WHERE NextMinusCurrent > 1
SQL Fiddle for SQL Server 2008 here. I hope it executes in SQL Server 2005 also!

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