【问题标题】:Oracle date compare broken because of DST由于 DST,Oracle 日期比较损坏
【发布时间】:2014-04-13 20:51:24
【问题描述】:

我们一直在调试通过 Hibernate 从运行 Java 的应用服务器执行的 SQL 查询的问题。错误:

[3/10/14 10:52:07:143 EDT] 0000a984 JDBCException W org.hibernate.util.JDBCExceptionReporter logExceptions SQL Error: 1878, SQLState: 22008
[3/10/14 10:52:07:144 EDT] 0000a984 JDBCException E org.hibernate.util.JDBCExceptionReporter logExceptions ORA-01878: specified field not found in datetime or interval

我们已经能够将范围缩小到下面的简单 SQL。

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

当我们在同一个数据库中运行它时,我们得到了错误:

ORA-01878: specified field not found in datetime or interval
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.

MY_TIMESTAMP 列定义为TIMESTAMP(6)

FWIW,如果我们将上面 SQL 中的比较从 >= 更改为 <=,则查询有效。

我们假设这与时间变化有关(我们在美国/纽约),但我们在调试时遇到了问题。

此外,我们在通过 MyBatis 运行的类似查询中看到了这个问题,错误看起来像:

### Error querying database.  Cause: java.sql.SQLException: ORA-01878: specified field not found in datetime or interval

### The error may involve defaultParameterMap
### The error occurred while setting parameters
### Cause: java.sql.SQLException: ORA-01878: specified field not found in datetime or interval

更新:Windows 上的一位队友通过取消选中“自动调整夏令时时钟”更改了她的 Windows 日期和时间设置,然后打开了一个新的 SQLDeveloper 实例。第二个实例能够毫无问题地运行查询,但第一个实例(使用旧的 DST 设置)仍然失败。

【问题讨论】:

    标签: oracle timezone timestamp dst


    【解决方案1】:

    为避免此错误,请考虑将 where 子句中的表达式显式转换为时间戳类型(没有时区的时间戳),方式如下:

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

    或者,您可以将会话时区显式设置为,例如“-05:00” - 纽约标准(冬季)时间,
    使用ALTER SESSION time_zone = '-05:00',或通过在所有客户端环境中设置 ORA_SDTZ 环境变量,
    详情见此链接:http://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG263

    但也要看真正在表的timestamp列中存储的是什么,例如时间戳2014-07-01 15:00:00实际上代表什么,是“冬令时”还是“夏令时”?


    CURRENT_TIMESTAMP 函数返回数据类型为 TIMESTAMP WITH TIME ZONE
    看到这个链接:http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions037.htm

    在比较时间戳和日期时,Oracle 隐式地将数据转换为更精确的数据类型使用会话时区!
    看到这个链接 --> http://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG251

    在我们的特定情况下,Oracle 将timestamp 列转换为timestamp with time zone 类型。

    Oracle 根据客户端环境确定会话时区。
    您可以使用此查询确定当前会话时区:

    select sessiontimezone from dual;
    
    例如,在我的PC(WIN 7)上,选中“”自动调整夏令时的时钟“时,此查询返回(在SQLDeveloper下):
    SESSIONTIMEZONE                                                           
    ---------------
    Europe/Belgrade 
    


    当我在 Windows 中取消选中此选项然后重新启动 SQLDeveloper 时,它会给出:

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

    前一个会话时区是一个带有区域名称的时区,Oracle 在日期计算中使用该区域的夏令时规则:

    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       
    


    后一个时区使用固定偏移量“+01:00”(始终为“冬令时”),Oracle 不会对其应用任何 DST 规则,它只是添加固定偏移量。

    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  
    

    请注意,出于好奇,Y 上面的结果代表两个不同的时间!!!
    014-05-29 01:30:00 EUROPE/BELGRADE 不等于:2014-05-29 01:30:00 +01:00

    但实际上:
    014-05-29 01:30:00 EUROPE/BELGRADE 等于:2014-05-29 01:30:00 +02:00

    以上只是为了让您了解简单的“取消选中框”可能会影响您的查询,以及当用户抱怨“此查询在 1 月份运行良好,但在 7 月份给出了错误的结果”时,应该在哪里挖掘。


    仍然是关于 ORA-01878 的主题 - 假设我的会话是 EUROPE/Warsaw 并且我的表包含这个时间戳(没有时区)

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

    请注意,在我所在的地区,2014 年的 DST 更改发生在 3 月 30 日凌晨 2:00
    它只是意味着在 3 月 30 日,晚上 2:00,我必须醒来并将手表从 2:00 拨到 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 知道,根据 DST 规则,此时间戳在我的地区无效,因为 3 月 30 日 2:30 没有时间 - 2:00 时钟移至 3 :00,没有时间 2:30。因此 Oracle 抛出错误 ORA-01878。

    但是这个查询工作得很好:

    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 
    

    这是导致此错误的原因 - 您的表包含 2014-03-09 2:30 左右的时间戳(对于纽约,DST 转变发生在 3 月 9 日和 11 月 2 日),而 Oracle 不知道如何转换它们从时间戳(不带 TZ)到带 TZ 的时间戳。


    最后一个问题 - 为什么>= 的查询不起作用,而<= 的查询却可以正常工作?

    它们工作/不工作,因为 SQLDeveloper 只返回前 50 行(可能是 100 ?这取决于设置)。该查询不会读取整个表,它会在获取前 50(100) 行时停止。
    将“工作”查询更改为,例如:

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

    这会强制查询读取表中的所有行,并且会出现错误,我 100% 确定。

    【讨论】:

      【解决方案2】:

      感谢 kordirko 非常详细的文章。我认为在未来,我们将寻找不同的方法来比较不易出错的日期。与此同时,我们能够找出问题以及临时和长期的解决方案。

      首先,关于这个问题的更多细节。事实证明,存储在数据库中 TIMESTAMP 字段中的值不正确。我们通过使用转储函数并检查字节看到了这一点。如果您查看下面输出中的第 5 个字节,您会看到小时(这实际上是小时 + 1,所以 5 实际上是凌晨 4 点)。对于 3AM 和 4AM 之间的值,您会注意到第 5 个字节是 3,表示 2AM。美国东部标准时间 2014 年 3 月 9 日凌晨 2 点不存在 - 根据 DST 规则和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
      

      经过大量研究和讨论,我们发现我们的 Oracle JDBC 驱动程序 (11.2.0.2) 版本可能一直在插入错误值。 Oracle 的information page on 11.2.0.3 引用了一个看起来像是我们的问题的错误:“9785135 DST conversion not correct using jdbc 11g timestamtz”。我们编写了一个快速测试类,它使用 11.2.0.2 和 11.2.0.3 驱动程序插入从 2014 年 3 月 9 日凌晨 1:50 到凌晨 4:00 的值。这是插入到数据库中的内容的 sn-p:

      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
      

      您会注意到,凌晨 3:00 的第二行时间戳的第 5 个字节不正确 (3)。这是使用 11.2.0.2 版本插入的。与 11.2.0.3 版本插入的相同值可以在第四行找到正确的第 5 个字节 (4)。

      这里的长期修复是更新我们的 JDBC 驱动程序。这里的短期修复是找到具有错误值的行并从 SQL Plus 对其运行更新语句以再次设置时间(使用相同的值,但 SQL Plus 会正确转换它们)。

      【讨论】:

      • 你是如何计算出什么值(数字)代表什么字节的?例如我无法理解30-JUL-15 14.59.21.173266000 +00:00Typ=188 Len=20: 223,7,7,30,14,59,21,0,80,212,83,10,0,0,5,0,0,0,0,0 吗? 223 不应该是天数 - 因此它应该是 211(一年中的实际日期?)注意:这些是 systimestampDUMP(systimestamp)
      • 好问题。不幸的是,我找不到指向 Oracle 文档的良好链接,但我找到了 a great Stack Overflow answer,它涵盖了您所要求的内容。
      • 哈哈,这是一个很好的答案,这是我自己找不到任何文档后发布的问题。
      • 非常感谢所有为回答这个问题做出贡献的人。今天早上,由于巴西开始实行夏令时,当我和我的同事遇到同样的问题时,它节省了很多时间。
      猜你喜欢
      • 1970-01-01
      • 2018-08-31
      • 1970-01-01
      • 2021-11-05
      • 1970-01-01
      • 1970-01-01
      • 2012-04-28
      • 2012-02-01
      • 2020-10-06
      相关资源
      最近更新 更多