データベースにおけるタイムスタンプ型とは日時を表現するためのデータ型です。レコードの作成日時や更新日時の管理など、特定の時間や日付を正確に記録する必要がある場合に使用されます。
多くのデータベースではタイムスタンプ型がタイムゾーンの情報を持つことができます。たとえばPostgreSQL やMySQL などでは、タイムゾーン付きのタイムスタンプ型 (TIMESTAMP WITH TIME ZONEやTIMESTAMPTZ) をサポートしており、これにより異なるタイムゾーン間での時間の変換や比較が容易になります。一方、タイムゾーンなしのタイムスタンプ型 (TIMESTAMP WITHOUT TIME ZONE) も存在し、特定のタイムゾーンに依存しない日時を扱いたい場合に使用されます。
本記事で取り上げるのはSnowflake のタイムスタンプ型です。Snowflake のタイムスタンプ型には3つのバリエーションがあります。それぞれのデータ型は異なる用途や特徴を持っています。
1. TIMESTAMP_LTZ (ローカルタイムゾーン付きタイムスタンプ)
Snowflake 内部でUTC 時間として保存され、クエリ実行時のセッションタイムゾーンに基づいて日時が取得されます。グローバルなアプリケーションで、異なるタイムゾーンから挿入されるデータの一貫性を保つために使用されます。
2. TIMESTAMP_NTZ (タイムゾーンなしタイムスタンプ)
Snowflake 内部でタイムゾーン情報なしのwall-clock 時間として保存され、取得時は保存されたそのままの日時として取得されます。タイムゾーンに依存しない日時を扱いたい場合に使用されます。
3. TIMESTAMP_TZ (タイムゾーン付きタイムスタンプ)
Snowflake 内部でUTC 時間と関連するタイムゾーンオフセットを保存し、各レコードに固有のタイムゾーンオフセットで取得されます。特定のタイムゾーンに関連する日時データの保存と表示に使用されます。
Snowflake 上でINSERT やUPDATE を実行し日時データを登録・更新すると、セッションタイムゾーンによって日時の変換が自動的に行われますが、これが意図しない結果を招くことがあります。これを防ぐ方法として、ALTER SESSION SET TIMEZONE 句でセッションタイムゾーンを設定する方法や、タイムゾーンを考慮しないTIMESTAMP_NTZ 型のカラムを使うといった回避策があります。
一方、Snowflake のJDBC ドライバーを使った操作では、JVM 実行環境のタイムゾーンの考慮も必要なため、日時変換のメカニズムがさらに複雑になります。そのためSnowflake に登録したい日時を、ドライバーを通して期待通り登録できないといった問題が頻繁に起こります。本記事ではそれらの問題に対し、目的の日時データを登録する方法や登録した日時と同じ値を取得する方法を紹介します。
ここからの例ではドライバー実行環境のJVM のデフォルトタイムゾーンを「Asia/Tokyo」(UTC+9:00)、Snowflake のセッションタイムゾーンを「America/Los_Angeles」(UTC-5:00) として説明します。
ドライバーでの日時データ登録の課題
例えばドライバー実行環境のJVM のタイムゾーンがAsia/Tokyo で、以下のコードを実行しTIMESTAMP_NTZ 型のカラムCOL_TIMESTAMP_NTZ に「2023-6-15 21:00:00」を登録します。
String query = "INSERT INTO TEST(COL_TIMESTAMP_NTZ) VALUES(?)";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setTimestamp(1, Timestamp.valueOf("2023-6-15 21:00:00"));
pstmt.executeUpdate();
上記コードを実行した後、Snowflake 上でCOL_TIMESTAMP_NTZ の値を取得すると「2023-06-15 07:00:00.000」が返されます。
これは、以下の処理の結果と推測できます。
1. ドライバーがローカルタイムゾーン「Asia/Tokyo」(UTC+9:00) の日時「2023-6-15 21:00:00」をUTC「2023-06-15 12:00:00」に変換してSnowflake に送信します。
2. Snowflake は受信した値「2023-06-15 12:00:00」をセッションタイムゾーン「America/Los_Angeles」(UTC-5:00) の日時に変換し、「2023-06-15 07:00:00」を登録します。
ここで、ドライバーでCOL_TIMESTAMP_NTZ の日時を取得するとSnowflake から取得した時と同様に「2023-06-15 07:00:00」が返ってきます。つまり、ドライバーを使って「2023-06-15 21:00:00」で登録したデータが、同じドライバーを介して「2023-06-15 07:00:00」として取得されることになります。これはSnowflake やJDBC ドライバーの仕様にもとづく結果ですが、プログラムやツールでドライバーを使う際には不便です。しかし、以下から紹介するようにドライバーの設定によってこの不一致を回避する方法があります。
UTC の日時として登録する方法
データ連携ツールによっては全ての日時データをUTC として扱うものがあります。そのようなツールでドライバーを使うときは入力値をUTC として登録・取得できると便利です。Snowflake に送られたUTC の日時データをセッションタイムゾーンに変換せず登録する方法として、CLIENT_TIMESTAMP_TYPE_MAPPING セッションパラメータを使う方法があります。
CLIENT_TIMESTAMP_TYPE_MAPPING セッションパラメータは、java.sql.Timestamp 型のデータをステートメントにバインドするときに、指定したSnowflake のTIMESTAMP 型に変換します。このセッションパラメータに、タイムゾーンを考慮しない型「TIMESTAMP_NTZ」を設定することで、送信された日時をそのまま登録することができます。Snowflake JDBC ドライバーでセッションパラメータを設定する場合、以下のようにプロパティに追加します。
Properties properties = new Properties();
~その他プロパティ設定~
properties.put("CLIENT_TIMESTAMP_TYPE_MAPPING","TIMESTAMP_NTZ");
String connectStr = "jdbc:snowflake://test.us-east-1.snowflakecomputing.com";
Connection conn = DriverManager.getConnection(connectStr, properties);
また、セッションタイムゾーンを指定するTIMEZONE セッションパラメータを「UTC」に設定することでも同じ結果が得られます。
CLIENT_TIMESTAMP_TYPE_MAPPING セッションパラメータの詳しい説明
CLIENT_TIMESTAMP_TYPE_MAPPING セッションパラメータはSnowflake に送信するTimestamp 型データを制御します。デフォルトのパラメータ値はTIMESTAMP_LTZ です。「2023-06-15 21:00:00」を送信すると、Snowflake に送信される日時はローカルタイムゾーンの時差が引かれた「2023-06-15 12:00:00」となります。また、パラメータ値がTIMESTAMP_LTZ の場合はSnowflake 側でセッションタイムゾーンへの変換が行われ、「2023-06-15 07:00:00」が登録されます。一方パラメータ値としてTIMESTAMP_NTZ を指定すると、送信される日時データはローカルタイムゾーンの時差を考慮せず「2023-06-15 21:00:00」となり、さらにSnowflake 側でセッションタイムゾーンへの変換が行われません。そのため、受信した「2023-06-15 21:00:00」がそのまま登録されます。
ローカルタイムゾーンの日時として登録する方法
入力した日時をUTC やセッションタイムゾーンへ変換せずそのままの日時で登録する方法として、文字列でバインディングする方法と、TIMEZONE セッションパラメータを指定する方法を紹介します。
文字列でバインディングする
以下のように、ステートメントへのバインド時にsetString() メソッドで文字列を設定するとそのままの日時が登録できます。最もシンプルな方法ですが、バインディングのデータ型を変更できないツールではこの方法は適用できません。
pstmt.setString(2, "2023-6-15 21:00:00");
TIMEZONE セッションパラメータを指定する
「UTC の日時として登録する方法」で紹介したTIMEZONE セッションパラメータを使い、TIMEZONE にローカルタイムゾーンを指定することで当該タイムゾーンの日時として登録することができます。例えばローカルタイムゾーンがAsia/Tokyo の環境で実行する場合、以下のようにTIMEZONE セッションパラメータに「Asia/Tokyo」を設定します。
Properties properties = new Properties();
~その他プロパティ設定~
properties.put("TIMEZONE","Asia/Tokyo");
String connectStr = "jdbc:snowflake://test.us-east-1.snowflakecomputing.com";
Connection conn = DriverManager.getConnection(connectStr, properties);
この設定で日時データ「2023-6-15 21:00:00」を登録すると以下のように動作します。
1. ドライバーがローカルタイムゾーン「Asia/Tokyo」(UTC+9:00) の日時「2023-6-15 21:00:00」をUTC「2023-06-15 12:00:00」に変換してSnowflake に送信します。
2. Snowflake は受信した値「2023-06-15 12:00:00」をセッションタイムゾーン「Asia/Tokyo」(UTC+9:00) に変換し、「2023-06-15 21:00:00」を登録します。
TIMEZONE セッションパラメータはTIMESTAMP_TZ 型にも有効です。パラメータに「Asia/Tokyo」を設定し、TIMESTAMP_TZ 型カラムに日時を登録すると以下のように「2023-06-15 21:00:00 +0900」が登録されます。
ドライバーで登録した日時データと同じ値を取得する方法
TIMESTAMP_NTZ 型カラムに対しドライバーを使って日時データを取得すると、Snowflake で取得する時と同じ日時が返されます。「ドライバーでの日時データ登録の課題」で説明したように、ドライバーで「2023-6-15 21:00:00」を入力するとSnowflake に「2023-06-15 07:00:00」が登録されますが、これをドライバーで取得すると入力した日時と異なる「2023-06-15 07:00:00」が返されます。ドライバーで登録する日時データと取得するデータを合わせる方法の一つとして、上の記事で説明した「ローカルタイムゾーンの日時として登録する方法」があります。
一方、Snowflake 上に格納されている値は気にせず、とにかく「ドライバーで登録する値と、ドライバーで取得する値を同じ日時にできればいい」といった場合はTIMESTAMP_LTZ 型の利用をお勧めします。この方法ではセッションパラメータは使用しません。
TIMESTAMP_LTZ 型はタイムゾーン付きの日時を保持します。このカラムの値をドライバーで取得するとローカルタイムゾーンに変換された値が返ってきます。以下のようにTIMESTAMP_LTZ 型のカラム COL_TIMESTAMP_LTZ に「2023-6-15 21:00:00」を登録すると、TIMESTAMP 型と同様に実行環境でのUTC への変換 (-9:00) およびセッションタイムゾーンへの変換 (-5:00) が行われ、「2023-06-15 07:00:00.000 -0500」が登録されます。
String query = "INSERT INTO TEST(COL_TIMESTAMP_LTZ) VALUES(?)";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setTimestamp(1, Timestamp.valueOf("2023-6-15 21:00:00"));
pstmt.executeUpdate();
このカラムの日時を以下のようにドライバーで取得すると逆の変換が行われ「2023-6-15 21:00:00」が返ってきます。
String query = "SELECT COL_TIMESTAMP_LTZ FROM TEST";
Statement stmt = conn.createStatement();
stmt.executeQuery(query);
ResultSet rs = stmt.getResultSet();
while(rs.next()){ System.out.println(rs.getString(1)); // rs.getTimestamp(1)も同じ値}
タイムスタンプ値をUTCで取得する方法
Snowflake に格納されているタイムスタンプ値をUTC で取得したい場合、基本的にはUTC として格納されるTIMESTAMP_NTZ 型カラムを使います。Snowflake のJDBC ドライバーではTIMESTAMP_LTZ 型カラムの値をUTC として取得することは困難です。CData のCData Snowflake JDBC Driver では、Other プロパティに「ConvertDateTimeToGMT=True」を設定することで、TIMESTAMP_LTZ 型カラムの値をUTC として取得することができます。
おわりに
本記事ではSnowflake が提供するJDBC ドライバーで目的の日時データを登録する方法や、登録した日時データと同じ値を取得する方法を紹介しました。