由于DST,Oracle日期比较已损坏

时间:2014-03-10 16:14:24

标签: oracle timezone timestamp dst

我们一直在调试从通过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中的比较从>=更改为<=,则查询有效。

我们假设这与时间变化有关(我们在America / New_York),但我们在尝试通过调试找出去哪里时遇到了问题。

此外,我们已经看到这个问题与通过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设置)仍然失败。

2 个答案:

答案 0 :(得分:16)

要避免此错误,请考虑使用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

但它还取决于真正存储在表格中的时间戳列中,例如时间戳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会使用会话时区隐含地将数据转换为更精确的数据类型
看到这个链接 - &gt; 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点。
它只是意味着在3月30日晚上2点,我必须醒来并将手表从2点移到3点;)

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%肯定。

答案 1 :(得分:8)

感谢kordirko非常详细的写作。我想在未来,我们将研究不同的方法来比较不容易出错的日期。与此同时,我们能够找出问题以及临时和长期解决方案。

首先,有关该问题的更多细节。事实证明,存储在数据库的TIMESTAMP字段中的值不正确。我们通过使用dump函数和检查字节来看到这一点。如果你看下面输出中的第5个字节,你会看到小时(这实际上是小时+ 1所以5实际上是4AM)。对于3AM和4AM之间的值,您会注意到第5个字节是3,代表2AM。 2014年3月9日凌晨2点在美国东部时间不存在 - 根据夏令时规则和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使用jdbc 11g timestamtz进行DST转换不正确”。我们编写了一个快速测试类,使用11.2.0.2和11.2.0.3驱动程序插入2014年3月9日上午1:50到凌晨4:00的值。这是插入数据库的内容片段:

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将正确转换它们)。