Convert UTC? timezone to EST in Oracle on select - sql

Our company's Oracle server is hosted in the east coast of the US and I believe follows a default timezone of EST. I have a stored procedure which logs messages but puts the current timestamp into a field declared as timestamp.
describe log_messages;
Name Null? Type
--------- -------- --------------
ENTRY_ID NOT NULL NUMBER
SEVERITY VARCHAR2(1)
DATE_TIME TIMESTAMP(6)
MESSAGE VARCHAR2(2048)
Usually, I do something like
insert into log_messages(severity,date_time,message)
values('I',current_timestamp,'some message');
If I do select * from log_messages, the time stamps look like this:
28-MAY-20 01.50.15.747963000 AM
However, the above time is actually 4 hours later than my current timezone.
select entry_id,severity,cast(date_time as timestamp with time zone) as date_time, date_time AT TIME ZONE 'EST' AS est,message
from log_messages
order by date_time desc;
In the above, it thinks the timestamp in the timestamp field is in EST time, and then when it converts the other, it subtracts 4 hours from it. Effectively, it is as though it writes the time in UTC, but then reads it in thinking it is EST, and subtracts 4 hours from it. How can I get the real time in my local EST time?

CURRENT_TIMESTAMP returns the current date and time in your session time zone, i.e. location or time zone settings of your Oracle Database server is not relevant.
You can interrogate your session time zone with select SESSIONTIMEZONE from dual; and change it with ALTER SESSION SET TIME_ZONE=...
However, as the data type is TIMESTAMP(6), the time zone information is lost as soon as the data is inserted.
When you run cast(date_time as timestamp with time zone) then Oracle takes your SESSIONTIMEZONE for conversion, actually Oracle runs cast(FROM_TZ(date_time, SESSIONTIMEZONE) as timestamp with time zone)
Unless you don't alter your session time zone, the two queries should return the same value..
See also How to handle Day Light Saving in Oracle database

Related

Difference between TIMESTAMP, TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE

I ran the same statements in two different databases: my Local DB and Oracle Live SQL.
CREATE TABLE test(
timestamp TIMESTAMP DEFAULT SYSDATE,
timestamp_tmz TIMESTAMP WITH TIME ZONE DEFAULT SYSDATE,
timestamp_local_tmz TIMESTAMP WITH LOCAL TIME ZONE DEFAULT SYSDATE
);
INSERT INTO test VALUES (DEFAULT, DEFAULT, DEFAULT);
SELECT * FROM test;
(all statements were executed at approximately the same time - 09:35 AM CET)
Results from my Local DB:
TIMESTAMP: 10-JAN-23 09.35.32.000000000 AM
TIMESTAMP WITH TIME ZONE: 10-JAN-23 09.35.32.000000000 AM EUROPE/BERLIN
TIMESTAMP WITH LOCAL TIME ZONE: 10-JAN-23 09.35.32.000000000 AM
Results from Oracle Live:
TIMESTAMP: 10-JAN-23 08.35.44.000000 AM
TIMESTAMP WITH TIME ZONE: 10-JAN-23 08.35.44.000000 AM US/PACIFIC
TIMESTAMP WITH LOCAL TIME ZONE: 10-JAN-23 08.35.44.000000 AM
After seeing the results, my questions are:
Why is Oracle Live's TIMESTAMP showing date in a different time zone (8.35 AM instead of 9.35 AM)?
Why does Oracle Live's TIMESTAMP WITH TIME ZONE return US/PACIFIC as time zone?
Is there any difference between TIMESTAMP and TIME STAMP WITH LOCAL TIME ZONE?
The different data types are described in the documentation.
The TIMESTAMP data type is an extension of the DATE data type. It stores year, month, day, hour, minute, and second values. It also stores fractional seconds, which are not stored by the DATE data type.
TIMESTAMP WITH TIME ZONE is a variant of TIMESTAMP that includes a time zone region name or time zone offset in its value.
TIMESTAMP WITH LOCAL TIME ZONE is another variant of TIMESTAMP. It differs from TIMESTAMP WITH TIME ZONE as follows: data stored in the database is normalized to the database time zone, and the time zone offset is not stored as part of the column data. When users retrieve the data, Oracle Database returns it in the users' local session time zone.
You are seeing a difference because you have different timezones, and you are defaulting the values to SYSDATE, which is the system DATE.
In your local database the system time zone (select dbtimezone from dual) seems to be based on CET, while the Live SQL database seems to be based on UTC, as Oracle recommends. As CET is an hour ahead of UTC/GMT, that explains the one-hour difference.
The TIMESTAMP value is just a simple cast, i.e. cast(SYSDATE as TIMESTAMP), so you get the same value you would if you queried SYSDATE directly, with zero fractional seconds added.
For the TIMESTAMP WITH TIME ZONE it has to store a time zone, and it has to get that from somewhere, and by default it uses your session time zone, not the database time zone. In your local DB that also seems to be CET, but Live SQL is defaulting the session time zone to US Pacific time - not unreasonable, given where Oracle is based. So now it's effectively doing from_tz(cast(SYSDATE as TIMESTAMP), SESSIONTIMEZONE) for that value, where for you SESSIONTIMEZONE is CET in one database and US/Pacific in the other.
For the TIMESTAMP WITH LOCAL TIME ZONE it is doing the same, but then normalising that back to the database time zone for storage (effectively cast(from_tz(cast(SYSDATE as TIMESTAMP), SESSIONTIMEZONE) at time zone DBTIMEZONE as TIMESTAMP) - not actually that internally, but gives you the idea), and converting back from the database time zone to your session time zone again when it is queried.
In both databases, if you alter session set time_zone = ... before inserting, and again to a different value before querying, then you'll see different results - the displayed time portion will stay the same for the first two columns, but the time zone will change for the WITH TIME ZONE, and the time will change for the WITH LOCAL TIME ZONE.
fiddle with different session time zones.
You can read more about all of this behaviour in the documentation I already linked to above.
If you use SYSTIMESTAMP instead of SYSDATE as the default for all of your columns then you will avoid the implicit conversion to your session time zone for the WITH TIME ZONE value, and that will always show the database time zone. The LOCAL column will still display in your session time zone, but they will all represent the same time. You will also still see the one-hour difference between the two databases, because they have different database time zones. You could consider defaulting the plain timestamp to sys_extract_utc(SYSTIMESTAMP), or defaulting them all (or at least the first two) to SYSTIMESTAMP at time zone 'UTC'.
fiddle with UTC-normalised values.

Postgres: meaning of `timestamp without time zone at time zone`

I am having some trouble understanding how to deal in Postgres with time zone semantic:
Consider this table
TABLE MyTable (
MyDate TIMESTAMP NOT NULL,
// other columns
);
And this query
SELECT *
FROM MyTable
WHERE // conditions
AND tstzrange(#{start} ::timestamp with time zone, #{end} ::timestamp with time zone] #> MyDate::timestamp without time zone at time zone 'CET'
I understand that Postgres only stores the epoch value i.e. no time zone info is ever stored, so I understand the need to specify timezone for start and end as they are formatted strings that Postgres needs to calculate the epoch for.
What I don't really understand is:
MyDate::timestamp without time zone at time zone 'CET'
Postgres knows the epoch for MyDate since it's their values are stored, why the need to "convert" to a time zone ?
What are we actually saying here and can this be simplified ?
Postgres never stores timezone information in timestamp or timestamptz. In the timestamptz case the timestamp being stored is rotated to a UTC value using either the timezone information in the presented timestamp or the value of the setting TimeZone and then stored. In the timestamp case that is not done. On output a timestamptz is rotated back from UTC to whatever the TimeZone setting is or what via at time zone <some_tz>. In the timestamp case the retrieved value is assumed to be the setting of TimeZone unless you override with at time zone <some_tz>. Best practices is that you use timestamptz.

How to get all data post midnight of different timezone?

I have a PostgreSQL table named testing with a column named creation_time as timestamp with time zone. The database timezone is UTC
Now I want to get all rows whose time is greater than 00:00 of the current day as per the timezone "America/New_York".
I know how to get all rows after local midnight:
SELECT * FROM testing
WHERE ( creation_time >= now()::date)
ORDER BY id DESC
But how to use this query with a different timezone?
Assuming "the current day" is also defined by NY time, not by the current timezone setting.
SELECT *
FROM testing
WHERE creation_time >= date_trunc('day', now() AT TIME ZONE 'America/New_York') AT TIME ZONE 'America/New_York'
ORDER BY id DESC;
Yes, AT TIME ZONE 'America/New_York' twice. No typo there.
now() AT TIME ZONE 'America/New_York') gets local NY time. date_trunc gets 00:00 of that day. The 2nd AT TIME ZONE 'America/New_York' converts the local time back to timestamptz, which we finally compare to.
If you want NY 00:00 of your local date, it's simpler:
WHERE creation_time >= CURRENT_DATE::timestamp AT TIME ZONE 'America/New_York'
Same time, but can be a different day!
CURRENT_DATE is the local date (date according to the time zone setting of the current session). Effectively the same as now()::date.
Further reading:
Ignoring time zones altogether in Rails and PostgreSQL

How to convert a column timezone from CET to CST in SQL query

Here I am trying to convert date column value from CET to CST.
I tried using NEW_TIME(SYSDATE ,'CET','CST') function, but it's giving an error saying unknown time zone. Issue here is CET is not recognised by oracle as valid timezone.
I tried using "at timezone" approach initially, but it's inserting the timezone name in the column value, which I don't want.
The three-character timezones don't adjust for daylight savings. To do that you need to use the fully specified timezone name. To illustrate, in Canada, there is central standard time, but the province of Saskatechewan does not use daylight savings, so if I want to convert right now to local time in summer where daylight savings is in play, and knowing the correct fully specified timezone names in the DB Timezone file (You can check the list installed in your DB by select * from V$TIMEZONE_NAMES;):
SELECT 'Central' locale
, extract(timezone_abbr from cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Central') tz_abbrv
, CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Central' as timestamp) local_time from dual
union all
SELECT 'Saskatchewan' locale
, extract(timezone_abbr from cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Saskatchewan') tz_abbrv
, CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Saskatchewan' as timestamp) local_time from dual;
LOCALE TZ_ABBRV LOCAL_TIME
Central CDT 03/08/2016 9:57:24.000000 AM
Saskatchewan CST 03/08/2016 8:57:24.000000 AM
Otherwise you would need to code when to switch between CST and DST in your calculations - bearing in mind that the rules for when daylight savings starts and ends has changed over time, and may change again in the future.
So respond to your comment about inserts, you need to first ensure that the column is defined to include the time zone (datatype "timestamp with timezone"), and change the CAST from "timestamp" to "timestamp with time zone" to make sure that the zone information is stored:
e.g.)
create table mbt (stz timestamp with time zone)
insert into mbt values (CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Indian/Maldives' as timestamp with time zone) )
insert into mbt values (CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Eastern' as timestamp with time zone) )
commit;
select * from mbt;
STZ
05/08/2016 8:06:49.000000 PM +05:00
05/08/2016 11:06:50.000000 AM -04:00
If you don't include the WITH TIME ZONE you should still get the changed value, but you won't be able to easily translate from local time to a different timezone as the values will be assumed to be in the server timezone:
drop table mbt;
create table mbt (stz timestamp);
insert into mbt values (CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Indian/Maldives' as timestamp) );
insert into mbt values (CAST(cast(add_months(sysdate,6) as timestamp) AT TIME ZONE 'Canada/Eastern' as timestamp ) );
commit;
select * from mbt;
STZ
-------------------------------
05-AUG-16 08.13.16.000000 PM
05-AUG-16 11.13.16.000000 AM
To remove timezone you can use cast(.... as Timesamp).
Check my example.
select cast( current_timestamp at time zone 'CST' as timestamp), cast( current_timestamp at time zone 'CST' as timestamp with time zone) from dual;

Accounting for DST in Postgres, when selecting scheduled items

I have a Postgres table of clock alarms (not really, but this is analogous, and easier to explain). Alarms are set by users with a 1 hour resolution, and users can be from many different timezones. The alarms are repeating daily. I want to reliably fetch the alarms that are supposed to go off at a particular hour of the day, and I am having problems with daylight saving time. How do I do this in the best way?
Example
Alfred and Lotta both live in Stockholm (+1 hour from UTC, but +2h
when it's DST). Sharon lives in Singapore (+8 hours from UTC, no
DST)
During winter, Alfred sets an alarm for 4 AM. The alarm should go off
at 4 AM local time, all year. During summer, Lotta sets an alarm
for 5 AM. Again, it should go off at 5 AM all year round.
Meanwhile, Sharon has set an alarm for 11 AM.
All of these can be stored in the database as 03:00 UTC.
If I query the database in the winter for alarms that should go off at
03:00 UTC, I want Alfred's and Sharon's alarms. Singapore is now +7h
from Sweden, so 11 AM in Singapore is 4 AM in Sweden. Lotta's alarm
should not go off for another hour.
Conversely, if I query the database in the summer for alarms that
should go off at 03:00 UTC, I want Lotta's and Sharon's alarms.
Singapore is +6h from Sweden now, so 11 AM in Singapore is 5 AM in
Sweden now. Sven's alarm went off an hour ago.
How do I store this, and query the database?
I can change the db schema if necessary. At the moment, we don't adjust for DST at all, and in fact just have an "hour" integer field (which seems dumb, a time field would be better).
It seems I need to store both a UTC time and timezone information, but I don't know how to best achieve this in Postgres. I've found that Postgres has some sort of concept of timezones, but no timezone field type as far as I can tell. Also, I guess I need to do some calculations in SQL to determine how to offset the UTC time in the select, based on the timezone data and the creation date. I'm not great with SQL…
I do want to solve this in Postgres, as there can be a lot of "alarms", and I want to avoid the performance issues that come with fetching all of them into Ruby and filter there. (Yes, this is a Rails app.)
Use timestamp with time zone (timestamptz) for calculations.
Times for alarms can be time [without time zone].
But you have to save the time zone explicitly for every row.
Never use time with time zone (timetz) It's a logically broken type, its use is discouraged by PostgreSQL. The manual:
The type time with time zone is defined by the SQL standard, but the
definition exhibits properties which lead to questionable usefulness.
In most cases, a combination of date, time, timestamp without timezone, and timestamp with time zone should provide a complete
range of date/time functionality required by any application.
Demo setup:
CREATE TABLE alarm(name text, t time, tz text);
INSERT INTO alarm VALUES
('Alfred', '04:00', 'Europe/Stockholm') -- Alfred sets an alarm for 4 AM.
, ('Lotta', '05:00', 'Europe/Stockholm') -- Lotta sets an alarm for 5 AM.
, ('Sharon', '11:00', 'Asia/Singapore'); -- Sharon has set an alarm for 11 AM.
It has to be time zone names (not abbreviations) to account for DST. Related:
Time zone names with identical properties yield different result when applied to timestamp
Get matching alarms for "today":
SELECT *
FROM alarm
WHERE (('2012-07-01'::date + t) AT TIME ZONE tz AT TIME ZONE 'UTC')::time
= '03:00'::time
('2012-7-1'::date + t) ... assemble timestamp [without time zone]
Could also just be now()::date + t for "today".
AT WITH TIME ZONE tz ... place timestamp at the saved time zone, resulting in timestamptz.
AT WITH TIME ZONE 'UTC' ... get according UTC timestamp
::time ... simplest way to extract the time component.
Here you can look up time zone names:
SELECT *
FROM pg_timezone_names
WHERE name ~~* '%sing%'
LIMIT 10;
db<>fiddle here - demonstrating summer / winter
Old sqlfiddle
You would do it by using a full time zone name, e.g. America/New_York rather than EDT/EST, and storing the hour in that time zone not UTC. You can then remain blissfully ignorant of the offset changes for daylight savings.
Something like the following should work:
-- CREATE TABLE time_test (
-- user_to_alert CHARACTER VARYING (30),
-- alarm_hour TIME,
-- user_timezone CHARACTER VARYING (30)
-- );
SELECT user_to_alert,
CASE
WHEN EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour) THEN TRUE
ELSE FALSE
END AS raise_alarm
FROM time_test;
Or:
SELECT user_to_alert
FROM time_test
WHERE EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour);
Given:
SET timezone = 'UTC';
CREATE TABLE tzdemo (
username text not null,
alarm_time_utc time not null,
alarm_tz_abbrev text not null,
alarm_tz text not null
);
INSERT INTO tzdemo (username, alarm_time_utc, alarm_tz_abbrev, alarm_tz) VALUES
('Alfred', TIME '04:00' AT TIME ZONE '+01:00', 'CET', 'Europe/Stockholm'),
('Lotta', TIME '05:00' AT TIME ZONE '+02:00', 'CEST', 'Europe/Stockholm'),
('Sharon', TIME '11:00' AT TIME ZONE '+08:00', 'SGT', 'Singapore');
Try:
SELECT username
FROM tzdemo
WHERE alarm_time_utc AT TIME ZONE alarm_tz_abbrev = TIME '03:00' AT TIME ZONE alarm_tz;
Result:
username
----------
Alfred
Sharon
(2 rows)
Principle:
Store the timezone offset the alarm was created in including whether it was or was not DST at the time
Also store the clock time converted to UTC
When querying, use the fact that full timezone names follow the current UTC rules for times to produce a time that's in the current time zone for the region. Compare to the stored timestamp in what the time zone was when that alarm was created.
This also allows you to cope with cases where the user changes location, and therefore changes timezone.
This approach can be extended by date-qualifying the timestamps when you want to do predictive querying, like "at what local time will alarm sound in location".
I'm not completely confident in this solution and would recommend careful testing.