web-dev-qa-db-fra.com

Comparaison de date Oracle interrompue en raison de l'heure d'été

Nous avons débogué un problème avec une requête SQL exécutée à partir d'un serveur d'applications exécutant Java via Hibernate. L'erreur:

[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

Nous avons été en mesure de réduire cela au simple SQL ci-dessous.

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

Lorsque nous l'exécutons dans la même base de données, nous obtenons l'erreur:

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.

La colonne MY_TIMESTAMP Est définie comme TIMESTAMP(6).

FWIW, si nous changeons la comparaison dans le SQL ci-dessus de >= En <=, La requête fonctionne.

Nous supposons que cela a quelque chose à voir avec le changement d'heure (nous sommes en Amérique/New_York) mais nous avons du mal à essayer de savoir où aller à partir d'ici avec notre débogage.

En outre, nous avons vu ce problème avec une requête similaire qui s'exécute via MyBatis et l'erreur ressemble à:

### 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

MISE À JOUR: Une coéquipière sous Windows a modifié ses paramètres de date et d'heure Windows en décochant "Ajuster automatiquement l'horloge pour l'heure d'été", puis a ouvert une nouvelle instance SQLDeveloper. La deuxième instance est capable d'exécuter la requête sans aucun problème, mais la première (avec l'ancien paramètre DST) échoue toujours.

22
Chris Williams

Merci à kordirko pour la rédaction extrêmement détaillée. Je pense qu'à l'avenir, nous examinerons différentes façons de comparer les dates qui ne sont pas aussi sujettes à l'erreur. Entre-temps, nous avons pu trouver le problème et une solution à la fois temporaire et à long terme.

Tout d'abord, plus de détails sur la question. Il s'avère que les valeurs stockées dans le champ TIMESTAMP de la base de données étaient incorrectes. Nous l'avons vu en utilisant la fonction de vidage et en examinant les octets. Si vous regardez le 5ème octet dans la sortie ci-dessous, vous verrez l'heure (c'est en fait l'heure + 1 donc 5 est en fait 4 heures du matin). Pour les valeurs comprises entre 3 h et 4 h, vous remarquerez que le 5e octet est 3, ce qui représente 2 h. 2 AM le 9 mars 2014 dans EST n'existe pas - il s'agit d'une heure incorrecte selon les règles DST et règles 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

Après de nombreuses recherches et discussions, nous nous sommes concentrés sur le fait que notre version du pilote Oracle JDBC (11.2.0.2) aurait pu insérer les mauvaises valeurs. Oracle's page d'informations sur 11.2.0. fait référence à un bug qui ressemble à notre problème: "La conversion DST 9785135 n'est pas correcte avec jdbc 11g timestamtz". Nous avons écrit une classe de test rapide qui insère des valeurs du 9 mars 2014 de 1h50 à 4h00 en utilisant les pilotes 11.2.0.2 et 11.2.0.3. Voici un extrait de ce qui a été inséré dans la base de données:

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

Vous remarquerez que le 5ème octet de l'horodatage sur la deuxième ligne pour 3h00 est incorrect (3). Cela a été inséré en utilisant la version 11.2.0.2. La même valeur insérée avec la version 11.2.0.3 peut être trouvée sur la quatrième ligne avec le 5ème octet correct (4).

La solution à long terme consiste à mettre à jour notre pilote JDBC. Le correctif à court terme consistait à trouver les lignes contenant les mauvaises valeurs et à exécuter une instruction de mise à jour à partir de SQL Plus pour définir à nouveau l'heure (en utilisant la même valeur, mais SQL Plus les convertira correctement).

9
Chris Williams

Pour éviter cette erreur, envisagez d'utiliser une conversion explicite de l'expression dans la clause where en un type d'horodatage (horodatage sans fuseau horaire), de cette manière:

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

Vous pouvez également définir explicitement le fuseau horaire de la session, par exemple '-05: 00' - pour l'heure standard de New York (hiver),
en utilisant ALTER SESSION time_zone = '-05:00', ou en définissant la variable d'environnement ORA_SDTZ dans tous les environnements du client,
voir ce lien pour plus de détails: http://docs.Oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG26

Mais cela dépend aussi de ce que vraiment est stocké dans la colonne d'horodatage du tableau, par exemple ce qu'est un horodatage 2014-07-01 15:00:00 représente en fait, est-ce une "heure d'hiver" ou une "heure d'été"?


CURRENT_TIMESTAMP la fonction renvoie une valeur de type de données TIMESTAMP WITH TIME ZONE
voir ce lien: http://docs.Oracle.com/cd/B19306_01/server.102/b14200/functions037.htm

Lors de la comparaison des horodatages et des dates, Oracle convertit implicitement les données en un type de données plus précis en utilisant le fuseau horaire de la session!
Voir ce lien -> http://docs.Oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG251

Dans notre cas particulier, Oracle convertit la colonne timestamp en timestamp with time zone type.

Oracle détermine un fuseau horaire de session à partir de l'environnement client.
Vous pouvez déterminer le fuseau horaire de la session actuelle à l'aide de cette requête:

select sessiontimezone from dual;

Par exemple sur mon PC (Win 7), lorsque l'option "" Ajuster automatiquement l'horloge pour l'heure d'été "est cochée, cette requête renvoie (sous SQLDeveloper):

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


Lorsque je décoche cette option dans Windows puis redémarre SQLDeveloper, cela donne:

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

L'ancien fuseau horaire de la session est un fuseau horaire avec un nom de région, pour lequel Oracle utilise les règles d'heure d'été pour cette région dans les calculs de date:

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       


Ce dernier fuseau horaire utilise un décalage fixe "+01: 00" (toujours l '"heure d'hiver"), et Oracle n'applique aucune règle d'heure d'été pour cela, il ajoute simplement le décalage fixe.

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  

Veuillez noter, par curiosité, que Y les résultats ci-dessus représentent deux moments différents !!!
014-05-29 01:30:00 EUROPE/BELGRADE n'est pas identique à: 2014-05-29 01:30:00 +01:00

mais en fait ceci:
014-05-29 01:30:00 EUROPE/BELGRADE est égal à: 2014-05-29 01:30:00 +02:00

Ce qui précède est uniquement destiné à vous faire comprendre à quel point la simple "décocher la case" peut affecter vos requêtes, et où creuser pour une raison lorsque les utilisateurs se plaignent " Juillet".


Et toujours sur le sujet de ORA-01878 - disons que ma session est EUROPE/Warsaw et ma table contient cet horodatage (sans fuseau horaire)

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

Notez que dans ma région, le changement d'heure d'été, en 2014 année, se produit le 30 mars à 2h00 du matin.
Cela signifie simplement que le 30 mars, à 2 heures du soir, je dois me réveiller et avancer ma montre de 2 heures à 3 heures;)

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 sait que cet horodatage n'est pas valide dans ma région selon les règles de l'heure d'été, car il n'y a pas d'heure 2h30 le 30 mars - à 2 heures : 00, l'horloge est déplacée à 3h00 et il n'y a pas d'heure à 2h30. Par conséquent, Oracle renvoie l'erreur ORA-01878.

Cependant, cette requête fonctionne parfaitement:

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 

Et c'est la raison de cette erreur - votre table contient des horodatages comme 2014-03-09 2:30 ou plus (pour New York, où les décalages d'heure d'été se produisent le 9 mars et le 2 novembre), et Oracle ne sait pas comment les convertir d'horodatage (sans TZ) en horodatage avec TZ.


La dernière question - pourquoi la requête avec >= ne fonctionne pas, mais la requête avec <= fonctionne bien?

Ils fonctionnent/ne fonctionnent pas, car SQLDeveloper ne renvoie que les 50 premières lignes (peut-être 100? Cela dépend des paramètres). La requête ne lit pas la table entière, elle s'arrête lorsque les 50 (100) premières lignes sont récupérées.
Remplacez la requête "de travail" par, par exemple:

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

Cela force la requête à lire toutes les lignes du tableau, et l'erreur apparaîtra, je suis sûr à 100%.

17
krokodilko