Configure MySQL to use UTC

A Java server I wrote was storing date/time values in MySQL incorrectly during the transition from Daylight Saving Time to Standard Time. The solution was to force MySQL to store everything in Coordinated Universal Time (UTC).

Test Program

I created a database table to store date/time values:

CREATE TABLE test_utc (
  id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
  dateTimeAsString varchar(255) NOT NULL,
  dateTimeSample datetime NOT NULL,
  timestampSample timestamp NOT NULL
);

I tested with the following timestamps

  • 2014-11-02 00:30 CDT
  • 2014-11-02 01:30 CDT
  • 2014-11-02 01:30 CST
  • 2014-11-02 02:30 CST
  • value of now()
  • value of utc_timestamp()

Note that 1:30am appears twice, once as CDT and once as CST because November 2, 2014 at 2:00am is when Daylight Saving Time changed back to Standard Time.

I wrote a small Java program that inserts the test timestamps using JDBC:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.junit.Test;

public class DateTimeUTCTest {

  @Test
  public void testDateTimeUTC() throws SQLException {
    final Calendar cal = getStartTime();
    final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz");

    /*
     * Clear the table of previous rows.
     */
    try (final Connection conn = getConnection()) {
      try (final PreparedStatement deleteStatement = conn.prepareStatement(
          "DELETE FROM test_utc")) {
        deleteStatement.execute();
      }

      /*
       * Insert the 4 dates 
       *  2014-11-02 00:30 CDT 
       *  2014-11-02 01:30 CDT 
       *  2014-11-02 01:30 CST 
       *  2014-11-02 02:30 CST 
       */
      final String insertPrefix = "INSERT INTO test_utc (dateTimeAsString, dateTimeSample, timestampSample) VALUES ";
      try (final PreparedStatement insertStatement = conn
          .prepareStatement(insertPrefix + "(?,?,?)")) {
        for (int i = 0; i < 4; ++i) {
          insertDateTime(insertStatement, cal, df);
          cal.add(Calendar.HOUR_OF_DAY, 1);
        }
      }
      
      /*
       * Insert the current timestamp using now().
       */
      try (final PreparedStatement insertStatement = conn
          .prepareStatement(insertPrefix + "(?,now(),now())")) {
        final String nowStr = df.format(new Date());
        insertStatement.setString(1, "now() - " + nowStr);
        insertStatement.execute();
      }
      
      /*
       * Insert the current timestamp using utc_timestamp().
       */
      try (final PreparedStatement insertStatement = conn
          .prepareStatement(insertPrefix
              + "(?,utc_timestamp(),utc_timestamp())")) {
        final String nowStr = df.format(new Date());
        insertStatement.setString(1, "utc_timestamp() - " + nowStr);
        insertStatement.execute();
      }
      
      /*
       * Read the rows inserted in the database and print out the results.
       */
      final String selectQuery = "SELECT * FROM test_utc order by id";
      try (final PreparedStatement readStatement = conn.prepareStatement(selectQuery);
          final ResultSet rs = readStatement.executeQuery()) {
        System.out.println("id\toriginalString\t\tdateTimeSample\t\ttimestampSample");
        while (rs.next()) {
          int index = 1;
          final int id = rs.getInt(index++);
          final String dateTimeAsString = rs.getString(index++);
          final Timestamp dateTimeSample = rs.getTimestamp(index++);
          final Timestamp timestampSample = rs.getTimestamp(index++);
          final String formattedDateTime = df.format(dateTimeSample);
          final String formattedTimestamp = df.format(timestampSample);
          System.out.println(id + "\t" + 
              dateTimeAsString + "\t" + 
              formattedDateTime + "\t" + 
              formattedTimestamp);
        }
      }
    }
  }

  /**
   * @return November 2, 2014 00:30:00, which is 30 minutes before DST ends.
   */
  private Calendar getStartTime() {
    final Calendar cal = Calendar.getInstance();
    cal.set(Calendar.MONTH, Calendar.NOVEMBER);
    cal.set(Calendar.DAY_OF_MONTH, 2);
    cal.set(Calendar.YEAR, 2014);
    cal.set(Calendar.HOUR_OF_DAY, 0);
    cal.set(Calendar.MINUTE, 30);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    return cal;
  }

  /**
   * @return JDBC  connection to the database
   */
  private Connection getConnection() throws SQLException {
    String jdbcUrl = "jdbc:mysql://localhost/watw-test";
<a id="fix"></a>//  jdbcUrl += "?useLegacyDatetimeCode=false&amp;serverTimezone=UTC";
    final Connection retval = DriverManager.getConnection(jdbcUrl, 
        "testuser", "test123");
    return retval;
  }

  /**
   * Given a Calendar, inserts a text, datetime, and timestamp version of the
   * Calendar timestamp in the database.
   */
  private void insertDateTime(final PreparedStatement insertStatement,
      final Calendar cal, final DateFormat df) throws SQLException {
    int index = 1;
    insertStatement.setString(index++, df.format(cal.getTime()));
    final Timestamp timestamp = new Timestamp(cal.getTimeInMillis());
    insertStatement.setTimestamp(index++, timestamp);
    insertStatement.setTimestamp(index++, timestamp);
    insertStatement.execute();
  }

}

Test Results

With the default MySQL configuration and the program as written, here is the output:

id original dateTime timestamp
192014-11-02 00:30 CDT2014-11-02 00:30 CDT2014-11-02 00:30 CDT
202014-11-02 01:30 CDT2014-11-02 01:30 CST2014-11-02 01:30 CST
212014-11-02 01:30 CST2014-11-02 01:30 CST2014-11-02 01:30 CST
222014-11-02 02:30 CST2014-11-02 02:30 CST2014-11-02 02:30 CST
23now() -
2015-07-01 00:51 CDT
2015-07-01 00:51 CDT2015-07-01 00:51 CDT
24utc_timestamp() -
2015-07-01 00:51 CDT
2015-07-01 05:51 CDT2015-07-01 05:51 CDT

The red boxes are errors.

On row 20, the original date was in CDT (Daylight Saving Time), but the retrieved date is CST (Standard Time). Since the timestamp was stored in a DATETIME column (with no time zone), MySQL stored the date 2014-11-02 01:30. When it retrieved the date, MySQL did not know if that time was in Daylight Saving Time or Standard Time because 1:30am happens twice, first in CDT, and then in CST.

I expected the TIMESTAMP column to not have this problem because the
MySQL documentation says

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.)

However, you can see that the TIMESTAMP column had the same behavior.

Separately, on row 24, utc_timestamp is totally broken. For display I converted the time to CDT and the time is 5 hours off. I was concerned that another developer might use utc_timestamp and record the wrong time.

Fixes

Two changes were necessary.

In the my.cnf file, I added the following line:

default-time-zone='+00:00'

and restarted MySQL.

In the JDBC connection URL, I added the parameters useLegacyDatetimeCode=false and serverTimezone=UTC. See the commented out line above in the method getConnection().

After those two changes, all existing timestamps had to be migrated to UTC. That migration was easy except for times between 1:00am and 1:59am on November 2, 2014. To know if those times were CST or CDT, I used the primary key, which was an incrementing integer, to determine the entries created before the time transition.

Results after Fixes

id original dateTime timestamp
492014-11-02 00:30 CDT2014-11-02 00:30 CDT2014-11-02 00:30 CDT
502014-11-02 01:30 CDT2014-11-02 01:30 CDT2014-11-02 01:30 CDT
512014-11-02 01:30 CST2014-11-02 01:30 CST2014-11-02 01:30 CST
522014-11-02 02:30 CST2014-11-02 02:30 CST2014-11-02 02:30 CST
53now() -
2015-07-01 01:31 CDT
2015-07-01 01:31 CDT2015-07-01 01:31 CDT
54utc_timestamp() -
2015-07-01 01:31 CDT
2015-07-01 01:31 CDT2015-07-01 01:31 CDT

The Lesson

Avoid a bug and schema migration months from now. Make sure your date/time values are stored in UTC.