Calculating price based on changing hourly rates - sql-server

Imagine I have a business that rents widgets by the minute, but the rental rates vary depending on the day of the week and hour of the day. I describe this information in a "Rates" table as follows:
CREATE TABLE Rates (
DayNumber int,
HourNumber int,
HourlyRate decimal(19,4),
PRIMARY KEY (DayNumber, HourNumber)
)
DayNumber HourNumber HourlyRate
--------- ---------- ----------
1 1 3.75
1 2 4.50
1 3 4.25
1 4 3.75
In the above table, the day number is retrieved from datepart(dw, Start), the hour number from datepart(hour, Start). It has 168 records (the number of hours in a standard week).
I have the rental information as follows in a "Rentals" table:
CREATE TABLE Rentals (
RentalId int,
CustomerId int,
Start datetimeoffset,
Finish datetimeoffset,
Cost decimal(19,4),
PRIMARY KEY (RentalId)
)
RentalId CustomerId Start Finish Cost
-------- ---------- --------------- --------------- ----
1 1 1/1/2016 6:11am 1/1/2016 2:34pm
2 1 1/2/2016 7:23am 1/3/2016 8:12am
Using T-SQL (SQL Server 2014 or better), I'd like to update the Rentals table to calculate the Cost column that considers the rate of each day-hour, summed up for the total rental period. Bonus points for efficiency.

Tested in SSMS, it updates the table now. It solved the issues below:
1) it works no matter whether or not START and FINISH are on the same day;
2) it works no matter whether or not START and FINISH are on the same week or month.
update rentals
set cost = (select sum(hourlyrate) from rates
where (daynumber > datepart(dw,start) and daynumber < datepart(dw,finish)) or
(daynumber = datepart(dw,start) and hournumber > datepart(hour,start)) or
(daynumber = datepart(dw,finish) and hournumber < datepart(hour,finish))
) +
(select hourlyrate from rates
where daynumber = datepart(dw,start) and hournumber = datepart(hour,start)
) * 1.00 * (60-datepart(minute, start))/60 +
(select hourlyrate from rates
where daynumber = datepart(dw,finish) and hournumber = datepart(hour,finish)
) * 1.00 * datepart(minute, finish)/60 -
(
Case when datediff(day,start,finish)%7 = 0 then 230 -- deal with same day case
when datediff(day,start,finish)%7 <> 0 then 0
end
) +
(select datediff(day,start,finish)/7 * sum(hourlyrate) from rates) -- deal with multiple weeks case

You can use a tally table to split record into one record per hour.
For example, the following rental
RentalId CustomerId Start Finish Cost
-------- ---------- --------------- --------------- ----
1 1 1/1/2016 1:30pm 1/1/2016 4:45pm
is processed using tally into
RentalId Start Finish Cost
-------- --------------- --------------- ----
1 1/1/2016 1:30pm 1/1/2016 2:00pm 1
1 1/1/2016 2:00pm 1/1/2016 3:00pm 2
1 1/1/2016 3:00pm 1/1/2016 4:00pm 3
1 1/1/2016 4:00pm 1/1/2016 4:45pm 4
With this, you can calculate the cost of each preprocessed records. You have to use rate per minute since not all records lasted a full hour.
Then, just sum these costs grouped by rental and you have got the cost of each rentals.
Here is the complete solution.
I used CTE for the tally table and preprocessed records.
;WITH
N0(_) AS (SELECT NULL UNION ALL SELECT NULL),
N1(_) AS (SELECT NULL FROM N0 AS L CROSS JOIN N0 AS R),
N2(_) AS (SELECT NULL FROM N1 AS L CROSS JOIN N1 AS R),
N3(_) AS (SELECT NULL FROM N2 AS L CROSS JOIN N2 AS R),
Tally AS (SELECT N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM N3 AS L CROSS JOIN N3 AS R),
PreprocessedData AS (SELECT Rent.RentalId,
BillingStart =( CASE WHEN Tally.N = 1 THEN
Rent.Start
ELSE
DATEADD(HOUR, DATEDIFF(HOUR, 0, DATEADD(HOUR, Tally.N - 1, Rent.Start)), 0)--Trim exceeding minutes
END),
BillingEnd = ( CASE WHEN DATEDIFF(HOUR, Rent.Start, Rent.Finish) < Tally.N THEN
Rent.Finish
ELSE
DATEADD(HOUR, DATEDIFF(HOUR, 0, DATEADD(HOUR, Tally.N, Rent.Start)), 0)--Trim exceeding minutes
END),
Rate.HourlyRate
FROM Rentals AS Rent
INNER JOIN Tally ON DATEDIFF(HOUR, Rent.Start, Rent.Finish) >= Tally.N - 1 -- DATEADD(HOUR, Tally.N, Rent.Start) < Rent.Finish
LEFT JOIN Rates AS Rate ON DATEPART(DW, DATEADD(HOUR, Tally.N - 1, Rent.Start)) = Rate.DayNumber
AND DATEPART(HOUR, DATEADD(HOUR, Tally.N - 1, Rent.Start)) = Rate.HourNumber
)
UPDATE Rentals
SET Cost = CalculateCostPerRental.CalculateCost
FROM Rentals
INNER JOIN (SELECT RentalId,
CalculateCost = SUM(HourlyRate * DATEDIFF(MINUTE, BillingStart, BillingEnd) /60)
FROM PreprocessedData
GROUP BY RentalId
HAVING SUM(CASE WHEN HourlyRate IS NOT NULL THEN 0 ELSE 1 END) = 0 /*Update only if all the rates where found*/) AS CalculateCostPerRental ON Rentals.RentalId = CalculateCostPerRental.RentalId
/*cost is null when there is a rate missing in rate table*/
As for performance, they are poor, but it is due to your database design. Without, changing the design, it's going to be really hard to do better than what this solution does. However, I would challenge if if you really need rocking performance for this task.
Disclaimer : You should do some testing before using this in production because I haven't tested every edge case there is. Also, you might have missing rates in your rate table.

Related

Create a select statement that returns a record for each day after a given created date

I have a Dimension table containing machines.
Each machine has a date created value.
I would like to have a Select statement that generates for each day after a certain start date the available number of machines. A machine is available after the date created on wards
As I have read only access to the database I am not able to create a physical calendar table
I hope somebody can help me solving my issue
I assume this is what you want. Based on this sample table:
USE tempdb;
GO
CREATE TABLE dbo.Machines
(
MachineID int,
CreatedDate date
);
INSERT dbo.Machines VALUES(1,'20200104'),(2,'20200202'),(3,'20200214');
Then say you wanted the number of active machines starting on January 1st:
DECLARE #StartDate date = '20200101';
;WITH x AS
(
SELECT n = 0 UNION ALL SELECT n + 1 FROM x
WHERE n < DATEDIFF(DAY, #StartDate, GETDATE())
),
days(d) AS
(
SELECT DATEADD(DAY, x.n, #StartDate) FROM x
)
SELECT days.d, MachineCount = COUNT(m.MachineID)
FROM days
LEFT OUTER JOIN dbo.Machines AS m
ON days.d >= m.CreatedDate
GROUP BY days.d
ORDER BY days.d
OPTION (MAXRECURSION 0);
Results:
d MachineCount
---------- ------------
2020-01-01 0
2020-01-02 0
2020-01-03 0
2020-01-04 1
2020-01-05 1
...
2020-01-31 1
2020-02-01 1
2020-02-02 2
2020-02-03 2
...
2020-02-12 2
2020-02-13 2
2020-02-14 3
2020-02-15 3
Clean up:
DROP TABLE dbo.Machines;
(Yes, some people hiss at recursive CTEs. You can replace it with any number of set generation techniques, some I talk about here, here, and here.)

Finding the Datediff between Records in same Table

IP QID ScanDate Rank
101.110.32.80 6 2016-09-28 18:33:21.000 3
101.110.32.80 6 2016-08-28 18:33:21.000 2
101.110.32.80 6 2016-05-30 00:30:33.000 1
I have a Table with certain records, grouped by Ipaddress and QID.. My requirement is to find out which record missed the sequence in the date column or other words the date difference is more than 30 days. In the above table date diff between rank 1 and rank 2 is more than 30 days.So, i should flag the rank 2 record.
You can use LAG in Sql 2012+
declare #Tbl Table (Ip VARCHAR(50), QID INT, ScanDate DATETIME,[Rank] INT)
INSERT INTO #Tbl
VALUES
('101.110.32.80', 6, '2016-09-28 18:33:21.000', 3),
('101.110.32.80', 6, '2016-08-28 18:33:21.000', 2),
('101.110.32.80', 6, '2016-05-30 00:30:33.000', 1)
;WITH Result
AS
(
SELECT
T.Ip ,
T.QID ,
T.ScanDate ,
T.[Rank],
LAG(T.[Rank]) OVER (ORDER BY T.[Rank]) PrivSRank,
LAG(T.ScanDate) OVER (ORDER BY T.[Rank]) PrivScanDate
FROM
#Tbl T
)
SELECT
R.Ip ,
R.QID ,
R.ScanDate ,
R.Rank ,
R.PrivScanDate,
IIF(DATEDIFF(DAY, R.PrivScanDate, R.ScanDate) > 30, 'This is greater than 30 day. Rank ' + CAST(R.PrivSRank AS VARCHAR(10)), '') CFlag
FROM
Result R
Result:
Ip QID ScanDate Rank CFlag
------------------------ ----------- ----------------------- ----------- --------------------------------------------
101.110.32.80 6 2016-05-30 00:30:33.000 1
101.110.32.80 6 2016-08-28 18:33:21.000 2 This is greater than 30 day. Rank 1
101.110.32.80 6 2016-09-28 18:33:21.000 3 This is greater than 30 day. Rank 2
While Window Functions could be used here, I think a self join might be more straight forward and easier to understand:
SELECT
t1.IP,
t1.QID,
t1.Rank,
t1.ScanDate as endScanDate,
t2.ScanDate as beginScanDate,
datediff(day, t2.scandate, t1.scandate) as scanDateDays
FROM
table as t1
INNER JOIN table as t2 ON
t1.ip = t2.ip
t1.rank - 1 = t2.rank --get the record from t2 and is one less in rank
WHERE datediff(day, t2.scandate, t1.scandate) > 30 --only records greater than 30 days
It's pretty self-explanatory. We are joining the table to itself and joining the ranks together where rank 2 gets joined to rank 1, rank 3 gets joined to rank 2, and so on. Then we just test for records that are greater than 30 days using the datediff function.
I would use windowed function to avoid self join which in many case will perform better.
WITH cte
AS (
SELECT
t.IP
, t.QID
, LAG(t.ScanDate) OVER (PARTITION BY t.IP ORDER BY T.ScanDate) AS beginScanDate
, t.ScanDate AS endScanDate
, DATEDIFF(DAY,
LAG(t.ScanDate) OVER (PARTITION BY t.IP ORDER BY t.ScanDate),
t.ScanDate) AS Diff
FROM
MyTable AS t
)
SELECT
*
FROM
cte c
WHERE
Diff > 30;

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

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

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