スポンサーリンク

【PostgreSQL】DATE / DATETIME / TIMESTAMP

スポンサーリンク

特徴の違い

出力サポート範囲(MySQL) サポート範囲(PostgreSQL)
DATEYYYY-MM-DD1000-01-01 ~ 9999-12-314713BC ~ 5874897 AD
DATETIMEYYYY-MM-DD hh:mm:ss1000-01-01 00:00:00 ~ 9999-12-31 00:00:00 なし
TIMESTAMP YYYY-MM-DD hh:mm:ss 1970-01-01 00:00:00 ~ 2038-01-19 03:14:07 4713BC ~ 294276AD

MySQLではDATETIME / TIMESTAMPを利用することができますが、サポート範囲がTIMESTAMPでは長期サポートが想定されるソフトウェアでは心許ないので、DATETIMEを使用する方がいいでしょう。

DATEは日付情報のみが必要な場合に利用します。

以降ではPostgreSQLでの例を示します。

Timestampを日付でグループ化したい

PostgreSQLではTimestampを直接dateにcastできますので、以下のようにdateにキャストした列でグループ化することで実現できます。

select timestamp_column::date from some_table group by timestamp_column::date;

Timestampな各日付における最終レコードの値を取得したい

上記の”Timestampを日付でグループ化したい”で説明した方法を利用します。

一度Timestampを日付に集約し、最大のtimestampを求めるサブクエリを作成します。その後、そのtimestampを持つレコードを抽出します。

select * from some_table where timestamp in
(select max(timestamp) from
(select timestamp::date as timestamp_column, timestamp from some_table) mytable
group by timestamp_column);

アプリ側で実装した場合

ちなみに一旦全てのデータを取得してきてから、アプリ側で実装すると以下のようになります。(Kotlin)

getAllDatas()
    .groupBy { data -> data.timestamp.toLocalDateTime().truncatedTo(ChronoUnit.DAYS) }
    .map { entry -> entry.value.maxBy { data -> data.timestamp } }

なお、DB側からアプリ側へのデータ移行が必要なため、レコード数が多い場合には上記のDB側で選択し必要なレコードのみ渡す処理よりも応答性が悪くなります。

JavaにおけるPostgreSQLのTIMESTAMPのマッピング

timestamp without time zone

timestamp without time zoneのマッピングはjava.sql.Timestampではなく、java.time.LocalDateTimeを使用した方がいいです。

理由としてはPostgreSQLに保存される値がシステム自身のTimeZoneの影響を受けるためです。テーブル定義は以下とし、タイムゾーンなしで常にUTCで保存したいとします。

CREATE TABLE timestamps(
     timestamp timestamp(6) NOT NULL
);

例えば以下のように実行したとします。(Kotlinなのは勘弁してください)

val currentTime = System.currentTimeMillis() //これはUTC
val timestamp = Timestamp(currentTime)       //UTCからTimestamp作成

println("currentTime:$currentTime")
println("timestamp.toString():${timestamp.toString()}")
println("timestamp.toGMTString():${timestamp.toGMTString()}")
println("timestamp.toLocaleString():${timestamp.toLocaleString()}")
println("timestamp:${timestamp.time}")

println("stored from database")
dao.insertTimestamp(timestamp)
dao.getLatestInsertedTimestamp().also {
    println("timestamp.toString():${it.timestamp.toString()}")
    println("timestamp.toString():${it.timestamp.time}")
}

val localDateTime = LocalDateTime.ofEpochSecond(timestamp.time / 1000, timestamp.nanos, ZoneOffset.UTC)
dao.insertLocalDateTime( localDateTime  ) 
rateDao.getLatestInsertedLocalDateTime () .also {
    println("localDateTime.toString():${it.timestamp}")
println("localDateTime.localDateTime.toEpochSecond(ZoneOffset.UTC):${it.timestamp.toEpochSecond(ZoneOffset.UTC) * 1000 + it.timestamp.nano / 1000000}")
}

この時の実行結果は以下になります。

currentTime:1670060051609
 timestamp.toString():2022-12-03 18:34:11.609
 timestamp.toGMTString():3 Dec 2022 09:34:11 GMT
 timestamp.toLocaleString():2022/12/03 18:34:11
 timestamp:1670060051609
 stored from database
 timestamp.toString():2022-12-03 18:34:11.609
 timestamp.toString():1670060051609
 localDateTime.toString():2022-12-03T09:34:11.609
 localDateTime.localDateTime.toEpochSecond(ZoneOffset.UTC):1670060051609

そもそもjava.sql.TimestampのtoStringがシステムのTimeZoneを反映して、JST+9の値が表示されています。ただ内部的にはunix timestampを正しく保持しているので、 java.sql.Timestamp とjava.time.LocalDateTime のどちらでも問題ないように見えます。

問題なのはPostgreSQLへの保存のされ方です。保存されたPostgreSQLのデータを確認すると以下のように表示されます。

期待するのはUTC時刻での保存なので、先行して入力した java.sql.Timestamp は期待した動きではありません。おそらくtimestamp.toString()の文字列リテラルを用いてマッピングするためです。

timestamp with time zone

timestamp with time zoneのマッピングはjava.sql.Timestampで問題ないです。java側での利便性を考えてOffsetDateTimeを使用しても問題ありません。

CREATE TABLE timestamps(
     timestamp timestamp(6) with time zone NOT NULL,
);

この場合、システムの持つタイムゾーンを考慮してUTCで保存することが可能です。

原則としてtimestamp with time zoneを使用する

表題通りで原則はこの通りです。わざわざtime zone情報を捨てる理由はありません。UTCでの使用を暗黙的に周知していたとしても、+00:00をつけるだけでそれを強制できるならすべきです。

PostgreSQLのtimezoneの確認方法

ちなみにPostgreSQLのtimezoneは以下のコマンドで確認できます。

show timezone;

PostgreSQLのtimezoneの変更方法

DB単位で変更するのは以下を使用します。

alter database database_name set timezone to 'Asia/Tokyo'

基本的にPostgreSQL全体で変更を適用できるように/var/lib/postgresql/data/postgresql.conf内の以下を変更します

timezone = 'Asia/Tokyo'
log_timezone = 'Asia/Tokyo'

dockerの場合はentry pointに以下の二つを追加する必要があります

environment:
  - TZ=Asia/Tokyo
  - PGTZ=Asia/Tokyo

Intellijのデータベースを使用している場合

IntellijのDatabase Toolはjdbcドライバー単体から動作環境のTimeZoneを取得できないため、自分で設定する必要があります。

これを設定しないと、いくらDockerやSessionのTimezoneを変えても設定したTimezoneで表示してくれません。注意してください。

タイトルとURLをコピーしました