Campos de fecha y hora de MySQL y horario de verano: ¿cómo puedo hacer referencia a la hora "extra"?


88

Estoy usando la zona horaria de América / Nueva York. En el otoño "retrocedemos" una hora - efectivamente "ganando" una hora a las 2 am. En el punto de transición sucede lo siguiente:

son las 01:59:00 -04: 00 y
luego 1 minuto después se convierte en:
01:00:00 -05: 00

Entonces, si simplemente dice "1:30 am", es ambiguo si se está refiriendo o no a la primera vez que llega la 1:30 o la segunda. Estoy tratando de guardar los datos de programación en una base de datos MySQL y no puedo determinar cómo guardar los tiempos correctamente.

Aquí está el problema:
"2009-11-01 00:30:00" se almacena internamente como 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" se almacena internamente como 2009-11-01 01:30:00 -05: 00

Esto está bien y es bastante esperado. Pero, ¿cómo puedo guardar nada para 01:30:00 -04: 00 ? La documentación no muestra ningún soporte para especificar el desplazamiento y, en consecuencia, cuando he intentado especificar el desplazamiento, se ha ignorado debidamente.

Las únicas soluciones en las que he pensado implican configurar el servidor en una zona horaria que no use el horario de verano y hacer las transformaciones necesarias en mis scripts (estoy usando PHP para esto). Pero eso no parece que deba ser necesario.

Muchas gracias por las sugerencias.


No sé lo suficiente sobre MySQL o PHP para formar una respuesta coherente, pero apuesto a que tiene algo que ver con la conversión hacia y desde UTC.
Mark Ransom

2
Internamente, todos están almacenados como UTC, ¿no?
Eli

4
Encontré web.ivy.net/~carton/rant/MySQL-timezones.txt una lectura interesante sobre el tema.
micahwittman

Buen enlace, micahwittman, muy útil.
Aaron

grandes preguntas. un problema común.
Vardumper

Respuestas:


47

Los tipos de fecha de MySQL están, francamente, rotos y no pueden almacenar todas las horas correctamente a menos que su sistema esté configurado en una zona horaria de desplazamiento constante, como UTC o GMT-5. (Estoy usando MySQL 5.0.45)

Esto se debe a que no puede almacenar ningún tiempo durante la hora antes de que finalice el horario de verano . No importa cómo ingrese las fechas, cada función de fecha tratará estos tiempos como si fueran durante la hora posterior al cambio.

La zona horaria de mi sistema es America/New_York. Intentemos almacenar 1257051600 (domingo, 01 de noviembre de 2009, 06:00:00 +0100).

Aquí está usando la sintaxis patentada INTERVAL:

SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3599 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3600 SECOND); # 1257055200

SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 1 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 0 SECOND); # 1257055200

Incluso FROM_UNIXTIME()no devolverá la hora exacta.

SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051599)); # 1257051599
SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051600)); # 1257055200

Por extraño que parezca, DATETIME será todavía almacenar y retorno (en forma de cadena única!) Veces dentro de una hora "perdido" cuando se inicia horario de verano (por ejemplo 2009-03-08 02:59:59). Pero usar estas fechas en cualquier función de MySQL es arriesgado:

SELECT UNIX_TIMESTAMP('2009-03-08 01:59:59'); # 1236495599
SELECT UNIX_TIMESTAMP('2009-03-08 02:00:00'); # 1236495600
# ...
SELECT UNIX_TIMESTAMP('2009-03-08 02:59:59'); # 1236495600
SELECT UNIX_TIMESTAMP('2009-03-08 03:00:00'); # 1236495600

La conclusión: si necesita almacenar y recuperar en todas las épocas del año, tiene algunas opciones indeseables:

  1. Establezca la zona horaria del sistema en GMT + algún desplazamiento constante. Por ejemplo, UTC
  2. Almacene las fechas como INT (como descubrió Aaron, TIMESTAMP ni siquiera es confiable)

  3. Imagina que el tipo DATETIME tiene una zona horaria de desplazamiento constante. Por ejemplo, si está dentro America/New_York, convierta su fecha a GMT-5 fuera de MySQL , luego almacénela como DATETIME (esto resulta ser esencial: vea la respuesta de Aaron). Entonces debe tener mucho cuidado al usar las funciones de fecha / hora de MySQL, porque algunos asumen que sus valores son de la zona horaria del sistema, otros (especialmente las funciones aritméticas de tiempo) son "independientes de la zona horaria" (pueden comportarse como si las horas fueran UTC).

Aaron y yo sospechamos que las columnas TIMESTAMP autogeneradas también están rotas. Ambos 2009-11-01 01:30 -0400y 2009-11-01 01:30 -0500se almacenarán como ambiguos 2009-11-01 01:30.


Gracias por toda su ayuda en este mrclay. Ha descrito la situación aquí con mucha precisión.
Aaron

Parece que la opción 3 es más segura para la aritmética de tiempo porque (parece que) las funciones se implementaron antes de que se agregara la funcionalidad DST. Por ejemplo, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') devuelve 2:00, que es correcto para UTC, pero en América / New_York los tiempos son 3 horas aparte.
Steve Clay

1
-1: Cometió el error de que las funciones de fecha / hora de MySQL operan en un tipo DATETIME, que es independiente de la zona horaria. Por lo tanto, el argumento que está pasando a UNIX_TIMSTAMP es select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;cuál es '2009-11-01 01:00:00'. UNIX_TIMESTAMP luego simplemente intenta convertir esto a UTC en el contexto de la zona horaria de la sesión; no intenta realizar la adición en el contexto de las reglas DST de esa zona horaria.
kbro

@kbro OK, pero el problema persiste. Si la sesión tz es America/New_York, no veo forma de almacenar 1257051600. ¿Verdad?
Steve Clay

77

Lo tengo resuelto para mis propósitos. Resumiré lo que aprendí (lo siento, estas notas son detalladas; son tanto para mi futura referencia como cualquier otra cosa).

Contrariamente a lo que dije en uno de mis comentarios anteriores, los campos de fecha y hora y TIMESTAMP no se comportan de manera diferente. Los campos TIMESTAMP (como indican los documentos) toman todo lo que les envíe en formato "AAAA-MM-DD hh: mm: ss" y conviértalo de su zona horaria actual a la hora UTC. Lo contrario ocurre de forma transparente cada vez que recupera los datos. Los campos DATETIME no realizan esta conversión. Toman todo lo que les envíes y lo almacenan directamente.

Ni los tipos de campo DATETIME ni TIMESTAMP pueden almacenar datos con precisión en una zona horaria que respete el horario de verano . Si almacena "2009-11-01 01:30:00", los campos no tienen forma de distinguir qué versión de la 1:30 am deseaba: la versión -04: 00 o -05: 00.

Ok, entonces debemos almacenar nuestros datos en una zona horaria que no sea DST (como UTC). Los campos TIMESTAMP no pueden manejar estos datos con precisión por las razones que explicaré: si su sistema está configurado en una zona horaria DST, entonces lo que ingresa en TIMESTAMP puede que no sea lo que obtiene. Incluso si le envía datos que ya ha convertido a UTC, seguirá asumiendo que los datos están en su zona horaria local y hará otra conversión a UTC. Este viaje de ida y vuelta local-a-UTC-back-to-local aplicado por TIMESTAMP tiene pérdidas cuando la zona horaria local observa el horario de verano (ya que "2009-11-01 01:30:00" se asigna a 2 horas posibles diferentes).

Con DATETIME puede almacenar sus datos en cualquier zona horaria que desee y estar seguro de que recibirá lo que envíe (no se verá obligado a realizar las conversiones de ida y vuelta con pérdidas que los campos TIMESTAMP le imponen). Entonces, la solución es usar un campo DATETIME y antes de guardar en el campo, convierta la zona horaria de su sistema en cualquier zona que no sea DST en la que desee guardarlo (creo que UTC es probablemente la mejor opción). Esto le permite crear la lógica de conversión en su lenguaje de secuencias de comandos para que pueda guardar explícitamente el equivalente UTC de "2009-11-01 01:30:00 -04: 00" o "" 2009-11-01 01:30: 00-05: 00 ".

Otra cosa importante a tener en cuenta es que las funciones matemáticas de fecha / hora de MySQL no funcionan correctamente alrededor de los límites DST si almacena sus fechas en un DST TZ. Así que una razón de más para ahorrar en UTC.

En pocas palabras, ahora hago esto:

Al recuperar los datos de la base de datos:

Interprete explícitamente los datos de la base de datos como UTC fuera de MySQL para obtener una marca de tiempo Unix precisa. Utilizo la función strtotime () de PHP o su clase DateTime para esto. No se puede hacer de manera confiable dentro de MySQL usando las funciones CONVERT_TZ () o UNIX_TIMESTAMP () de MySQL porque CONVERT_TZ solo generará un valor 'YYYY-MM-DD hh: mm: ss' que sufre problemas de ambigüedad, y UNIX_TIMESTAMP () asume su La entrada está en la zona horaria del sistema, no en la zona horaria en la que los datos se almacenaron REALMENTE (UTC).

Al almacenar los datos en la base de datos:

Convierta su fecha a la hora UTC precisa que desee fuera de MySQL. Por ejemplo: con la clase DateTime de PHP puede especificar "2009-11-01 1:30:00 EST" de forma distinta a "2009-11-01 1:30:00 EDT", luego convertirlo a UTC y guardar la hora UTC correcta. a su campo DATETIME.

Uf. Muchas gracias por el aporte y la ayuda de todos. Con suerte, esto le ahorrará a alguien más dolores de cabeza en el futuro.

Por cierto, estoy viendo esto en MySQL 5.0.22 y 5.0.27


13

Creo que el enlace de micahwittman tiene la mejor solución práctica para estas limitaciones de MySQL: establezca la zona horaria de la sesión en UTC cuando se conecte:

SET SESSION time_zone = '+0:00'

Luego, simplemente envíele las marcas de tiempo de Unix y todo debería estar bien.


Este consejo funciona bien. El problema se resuelve después de iniciar todas las conexiones en mi grupo con la declaración dada.
snowindy

4

Este hilo me volvió loco ya que usamos TIMESTAMPcolumnas con On UPDATE CURRENT_TIMESTAMP(es decir:) recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPpara rastrear registros cambiados y ETL en un almacén de datos.

En caso de que alguien se pregunte, en este caso, TIMESTAMPcompórtese correctamente y puede diferenciar entre las dos fechas similares convirtiendo la TIMESTAMPmarca de tiempo a Unix:

select TestFact.*, UNIX_TIMESTAMP(recordTimestamp) from TestFact;

id  recordTimestamp         UNIX_TIMESTAMP(recordTimestamp)
1   2012-11-04 01:00:10.0   1352005210
2   2012-11-04 01:00:10.0   1352008810

3

Pero, ¿cómo guardo algo de 01:30:00 a 04:00?

Puede convertir a UTC como:

SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');


Aún mejor, guarde las fechas como un campo TIMESTAMP . Eso siempre se almacena en UTC, y UTC no conoce el horario de verano / invierno.

Puede convertir de UTC a hora local usando CONVERT_TZ :

SELECT CONVERT_TZ(UTC_TIMESTAMP(),'+00:00','SYSTEM');

Donde '+00: 00' es UTC, la zona horaria de origen y 'SYSTEM' es la zona horaria local del sistema operativo donde se ejecuta MySQL.


Gracias por la respuesta. Lo mejor que puedo decir, a pesar de lo que dicen los documentos, los campos TIMESTAMP y Datetime se comportan de manera idéntica: almacenan sus datos en UTC, pero esperan que su entrada sea en hora local y la convierten automáticamente a UTC, si convierto a UTC primero, la base de datos no tiene idea de que hice eso y agrega 4 (o 5, dependiendo de si somos DST o no) más horas al tiempo. Entonces, el problema sigue siendo: ¿cómo especifico 2009-11-01 01:30:00 -04: 00 como entrada?
Aaron

Bueno, he descubierto que la fuente de la mayor parte de mi confusión es el hecho de que la función UNIX_TIMESTAMP () siempre interpreta su parámetro de fecha en relación con la zona horaria actual, ya sea que extraiga o no los datos de un campo TIMESTAMP o DATETIME. . Esto tiene sentido ahora que lo pienso. Actualizaré más después.
Aaron

2

Mysql resuelve inherentemente este problema usando la tabla time_zone_name de mysql db. Utilice CONVERT_TZ mientras CRUD para actualizar la fecha y hora sin preocuparse por el horario de verano.

SELECT
  CONVERT_TZ('2019-04-01 00:00:00','Europe/London','UTC') AS time1,
  CONVERT_TZ('2019-03-01 00:00:00','Europe/London','UTC') AS time2;

1

Estaba trabajando en registrar recuentos de visitas de páginas y mostrar los recuentos en un gráfico (usando el complemento Flot jQuery). Llené la tabla con datos de prueba y todo se veía bien, pero noté que al final del gráfico los puntos tenían un día de descanso de acuerdo con las etiquetas en el eje x. Después del examen, noté que el recuento de vistas del día 2015-10-25 se recuperó dos veces de la base de datos y se pasó a Flot, por lo que todos los días posteriores a esta fecha se movieron un día a la derecha.
Después de buscar un error en mi código por un tiempo, me di cuenta de que esta fecha es cuando tiene lugar el DST. Luego llegué a esta página SO ...
... pero las soluciones sugeridas eran excesivas para lo que necesitaba o tenían otras desventajas. No me preocupa mucho no poder distinguir entre marcas de tiempo ambiguas. Solo necesito contar y mostrar registros por días.

Primero, recupero el rango de fechas:

SELECT 
    DATE(MIN(created_timestamp)) AS min_date, 
    DATE(MAX(created_timestamp)) AS max_date 
FROM page_display_log
WHERE item_id = :item_id

Luego, en un bucle for, comenzando con min_date, terminando con max_date, por paso de un día ( 60*60*24), estoy recuperando los recuentos:

for( $day = $min_date_timestamp; $day <= $max_date_timestamp; $day += 60 * 60 * 24 ) {
    $query = "
        SELECT COUNT(*) AS count_per_day
        FROM page_display_log
        WHERE 
            item_id = :item_id AND
            ( 
                created_timestamp BETWEEN 
                '" . date( "Y-m-d 00:00:00", $day ) . "' AND
                '" . date( "Y-m-d 23:59:59", $day ) . "'
            )
    ";
    //execute query and do stuff with the result
}

Mi solución final y rápida a mi problema fue esta:

$min_date_timestamp += 60 * 60 * 2; // To avoid DST problems
for( $day = $min_date_timestamp; $day <= $max_da.....

Así que no estoy mirando el bucle al comienzo del día, sino dos horas después . El día sigue siendo el mismo, y todavía estoy recuperando los recuentos correctos, ya que solicito explícitamente a la base de datos registros entre las 00:00:00 y las 23:59:59 del día, independientemente de la hora real de la marca de tiempo. Y cuando el tiempo salta una hora, todavía estoy en el día correcto.

Nota: Sé que este es un hilo de 5 años y sé que no es una respuesta a la pregunta de OP, pero podría ayudar a personas como yo que encontraron esta página en busca de una solución al problema que describí.


Probablemente no sea relevante para la pregunta real, pero esto es terriblemente ineficiente, ¡y nadie debería copiarlo! En su lugar, emita una única consulta como:
Doin

"SELECT CAST(created_timestamp AS date) day,COUNT(*) WHERE item_id=:item_id AND (created_timestamp BETWEEN '".date("Y-m-d 00:00:00", $min_date_timestamp)."' AND '".date("Y-m-d 23:59:59", $max_date_timestamp)."') GROUP BY day ORDER BY day";
Hasta el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.