How to schedule an Oracle dbms_scheduler Job timezone and DST safely - oracle11g

I am trying to setup a DBMS_SCHEDULER Job to run exactly at 1 AM on 1st of January every year on Oracle 11g. How to setup its attributes to be absolutely sure it wont get executed in wrong hour, because of timezone differences nor Daylight Savings Time.
I have spent plenty of time going through Oracle documentation, but I have still not reached the level of certainity.
Just btw, here are the rules which I found and consider relevant to the subject:
Job attributes
start_date This attribute specifies the first date on which this job is scheduled to start. If start_date and repeat_interval are left null, then the job is scheduled to run as soon as the job is enabled.
For repeating jobs that use a calendaring expression to specify the repeat interval, start_date is used as a reference date. The first time the job will be scheduled to run is the first match of the calendaring expression that is on or after the current date.
The Scheduler cannot guarantee that a job will execute on an exact time because the system may be overloaded and thus resources unavailable.
repeat_interval This attribute specifies how often the job should repeat. You can specify the repeat interval by using calendaring or PL/SQL expressions.
The expression specified is evaluated to determine the next time the job should run. If repeat_interval is not specified, the job will run only once at the specified start date. See "Calendaring Syntax" for further information.
Rules in Calendaring syntax
The calendaring syntax does not allow you to specify a time zone.
Instead the Scheduler retrieves the time zone from the start_date
argument. If jobs must follow daylight savings adjustments you must
make sure that you specify a region name for the time zone of the
start_date. For example specifying the start_date time zone as
'US/Eastern' in New York will make sure that daylight saving
adjustments are automatically applied. If instead the time zone of
the start_date is set to an absolute offset, such as '-5:00',
daylight savings adjustments are not followed and your job execution
will be off by an hour half of the year.
When start_date is NULL, the Scheduler will determine the time zone for the repeat interval as follows:
It will check whether the session time zone is a region name. The session time zone can be set by either:
Issuing an ALTER SESSION statement, for example: SQL> ALTER SESSION
SET time_zone = 'Asia/Shanghai'; Setting the ORA_SDTZ environment
variable.
If the session time zone is an absolute offset instead of a region name, the Scheduler will use the value of the DEFAULT_TIMEZONE Scheduler attribute. For more information, see the SET_SCHEDULER_ATTRIBUTE Procedure.
If the DEFAULT_TIMEZONE attribute is NULL, the Scheduler will use the time zone of systimestamp when the job or window is enabled.

You may use this to make sure you pass a timestamp with time zone and that the start date will have a timezone name (US/Eastern) instead of an offset (ex: +5:00). This way, as the above fragments from the oracle docs mention, the Scheduler will keep track of DST.
-- Create a SCHEDULE
declare
v_start_date timestamp with time zone;
BEGIN
select localtimestamp at time zone 'US/Eastern' into v_start_date from dual; --US/Eastern
DBMS_SCHEDULER.CREATE_SCHEDULE(
schedule_name => 'SAMPLE_SCHEDULE',
start_date => v_start_date,
repeat_interval => 'FREQ=DAILY; BYHOUR=10; BYMINUTE= 15',
comments => 'Runs daily at the specified hour.');
END;
To make sure you have set it properly you can run this:
ALTER SESSION SET nls_timestamp_tz_format = 'MM-DD-YYYY HH24:MI:SS tzr tzd';
Now, create two schedules, one as above and one using sysdate as the start_date parameter and execute the query below.
-- Check the TIMEZONE
select * from USER_SCHEDULER_SCHEDULES;
v1:
27-MAR-14 11.44.24.929282 AM **US/EASTERN**
v2:
27-MAR-14 05.44.54.000000 PM **+05:00**

I am unsure if this answer truly passes the rules of an answer on this site, but after spending a lot of time googling I came up with the following solution:
start_date => CAST(trunc(sysdate, 'YEAR')+2/24 AS TIMESTAMP) at time zone 'Europe/Berlin'
I believe this is closest to safest solution because:
It uses timestamp instead of date - i believe it forces the job to be truly executed on given time in given timezone, while ignoring DMBS_SCHEDULER default_timezone. I found also some suggestions that say that it is also unsafe to use directly timestamp, that only this cast is safe
I selected manually the timezone I need, with the hope, that it would not come to conflict with local settings. Altough it is unclear to me, whether it is now truly unrelated to SESSIONTIMEZONE, or DBTIMEZONE and whether it affects the proper time of run.
I have used a little hack, even though the request is that the job should start after midnight, I have set it to 2AM, with the hope that even in case of bad time zone and bad daylight savings it would get moved max +-2 hours.
I would be happier with the solution, if I would be absolutely clear on when the job actually gets executed with the respect of local time of a server, SESSIONTIMEZONE, DBTIMEZONE, start_date Time Zone and a DBMS_SCHEDULER time zone.
I am also unhappy with the Time Zone specification, since its has 4 abbreviations linked with it - LMT, CET, CEST, CEMT, where CEST seems to me like being completely wrong. My target is to use CET with Daylight savings(winter!=summer).

Actually I have never tried it with .CREATE_SCHEDULE method but .CREATE_JOB has also start_date parameter and what works for me is just plain
start_date => TO_TIMESTAMP_TZ('00:10 Europe/Rome','hh24:mi tzr'
It retains 'Europe/Rome' when I query dba_scheduler_jobs:
SELECT job_name, TO_CHAR(start_date) start_date,
TO_CHAR(next_run_date) next_run_date
FROM dba_scheduler_jobs;

To add a bit more info to this, when you want to check if modification was successful.
Run query: select * from all_scheduler_jobs where owner='schema_name'.
There you can see in the field start_date, which has type timestamp with timezone, that it contains data like: 2017-12-05 01:55:00,000000000 EUROPE/YOUR_CITY
Having time zone info at the end confirms that it is properly saved for the job.
Then, also, next_run_date is aligned with start_date and it should also show time zone details.

SELECT DBMS_SCHEDULER.STIME FROM DUAL;
Reference

Related

Update in Project default_time_zone not affecting timestamp

As part of setting GCP project level timezone to AEST, I have run the following command -
ALTER PROJECT `gcp-abc-def`
SET OPTIONS ( `region-us.default_time_zone` = 'Australia/Sydney')
Doing so, I see that current_datetime() is getting changed to AEST whereas timestamp remains UTC, as can be seen below.
Can someone help how this can be remedied? What other settings to be changed?
I see that current_datetime() is getting changed to AEST whereas timestamp remains UTC this is because the current_timestamp function shows time in timestamp type. A timestamp does not have a time zone; it represents the same instant in time globally. When querying for CURRENT_TIMESTAMP() it shows explicitly in UTC by having zero time zone offset. When you convert a timestamp to some other type that isn't tied to a particular timezone, you can specify the timezone for the conversion.You can use format_timestamp to convert the timestamp into your zone specific time.
Example:
ALTER PROJECT `gcp-abc-def`
SET OPTIONS ( `region-us.default_time_zone` = 'Australia/Sydney');
select current_datetime() as cdt, current_timestamp() as cts,format_timestamp('%c',current_timestamp(),'Australia/Sydney') as cts2

How does this teradata timestamp with timezone example make sense?

In the teradata documentation it says:
"Suppose an installation is in the PST time zone and it is New Years Eve, 1998-12-31 20:30 local time.
The system TIMESTAMP WITH TIME ZONE for the indicated time is ' 1999-01-01 04:30-08:00 ' internally."
This does not mesh with my understanding. I figure it ought to be '1999-01-01 04:30+00:00' internally because it should be stored in UTC.
Or, it can be stored as a the local time with a -8 offset, but this example seems to mix the two. Perhaps I am misunderstanding the text?
Not sure if this is an answer, but it's too long for a comment.
That "internal" storage part is very misleading. We don't care how Teradata stores anything internally.
I find this easier to look at using BTEQ, since SQL Assistant doesn't show timezones, at least by default. So, assuming you've logged into BTEQ...
--set session timezone to pst (GMT - 8)
SET TIME ZONE INTERVAL -'08:00' HOUR TO MINUTE ;
create volatile table vt_foo (
ts_w_zone timestamp(0) with time zone,
ts_wo_zone timestamp) on commit preserve rows;
insert into vt_foo
select
cast('1998-12-31 20:30:00' as timestamp(0)),
cast('1998-12-31 20:30:00' as timestamp);
select * from vt_foo;
Currently the two values (with and without tz) will match.
ts_w_zone ts_wo_zone
------------------------- --------------------------
1998-12-31 20:30:00-08:00 1998-12-31 20:30:00.000000
Now let's change the timezone for your session to something else, and look at what we get.
SET TIME ZONE INTERVAL -'03:00' HOUR TO MINUTE ;
select * from vt_foo;
ts_w_zone ts_wo_zone
------------------------- --------------------------
1998-12-31 20:30:00-08:00 1999-01-01 01:30:00.000000
The timestamp with zone is still the same. Displaying it without timezone is automatically converting it to your session timezone, which in this example is GMT -3.
EDIT:
Technically, Teradata is actually storing the time with timezone as GMT (1999-01-01 04:30:00) with the timezone offset (-8). That's where the documentation gets the  1999-01-01 04:30-08:00 value from). But that is not how it displays it.

Updating entries in a database based on time

I have a model called "ticket" that has a start time and end time. I would like to be able to sort/divide tickets by time on the front-end. There will be 3 categories: past (now > end_time), current (start_time < now < end_time), future (now < start_time). Where now represents the real UTC time. I am thinking of having a "state" field in the ticket model which will contain the value "past", "current", or "future". I need a way to update the state of tickets based on time.
Would a cron job running every minute be an appropriate solution for this? It will iterate through all entries and do a check if the state should be updated and then perform the update if necessary. Is this solution scalable? Is there a better solution?
If it is necessary info I am thinking of using Firebase for the database and Google App Engine for the cron job.
In most cases it is the wrong approach to save data into a db which gets invalidated by time but can be calculated from the same record's data. There is also a risk of inconsistencies if the dates get updated but the cronjob didn't run yet or when dates are close to now.
I would suggest you to always calculate that info in your db queries by using the date fields. In MySQL this would work similar to this:
SELECT
(IF (end_time < NOW()) THEN 'past' ELSE IF (start_time < NOW()) THEN 'current' ELSE 'future') AS state
FROM table
Alternatively you can just fetch the start_time and end_time fields and handle the state in your application. If you want to query the entries by status, then you can also use the date columns in the filtering clauses.
This drops the need to have a cron job update the status.

Can't subtract datetime and timestamp in django?

I have a field timestamp = models.DateTimeField(auto_now_add=True) in the db. I want to find the difference between that timestamp and datetime.now().
When I tried datetime.now() - timestamp, I get the error:
can't subtract offset-naive and offset-aware datetimes
How do I fix this?
This error refers to how times are stored by python. According to the python documentation:
There are two kinds of date and time objects: “naive” and “aware”. This distinction refers to whether the object has any notion of time zone, daylight saving time, or other kind of algorithmic or political time adjustment.
The django documentation also states that:
When time zone support is disabled, Django uses naive datetime objects
in local time. This is simple and sufficient for many use cases. In
this mode, to obtain the current time, you would write:
import datetime
now = datetime.datetime.now()
When time zone support is enabled,
Django uses time-zone-aware datetime objects. If your code creates
datetime objects, they should be aware too. In this mode, the example
above becomes:
import datetime
from django.utils.timezone import utc
now = datetime.datetime.utcnow().replace(tzinfo=utc)
You should determine whether or not you want timezone awareness in your site and then adjust your stored times accordingly. To convert an aware dt to naive you can use the pytz module and do this:
naive_dt = aware_dt.replace(tzinfo=None)
This works because all python datetimes have an optional timezone attribute, tzinfo, which can be used to store information on the dt's offset from UTC time.
Holá
The short answer is:
tz_info = your_timezone_aware_variable.tzinfo
diff = datetime.datetime.now(tz_info) - your_timezone_aware_variable:
You must add the timezone info to your now() time.
But you must add the same timezone of the variable, thats why I first read the tzinfo attribute.

SQL - Hours of operation

I'm having a hard time wrapping my head around what seems to be a somewhat simple issue. Let's say that I have a business whose hours are 12PM - 3AM daily. Each customer gets a bonus once per day based on their initial purchase for that day. So, let's say they spend twenty bucks on their first transaction that day -- they might get a twenty percent discount on that transaction, and that's it for the day.
I'm trying to figure out the most accurate way to check the last bonus that was given and make sure that the customer is eligible for one. I can't do a simple 24-hour check, obviously, because if a customer comes in at 11 PM Monday, for instance, and again at noon Tuesday, they will not get their second bonus.
We are using a VB6 frontend for our POS, with a SQL Server 2008 R2 database. Each time a bonus is applied, it is audited on the database side, so I can easily query the last time the bonus was applied.
EDIT: I should note that, for various reasons, the solution cannot include making any changes to the structure of the database.
I'm not sure on which side (VB or SQL) you want to apply the biz logic but in either case the process should be the same: You need to persist each customer's daily hours of operation with two attributes:
Time (the time of day that they open for business)
TimeSpan (number of hours of operation)
You then check if a transaction's time is between Time and Time + TimeSpan to calculate your business logic and the customer's bonus. Both calculations are fairly trivial in VB and SQL. You just need to make sure you persist the data logically and use it consistently.
I think your answer would be cleaner if you modified it to something like:
IF #LastBonus BETWEEN #store_open AND #store_close
BEGIN
SET #BonusDue = 0
END
ELSE
BEGIN
SET #BonusDue = 1
END
where you figure the store open and close dates based on a fixed times that are added to the date part of the last bonus. Something like
Set #openTime = '12:00'
Convert(date, #LastBonus) + #openTime
And then adding the timespan (as was suggested) to get the close time. It might be a little tricky because if it's after midnight, the open time would need to be added to the previous date instead, but you could probably work this out with a CASE statement. I'd try it out myself if my baby didn't have an ear infection. Hope that is useful to you.
How about:
IF (DATEPART(dayofyear, [lastBonusTime]) <> DATEPART(dayofyear, GetDate())) ...
Where lastBonusTime is the time of the last bonus transaction ?
You can look at the problem a bit differently. If a customer is here now (GETDATE()), has it been over 24 hours since their last purchase?
So, something like
SELECT *
FROM Customers c
INNER JOIN CustomerVisits cv
ON c.CustomerId=cv.CustomerId
INNER JOIN CustomerBonus cb
ON cv.VisitId=cb.VisitId
WHERE c.CustomerId=#CustomerId
AND LastVisitDt BETWEEN
(
DATEADD(hh,12,convert(DATE, LastVisitDt))
)
AND
(
DATEADD(hh,27,convert(DATE, LastVisitDt))
)
AND DATEADD(hh,24,LastVisitDT)<=GETDATE()
I would also consider the specifics of the data--the above is NOT TUNED FOR PERFORMANCE AT ALL. I just wanted to explain my thought process.
In the interest of separating your concerns, I would add a new table, like CUSTOMER_BONUS, with these columns:
BonusStart datetime
BonusEnd datetime
CustomerID int/uniqueidentifier/whatever
TransactionID int/whatever (points to what qualified for the bonus)
When you apply a bonus for a customer for a day, write a new record into this table for the period that it applies to. Presence of a record in this table indicates that the customer is not eligible for another bonus between BonusStart and BonusEnd. When you create a new sale, look in this table. If the record exists, no bonus, but if not, apply the bonus and create a new record here.
I came up with an answer that I'm content with but it's a little kludgy and I would be more than happy to accept a more elegant answer if one is provided. Also, I haven't thoroughly tested this since it's getting late in the day, but if there are flaws in my logic, I will happily revise or accept an answer with revisions.
Basically, I'm just going to determine that the day of the week in terms of a business day is whatever day started four hours ago. This means that all the way up through 3:59 AM, "today" will be considered the day before, which is correct for these hours of operation (I'm overshooting the 3 AM closing time to account for a site deciding to stay open a little later). I then compare this span of time to the most recent time a bonus was applied to that customer's account, using the same rules. If the two match, the bonus has been applied this business day. If they are different, it has not, and the customer is eligible.
DECLARE #CustID AS int
DECLARE #LastBonus AS date
DECLARE #BonusDue AS bit
SET #LastBonus = (SELECT TOP 1 [DateTime] FROM Audit WHERE CustomerID = #CustID AND TransactionType = 'BONUS' ORDER BY [DateTime] DESC)
IF (SELECT DATEADD(hh, -4, CURRENT_TIMESTAMP)) <>
(SELECT DATEADD(hh, -4, #LastBonus))
BEGIN
SET #BonusDue = 1
END
ELSE
BEGIN
SET #BonusDue = 0
END
If I throw this in a stored procedure, I can simply throw a customer ID at it and have it spit out a bit that will show me 1 if the customer is eligible, 0 otherwise. What I don't like about it is that if a customer's hours of operation end up getting much earlier, I'll be sunk (I guess at about 7:00 AM, when simply subtracting four hours will overlap into the previous business day, but subtracting less will not be enough to reach the previous business day). So it will work for the time being, but I'd love to see a better solution.

Resources