I have one table in SQL Server where I want to add a column that will show this year's date for the exact transition dates for European Summer Time.
European Summer Time is observed across three time zones, beginning at 01:00 UTC/WET on the last Sunday in March and ending at 01:00 UTC on the last Sunday in October each year.
How do you change this into a SQL function?
On Wikipedia I was able to find this:
The formula used to calculate the beginning of European Summer Time is
Sunday (31 − ((((5 × y) ÷ 4) + 4) mod 7)) March at 01:00 UTC
https://en.wikipedia.org/wiki/Summer_Time_in_Europe
Be careful. DST is a non-technical, but political policy and can vary over time (countries change DST adjustments all the time) which requires an up-to-date timezone database.
SQL Server itself is not concerned with timezones: it does not perform any timezone conversion itself, only UTF-offset conversion (when using datetimeoffset when the instantaneous local-to-UTC conversion is known). It cannot tell you anything about zone locations or future offsets, as those are arguably business rules that belong outside the domain of the persistence layer (i.e. SQL Server). When storing values in a database always used either UTC datetime or datetimeoffset values. Use UTC datetime when zone information is irrelevant to the user (i.e. don't invent your own datetime+int system, use datetimeoffset).
Along those lines, SQL Server does not provide any API or functionality for accessing the system (the Windows OS') timezone database from T-SQL code.
You would need to do this from application code. In .NET you can use the TimeZoneInfo class which uses Windows' timezone database to get the DST dates:
TimeZoneInfo.AdjustmentRule.DateStart
TimeZoneInfo.AdjustmentRule.DateEnd
TimeZoneInfo.AdjustmentRule.DaylightTransitionStart
TimeZoneInfo.AdjustmentRule.DaylightTransitionEnd
https://msdn.microsoft.com/en-us/library/system.timezoneinfo.adjustmentrule(v=vs.110).aspx
Depending on what the criteria you're inputting into whatever function you want to produce here is, the specific code is going to be different.
The following, for example, produces a list of the dates/times for the next 100 years in the timezone this is run in.
; WITH C1(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) T(N))
, C2(N, RN) AS (SELECT 1, ROW_NUMBER() OVER (ORDER BY NEWID()) FROM C1 a CROSS JOIN C1 b)
, TYears AS (SELECT YEAR(GETDATE()) + RN - 1 [Years] FROM C2)
SELECT DATEADD(hh
, DATEDIFF(hh, GETUTCDATE(), GETDATE())
, DATEADD(dd
, 0 - (1 + DATEDIFF(dd, 0, CONVERT(DATE, CONVERT(NVARCHAR, [Years]) + '-03-31')) % 7) % 7
, CONVERT(DATETIME, CONVERT(NVARCHAR, [Years]) + '-03-31 01:00:00'))) [StartDateTime]
, DATEADD(hh
, DATEDIFF(hh, GETUTCDATE(), GETDATE())
, DATEADD(dd
, 0 - (1 + DATEDIFF(dd, 0, CONVERT(DATE, CONVERT(NVARCHAR, [Years]) + '-10-31')) % 7) % 7
, CONVERT(DATETIME, CONVERT(NVARCHAR, [Years]) + '-10-31 01:00:00'))) [EndDateTime]
FROM TYears
The basic premise being find the last Sunday of March for that year, find the UTC +/- value, add it to 1AM of the last Sunday of March.
Related
I have a table full of daily aggregate data, but I occasionally need to pull weekly aggregate data, and provide info on increases or decreases. For that reason, I was considering using T-SQL DATEPART functionality to get week-number and year info for dates.
For example, I can get the following info using today's date (9/11/2020):
#nowWeekNumber int = datepart(wk,#today), --yields 37
#nowYear int = datepart(year,#today), --yields 2020
Using that logic, I could then gather info on records where year is 2020 and weekNumber is 36, and then I could compare those numbers to get a weekly increase/decrease. (Or maybe I'd compare weeks 35 and 36 to ensure that I'm dealing w/ entire weeks, but you get the picture)
However, if the date is 2021-01-03, that's going to return a year of 2021, and a weekNumber of 2. If I subtract a week, I'm going to get year 2021 and weekNumber 1. That weekNumber is only going to contain January 1st and 2nd, because 12/27 thru 12/31 are considered year 2020 and weekNumber 53 (even though the calendar week is 12/27 thru 1/2).
In other words, I don't think I can use weekNumber to gather weekly data, even though that would be fairly convenient. I'm aware that I can use DATEADD functions to grab the start and end-date for consecutive weeks, and I can then gather aggregate data for records BETWEEN those dates, but is there a more-convenient way to do this?
Why don't you consider using dateDiff as key function? As...
select dateDiff(wk, 0, getDate())
Returns a single integer for the whole week (6297 for '20200911') and :
select dateAdd(wk, dateDiff(wk, 0, getDate()), 0),
dateAdd(dd, 6, dateAdd(wk, dateDiff(wk, 0, getDate()), 0))
or
select dateAdd(wk, 6297, 0),
dateAdd(dd, 6, dateAdd(wk, 6297, 0))
gives you the 1st and last day of that week.
You can use DATEPART but instead of wk you can use the iso week. Then you don't have the problem with a week being split in 2. To be sure also use SET DATEFIRST to define exactly on which day the week starts.
SET DATEFIRST 1; --use monday as first day of the week
SELECT datepart(iso_week,'2021-01-01');
SELECT datepart(iso_week,'2021-01-03');
SELECT datepart(iso_week,'2021-01-04');
The other option is to create your own calendar table and join that to your daily table.
EDIT: for a week start on sunday
SET DATEFIRST 7;
SELECT DATEPART(WEEK, DATEADD( DAY, 1-DATEPART(WEEKDAY,'2020-12-27'),'2020-12-27' ) )
SELECT DATEPART(WEEK, DATEADD( DAY, 1-DATEPART(WEEKDAY,'2020-12-28'),'2020-12-28' ) )
SELECT DATEPART(WEEK, DATEADD( DAY, 1-DATEPART(WEEKDAY,'2021-01-01'),'2021-01-01' ) )
SELECT DATEPART(WEEK, DATEADD( DAY, 1-DATEPART(WEEKDAY,'2021-01-02'),'2021-01-02' ) )
Sorry if the Title is confusing but it's hard to explain what I'm after in one phrase.
I'm currently producing a report based on the production for the week. I start off my CTE construction with the following to get the days Monday to Friday of the current week:
WITH
cte_Date AS
(
SELECT
CAST(DateTime AS date) AS Date
FROM
( VALUES
(GETDATE()
)
, (DATEADD(day,-1,GETDATE()))
, (DATEADD(day,-2,GETDATE()))
, (DATEADD(day,-3,GETDATE()))
, (DATEADD(day,-4,GETDATE()))
, (DATEADD(day,-5,GETDATE()))
, (DATEADD(day,-6,GETDATE())) ) AS LastSevenDays(DateTime)
WHERE
DATENAME(weekday, DateTime) = 'Monday'
UNION ALL
SELECT
DATEADD(day,1,Date)
FROM
cte_Date
WHERE
DATENAME(weekday,Date) <> 'Friday'
)
This is working fine. I have made the report available to users so they can run it anytime however sometimes nobody is available to run it last thing Friday. This means they don't get to see the full production for Friday and then the following week the CTE days change.
I'm trying to keep this a one-click affair so rather than introduce date parameters I proposed to the users that we adjust the query such that if they run the report before midday on "Monday" then it will show them last week's figures and they were happy with this (me and my big mouth). I put Monday in quotes because what we really mean of course is the first production day of the week.
My primary data table (which we'll call MyData) has a datetime field named DateTime (really!) that I can reference to determine the first day of production for the week.
One final caveat: Due to the layout of the report the users insisted that they always want to see the five days Monday to Friday, even if there is no production on a given day. (Consequently I do a LEFT JOIN from cte_Date to all other tables required.) So to be clear, right now as I'm typing this it's 11:45am local time on Tuesday and yesterday happened to be a public holiday here so running the report now should return Monday to Friday last week, but running it in 20 minutes time should return Monday to Friday this week.
Please help, my poor brain is getting twisted trying to figure it out.
There are a few different ways you can tackle this, but they all boil down to the same thing: you need a way of figuring out whether it's before or after 12pm on the first working day of the current week, then you need to get the Monday of the current "production week".
Let's just say, for simplicity's sake, you have some sort of table that contains public holidays (or non-production days). To find out whether it's the first day of the current production week, you basically just have to add the number of days in a row since the start of the week that have been public holidays.
Then you need to figure out whether it's before or after 12pm of that day.
If it's before you want last week's Monday-Friday. If it's after, you want this week's Monday-Friday.
Here's one way you might do this:
DECLARE #NonProductionDays TABLE (NPD DATE UNIQUE NOT NULL); -- Public holiday table.
INSERT #NonProductionDays (NPD) VALUES ('2017-09-25');
DECLARE #i INT = -- You don't need a variable for this, but just to keep things simple...
(
SELECT COUNT(*) -- Extract number of public holidays in a row this week before current date.
FROM #NonProductionDays AS N
WHERE DATEDIFF(WEEK, 0, N.NPD) = DATEDIFF(WEEK, 0, GETDATE())
AND N.NPD <= GETDATE()
AND (DATENAME(WEEKDAY, N.NPD) = 'Monday' OR EXISTS (SELECT 1 FROM #NonProductionDays AS N2 WHERE N2.NPD = DATEADD(DAY, -1, N.NPD)))
);
SELECT D = CAST(DATEADD(DAY, T.N, DATEADD(WEEK, DATEDIFF(HOUR, DATEADD(DAY, #i, '1900-01-01 12:00:00'), GETDATE()) / 24 / 7, '1900-01-01')) AS DATE)
FROM (VALUES (0), (1), (2), (3), (4)) AS T(N);
/*
Breaking this down:
X = DATEADD(DAY, #i, '1900-01-01 12:00:00')
-- Adds the number of NPD days this week to '1900-01-01 12:00:00'
-- So, for example, X would be '1900-01-02 12:00:00' this week
Y = DATEDIFF(HOUR, X, GETDATE()) / 24 / 7
-- The number of weeks between X and now, by taking the number of hours and dividing by 24 then by 7
-- The division is necessary to compare the hour.
-- So, for example, as of 11am on the September 26 2017, you'd get 6142.
-- As of 12pm on September 26 2017, you'd get 6143.
Z = DATEADD(WEEK, Y, '1900-01-01')
-- Just adds Y weeks to 1900-01-01, which was a Monday. This tells you the Monday of the current "production week".
-- So, for example, as of 11am on September 26 2017, you'd get '2017-09-18 00:00:00.000'.
-- As of 12pm on September 26 2017, you'd get '2017-09-25 00:00:00.000'.
Then we cast this as a date and add 0/1/2/3/4 days to it to get Monday, Tuesday, Wednesday, Thursday and Friday of the current "production week".
*/
I'm not sure I came up with the most efficient approach, but after a week of tossing it about in my brain this is what I came up with. I approached the problem from the opposite direction of that suggested by #ZLK.
My existing logic was already giving me the Monday of this week so in a subquery I looked for the first production record after Monday, stripped off the time with a DATEDIFF and made it midday with a DATEADD. I was then able to compare the current Date/Time with midday of the first production day to determine whether to reduce the date by one week.
I replaced this SELECT clause:
SELECT
CAST(DateTime AS date) AS Date
with this one:
SELECT -- Monday this week if it's after midday on the first production day otherwise Monday last week
DATEADD(week,IIF(GETDATE()>=DATEADD(hour,12,(
SELECT DATEDIFF(day,0,MIN(DateTime))
FROM MyData
WHERE CAST(MyData.DateTime AS date) >= CAST(LastSevenDays.DateTime AS date)
)),0,-1),CAST(LastSevenDays.DateTime AS date)) AS Date
To cater for the case where a new week has commenced but the operator runs the report before production starts I carefully arranged the boolean condition inside my IIF clause so that the empty result set from the subquery would mean the test returned FALSE and the operator would still see last week's figures.
(#ZLK, Thanks for your input - you did help my thinking a bit but I don't think your answer should be marked as correct. What I've come up with here is what I was originally requesting and didn't require the use of a static table.)
I have a requirement to pull records from SQL Server from a specific hour every day, say at 4 PM.
So if I am running query at 3 PM today, it should pull records from previous day 4 PM till today 3 PM (23 hours).
On the other hand, if run query at 4:30 PM today, it should pull query from today 4 PM to 4:30 PM (only for half an hour) and so on
How could I do this in plain SQL in SQL Server?
Here is how I would do it:
SELECT *
FROM TableName
WHERE ColumnName <= GETDATE()
AND ColumnName >=
CASE WHEN DATEPART(HOUR, GETDATE()) < 4 THEN
DATEADD(HH, 4, CAST(CAST(GETDATE()-1 AS date) AS datetime))
ELSE
DATEADD(HH, 4, CAST(CAST(GETDATE() AS date) AS datetime))
END
Points of interest:
CAST(GETDATE() AS date) will return the date part of the current datetime.
Casting back to datetime allows you to use DateAdd to get the hour you want
using -1 with date values subtracts a day from the date (+1 will add a day, of course)
using the CASE...WHEN allows you to write a single where clause that fits both scenarios described in the question.
Please try the below query, i have created it for stored procedures that had been modified on specific date:
Select * from sys.procedures where CONVERT(date,modify_date,101) =
CONVERT(date,DATEADD(DD,-5, GETDATE()),101)
AND DATEPART(hh,modify_date) >= 1 AND DATEPART(hh,modify_date) <= 22
You can pass "CONVERT(date,DATEADD(DD,-5, GETDATE()),101)" GEDATE only instead to - minus five days, that was done only to fetch records for me.
and in hours you can specify the range as you want.
Hope this will work for you.
I think this might help you
select 1 from table where Col1 between case when hour(getdate())>4 then convert(datetime,convert(varchar(20),year(getdate()))+'-'+convert(varchar(20),month(getdate()))+'-'+convert(varchar(20),day(getdate()))+' 04:00') else dateadd(hh,-23,getdate()) end and getdate()
I'm stuck. Need help.
I store UTC dates in database.
Example rows:
GeneratedAt:
2011-06-08 23:30
2011-06-09 03:30
2011-06-09 15:30
Local time for my user is -2 hours (Central Europe). When I want rows from 09 then I have 3 rows.
Problem is with GROUP BY day for reporting purposes. I have 1 for 08 and 2 for 09 but this is not true for my local time.
Everywhere I see: "store data in UTC". How to do this properly?
UPDATE 1:
For data access I'm using NHibernate and I prefer solution independent of the database engine. So, I'm looking for solution with something like Date/Time Dimension table (or something like that).
My data table has columns like this:
GeneratedAt (datetime)
GeneratedAt_Year (int)
GeneratedAt_Month (int)
GeneratedAt_Day (int)
GeneratedAt_Hour (int)
Thanks to that I can easily grouping by: year, year+month, year+month+day, year+month+day+hour. Unfortunately this is UTC. :(
How to refactor this solution to deal with user timezones?
You could create a view of that table which provides the datetime value in the desired Central Europe timezone by applying a DATEADD function.
So if your table columns are: id, other_id, evt_time
then the select statement (for the view definition) would be:
SELECT id, other_id, evt_time, DATEADD( hh, -2, evt_time ) AS evt_time_ce
FROM MyTable
then you can use the view and apply the GROUP_BY to the evt_time_ce column
I have a similar issue in general, but my time difference is 8 hours.
I use dateadd(hour,8, [Timestamp]) to select the local time, and dateadd(hour,-8, #dateFrom) in WHERE clauses - which should work for GROUP BY as well.
For example:
DECLARE #dateFrom datetime, #dateUntil datetime
SET #dateFrom = '2011-06-20 00:00:02.000'
SET #dateUntil = '2011-06-22 10:00:00.000'
SELECT TOP 100
dateadd(hour, 8, [Timestamp]) LocalTime,
*
FROM [Log] L (nolock)
WHERE L.[Timestamp] BETWEEN dateadd(hour, -8, #dateFrom) AND dateadd(hour, -8, #dateUntil)
ORDER BY LogID DESC
SQL Server 2016 has introduced AT TIME ZONE that can help with this, including handling daylight savings time.
AT TIME ZONE lets you convert dates between time zones listed in the windows registry. You can see what timezones are available by running a simple SELECT:
select * from sys.time_zone_info
Here's the SQL that converts your UTC stored date into a date at another timezone
GeneratedAt
AT TIME ZONE 'UTC'
AT TIME ZONE 'Central Europe Standard Time'
So your SQL to group by day based on a local timezone might look something like this:
SELECT DATEADD(DAY,0, DATEDIFF(DAY, 0, GeneratedAt
AT TIME ZONE 'UTC'
AT TIME ZONE 'Central Europe Standard Time'))
FROM YourTable
GROUP BY DATEADD(DAY,0, DATEDIFF(DAY, 0, GeneratedAt
AT TIME ZONE 'UTC'
AT TIME ZONE 'Central Europe Standard Time'))
Here's a good article about the topic if you want to read more.
I have a question about how to “pivot/total” (for want of a better word) some data around in SQL server. The data is basically staff shifts and then hours lost from those shifts. So for example I have a record in one table
Staff_ID Shift_start Shift_end
37 09:00 17:30
And then we would give that person a lunch in another table
Staff_ID Start End
37 13:00 14:00
Of course we have more agents throughout the day and the scheme above is simplified but you get the idea. This is then transformed into the number of staff in 15 minutes
Interval Staff
09:00 5
09:15 7
09:30 6
And so on.
At the moment SQL server stores all of this but to “total” the agents up I have to bring things into Access and using arrays work out the number of staff in each 15 minute period and then save this data back to the database. It’s a quick process (<1500ms) but what I’m looking for is a way to do this in SQL server itself and not have to bring things to Access and write it back.
Am I barking up the wrong tree with this one?
EDIT:
I'm using SQL server 2008R2 Express
Update
Try this:
;with Intervals(start) as --00:00 - 23:45
(
select dateadd(minute, 0,0)
union all
select dateadd(minute, 15, start) from Intervals
where datediff(hour, 0, dateadd(minute, 15, start))<24
)
select convert(varchar, i.start, 108) [Interval], count(*) [Staff]
from Intervals i
join
(
select cast('09:31:29' as datetime) [start], cast('17:11:29' as datetime) [end] union all
select cast('10:43:12' as datetime), cast('18:21:29' as datetime) union all
select cast('11:59:53' as datetime), cast('19:51:29' as datetime)
)s
on cast(convert(varchar(10), s.start, 108)as datetime) <= i.start
and dateadd(minute, 15, i.start) <= cast(convert(varchar(10), s.[end], 108) as datetime)
group by convert(varchar, i.start, 108)
You have a few approaches you could try. One would be to take exactly what you have in Access and convert it to SQL. If there's part of that you're not sure how to do, post it here and we can help.
Another would be to use a cursor (as opposed to a single set function) to iterate through either each 15 minute time period, loading staff working during that period, or each staff person's schedule, populating all of their working 15-minute time periods. You may be doing this in Access already, I can't tell.
Since they're the same 15-minute periods each day, you can store the times in a table and do an outer join on them, but the performance could be worse than your Access process.