PostgreSQLのドキュメント 8.5.1.3. タイムスタンプ
には以下のような仕様が書かれています。
timestampz
の内部に格納されている値は
UTC
である
入力文字列にタイムゾーンが指定されていれば、そのタイムゾーンを元にUTCに変換され保持される
timestampz
の値を取得すると、UTCから現行のタイムゾーンに変換されて表示される
1,2 は timestamp
with time zone
という名称から、書き込み時のタイムゾーンも保持していると勘違いしちゃいがちですが、実際はそうじゃないよと認識すればOKです。理解できました。
個人的には、3は少しややこしいかなと思います。現行のタイムゾーンとは、すなわちDBセッションで有効なタイムゾーンを用いられると考えられ、確かに
to_char()
で文字列化したときや、
psql
など一部のクライアントツール(いわゆるテキストフォーマットでやり取りする場合)に対しては正しいです。一方で、
jackc/pgx
のドライバー経由でDBを利用するクライアントアプリの世界から見ると、これは適用されません(理由を先に書くと、バイナリフォーマットではタイムゾーン情報を送信しないからです)。
lib/pq から jackc/pgx への移行
GoとPoatgreSQLでCOPY
package main
import ( "context" "fmt" "log" "time"
"github.com/jackc/pgx/v5" )
func main() { ctx := context.Background()
connURL := "postgres://postgres:pass@localhost:5432/postgres?sslmode=disable" conn, err := pgx.Connect(ctx, connURL) if err != nil { log.Fatalf("pgx connect: %v", err) } defer conn.Close(ctx)
jstZone := time.FixedZone("Asia/Tokyo", 9*60*60)
args := [][]any{ {"0001", time.Now()}, {"0002", time.Now().In(jstZone)}, }
copyCount, err := conn.CopyFrom(ctx, pgx.Identifier{"event"}, []string{"event_id", "event_at"}, pgx.CopyFromRows(args))
if err != nil { log.Fatalf("copy exec: %v", err) }
fmt.Printf("copy: %d\n", copyCount)
}
|
実行して以下のような実行結果が出れば登録できました。
https://pkg.go.dev/time#Location
動かすと、
0001
はUTC、
0002
はJSTにしたtime.Timeの値をDBに登録したのですが、結果は
どちらもUTC
になっていることがわかります。
$ TZ=UTC go run . 0001 2023-10-21T12:04:20Z
0002 2023-10-21T12:04:20Z
|
OSがWindowsの場合は
TZ
環境変数を読み込んでくれなかったので、tzutilコマンドで切り替えます。こちらも当然結果は同じく、2レコードとも、AtフィールドがUTCのタイムゾーンとなっていることが確認できます。
$ tzutil /s "UTC"
$ go run . 0001 2023-10-21T12:04:20Z 0002 2023-10-21T12:04:20Z
$ tzutil /s "Tokyo Standard Time"
|
今度はタイムゾーンをJSTに登録すると2レコードともJSTのタイムゾーンになることが確認できます(Windows側の実行例は割愛します)。
$ $ TZ=Asia/Tokyo go run . 0001 2023-10-21T21:04:20+09:00 0002 2023-10-21T21:04:20+09:00
|
つまり、最初に書いた挙動をすることがわかります。
timestampz カラムをGoのアプリで読み取りする時は、
Go側のタイムゾーン設定に依存
する(環境変数
TZ
や端末のタイムゾーンなど、time.Timeパッケージの仕様の値が用いられる)
DBのセッションで有効になっているタイムゾーンが、Goアプリの time.Time のタイムゾーンで利用されるわけでもない
まして、書き込み時に利用したタイムゾーンが使われるわけでもない
GoでJSTのタイムゾーンを扱う方法
の記事にあるように、DATA RACEする可能性があるようです。回避するためにブランクimportなどの措置も面倒なので万能の解では無いですが、お手軽ではあります(私もよく利用してしまいます)。
個別対応になるため設定し忘れが怖いですが、プロジェクトルールとして
time.In()
を設定するという決めにするというのもあります。
var jst = time.FixedZone("Asia/Tokyo", 9*60*60)
func main() { for _, e := range events { fmt.Printf("%s %s\n", e.ID, e.At.In(jst)) }
|
書き込み時はともかく、読込み時もタイムゾーンを指定しないとならないのは、少し釈然としないところもありますが、忘れないようにしましょう。
PostgreSQLのTimeZoneを理解する
が詳しいのでそちらも参照ください。
この記事で簡単にざっくり説明しますと、次の優先度で決まります。
SET TIMEZONE TO 'xxx'
で指定された値
コネクション接続時に指定された値
postgresql.confに書かれたデフォルト値
SET timezone TO …と等価である
とありますが、DB側のログにはSET timezone To … が出てこなかったので、接続時に指定していると思われます)。
環境変数
PGTZ
でのタイムゾーンを
UTC
にした、
psql
コマンドを利用する例です。
$ PGTZ=UTC PGPASSWORD=pass psql -h localhost -p 5432 -U postgres -c 'select * from event;' event_id | event_at ----------+------------------------------- 0001 | 2023-10-21 12:04:20.445974+00 0002 | 2023-10-21 12:04:20.445974+00 (2 rows)
|
ちなみに、DBeaver 23.2.2 では、
set timezone to 'UTC'
などをしても
timestampz
カラムを表示する際に利用するタイムゾーンに変わりはありませんでした(
+0900
のまま)。DBeaverはローカルのタイムゾーンを利用するため、もしローカル端末のタイムゾーンと、DBのタイムゾーンが異なる場合は、
dbeaver.ini
に
-Duser.timezone=xxx
を追加して、タイムゾーンを一致させる必要があるようです。
Scanning of timestamp without time zone forces UTC #924
を読んだところ、いくつか課題があるようです。
PostgreSQL側で持つタイムゾーンと、クライアント側で持つタイムゾーンに互換性があるとは限らない
夏時間のため単純にテキストから時刻に変換すると、壊れる可能性がある
これらの理由のため、対応は難しいようです。PRコントリビュートチャンスかと思いましたが、やはり簡単ではないですね…。
How do you set the timezone connection variable? #520
が上がっていますが、少なくてもpgxにはそのような機能は無いようです。あまり探していませんがそういったライブラリが無いような気がします(みんな、
TZ
で指定するか、
time.Local
に設定している?)。pgxは拡張性が高いパッケージなので、下回りを操作すれば実現できるかもしれませんが、私は試していません。こうやれば良いよと言うのがあればぜひ教えてください。
select current_setting('TIMEZONE')
で取得したタイムゾーンを、
time.In()
に設定して欲しい気持ちはよくわかります。
https://www.postgresql.jp/docs/15/datatype-datetime.html
https://scientre.hateblo.jp/entry/20150407/datetime_with_time_zone
https://anakage.com/blog/how-to-setup-time-zone-in-windows-system/
https://tutuz-tech.hatenablog.com/entry/2021/01/30/192956
https://github.com/jackc/pgx/issues/520
https://stackoverflow.com/questions/72771272/how-to-setup-pgx-to-get-utc-values-from-db
https://www.postgresql.org/docs/current/protocol-overview.html#PROTOCOL-FORMAT-CODES
-
はじめに
-
timestampz の仕様
-
timestampz を扱う際の留意事項
-
環境構築
-
検証用のテーブル作成
-
Go経由でDB操作
-
データ書き込み
-
データ読み込み
-
接続URLにタイムゾーンを設定すると?
-
Goの設定として
-
FAQ
-
DBセッションで用いるタイムゾーンってどう決まるの?
-
psql などのSQLクライアントとして用いると、DB側のタイムゾーンを利用しているようですが?
-
timestampz だけPostgreSQLのDBサーバからテキストフォーマットで受け取れば、セッションのタイムゾーン付きで受信できるため、それを元に time.Time にタイムゾーンを指定すればよいでは?
-
DB接続時のセッションで有効なタイムゾーンってGoアプリの場合、どこに影響するの?
-
DB接続セッションのタイムゾーン値を利用してクエリ結果を time.Time 型のフィールドにマッピングするときに自動で設定する実装をしたい
-
go-sql-driver/MySQL みたいな loc=Asia%2FTokyo” オプションは無いの?
-
まとめ
-
参考
カテゴリー
Programming (539)
Infrastructure (321)
Culture (115)
DataScience (82)
IoT (37)
DB (33)
DevOps (29)
Business (26)
Management (23)
認証認可 (22)
Security (22)
VR (15)
Design (11)