postgres の current_timestamp 関数を使ったクエリをアプリケーションに実装していたところ、本番データベースにタイムゾーンのずれによる時刻の不整合を招きいれてしまった。

TL;DR

sato11/postgres-timezone-pitfall (https://github.com/sato11/postgres-timezone-pitfall)

再現する条件

アプリケーションのタイムゾーンとして UTC を使っていないこと

 筆者の属するプロジェクトでは rails (activerecord) を使っており、タイムゾーンの設定は次のように JST を指定している。ここで UTC を指定しているような場合、特に問題はないだろうと思われる。

# config/application.rb

module SomeApplication
  class Application < Rails::Application
    ...
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    # config.active_record.default_timezone = :utc
    ...
  end
end

データベースに Amazon RDS を利用していること

 RDS のタイムゾーンについて、AWSの公式FAQに次のようなエントリがある。

Amazon RDS DB インスタンスのタイムゾーンを変更する (https://aws.amazon.com/jp/premiumsupport/knowledge-center/rds-change-time-zone/)

すべての Amazon RDS DB インスタンスは、デフォルトで UTC/GMT 時間を使用します。タイムゾーンの変更はオプションです。

データベースレイヤーで UTC タイムゾーンを使用するのがベストプラクティスです。UTC は夏時間 (DST) を持たないため、後でシフトするときに時間を調整する必要はありません。

ローカルタイムゾーンを使用する必要がある場合は、代わりにアプリケーションレイヤーでタイムゾーンを変換します。

より具体的には

UPDATE users SET updated_at = current_timestamp;

のように current_timestamp を直接埋め込んだクエリを発行すると、データベースのタイムゾーン、すなわち UTC が採用されて、アプリケーションのタイムゾーンと齟齬が生じる。

 アプリケーション層でローカル時間を生成してクエリに渡すには、次のようなアプローチをとる必要がある。以下は activerecord を前提に記述するが、やるべきことはどの言語/フレームワークを採用していても変わらないはず。

ActiveRecord::Base.sanitize_sql_array(['UPDATE users SET udpated_at = ?', Time.current])

 もっとも、多くの場合はこのようなまわりくどい書き方をする必要がない可能性も高い。 activerecord であれば、単に user.touch と記述すれば上記のユースケースは満たせる。

 とはいえ筆者の場合、ある程度の大きさのバッチタスクの一部にて、大量のレコードをインスタンス化せずに一括更新する箇所でこの問題に遭遇した。似たような条件に出くわすことはないとはいえず、またそのような場合には十分な注意が必要となる。