Campos de data e hora do MySQL e horário de verão - como faço referência à hora “extra”?

88

Estou usando o fuso horário da América / Nova York. No outono, "recuamos" uma hora - efetivamente "ganhando" uma hora às 2h. No ponto de transição acontece o seguinte:

é 01:59:00 -04: 00
e 1 minuto depois torna-se:
01:00:00 -05: 00

Portanto, se você simplesmente disser "1h30", será ambíguo se você está se referindo à primeira vez ou não à 1h30. Estou tentando salvar dados de agendamento em um banco de dados MySQL e não consigo determinar como salvar os horários corretamente.

Aqui está o problema:
"2009-11-01 00:30:00" é armazenado internamente como 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" é armazenado internamente como 01-11-2009 01:30:00 -05: 00

Isso é bom e bastante esperado. Mas como faço para salvar algo em 01:30:00 -04: 00 ? A documentação não mostra nenhum suporte para a especificação do deslocamento e, portanto, quando tentei especificar o deslocamento, ele foi devidamente ignorado.

As únicas soluções que pensei envolvem definir o servidor para um fuso horário que não use o horário de verão e fazer as transformações necessárias em meus scripts (estou usando PHP para isso). Mas isso não parece ser necessário.

Muito obrigado por todas as sugestões.

Aaron
fonte
Não sei o suficiente sobre MySQL ou PHP para formar uma resposta coerente, mas aposto que tem algo a ver com a conversão de e para UTC.
Mark Ransom
2
Internamente, eles são armazenados como um UTC, não?
Eli
4
Achei web.ivy.net/~carton/rant/MySQL-timezones.txt uma leitura interessante sobre o assunto.
micahwittman
Bom link, micahwittman - muito útil.
Aaron
ótimas perguntas. um problema comum.
Vardumper

Respostas:

47

Os tipos de data do MySQL são, francamente, quebrados e não podem armazenar todos os horários corretamente, a menos que seu sistema esteja configurado para um fuso horário de deslocamento constante, como UTC ou GMT-5. (Estou usando MySQL 5.0.45)

Isso ocorre porque você não pode armazenar qualquer hora durante a hora antes do horário de verão . Não importa como você insere as datas, cada função de data tratará esses horários como se fossem da hora após a troca.

O fuso horário do meu sistema é America/New_York. Vamos tentar armazenar 1257051600 (Dom, 01 de novembro de 2009 06:00:00 +0100).

Aqui está usando a sintaxe proprietária 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

Mesmo FROM_UNIXTIME()não retornará o tempo exato.

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

Curiosamente, DATETIME vai ainda armazenar e retorno (na única forma corda!) Vezes dentro de uma hora "perdido" quando horário de verão começa (por exemplo 2009-03-08 02:59:59). Mas usar essas datas em qualquer função MySQL é arriscado:

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

Conclusão: se você precisa armazenar e recuperar todas as épocas do ano, tem algumas opções indesejáveis:

  1. Defina o fuso horário do sistema como GMT + algum deslocamento constante. Ex: UTC
  2. Armazene datas como INTs (como Aaron descobriu, TIMESTAMP nem é confiável)

  3. Finja que o tipo DATETIME tem algum fuso horário de deslocamento constante. Por exemplo, se você estiver em America/New_York, converta sua data para GMT-5 fora do MySQL e armazene como DATETIME (isso é essencial: veja a resposta de Aaron). Então você deve tomar muito cuidado ao usar as funções de data / hora do MySQL, porque alguns assumem que seus valores são do fuso horário do sistema, outros (especialmente funções aritméticas de tempo) são "agnósticos de fuso horário" (eles podem se comportar como se os horários fossem UTC).

Aaron e eu suspeitamos que as colunas TIMESTAMP de geração automática também estão quebradas. Ambos 2009-11-01 01:30 -0400e 2009-11-01 01:30 -0500serão armazenados como ambíguos 2009-11-01 01:30.

Steve Clay
fonte
Obrigado por toda sua ajuda neste mrclay. Você descreveu a situação aqui com muita precisão.
Aaron
Parece que a opção 3 é realmente mais segura para aritmética de tempo porque (parece que) as funções foram implementadas antes da funcionalidade DST ser adicionada. Por exemplo, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') retorna 2:00, o que é correto para UTC, mas em America / New_York os tempos são 3 horas separados.
Steve Clay
1
-1: Você cometeu o erro de que as funções de data / hora do MySQL operam em um tipo DATETIME, que é independente do fuso horário. O argumento que você está passando para UNIX_TIMSTAMP é, portanto, select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;qual é '2009-11-01 01:00:00'. UNIX_TIMESTAMP então simplesmente tenta converter isso para UTC no contexto do fuso horário da sessão - ele não tenta realizar a adição no contexto das regras de horário de verão desse fuso horário.
kbro de
@kbro OK, mas o problema permanece. Se a sessão tz for America/New_York, não vejo como armazenar 1257051600. Você vê?
Steve Clay
75

Eu tenho isso planejado para meus propósitos. Vou resumir o que aprendi (desculpe, essas notas são prolixas; são tanto para minha referência futura quanto qualquer outra coisa).

Ao contrário do que eu disse em um dos meus comentários anteriores, campos DATETIME e TIMESTAMP fazer se comportam de forma diferente. Os campos TIMESTAMP (como os documentos indicam) pegam tudo o que você enviar no formato "AAAA-MM-DD hh: mm: ss" e convertem do seu fuso horário atual para o horário UTC. O inverso acontece de forma transparente sempre que você recupera os dados. Os campos DATETIME não fazem essa conversão. Eles pegam tudo o que você envia e apenas armazenam diretamente.

Nem os tipos de campo DATETIME nem TIMESTAMP podem armazenar dados com precisão em um fuso horário que observe o DST . Se você armazenar "2009-11-01 01:30:00", os campos não terão como distinguir qual versão de 1:30 da manhã você deseja - a versão -04: 00 ou -05: 00.

Ok, então devemos armazenar nossos dados em um fuso horário diferente do horário de verão (como UTC). Os campos TIMESTAMP são incapazes de lidar com esses dados com precisão por motivos que explicarei: se o seu sistema estiver configurado para um fuso horário DST, então o que você colocar em TIMESTAMP pode não ser o que você receberá de volta. Mesmo se você enviar dados já convertidos para UTC, ele ainda assumirá os dados em seu fuso horário local e fará outra conversão para UTC. Esta viagem de ida e volta local-para-UTC-de-volta-para-local imposta por TIMESTAMP tem perdas quando seu fuso horário local segue o horário de verão (desde "2009-11-01 01:30:00" mapeia para 2 horários diferentes possíveis).

Com DATETIME você pode armazenar seus dados em qualquer fuso horário que desejar e ter a certeza de que receberá de volta tudo o que enviar (você não será forçado a fazer conversões de ida e volta com perdas que os campos do TIMESTAMP impõem a você). Portanto, a solução é usar um campo DATETIME e, antes de salvar no campo, converter do fuso horário do seu sistema para qualquer fuso horário não DST em que você deseja salvá-lo (acho que UTC é provavelmente a melhor opção). Isso permite que você construa a lógica de conversão em sua linguagem de script para que possa salvar explicitamente o equivalente UTC de "2009-11-01 01:30:00 -04: 00" ou "" 2009-11-01 01:30: 00 -05: 00 ".

Outra coisa importante a se notar é que as funções matemáticas de data / hora do MySQL não funcionam corretamente em torno dos limites do DST se você armazenar suas datas em um DST TZ. Portanto, mais um motivo para economizar em UTC.

Resumindo, agora faço o seguinte:

Ao recuperar os dados do banco de dados:

Interprete explicitamente os dados do banco de dados como UTC fora do MySQL para obter um carimbo de data / hora Unix preciso. Eu uso a função strtotime () do PHP ou sua classe DateTime para isso. Isso não pode ser feito de forma confiável dentro do MySQL usando as funções CONVERT_TZ () ou UNIX_TIMESTAMP () do MySQL porque CONVERT_TZ só produzirá um valor 'YYYY-MM-DD hh: mm: ss' que sofre de problemas de ambigüidade, e UNIX_TIMESTAMP () assume seu a entrada está no fuso horário do sistema, não no fuso horário em que os dados foram REALMENTE armazenados (UTC).

Ao armazenar os dados no banco de dados:

Converta sua data para a hora UTC precisa que você deseja fora do MySQL. Por exemplo: com a classe DateTime do PHP, você pode especificar "2009-11-01 1:30:00 EST" distintamente de "2009-11-01 1:30:00 EDT", então convertê-lo para UTC e salvar a hora UTC correta ao seu campo DATETIME.

Ufa. Muito obrigado pela contribuição e ajuda de todos. Esperançosamente, isso evita que alguém tenha algumas dores de cabeça no futuro.

BTW, estou vendo isso no MySQL 5.0.22 e 5.0.27

Aaron
fonte
13

Acho que o link de micahwittman tem a melhor solução prática para essas limitações do MySQL: Defina o fuso horário da sessão como UTC ao conectar:

SET SESSION time_zone = '+0:00'

Depois, basta enviar carimbos de data / hora Unix e tudo ficará bem.

Steve Clay
fonte
Este conselho funciona bem. O problema é resolvido depois de inicializar todas as conexões em meu pool com a instrução fornecida.
snowindy
4

Esse tópico me deixou louco, pois usamos TIMESTAMPcolunas com On UPDATE CURRENT_TIMESTAMP(ou seja recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP:) para rastrear registros alterados e ETL para um datawarehouse.

Caso alguém se pergunte, neste caso, TIMESTAMPcomporte-se corretamente e você pode diferenciar entre as duas datas semelhantes convertendo o TIMESTAMPcarimbo de data / hora em 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
Manuel Darveau
fonte
3

Mas como faço para salvar algo em 01:30:00 -04: 00?

Você pode converter para UTC como:

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


Melhor ainda, salve as datas como um campo TIMESTAMP . Isso sempre é armazenado no UTC, e o UTC não sabe sobre o horário de verão / inverno.

Você pode converter de UTC para hora local usando CONVERT_TZ :

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

Onde '+00: 00' é UTC, o fuso horário de e 'SYSTEM' é o fuso horário local do sistema operacional onde o MySQL é executado.

Andomar
fonte
Obrigado pela resposta. Pelo que posso dizer, apesar do que dizem os documentos, os campos TIMESTAMP e Datetime estão se comportando de forma idêntica: eles armazenam seus dados em UTC, mas esperam que sua entrada esteja no horário local e os convertem automaticamente em UTC - se eu converter para Primeiro UTC, o banco de dados não tem ideia de que fiz isso e adiciona 4 (ou 5, dependendo se somos ou não DST) mais horas ao tempo. Portanto, o problema permanece: como faço para especificar 2009-11-01 01:30:00 -04: 00 como entrada?
Aaron
Bem, descobri que a fonte da maior parte da minha confusão é o fato de que a função UNIX_TIMESTAMP () sempre interpreta seu parâmetro de data em relação ao fuso horário atual, esteja você extraindo ou não os dados de um campo TIMESTAMP ou DATETIME . Isso faz sentido agora que penso nisso. Vou atualizar mais depois.
Aaron
2

O Mysql inerentemente resolve este problema usando a tabela time_zone_name do mysql db. Use CONVERT_TZ enquanto CRUD para atualizar a data e hora sem se preocupar com o horário de verão.

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;
Arpan Jain
fonte
1

Eu estava trabalhando no registro de contagens de visitas de páginas e exibindo as contagens em gráfico (usando o plugin Flot jQuery). Preenchi a tabela com dados de teste e tudo parecia bem, mas notei que no final do gráfico os pontos estavam um dia fora de acordo com os rótulos no eixo x. Após exame, notei que a contagem de visualizações para o dia 2015-10-25 foi recuperada duas vezes do banco de dados e passada para o Flot, portanto, todos os dias após essa data foram movidos um dia para a direita.
Depois de procurar um bug no meu código por um tempo, percebi que essa data é quando ocorre o DST. Então eu vim para esta página do SO ...
... mas as soluções sugeridas eram um exagero para o que eu precisava ou tinham outras desvantagens. Não estou muito preocupado em não ser capaz de distinguir entre carimbos de data / hora ambíguos. Eu só preciso contar e exibir os registros por dias.

Primeiro, recupero o intervalo de datas:

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

Então, em um loop for, começando com min_date, terminando com max_date, por etapa de um dia ( 60*60*24), estou recuperando as contagens:

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
}

Minha solução final e rápida para o meu problema foi esta:

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

Portanto, não estou encarando o loop no início do dia, mas duas horas depois . O dia ainda é o mesmo, e ainda estou recuperando as contagens corretas, pois solicito explicitamente ao banco de dados os registros entre 00:00:00 e 23:59:59 do dia, independentemente da hora real do carimbo de data / hora. E quando o tempo salta uma hora, ainda estou no dia correto.

Nota: Eu sei que este é um tópico de 5 anos, e eu sei que esta não é uma resposta para a pergunta dos OPs, mas pode ajudar pessoas como eu que encontraram esta página em busca de solução para o problema que descrevi.

Lukas
fonte
Provavelmente não é relevante para a questão real, mas isso é terrivelmente ineficiente e ninguém deveria copiá-lo! Em vez disso, emita uma ú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";
Doin