Oracle date compare broken because of DST

后端 未结 2 1325
隐瞒了意图╮
隐瞒了意图╮ 2020-12-17 09:53

We\'ve been debugging an issue with a SQL query executed from an app server running Java via Hibernate. The error:

[3/10/14 10:52:07:143 EDT] 0000a984 JDBCEx         


        
相关标签:
2条回答
  • 2020-12-17 10:06

    To avoid this error, consider using an explicit cast of the expression in the where clause to a timestamp type (timestamp without timezone), in this way:

    select * 
    from MY_TABLE T
    where T.MY_TIMESTAMP >= cast(CURRENT_TIMESTAMP - interval '1' hour As timestamp );
    

    Alternatively you can explicitely set the session time zone to, for example '-05:00' - for New York standard (winter) time,
    using ALTER SESSION time_zone = '-05:00', or by setting ORA_SDTZ environment variable in all client's environments,
    see this link for details: http://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG263

    But it also depends on what really is stored in the timestamp column in the table, for example what a timestamp 2014-07-01 15:00:00 represents in fact, is it a "winter time" or a "summer time" ?


    CURRENT_TIMESTAMP function returns a value of datatype TIMESTAMP WITH TIME ZONE
    see this link: http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions037.htm

    While comparing timestamps and dates, Oracle implicitely converts the data to the more precise data type using the session time zone !
    See this link --> http://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG251

    In our particular case, Oracle casts timestamp column to the timestamp with time zone type.

    Oracle determines a session timezone from the client environment.
    You can determine current session timezone using this query:

    select sessiontimezone from dual;
    

    For example on my PC (Win 7), when the option ""Automatically adjust clock for Daylight Saving Time" is checked, this query returns (under SQLDeveloper):

    SESSIONTIMEZONE                                                           
    ---------------
    Europe/Belgrade 
    


    When i uncheck this option in Windows and then restart SQLDeveloper, it gives:

    SESSIONTIMEZONE                                                           
    ---------------
    +01:00     
    

    The former session timezone is a timezone with a region name, for which Oracle uses the Daylight Saving Time rules for this region in date calculations:

    alter session set time_zone = 'Europe/Belgrade';
    select cast( timestamp '2014-01-29 01:30:00' as timestamp with time zone ) As x,
           cast( timestamp '2014-05-29 01:30:00' as timestamp with time zone ) As y
    from dual;
    
    session SET altered.
    X                            Y                          
    ---------------------------- ----------------------------
    2014-01-29 01:30:00 EUROPE/B 2014-05-29 01:30:00 EUROPE/B 
    ELGRADE                      ELGRADE       
    


    The latter timezone uses a fixed offset "+01:00" (always the "Winter time"), and Oracle does not apply any DST rules for it, it simply adds the fixed offset.

    alter session set time_zone = '+01:00';
    select cast( timestamp '2014-01-29 01:30:00' as timestamp with time zone ) As x,
           cast( timestamp '2014-05-29 01:30:00' as timestamp with time zone ) As y
    from dual;
    
    session SET altered.
    X                            Y                          
    ---------------------------- ----------------------------
    2014-01-29 01:30:00 +01:00   2014-05-29 01:30:00 +01:00  
    

    Please note, for curiosity's sake, that Y results in the above represent two different times !!!
    014-05-29 01:30:00 EUROPE/BELGRADE is not the same as: 2014-05-29 01:30:00 +01:00

    but actually this:
    014-05-29 01:30:00 EUROPE/BELGRADE is equal to: 2014-05-29 01:30:00 +02:00

    The above is only to make you aware of how simple "box un-checking" could affect your queries, and where to dig for a reason when users complain "this query worked fine in January, but gave wrong results in July".


    And still on the topic of ORA-01878 - let say my session is EUROPE/Warsaw and my table containts this timestamp (without time zone)

    'TIMESTAMP'2014-03-30 2:30:00'
    

    Note that in my region the DST change, in 2014 year, occurs on 30 of march at 2:00 a.m.
    It simply means that on march 30, at 2:00 at night, I must wake up and shift my watch forward from 2:00 to 3:00 ;)

    alter session set time_zone = 'Europe/Warsaw';
    select cast( TIMESTAMP'2014-03-30 2:30:00' as timestamp with time zone ) As x
    from dual;
    
    SQL Error: ORA-01878: podane pole nie zostało znalezione w dacie-godzinie ani w interwale
    01878. 00000 -  "specified field not found in datetime or interval"
    *Cause:    The specified field was not found in the datetime or interval.
    *Action:   Make sure that the specified field is in the datetime or interval.
    

    Oracle knows, that this timestamp is not valid in my region according to DST rules, because there is no time 2:30 on 30 of march - at 2:00 the clock is moved to 3:00, and there is no time 2:30. Therefore Oracle throws the error ORA-01878.

    However this query works perfectly fine:

    alter session set time_zone = '+01:00';
    select cast( TIMESTAMP'2014-03-30 2:30:00' as timestamp with time zone ) As x
    from dual;
    
    session SET altered.
    X                          
    ----------------------------
    2014-03-30 02:30:00 +01:00 
    

    And this is a reason of this error - your table contains timestamps like 2014-03-09 2:30 or so (for New York, where DST shifts occur on 9 of March and 2 of November), and Oracle doesn't know how to convert them from timestamp (without TZ) to timestamp with TZ.


    The last question - why the query with >= doesn't work, but the query with <= works fine ?

    They work/don'n work, because SQLDeveloper returns only first 50 rows (maybe 100 ? It depends on settings). The query doesn't read the whole table, it stops when first 50(100) rows are fetched.
    Change the "working" query to, for example:

    select sum( EXTRACT(HOUR from MY_TIMESTAMP) ) from MY_TABLE 
    where MY_TIMESTAMP <= (CURRENT_TIMESTAMP - interval '1' hour );
    

    This force the query to read all rows in the table, and the error will appear, I'am 100% sure.

    0 讨论(0)
  • 2020-12-17 10:10

    Thanks to kordirko for the extremely detailed write up. I think in the future, we will be looking at different ways to compare dates that aren't as prone to error. In the meantime, we were able to figure out the problem and both a temporary and long-term solution.

    First, more details on the issue. It turns out that the values being stored in the TIMESTAMP field in the database were incorrect. We saw this by using the dump function and examining the bytes. If you look at the 5th byte in the output below, you'll see the hour (this is actually the hour + 1 so 5 is actually 4AM). For the values between 3AM and 4AM, you'll notice that the 5th byte is 3 which represents 2AM. 2 AM March 9, 2014 in EST doesn't exist - this is an incorrect time according to DST rules and Oracle's rules.

    09-MAR-14 03.06.21.522000000 AM         Typ=180 Len=11: 120,114,3,9,3,7,22,31,29,22,128
    09-MAR-14 03.32.37.869000000 AM         Typ=180 Len=11: 120,114,3,9,3,33,38,51,203,227,64
    09-MAR-14 03.36.49.804000000 AM         Typ=180 Len=11: 120,114,3,9,3,37,50,47,236,17,0
    09-MAR-14 03.43.47.328000000 AM         Typ=180 Len=11: 120,114,3,9,3,44,48,19,140,226,0
    09-MAR-14 03.47.55.255000000 AM         Typ=180 Len=11: 120,114,3,9,3,48,56,15,50,253,192
    09-MAR-14 03.55.45.129000000 AM         Typ=180 Len=11: 120,114,3,9,3,56,46,7,176,98,64
    09-MAR-14 04.05.03.325000000 AM         Typ=180 Len=11: 120,114,3,9,5,6,4,19,95,27,64
    09-MAR-14 04.28.41.267000000 AM         Typ=180 Len=11: 120,114,3,9,5,29,42,15,234,24,192
    09-MAR-14 04.35.16.072000000 AM         Typ=180 Len=11: 120,114,3,9,5,36,17,4,74,162,0
    09-MAR-14 04.41.07.260000000 AM         Typ=180 Len=11: 120,114,3,9,5,42,8,15,127,73,0
    09-MAR-14 04.46.31.047000000 AM         Typ=180 Len=11: 120,114,3,9,5,47,32,2,205,41,192
    09-MAR-14 04.53.33.471000000 AM         Typ=180 Len=11: 120,114,3,9,5,54,34,28,18,227,192
    

    After much research and discussion, we zeroed in on the fact that our version of the Oracle JDBC driver (11.2.0.2) might've been inserting the bad values. Oracle's information page on 11.2.0.3 references a bug that looks like it's our issue: "9785135 DST conversion not correct using jdbc 11g timestamtz". We wrote a quick test class that inserts values from March 9, 2014 1:50 AM to 4:00 AM using both the 11.2.0.2 and 11.2.0.3 driver. Here's a snippet of what was inserted into the db:

    DRIVER_V         JAVA_DATE_AS_STRING              ORACLE_TIMESTAMP                        DUMP(ORACLE_TIMESTAMP)
    11.2.0.2.0/11/2  Sun Mar 09 01:50:00 EST 2014     09-MAR-14 01.50.00.000000000 AM         Typ=180 Len=7: 120,114,3,9,2,51,1
    11.2.0.2.0/11/2  Sun Mar 09 03:00:00 EDT 2014     09-MAR-14 03.00.00.000000000 AM         Typ=180 Len=7: 120,114,3,9,3,1,1 --Invalid timestamp
    11.2.0.3.0/11/2  Sun Mar 09 01:50:00 EST 2014     09-MAR-14 01.50.00.000000000 AM         Typ=180 Len=7: 120,114,3,9,2,51,1
    11.2.0.3.0/11/2  Sun Mar 09 03:00:00 EDT 2014     09-MAR-14 03.00.00.000000000 AM         Typ=180 Len=7: 120,114,3,9,4,1,1 --Correct timestamp
    

    You'll notice that the 5th byte of the timestamp on the second row for 3:00 AM is incorrect (3). This was inserted using the 11.2.0.2 version. The same value inserted with the 11.2.0.3 version can be found on the fourth line with the correct 5th byte (4).

    The long term fix here is to update our JDBC driver. The short term fix here was to find the rows that have the bad values and run an update statement on them from SQL Plus to set the time again (using the same value but SQL Plus will convert them correctly).

    0 讨论(0)
提交回复
热议问题