[新機能]Snowflakeで期間の管理・比較ができるデータ型「PERIOD」を試してみた
さがらです。
Snowflakeの10.32リリースで、PERIODデータ型がGeneral Availabilityになりました。
PERIODデータ型を使うと、「いつからいつまで有効か」という時間範囲を、開始日時・終了日時の2カラムに分けるのではなく、1つの値としてまとめて扱えるようになります。
今回は、ECサイトの顧客契約・料金プランの履歴管理を想定し、顧客ごとの料金プランがいつ有効だったのかをPERIOD(DATE)で管理してみます。
従来の開始日・終了日カラムを使った管理と比較しながら、期間の重複判定や指定日時点の契約状況の確認など、実際のユースケースに沿って試してみます。
機能概要
PERIODデータ型は、同じ要素型を持つ開始値と終了値からなる、時間軸上の特定範囲を1つの値として保持するデータ型です。
要素型として、以下の5種類を指定できます(TIME・TIMESTAMP系はスケール0〜9も指定可能です)。
PERIOD(DATE)PERIOD(TIME)PERIOD(TIMESTAMP_NTZ)PERIOD(TIMESTAMP_LTZ)PERIOD(TIMESTAMP_TZ)
単なる経過時間の長さを表すINTERVALとは異なり、PERIODは「タイムライン上のどこからどこまでか」という具体的な位置を持ちます。
例えば、以下のような情報の管理に向いています。
- 顧客の契約有効期間
- 商品価格の適用期間
- 従業員の在籍期間
- キャンペーンの実施期間
- マスターデータのバージョン有効期間
PERIOD値の作り方は主に3通りあります。
-- 1. PERIOD_CONSTRUCT関数
SELECT PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-04-01');
-- 2. 型付きリテラル
SELECT PERIOD(DATE) '[2026-01-01, 2026-04-01)';
-- 3. 文字列からのキャスト
SELECT '[2026-01-01, 2026-04-01)'::PERIOD(DATE);
主な関数は以下のとおりです(詳細は公式Docを参照ください)。
| 分類 | 関数 | 概要 |
|---|---|---|
| 構築・アクセス | PERIOD_CONSTRUCT |
開始値・終了値からPERIODを構築 |
| 構築・アクセス | PERIOD_BEGIN / PERIOD_END |
開始値(含む)・終了値(含まない)を取得 |
| 比較 | PERIOD_CONTAINS |
別のPERIODや日時を含むか判定 |
| 比較 | PERIOD_OVERLAPS |
2つのPERIODが重複するか判定 |
| 比較 | PERIOD_MEETS |
2つのPERIODが隣接しているか判定 |
| 比較 | PERIOD_EQUALS |
2つのPERIODの境界が一致するか判定 |
| 比較 | PERIOD_PRECEDES / PERIOD_SUCCEEDS |
一方がもう一方より完全に前・後にあるか判定 |
| 比較 | PERIOD_IMMEDIATELY_PRECEDES / PERIOD_IMMEDIATELY_SUCCEEDS |
一方がもう一方に隙間なく先行・後続するか判定 |
| 集合演算 | PERIOD_INTERSECT |
重複部分を取得(重複しない場合はNULL) |
| 集合演算 | PERIOD_LDIFF / PERIOD_RDIFF |
他方が始まる前・終わった後に残る部分を取得 |
今回の検証では、この中から実際のユースケースに絡む代表的な関数をいくつか使ってみます。
半開区間 [begin, end) として扱われる
PERIODは、開始値を含み、終了値を含まない半開区間です。
[開始値, 終了値)
例えば、以下の2つの期間を考えます。
[2026-01-01, 2026-04-01)
[2026-04-01, 2026-07-01)
この2つの期間は2026年4月1日で接していますが、重複はしていません。前半の期間は2026年4月1日を含まず、後半の期間は2026年4月1日を含むためです。
この形式にしておくと、料金プランの切り替えや契約更新のように、期間が連続するデータを扱いやすくなります。
他のDWH・DB製品との比較
期間を専用のデータ型・機能として扱える製品は、Snowflake以外にも存在します。せっかくなので、簡単に分かる範囲で調べてみました。
BigQuery
BigQueryにはRANGEデータ型があります。
RANGE<DATE> '[2026-01-01, 2026-04-01)'
BigQueryのRANGEも、下限を含み上限を含まない半開区間です。要素型としてDATE・DATETIME・TIMESTAMPを指定できます。
SnowflakeのPERIODと似ていますが、BigQueryではUNBOUNDEDまたはNULLを指定することで、片側が無期限の範囲を直接表現できます。一方、SnowflakeのPERIODでは無限値やNULL境界はサポートされておらず、「現在も有効」を表現する場合は9999-12-31のような遠い将来の日付を終端値とする運用でカバーする必要があります。
PostgreSQL
PostgreSQLにはRange Typesがあります。
代表的な組み込み型として、以下が用意されています。
daterangetsrangetstzrangeint4rangeint8rangenumrange
PostgreSQLのRange Typesは、日付・タイムスタンプだけでなく整数や数値の範囲も扱える点、さらに各Range Typeに対応するdatemultirangeのようなMultirange Type(不連続な範囲の集合)が自動的に提供される点が特徴です。
SELECT daterange('2026-01-01', '2026-04-01', '[)');
また、境界の包含・除外を[・](含む)や(・)(含まない)で自由に組み合わせられます。SnowflakeのPERIODは[begin, end)の半開区間に固定されており、この点はPostgreSQLの方が柔軟です。
事前準備
検証用スキーマを作成
まず、検証用のデータベースとスキーマを作成します。環境に合わせて、既存のデータベース・スキーマを利用しても問題ありません。
CREATE OR REPLACE DATABASE PERIOD_SAMPLE_DB;
CREATE OR REPLACE SCHEMA PERIOD_SAMPLE_DB.PUBLIC;
USE DATABASE PERIOD_SAMPLE_DB;
USE SCHEMA PUBLIC;
契約・料金プラン履歴テーブルを作成
顧客ごとの料金プラン履歴を管理するテーブルを作成します。従来であれば、VALID_FROM・VALID_TOという2カラムで定義することが多いと思います。
今回は、開始値と終了値をまとめてVALID_PERIODカラムに格納します。
CREATE OR REPLACE TABLE CUSTOMER_PLAN_HISTORY (
CUSTOMER_ID NUMBER,
PLAN_VERSION NUMBER,
PLAN_NAME VARCHAR,
MONTHLY_FEE NUMBER(10, 2),
VALID_PERIOD PERIOD(DATE)
);
DESC TABLE CUSTOMER_PLAN_HISTORY;
VALID_PERIODの型がPERIOD(DATE)と表示されていれば、テーブル作成は完了です。

試してみた
1. 検証用データを登録する
顧客ごとに料金プランが切り替わるケースを想定し、ダミーデータを登録します。
-- 顧客料金プランの検証用データ
-- PERIODは半開区間 [開始日, 終了日) として登録する
-- CUSTOMER_ID=8のみ、期間が重複する不正データを意図的に含む
INSERT INTO CUSTOMER_PLAN_HISTORY
(CUSTOMER_ID, PLAN_VERSION, PLAN_NAME, MONTHLY_FEE, VALID_PERIOD)
VALUES
-- 顧客1: FREE -> STANDARD -> PREMIUMへ切り替わる履歴
(1, 1, 'FREE', 0.00, PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-03-01')),
(1, 2, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-03-01', DATE '2026-06-01')),
(1, 3, 'PREMIUM', 2980.00, PERIOD_CONSTRUCT(DATE '2026-06-01', DATE '9999-12-31')),
-- 顧客2: STANDARDを継続利用、途中で値上げ
(2, 1, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-01-15', DATE '2026-07-01')),
(2, 2, 'STANDARD', 1080.00, PERIOD_CONSTRUCT(DATE '2026-07-01', DATE '9999-12-31')),
-- 顧客3: PREMIUMからSTANDARDへダウングレード
(3, 1, 'PREMIUM', 2980.00, PERIOD_CONSTRUCT(DATE '2026-02-01', DATE '2026-05-01')),
(3, 2, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-05-01', DATE '2026-10-01')),
(3, 3, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-10-01', DATE '9999-12-31')),
-- 顧客4: 短期間だけTRIALを利用
(4, 1, 'TRIAL', 0.00, PERIOD_CONSTRUCT(DATE '2026-03-01', DATE '2026-03-15')),
(4, 2, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-03-15', DATE '2026-09-01')),
(4, 3, 'PREMIUM', 2980.00, PERIOD_CONSTRUCT(DATE '2026-09-01', DATE '9999-12-31')),
-- 顧客5: 長期間PREMIUMを利用、年末に値上げ
(5, 1, 'PREMIUM', 2980.00, PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-12-31')),
(5, 2, 'PREMIUM', 3480.00, PERIOD_CONSTRUCT(DATE '2026-12-31', DATE '9999-12-31')),
-- 顧客6: 料金改定を複数回実施
(6, 1, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-04-01')),
(6, 2, 'STANDARD', 1180.00, PERIOD_CONSTRUCT(DATE '2026-04-01', DATE '2026-08-01')),
(6, 3, 'STANDARD', 1280.00, PERIOD_CONSTRUCT(DATE '2026-08-01', DATE '9999-12-31')),
-- 顧客7: キャンペーン期間のみ特別プランを適用
(7, 1, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-04-01')),
(7, 2, 'CAMPAIGN', 500.00, PERIOD_CONSTRUCT(DATE '2026-04-01', DATE '2026-05-01')),
(7, 3, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-05-01', DATE '9999-12-31')),
-- 顧客8: 登録ミスにより期間が重複した不正データ
(8, 1, 'STANDARD', 980.00, PERIOD_CONSTRUCT(DATE '2026-01-01', DATE '2026-06-01')),
(8, 2, 'PREMIUM', 2980.00, PERIOD_CONSTRUCT(DATE '2026-05-01', DATE '2026-08-01'));
2. PERIOD値の表示形式と、境界値を取り出す関数を確認する
登録したPERIOD値をそのまま確認します。
SELECT
CUSTOMER_ID,
PLAN_VERSION,
PLAN_NAME,
VALID_PERIOD,
PERIOD_BEGIN(VALID_PERIOD) AS VALID_FROM,
PERIOD_END(VALID_PERIOD) AS VALID_TO
FROM CUSTOMER_PLAN_HISTORY
WHERE CUSTOMER_ID = 1
ORDER BY PLAN_VERSION;
PERIODは[開始値, 終了値)という表示形式で返ってきます。PERIOD_BEGIN・PERIOD_ENDを使うと、従来のVALID_FROM・VALID_TOカラムに近い形で開始日・終了日を個別に取り出せます。

3. 指定した日付時点で有効な料金プランを取得する
実際のユースケースとして、2026年5月15日時点で有効な料金プランを確認してみます。
SELECT
CUSTOMER_ID,
PLAN_NAME,
MONTHLY_FEE,
VALID_PERIOD
FROM CUSTOMER_PLAN_HISTORY
WHERE PERIOD_CONTAINS(VALID_PERIOD, DATE '2026-05-15')
ORDER BY CUSTOMER_ID;
PERIOD_CONTAINSは、PERIODが指定した日時を含んでいるかを判定する関数です。このクエリでは、2026年5月15日を含む料金プランだけが取得されます。
例えば顧客1の場合、[2026-03-01, 2026-06-01)は取得されますが、2026年5月15日より前に終了している[2026-01-01, 2026-03-01)は取得されません。

4. 半開区間の境界値の挙動を確認する
PERIODの重要なポイントである、半開区間の動作を確認します。顧客1の料金プランには、以下の2つの隣接する期間があります。
[2026-01-01, 2026-03-01)
[2026-03-01, 2026-06-01)
2026年3月1日時点で有効なプランを検索します。
SELECT
PLAN_VERSION,
PLAN_NAME,
VALID_PERIOD,
PERIOD_CONTAINS(VALID_PERIOD, DATE '2026-03-01') AS CONTAINS_2026_03_01
FROM CUSTOMER_PLAN_HISTORY
WHERE CUSTOMER_ID = 1
ORDER BY PLAN_VERSION;
終了値である2026年3月1日は前の期間には含まれず、次の期間の開始値としてのみ含まれます。この挙動のおかげで、期間を隙間なく隣接させた料金プランの切り替えを、境界値の重複なしに管理できます。

5. キャンペーン期間と料金プランの重複を判定する
2026年4月1日から2026年6月1日まで実施するキャンペーンを想定し、この期間と料金プランの有効期間が重複している顧客を探します。
WITH campaign AS (
SELECT PERIOD_CONSTRUCT(DATE '2026-04-01', DATE '2026-06-01') AS CAMPAIGN_PERIOD
)
SELECT
h.CUSTOMER_ID,
h.PLAN_NAME,
h.VALID_PERIOD,
PERIOD_OVERLAPS(h.VALID_PERIOD, c.CAMPAIGN_PERIOD) AS IS_OVERLAPPED,
PERIOD_INTERSECT(h.VALID_PERIOD, c.CAMPAIGN_PERIOD) AS OVERLAPPED_PERIOD
FROM CUSTOMER_PLAN_HISTORY h
CROSS JOIN campaign c
ORDER BY h.CUSTOMER_ID, h.PLAN_VERSION;
PERIOD_OVERLAPSは2つのPERIODが1点でも重複しているかを判定し、PERIOD_INTERSECTは実際に重複している部分を返します。
例えば顧客3の[2026-02-01, 2026-05-01)は、キャンペーン期間[2026-04-01, 2026-06-01)と部分的に重複しており、OVERLAPPED_PERIODには[2026-04-01, 2026-05-01)が返ります。

6. 隣接している期間と重複している期間を区別する
料金プランの履歴管理では、「期間が隙間なく隣接しているだけ」なのか「期間が重複してしまっている」のかを区別したいケースがあります。
WITH history_with_prev AS (
SELECT
CUSTOMER_ID,
PLAN_VERSION,
PLAN_NAME,
VALID_PERIOD,
LAG(VALID_PERIOD) OVER (
PARTITION BY CUSTOMER_ID ORDER BY PLAN_VERSION
) AS PREVIOUS_PERIOD
FROM CUSTOMER_PLAN_HISTORY
)
SELECT
CUSTOMER_ID,
PLAN_VERSION,
PLAN_NAME,
PREVIOUS_PERIOD,
VALID_PERIOD,
PERIOD_MEETS(PREVIOUS_PERIOD, VALID_PERIOD) AS IS_ADJACENT,
PERIOD_OVERLAPS(PREVIOUS_PERIOD, VALID_PERIOD) AS IS_OVERLAPPED
FROM history_with_prev
WHERE PREVIOUS_PERIOD IS NOT NULL
ORDER BY CUSTOMER_ID, PLAN_VERSION;
PERIOD_MEETSは2つのPERIODが隙間なく隣接しているかを判定する関数です。正常に切り替わっている顧客1〜7ではIS_ADJACENT = TRUE・IS_OVERLAPPED = FALSEとなります。
一方、意図的に不正データを入れた顧客8では、以下のようにIS_ADJACENT = FALSE・IS_OVERLAPPED = TRUE`になり、期間の重複が検出できます。

このように、PERIODの比較関数を使うと、SCD Type 2のような履歴データに対する期間の重複チェックを、開始日・終了日の大小比較を自前で書かずに実装できます。
7. TIMESTAMP_NTZを要素型にしたPERIODでオペレーター勤務時間を管理する
最後に、日付単位ではなく時刻単位でPERIODを使うケースも試しておきます。コールセンターのオペレーター勤務時間を想定し、PERIOD(TIMESTAMP_NTZ)のテーブルを作成します。
CREATE OR REPLACE TABLE OPERATOR_SHIFT (
OPERATOR_ID NUMBER,
SHIFT_PERIOD PERIOD(TIMESTAMP_NTZ)
);
INSERT INTO OPERATOR_SHIFT
(OPERATOR_ID, SHIFT_PERIOD)
VALUES
(101, PERIOD_CONSTRUCT('2026-09-15 09:00:00'::TIMESTAMP_NTZ, '2026-09-15 17:00:00'::TIMESTAMP_NTZ)),
(102, PERIOD_CONSTRUCT('2026-09-15 13:00:00'::TIMESTAMP_NTZ, '2026-09-15 21:00:00'::TIMESTAMP_NTZ));
2026年9月15日15時時点で勤務中のオペレーター、および2人の勤務時間の重複部分を確認します。
SELECT
a.OPERATOR_ID AS OPERATOR_A,
b.OPERATOR_ID AS OPERATOR_B,
PERIOD_OVERLAPS(a.SHIFT_PERIOD, b.SHIFT_PERIOD) AS IS_OVERLAPPED,
PERIOD_INTERSECT(a.SHIFT_PERIOD, b.SHIFT_PERIOD) AS OVERLAPPED_SHIFT
FROM OPERATOR_SHIFT a
JOIN OPERATOR_SHIFT b
ON a.OPERATOR_ID < b.OPERATOR_ID;

このように、契約期間だけでなく、勤務時間や設備の稼働時間のような時刻単位の範囲管理にもPERIODを利用できることが確認できました。
最後に
SnowflakeのPERIODデータ型を使って、顧客の料金プラン履歴を管理することを試してみました。
今回のような料金プランの履歴管理は、開始日・終了日を個別カラムで管理する従来の方法でも実装できます。一方で、PERIODを使うと期間同士の重複・包含関係を専用の関数で表現できるため、BETWEENや不等号を組み合わせた自前のロジックを書かずに済み、クエリの可読性が上がると感じました。
特に、SCD Type 2の履歴管理、契約・料金プランの有効期間管理、キャンペーン期間と契約期間の照合、勤務時間・設備稼働時間の管理などのユースケースでは効果を発揮しそうです。
一方で、2026年9月16日時点ではVARIANT内での利用、Icebergテーブルでの利用などに制限があります。「現在も有効」を表す期間には9999-12-31のような終端値を使う必要があるため、実際に導入する際は、アプリケーションやデータ連携処理側でこの終端値のルールをあらかじめ決めておく必要がありそうです。
うまく使えれば便利なデータ型だと思います、ぜひご検討ください。




