MySQLのtime_zoneとgo-sql-driver/mysqlの設定

(2018-10-02)

MySQLのtime_zoneとgo-sql-driver/mysqlの設定による挙動を確認する。

> select version();
    +-----------+
    | version() |
    +-----------+
    | 5.7.21    |
    +-----------+

タイムゾーンがロードされていない場合はロードする。

> select count(*) from mysql.time_zone \\G;
*************************** 1. row ***************************
count(*): 0

$ mysql_tzinfo_to_sql /usr/share/zoneinfo/ | mysql -u root mysql

time_zoneはデフォルト値のSYSTEM。つまりJSTで、my.cnfのdefault-time-zoneで変更できる。 NOW()もJSTの時間を返している。

> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | SYSTEM |
+------------------+--------+

> SELECT NOW();
mysql> SELECT NOW() \G;
*************************** 1. row ***************************
NOW(): 2018-10-02 20:26:29

DATETIMEはそのまま格納され返される。 TIMESTAMPはUTCに変換して格納され、 返すときにtime_zoneに変換される。 したがってtime_zoneを変更するとDATETIMEは変わらず、TIMESTAMPは変わる。

> CREATE TABLE t (
    dt DATETIME,
    ts TIMESTAMP
);
> INSERT INTO t VALUES (NOW(), NOW());
> select * from t \G;
*************************** 1. row ***************************
dt: 2018-10-02 20:27:13
ts: 2018-10-02 20:27:13

> SET SESSION time_zone = "UTC";
> select NOW() \G;
*************************** 1. row ***************************
NOW(): 2018-10-02 11:27:56

> select * from t \G;
*************************** 1. row ***************************
dt: 2018-10-02 20:27:13
ts: 2018-10-02 11:27:13

go-sql-driver/mysql

go-sql-driver/mysqlで Data Source Nameにlocとtime_zoneを付けて実行してみる。 time.Localの影響を確認するためUTCでもJSTでもないUS/Alaskaにしている。

package main

import (
    "database/sql"
    "fmt"
    "math"
    "time"

    _ "github.com/go-sql-driver/mysql"
)
    
const format = "2006-01-02 15:04:05 Z0700"
    
func main() {
    var err error
    if time.Local, err = time.LoadLocation("US/Alaska"); err != nil {
        panic(err)
    }
    now := time.Now()
    fmt.Printf("%15s: %s\n", "time.Now()", now.Format(format))
    fmt.Println("* none")
    run(now, "root:@/hoge?parseTime=true")
    fmt.Println("* loc (mysql's timezone != Local timezone)")
    run(now, "root:@/hoge?parseTime=true&loc=Local")
    fmt.Println("* loc & time_zone")
    run(now, "root:@/hoge?parseTime=true&loc=Local&time_zone=%27US%2FAlaska%27")
}

func run(now time.Time, src string) {
    db, err := sql.Open("mysql", src)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    if _, err := db.Exec("DELETE FROM t"); err != nil {
        panic(err)
    }

    if _, err := db.Exec("INSERT INTO t VALUES (NOW(), NOW())"); err != nil {
        panic(err)
    }

    if _, err := db.Exec("INSERT INTO t VALUES (?, ?)", now, now); err != nil {
        panic(err)
    }

    rows, err := db.Query("SELECT dt, ts FROM t")
    if err != nil {
        panic(err)
    }
    i := 0
    title := []string{"mysql NOW()", "go time.Now()"}
    for rows.Next() {
        var datetime, timestamp time.Time
        if err := rows.Scan(&datetime, &timestamp); err != nil {
            panic(err)
        }
        fmt.Printf("%15s: %s, %s -> %v\n",
            title[i],
            datetime.Format(format),
            timestamp.Format(format),
            math.Abs(float64(datetime.Unix()-now.Unix())) < 10,
        )
        i++
    }
}

locはtime.Timeのタイムゾーンで、付けないとUTCになる。 これはMySQLサーバーのtime_zoneには影響せず、NOW()の値はLocalと異なりJSTなので値がおかしくなる。 一方、time.Now()の方はタイムゾーンが考慮されているので値自体は正しい。 time_zoneを付けるとSET time_zoneしてくれてNOW()の値も正しくなる。

$ go run main.go
        time.Now(): 2018-10-02 03:43:13 -0800
* none
    mysql NOW(): 2018-10-02 20:43:13 Z, 2018-10-02 20:43:13 Z -> false
    go time.Now(): 2018-10-02 11:43:14 Z, 2018-10-02 11:43:14 Z -> true
* loc (mysql's timezone != Local timezone)
    mysql NOW(): 2018-10-02 20:43:13 -0800, 2018-10-02 20:43:13 -0800 -> false
    go time.Now(): 2018-10-02 03:43:14 -0800, 2018-10-02 03:43:14 -0800 -> true
* loc & time_zone
    mysql NOW(): 2018-10-02 03:43:13 -0800, 2018-10-02 03:43:13 -0800 -> true
    go time.Now(): 2018-10-02 03:43:14 -0800, 2018-10-02 03:43:14 -0800 -> true