web-dev-qa-db-ja.com

DSTのためにOracleの日付比較が壊れている

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インスタンスを開きました。 2番目のインスタンスは問題なくクエリを実行できますが、最初のインスタンスは(古いDST設定で)引き続き失敗します。

22
Chris Williams

非常に詳細な記事を書いてくれたkordirkoに感謝します。将来的には、エラーが発生しにくい日付を比較するさまざまな方法を検討する予定です。その間、問題と一時的および長期的な解決策の両方を理解することができました。

最初に、問題の詳細。データベースのTIMESTAMPフィールドに格納されている値が正しくないことがわかりました。これを確認するには、ダンプ機能を使用してバイトを調べます。以下の出力の5番目のバイトを見ると、時間がわかります(これは実際には時間+ 1なので、5は実際には4AMです)。 3AMと4AMの間の値の場合、5番目のバイトは2AMを表す3であることがわかります。 2014年3月9日午前2時(EST)は存在しません。これは、DSTルールおよび Oracleのルール によると、正しくない時間です。

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の 11.2.0.3の情報ページ は、「9785135 DST変換がjdbc 11gタイムスタンプを使用すると正しくない」という問題のように見えるバグを参照しています。 11.2.0.2と11.2.0.3の両方のドライバーを使用して、2014年3月9日1:50 AMから4:00 AMまでの値を挿入するクイックテストクラスを作成しました。以下は、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

午前2時の2行目のタイムスタンプの5番目のバイトが正しくないことに気づくでしょう(3)。これは、11.2.0.2バージョンを使用して挿入されました。 11.2.0.3バージョンで挿入された同じ値は、正しい5番目のバイト(4)の4行目にあります。

ここでの長期的な修正は、JDBCドライバーを更新することです。ここでの短期的な修正は、不正な値を持つ行を見つけてSQL Plusから更新ステートメントを実行し、時刻を再度設定することでした(同じ値を使用しますが、SQL Plusはそれらを正しく変換します)。

9
Chris Williams

このエラーを回避するには、次のように、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#NLSPG26

ただし、テーブルのタイムスタンプ列に実際に保存されているreallyにも依存します。たとえば、タイムスタンプ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の結果は2つの異なる時間を表すことに注意してください。
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年の夏時間の変更は3月30日の午前2時に発生することに注意してください。
それは単に、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.

オラクルは、このタイムスタンプがDSTルールに従って私の領域では無効であることを認識しています。これは、3月30日の2:30の時間がないためです。 :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のようなタイムスタンプが含まれ(ニューヨークの場合、3月9日と11月2日にDSTシフトが発生します)、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%確実です。

17
krokodilko