Campi data / ora di MySQL e ora legale: come faccio a fare riferimento all'ora "extra"?


89

Sto usando il fuso orario America / New York. In autunno "torniamo indietro" di un'ora, "guadagnando" effettivamente un'ora alle 2 del mattino. Al punto di transizione accade quanto segue:

è 01:59:00 -04: 00
poi 1 minuto dopo diventa:
01:00:00 -05: 00

Quindi, se dici semplicemente "1:30 am" è ambiguo se ti riferisci o meno alla prima volta 1:30 o alla seconda. Sto cercando di salvare i dati di pianificazione su un database MySQL e non riesco a determinare come salvare correttamente i tempi.

Ecco il problema:
"2009-11-01 00:30:00" viene memorizzato internamente come 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" viene memorizzato internamente come 01/11/2009 01:30:00 -05: 00

Questo va bene e abbastanza previsto. Ma come faccio a salvare qualcosa su 01:30:00 -04: 00 ? La documentazione non mostra alcun supporto per specificare l'offset e, di conseguenza, quando ho provato a specificare l'offset è stato debitamente ignorato.

Le uniche soluzioni a cui ho pensato riguardano l'impostazione del server su un fuso orario che non utilizzi l'ora legale e le trasformazioni necessarie nei miei script (sto usando PHP per questo). Ma non sembra che dovrebbe essere necessario.

Molte grazie per eventuali suggerimenti.


Non so abbastanza su MySQL o PHP per formare una risposta coerente, ma scommetto che ha qualcosa a che fare con la conversione da e verso UTC.
Mark Ransom

2
Internamente sono tutti memorizzati come UTC, no?
Eli

4
Ho trovato web.ivy.net/~carton/rant/MySQL-timezones.txt una lettura interessante sull'argomento.
micahwittman

Buon collegamento, micahwittman - molto utile.
Aaron

grandi domande. un problema comune.
Vardumper

Risposte:


47

I tipi di data di MySQL sono, francamente, non funzionanti e non possono memorizzare tutti gli orari correttamente a meno che il tuo sistema non sia impostato su un fuso orario con offset costante, come UTC o GMT-5. (Sto usando MySQL 5.0.45)

Questo perché non è possibile memorizzare alcun orario durante l'ora prima della fine dell'ora legale . Indipendentemente dal modo in cui inserisci le date, ogni funzione data tratterà questi orari come se fossero durante l'ora successiva al cambio.

Il fuso orario del mio sistema è America/New_York. Proviamo a memorizzare 1257051600 (Sun, 01 Nov 2009 06:00:00 +0100).

Di seguito viene utilizzata la sintassi proprietaria 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

Anche FROM_UNIXTIME()non restituirà l'ora esatta.

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

Stranamente, DATETIME continuerà a memorizzare e restituire (solo in forma di stringa!) I tempi entro l'ora "persa" quando inizia l'ora legale (ad esempio 2009-03-08 02:59:59). Ma utilizzare queste date in qualsiasi funzione MySQL è rischioso:

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

Il takeaway: se devi archiviare e recuperare ogni volta durante l'anno, hai alcune opzioni indesiderabili:

  1. Imposta il fuso orario del sistema su GMT + un offset costante. Ad esempio UTC
  2. Memorizza le date come INT (come ha scoperto Aaron, TIMESTAMP non è nemmeno affidabile)

  3. Immagina che il tipo DATETIME abbia un fuso orario di offset costante. Ad esempio, se sei in America/New_York, converti la tua data in GMT-5 al di fuori di MySQL , quindi memorizza come DATETIME (questo risulta essere essenziale: vedi la risposta di Aaron). Quindi devi fare molta attenzione nell'usare le funzioni data / ora di MySQL, perché alcuni presumono che i tuoi valori siano del fuso orario di sistema, altri (specialmente le funzioni aritmetiche dell'ora) sono "indipendenti dal fuso orario" (possono comportarsi come se l'ora fosse UTC).

Aaron e io sospettiamo che anche le colonne TIMESTAMP con generazione automatica siano interrotte. Entrambi 2009-11-01 01:30 -0400e 2009-11-01 01:30 -0500verranno archiviati come ambigui 2009-11-01 01:30.


Grazie per tutto il tuo aiuto su questo mrclay. Hai delineato la situazione qui in modo molto accurato.
Aaron

Sembra che l'opzione 3 sia effettivamente più sicura per l'aritmetica del tempo perché (sembra che) le funzioni siano state implementate prima che fosse aggiunta la funzionalità DST. Ad esempio TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') restituisce 2:00, che è corretto per UTC, ma in America / New_York gli orari sono di 3 ore a parte.
Steve Clay il

1
-1: Hai commesso l'errore che le funzioni data / ora di MySQL operino su un tipo DATETIME, che è indipendente dal fuso orario. L'argomento che stai passando a UNIX_TIMSTAMP è quindi select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;quale è '2009-11-01 01:00:00'. UNIX_TIMESTAMP quindi tenta semplicemente di convertirlo in UTC nel contesto del fuso orario della sessione - non tenta di eseguire l'aggiunta nel contesto delle regole dell'ora legale di quel fuso orario.
kbro

@kbro OK, ma il problema rimane. Se la sessione tz è America/New_York, non vedo alcun modo per memorizzare 1257051600. Davvero?
Steve Clay,

78

L'ho capito per i miei scopi. Riassumerò ciò che ho imparato (scusate, queste note sono prolisse; sono tanto per il mio futuro riferimento quanto per qualsiasi altra cosa).

Contrariamente a quanto ho detto in uno dei miei commenti precedenti, i campi DATETIME e TIMESTAMP si comportano in modo diverso. I campi TIMESTAMP (come indicano i documenti) prendono quello che invii nel formato "AAAA-MM-GG hh: mm: ss" e lo convertono dal fuso orario corrente all'ora UTC. Il contrario avviene in modo trasparente ogni volta che si recuperano i dati. I campi DATETIME non effettuano questa conversione. Prendono quello che gli mandi e lo immagazzinano direttamente.

Né i tipi di campo DATETIME né TIMESTAMP possono memorizzare accuratamente i dati in un fuso orario che osserva l'ora legale . Se memorizzi "2009-11-01 01:30:00" i campi non hanno modo di distinguere quale versione di 1:30 am desideri - la versione -04: 00 o -05: 00.

Ok, quindi dobbiamo memorizzare i nostri dati in un fuso orario diverso dall'ora legale (come UTC). I campi TIMESTAMP non sono in grado di gestire questi dati in modo accurato per ragioni che spiegherò: se il tuo sistema è impostato su un fuso orario DST, ciò che inserisci in TIMESTAMP potrebbe non essere quello che ottieni. Anche se gli invii dati che hai già convertito in UTC, assumerà comunque che i dati siano nel tuo fuso orario locale ed eseguirà ancora un'altra conversione in UTC. Questo viaggio di andata e ritorno da locale a UTC di ritorno a locale applicato TIMESTAMP non è valido quando il fuso orario locale osserva l'ora legale (dal momento che "2009-11-01 01:30:00" corrisponde a 2 diversi orari possibili).

Con DATETIME puoi memorizzare i tuoi dati in qualsiasi fuso orario e avere la certezza che riceverai quello che invierai (non sarai costretto alle conversioni di andata e ritorno con perdite che i campi TIMESTAMP ti impongono). Quindi la soluzione è utilizzare un campo DATETIME e prima di salvare nel campo convertire dal fuso orario del sistema in qualsiasi zona non DST in cui si desidera salvarlo (penso che UTC sia probabilmente l'opzione migliore). Ciò ti consente di creare la logica di conversione nel tuo linguaggio di scripting in modo da poter salvare esplicitamente l'equivalente UTC di "2009-11-01 01:30:00 -04: 00" o "" 2009-11-01 01:30: 00 -05: 00 ".

Un'altra cosa importante da notare è che le funzioni matematiche di data / ora di MySQL non funzionano correttamente attorno ai limiti dell'ora legale se si memorizzano le date in una TZ dell'ora legale. Quindi motivo in più per risparmiare in UTC.

In poche parole ora lo faccio:

Quando si recuperano i dati dal database:

Interpreta esplicitamente i dati dal database come UTC al di fuori di MySQL per ottenere un timestamp Unix accurato. Per questo utilizzo la funzione strtotime () di PHP o la sua classe DateTime. Non può essere eseguito in modo affidabile all'interno di MySQL utilizzando le funzioni CONVERT_TZ () o UNIX_TIMESTAMP () di MySQL perché CONVERT_TZ produrrà solo un valore 'YYYY-MM-DD hh: mm: ss' che soffre di problemi di ambiguità, e UNIX_TIMESTAMP () assume il suo l'input è nel fuso orario del sistema, non nel fuso orario in cui i dati sono stati EFFETTIVAMENTE memorizzati (UTC).

Quando si memorizzano i dati nel database:

Converti la tua data nell'ora UTC precisa che desideri al di fuori di MySQL. Ad esempio: con la classe DateTime di PHP puoi specificare "2009-11-01 1:30:00 EST" distintamente da "2009-11-01 1:30:00 EDT", quindi convertirlo in UTC e salvare l'ora UTC corretta nel campo DATETIME.

Uff. Grazie mille per il contributo e l'aiuto di tutti. Si spera che questo faccia risparmiare a qualcun altro qualche mal di testa lungo la strada.

BTW, lo vedo su MySQL 5.0.22 e 5.0.27


13

Penso che il collegamento di micahwittman abbia la migliore soluzione pratica a queste limitazioni di MySQL: imposta il fuso orario della sessione su UTC quando ti connetti:

SET SESSION time_zone = '+0:00'

Quindi gli invii i timestamp Unix e tutto dovrebbe andare bene.


Questo consiglio funziona bene. Il problema è stato risolto dopo aver avviato tutte le connessioni nel mio pool con l'istruzione fornita.
snowindy

4

Questo thread mi ha fatto impazzire poiché usiamo TIMESTAMPcolonne con On UPDATE CURRENT_TIMESTAMP(ie recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP:) per tenere traccia dei record modificati e ETL in un datawarehouse.

Nel caso qualcuno si chieda, in questo caso, TIMESTAMPcomportati correttamente e puoi distinguere tra le due date simili convertendo il TIMESTAMPtimestamp in 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

Ma come faccio a salvare qualcosa su 01:30:00 -04: 00?

Puoi convertire in UTC come:

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


Ancora meglio, salva le date come TIMESTAMP campo . Viene sempre memorizzato in UTC e UTC non conosce l'ora legale / solare.

Puoi convertire da UTC a ora locale utilizzando CONVERT_TZ :

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

Dove "+00: 00" è UTC, il fuso orario da e "SYSTEM" è il fuso orario locale del sistema operativo in cui viene eseguito MySQL.


Grazie per la risposta. Come meglio posso dire, nonostante quello che dicono i documenti, i campi TIMESTAMP e Datetime si comportano in modo identico: memorizzano i loro dati in UTC, ma si aspettano che il loro input sia nell'ora locale e lo convertono automaticamente in UTC, se lo converto in Prima UTC il database non ha idea che io l'abbia fatto e aggiunge 4 (o 5, a seconda che siamo o meno l'ora legale) in più all'ora. Quindi il problema rimane: come faccio a specificare 2009-11-01 01:30:00 -04: 00 come input?
Aaron

Bene, ho scoperto che la fonte della maggior parte della mia confusione è il fatto che la funzione UNIX_TIMESTAMP () interpreta sempre il suo parametro di data relativo al fuso orario corrente indipendentemente dal fatto che tu stia estraendo i dati da un campo TIMESTAMP o DATETIME . Questo ha senso ora che ci penso. Aggiornerò più tardi.
Aaron

2

Mysql risolve intrinsecamente questo problema utilizzando la tabella time_zone_name da mysql db. Usa CONVERT_TZ mentre CRUD per aggiornare il datetime senza preoccuparti dell'ora legale.

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

Stavo lavorando alla registrazione dei conteggi delle visite delle pagine e alla visualizzazione dei conteggi nel grafico (utilizzando il plugin Flot jQuery). Ho riempito la tabella con i dati del test e tutto sembrava a posto, ma ho notato che alla fine del grafico i punti erano un giorno di riposo in base alle etichette sull'asse x. Dopo l'esame ho notato che il conteggio delle visualizzazioni per il giorno 25-10-2015 è stato recuperato due volte dal database e passato a Flot, quindi ogni giorno dopo questa data è stato spostato di un giorno a destra.
Dopo aver cercato un bug nel mio codice per un po 'mi sono reso conto che questa data è quando si verifica l'ora legale. Poi sono arrivato a questa pagina SO ...
... ma le soluzioni suggerite erano eccessive per ciò di cui avevo bisogno o avevano altri svantaggi. Non sono molto preoccupato di non essere in grado di distinguere tra timestamp ambigui. Devo solo contare e visualizzare i record al giorno.

Innanzitutto, recupero l'intervallo di date:

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

Quindi, in un ciclo for, iniziando con min_date, finendo con max_date, passo di un giorno ( 60*60*24), sto recuperando i conteggi:

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
}

La mia soluzione finale e rapida al mio problema è stata questa:

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

Quindi non sto fissando il ciclo all'inizio della giornata, ma due ore dopo . Il giorno è sempre lo stesso e sto ancora recuperando i conteggi corretti, poiché chiedo esplicitamente al database i record tra le 00:00:00 e le 23:59:59 del giorno, indipendentemente dall'ora effettiva del timestamp. E quando l'ora salta di un'ora, sono ancora nel giorno corretto.

Nota: so che questo è un thread di 5 anni e so che questa non è una risposta alla domanda degli OP, ma potrebbe aiutare le persone come me che hanno incontrato questa pagina alla ricerca di una soluzione al problema che ho descritto.


Probabilmente non pertinente alla domanda reale, ma questo è orribilmente inefficiente e nessuno dovrebbe copiarlo! Invece, esegui una singola query come:
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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.