Query grouped by calendar week / Issue: Date expands over more than one year - sql

I have an SQL Query which calculates a quote of two sums. This quote shall be calculated on a weekly basis. Therefore is used Datepart('ww', Date, 2, 2) to get the calendar week.
This query worked fine during 2014, but now I am facing a problem.
The users choses with date pickers in a form from and to date which is then used in the where clause to only select relevant records.
If they chose a from date from past year (e.g. 2014) and a to date from this year my query shows irrelvant data, because it does not cosinder the year just the calendar week.
So, how group by calendar week AND only chose records from the correct year.
SELECT DatePart('ww',Date,2,2) AS WEEK,
Year(Workload_Date) AS [YEAR],
(SUM(Value1)+SUM(Value2))AS [Total1],
(SUM(Value3)+SUM(Value4)) AS [Total2],
((SUM(Value1)+SUM(Value2))/(SUM(Value3)+SUM(Value4))) AS [Quote]
FROM tbl
WHERE DatePart('ww',Date,2,2) Between DatePart('ww',FromDatePickerField,2,2) And DatePart('ww',ToDatePickerField,2,2)
GROUP BY DatePart('ww',Date,2,2), Year(Date)
ORDER BY Year(Date), DatePart('ww',Date,2,2);
The table contains one record per day.
Date | Value1 | Value2 | Value3 | Value4 |
01.01.2014 | 4 | 3 | 2 | 3 |
02.01.2014 | 4 | 3 | 9 | 3 |
03.01.2014 | 4 | 3 | 4 | 1 |
04.01.2014 | 4 | 3 | 1 | 3 |
...
01.01.2015 | 4 | 3 | 6 | 3 |
02.01.2015 | 4 | 3 | 3 | 7 |

You could base your logic on Week_Ending_date instead of the week number and year, that way you could aggregate all you data on a weekly basis and let SQL handle the week/year detection logic.
Incase, you have a date range that spans 2 years, even then the calculations will be based on the week_ending_date and should work out correctly.
Something like...
SELECT DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE) AS WEEK_ENDING_DATE
,Year(DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)) AS [YEAR]
,(SUM(Value1) + SUM(Value2)) AS [Total1]
,(SUM(Value3) + SUM(Value4)) AS [Total2]
,((SUM(Value1) + SUM(Value2)) / (SUM(Value3) + SUM(Value4))) AS [Quote]
FROM tbl
WHERE DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE) BETWEEN DATEADD(dd, 7-(DATEPART(dw, FromDatePickerField)), FromDatePickerField)
AND DATEADD(dd, 7-(DATEPART(dw, ToDatePickerField)), ToDatePickerField)
and date >= FromDatePickerField
and date <= ToDatePickerField
GROUP BY DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)
ORDER BY DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)

I see 2 possible solutions.
1) You stop the user while using the datepicker on the form whenever the years of the two datepickers are different. Maybe a MsgBox or something,
2) You change the to-DatePicker to the from-Year.
DECLARE #MinDate DATE = '01.11.2014'
DECLARE #tmpDate DATE = '12.02.2015'
DECLARE #MaxDate DATE = (CASE WHEN Year(#MinDate) != Year(#tmpDate) THEN Convert(DATE, '31.12.' + Convert(VARCHAR(4), Year(#MinDate))) ELSE #tmpDate END)
SELECT #MinDate, #tmpDate, #MaxDate
Result :
minDate tmpDate maxDate
01.11.2014 12.02.2015 31.12.2014

Related

Reporting on time information using start and end time

Is it possible to create a report that sums hours for a day grouped by an Id using a start and end time stamp?
I need to be able to split time that spans days and take part of that time and sum to the correct date group.
NOTE: The date ids are to a date dimension table.
------------------------------------------------------------------------------
TaskId | StartDateId | EndDateId | StartTime | EndTime
------------------------------------------------------------------------------
2 | 20190317 | 20190318 | 2019-03-17 16:30:00 | 2019-03-18 09:00:00
------------------------------------------------------------------------------
1 | 20190318 | 20190318 | 2019-03-18 09:00:00 | 2019-03-18 16:30:00
------------------------------------------------------------------------------
2 | 20190318 | 20190319 | 2019-03-18 16:30:00 | 2019-03-19 09:00:00
------------------------------------------------------------------------------
So based on this, the desired report output would be:
-------------------------
Date | Task | Hours
-------------------------
2019-03-17 | 2 | 7.5
-------------------------
2019-03-18 | 1 | 7.5
-------------------------
2019-03-18 | 2 | 16.5
-------------------------
...
The only working solution I have managed to implement is splitting records so that no record spans multiple days. I was hoping to find a report query solution, rather than an ETL base based solution.
I have tried to simulate your problem here: https://rextester.com/DEV45608 and I hope it helps you :) (The CTE GetDates can be replaced by your date dimension)
DECLARE #minDate DATE
DECLARE #maxDate DATE
CREATE TABLE Tasktime
(
Task_id INT,
Start_time DATETIME,
End_time DATETIME
);
INSERT INTO Tasktime VALUES
(2,'2019-03-17 16:30:00','2019-03-18 09:00:00'),
(1,'2019-03-18 09:00:00','2019-03-18 16:30:00'),
(2,'2019-03-18 16:30:00','2019-03-19 09:00:00');
SELECT #mindate = MIN(Start_time) FROM Tasktime;
SELECT #maxdate = MAX(End_time) FROM Tasktime;
;WITH GetDates AS
(
SELECT 1 AS counter, #minDate as Date
UNION ALL
SELECT counter + 1, DATEADD(day,counter,#minDate)
from GetDates
WHERE DATEADD(day, counter, #minDate) <= #maxDate
)
SELECT counter, Date INTO #tmp FROM GetDates;
SELECT
g.Date,
t.Task_id,
SUM(
CASE WHEN CAST(t.Start_time AS DATE) = CAST(t.End_time AS DATE) THEN
DATEDIFF(second, t.Start_time, t.End_time) / 3600.0
WHEN CAST(t.Start_time AS DATE) = g.Date THEN
DATEDIFF(second, t.Start_time, CAST(DATEADD(day,1,g.Date) AS DATETIME)) / 3600.0
WHEN CAST(t.End_time AS DATE) = g.Date THEN
DATEDIFF(second, CAST(g.Date AS DATETIME), t.End_time) / 3600.0
ELSE
24.0
END) AS hours_on_the_day_for_the_task
from
#tmp g
INNER JOIN
Tasktime t
ON
g.Date BETWEEN CAST(t.Start_time AS DATE) AND CAST(t.End_time AS DATE)
GROUP BY g.Date, t.Task_id
The Desired Date can be joined to the date dimension and return the "calendar date" and you can show that date in the report.
As for the HOURS.. when you are retrieving your dataset in SQL, just do this.. it is as simple as:
cast(datediff(MINUTE,'2019-03-18 16:30:00','2019-03-19 09:00:00') /60.0 as decimal(13,1)) as 'Hours'
So in your case it would be
cast(datediff(MINUTE,sometable.startdate,sometable.enddate) /60.0 as decimal(13,1)) as 'Hours'
Just doing a HOUR will return the whole hour.. and dividing by 60 will return a whole number. Hence the /60.0 and the cast

Dynamically calculate how many months have passed - SQL Server

I am trying to calculate how many months ago the date field was
I have a table
CREATE TABLE Date(
Date Date
);
INSERT INTO Date (Date)
VALUES ('05-01-18'),
('04-01-18'),
('03-01-18'),
('02-01-18'),
('01-01-18'),
('12-01-17'),
('11-01-17');
And a query
SELECT Date ,
MONTH(Date),
CASE WHEN MONTH(Date) = MONTH(GETDATE()) Then 'Current Month'
WHEN MONTH(Date) = MONTH(GETDATE()) -1 Then '1 Month Ago'
WHEN MONTH(Date) = MONTH(GETDATE()) -2 Then '2 Month Ago'
ELSE 'n/a' END AS [Months Ago]
FROM Date
Which gives me the correct result:
| Date | | Months Ago |
|------------|----|---------------|
| 2018-05-01 | 5 | Current Month |
| 2018-04-01 | 4 | 1 Month Ago |
| 2018-03-01 | 3 | 2 Month Ago |
| 2018-02-01 | 2 | n/a |
| 2018-01-01 | 1 | n/a |
| 2017-12-01 | 12 | n/a |
| 2017-11-01 | 11 | n/a |
But is there anyway to create this dynamically instead of keep having to write case expressions. So if anyone add's more dates in the future this will just work without having to add more cases?
You exactly want datediff():
select datediff(month, date, getdate()) as num_months_ago
datediff() counts the number of month boundaries between two dates. So, Dec 31 is "one month before" Jan 1. This appears to be the behavior that you want.
I don't see an advantage to putting this in a string format.
In case you do want this to have a string format:
SELECT D.[Date],
DATEPART(MONTH,D.[Date]) AS [Month],
CASE WHEN V.DD = 0 THEN 'Current Month'
WHEN V.DD = 1 THEN '1 Month Ago'
ELSE CONVERT(varchar(4), V.DD) + ' Months ago' END AS MonthsAgo
FROM [Date] D
CROSS APPLY (VALUES(DATEDIFF(MONTH, D.[Date], GETDATE()))) V(DD);
I, however, agree with Gordon, SQL Server isn't really the palce to do that type of formatting. :)

Discard existing dates that are included in the result, SQL Server

In my database I have a Reservation table and it has three columns Initial Day, Last Day and the House Id.
I want to count the total days and omit those who are repeated, for example:
+-------------+------------+------------+
| | Results | |
+-------------+------------+------------+
| House Id | InitialDay | LastDay |
+-------------+------------+------------+
| 1 | 2017-09-18 | 2017-09-20 |
| 1 | 2017-09-18 | 2017-09-22 |
| 19 | 2017-09-18 | 2017-09-22 |
| 20 | 2017-09-18 | 2017-09-22 |
+-------------+------------+------------+
If you noticed the House Id with the number 1 has two rows, and each row has dates but the first row is in the interval of dates of the second row. In total the number of days should be 5 because the first shouldn't be counted as those days already exist in the second.
The reason why this is happening is that each house has two rooms, and different persons can stay in that house on the same dates.
My question is: how can I omit those cases, and only count the real days the house was occupied?
In your are using SQL Server 2012 or higher you can use LAG() to get the previous final date and adjust the initial date:
with ReservationAdjusted as (
select *,
lag(LastDay) over(partition by HouseID order by InitialDay, LastDay) as PreviousLast
from Reservation
)
select HouseId,
sum(case when PreviousLast>LastDay then 0 -- fully contained in the previous reservation
when PreviousLast>=InitialDay then datediff(day,PreviousLast,LastDay) -- overlap
else datediff(day,InitialDay,LastDay)+1 -- no overlap
end) as Days
from ReservationAdjusted
group by HouseId
The cases are:
The reservation is fully included in the previous reservation: we only need to compare end dates because the previous row is obtained ordering by InitialDay, LastDay, so the previous start date is always minor or equal than the current start date.
The current reservation overlaps with the previous: in this case we adjust the start and don't add 1 (the initial day is already counted), this case include when the previous end is equal to the current start (is a one day overlap).
There is no overlap: we just calculate the difference and add 1 to count also the initial day.
Note that we don't need extra condition for the reservation of a HouseID because by default the LAG() function returns NULL when there isn't a previous row, and comparisons with null always are false.
Sample input and output:
| HouseId | InitialDay | LastDay |
|---------|------------|------------|
| 1 | 2017-09-18 | 2017-09-20 |
| 1 | 2017-09-18 | 2017-09-22 |
| 1 | 2017-09-21 | 2017-09-22 |
| 19 | 2017-09-18 | 2017-09-27 |
| 19 | 2017-09-24 | 2017-09-26 |
| 19 | 2017-09-29 | 2017-09-30 |
| 20 | 2017-09-19 | 2017-09-22 |
| 20 | 2017-09-22 | 2017-09-26 |
| 20 | 2017-09-24 | 2017-09-27 |
| HouseId | Days |
|---------|------|
| 1 | 5 |
| 19 | 12 |
| 20 | 9 |
select house_id,min(initialDay),max(LastDay)
group by houseId
If I understood correctly!
Try out and let me know how it works out for you.
Ted.
While thinking through your question I came across the wonder that is the idea of a Calendar table. You'd use this code to create one, with whatever range of dates your want for your calendar. Code is from http://blog.jontav.com/post/9380766884/calendar-tables-are-incredibly-useful-in-sql
declare #start_dt as date = '1/1/2010';
declare #end_dt as date = '1/1/2020';
declare #dates as table (
date_id date primary key,
date_year smallint,
date_month tinyint,
date_day tinyint,
weekday_id tinyint,
weekday_nm varchar(10),
month_nm varchar(10),
day_of_year smallint,
quarter_id tinyint,
first_day_of_month date,
last_day_of_month date,
start_dts datetime,
end_dts datetime
)
while #start_dt < #end_dt
begin
insert into #dates(
date_id, date_year, date_month, date_day,
weekday_id, weekday_nm, month_nm, day_of_year, quarter_id,
first_day_of_month, last_day_of_month,
start_dts, end_dts
)
values(
#start_dt, year(#start_dt), month(#start_dt), day(#start_dt),
datepart(weekday, #start_dt), datename(weekday, #start_dt), datename(month, #start_dt), datepart(dayofyear, #start_dt), datepart(quarter, #start_dt),
dateadd(day,-(day(#start_dt)-1),#start_dt), dateadd(day,-(day(dateadd(month,1,#start_dt))),dateadd(month,1,#start_dt)),
cast(#start_dt as datetime), dateadd(second,-1,cast(dateadd(day, 1, #start_dt) as datetime))
)
set #start_dt = dateadd(day, 1, #start_dt)
end
select *
into Calendar
from #dates
Once you have a calendar table your query is as simple as:
select distinct t.House_id, c.date_id
from Reservation as r
inner join Calendar as c
on
c.date_id >= r.InitialDay
and c.date_id <= r.LastDay
Which gives you a row for each unique day each room was occupied. If you need a sum of how many days each room was occupied it becomes:
select a.House_id, count(a.House_id) as Days_occupied
from
(select distinct t.House_id, c.date_id
from so_test as t
inner join Calendar as c
on
c.date_id >= t.InitialDay
and c.date_id <= t.LastDay) as a
group by a.House_id
Create a table of all the possible dates and then join it to the Reservations table so that you have a list of all days between InitialDay and LastDay. Like this:
DECLARE #i date
DECLARE #last date
CREATE TABLE #temp (Date date)
SELECT #i = MIN(Date) FROM Reservations
SELECT #last = MAX(Date) FROM Reservations
WHILE #i <= #last
BEGIN
INSERT INTO #temp VALUES(#i)
SET #i = DATEADD(day, 1, #i)
END
SELECT HouseID, COUNT(*) FROM
(
SELECT DISTINCT HouseID, Date FROM Reservation
LEFT JOIN #temp
ON Reservation.InitialDay <= #temp.Date
AND Reservation.LastDay >= #temp.Date
) AS a
GROUP BY HouseID
DROP TABLE #temp

Turning week number into week commencing in SSRS/SQL Server

I have a report that is grouped on week number but for presentation reasons want it to be week commencing.
Select
datepart(wk,[rhStartTime]) as [week number]
...
group by datepart(wk,[rhStartTime]),[rhOperatorName])
where
[week number] >= #StartWeek
and [week number] <= #EndWeek
My report parameters use week number to filter the data with #StartWeek and #EndWeek being integers that plug into the SQL. My question is one of presentation. It is tough for users to understand what Week 15 means in context so I would like to alter my output to show Week Commencing rather than week number but for the backend to still use weeknumber. I also don't want users to be able to pick any date because they will invariably pick dates that span multiple weeks without a full weeks data.
I look at similar questions and one here
SO question
recommended SQL of the format
DATEADD(dd, -(DATEPART(dw, WeddingDate)-1), WeddingDate) [WeekStart]
But plugging my columns into that format gave me a bit of a mess. It didn't group how I was expecting.
SELECT
DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])) as [week commencing]
,datepart(wk,[rhStartTime])) as [week number]
...
group by datepart(wk,[rhStartTime])),DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])),[rhoperatorname]
I got this output
where I was looking for all those week 15s to be grouped together with just one week commencing date.
Try This will work.This retrieves the dates eliminating time part of it
SELECT
Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time])) as [week number]
...
from Table X
group by Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time]))
,[Agent Name]
I think your problem is in how you are using the examples you have seen elsewhere and not with the examples themselves, as I have just tested the logic and it seems to be working for me without issue, as you can see in the script below.
I think your main problem is that you are not removing the time portion of your StartTime values, which you will need to do if you want to group all values that occur on the same day. The easiest way to do this is to simply cast or convert the values to date data types:
select cast(StartTime as date) as CastToDate
,convert(date, StartTime, 103) as ConvertToDate -- You may need to use 101 depending on your server setting for dd/mm/yyyy or mm/dd/yyyy
Script:
declare #StartDate date = '20170325'
,#EndDate date = '20170403';
-- Tally table to create dates to use in functions:
with n(n) as(select n from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n(n))
,d(d) as(select top(datediff(d,#StartDate,#EndDate)+1) dateadd(d,row_number() over (order by (select null))-1,#StartDate) from n n1,n n2,n n3,n n4,n n5,n n6)
select d
,datepart(week,d) as WeekNumber
,DATEADD(dd, -(DATEPART(dw, d)-1), d) as WeekCommencing
from d
order by d;
Output:
+------------+------------+----------------+
| d | WeekNumber | WeekCommencing |
+------------+------------+----------------+
| 2017-03-25 | 12 | 2017-03-19 |
| 2017-03-26 | 13 | 2017-03-26 |
| 2017-03-27 | 13 | 2017-03-26 |
| 2017-03-28 | 13 | 2017-03-26 |
| 2017-03-29 | 13 | 2017-03-26 |
| 2017-03-30 | 13 | 2017-03-26 |
| 2017-03-31 | 13 | 2017-03-26 |
| 2017-04-01 | 13 | 2017-03-26 |
| 2017-04-02 | 14 | 2017-04-02 |
| 2017-04-03 | 14 | 2017-04-02 |
+------------+------------+----------------+
Replace the field value in your SQL code with the expression below to remove time
DATEADD(dd, -(DATEPART(dw,[rhStartTime]) -1), DATEDIFF(dd, 0, [rhStartTime]) )
You can also achieve the same result by using the expression below in SSRS (change it to match your date field)
= DATEADD("d", - DATEPART(DateInterval.Weekday,Fields!rhStartTime.Value) +1,Fields!rhStartTime.Value)
Thanks for the answers. I'm sure they probably would have worked if I were more competent. In the end I created a simple table on my server with year,weeknumber,commencedate as the column headings and manually created them in excel. Then I linked my results as a cte to that table where year = 2017 and cte.weeknumber = commencedate.weeknumber It seems to have worked.
Now in my SSRS report parameter I am using weeknumber as the value and commence date as the label. So I don't have to change any of the other configuration.

SQL: Comparing Dates with only Month and Year columns

I have a table MonthlyShipments that looks like:
partnumber | quantity | month | year |
part1 | 12 | 6 | 2011 |
part1 | 22 | 5 | 2011 |
part1 | 32 | 4 | 2011 |
part1 | 42 | 3 | 2011 |
part1 | 52 | 2 | 2011 |
I want to sum the quantities of the past 3 months, excluding the current month. My where clause currently looks like:
where
MonthlyShipments.Month <> MONTH(GETDATE()) AND
CAST(
(CAST(MonthlyShipments.Month as nvarchar(2)) +
'-01-' +
CAST(MonthlyShipments.Year as nvarchar(4))) as DateTime)
> DATEADD(m, -4, GETDATE())
It works, but its ugly and insulting. Any advice on making it prettier? Much appreciate!
Not much better...
DATEDIFF(
month,
DATEADD(Year, MonthlyShipments.Year-1900,
DATEADD(Month, MonthlyShipments.Month-1, 0)
),
GETDATE()
) BETWEEN 1 AND 3
however the nested DATEADD can be made a computed and indexed column
ALTER TABLE MonthlyShipments ADD
ShipDate AS DATEADD(Year, MonthlyShipments.Year-1900,
DATEADD(Month, MonthlyShipments.Month-1, 0)
)
which gives
WHERE DATEDIFF(month, ShipDate, GETDATE()) BETWEEN 1 AND 3
Can you add a date column? If you need to do date calculations and you don't want things to get ugly this is probably going to be a requirement. Even a computed column would work...
Then you can just do something like this:
WHERE datecolumn < DATEADD(month, -3, GETDATE())
You can convert your month+date to date and then compare it with current date:
WHERE DATEDIFF(m,CONVERT(DATE, [month]+'/01/'+[year],101), GETDATE())
BETWEEN 1 AND 3
Or as it was said by others you can create a new computed column that holds date type
If you have a (year,month) index, this will make use of it, I think:
FROM MonthlyShipments ms
WHERE ( ms.year = YEAR(GetDate())
AND ms.month BETWEEN MONTH(GetDate())-3
AND MONTH(GetDate())-1
)
OR ( ms.year = YEAR(GetDate())-1
AND ms.month BETWEEN 12 + MONTH(GetDate())-3
AND 12 + MONTH(GetDate())-1
)
Not looking much prettier though.