[新機能]Snowflakeで期間の管理・比較ができるデータ型「PERIOD」を試してみた

[新機能]Snowflakeで期間の管理・比較ができるデータ型「PERIOD」を試してみた

Snowflakeの10.32リリースで正式リリースされたPERIODデータ型を使って、顧客の料金プラン履歴管理を試してみました。開始日・終了日の2カラムではなく1つの値で時間範囲を表現でき、期間の重複判定や指定日時点の契約確認が簡潔に実装できます。
2026.09.16

さがらです。

Snowflakeの10.32リリースで、PERIODデータ型がGeneral Availabilityになりました。

PERIODデータ型を使うと、「いつからいつまで有効か」という時間範囲を、開始日時・終了日時の2カラムに分けるのではなく、1つの値としてまとめて扱えるようになります。

https://docs.snowflake.com/en/release-notes/2026/10_32

https://docs.snowflake.com/en/sql-reference/data-types-period

https://docs.snowflake.com/en/sql-reference/functions-period

今回は、ECサイトの顧客契約・料金プランの履歴管理を想定し、顧客ごとの料金プランがいつ有効だったのかをPERIOD(DATE)で管理してみます。

従来の開始日・終了日カラムを使った管理と比較しながら、期間の重複判定や指定日時点の契約状況の確認など、実際のユースケースに沿って試してみます。

機能概要

PERIODデータ型は、同じ要素型を持つ開始値と終了値からなる、時間軸上の特定範囲を1つの値として保持するデータ型です。

要素型として、以下の5種類を指定できます(TIMETIMESTAMP系はスケール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も、下限を含み上限を含まない半開区間です。要素型としてDATEDATETIMETIMESTAMPを指定できます。

SnowflakeのPERIODと似ていますが、BigQueryではUNBOUNDEDまたはNULLを指定することで、片側が無期限の範囲を直接表現できます。一方、SnowflakeのPERIODでは無限値やNULL境界はサポートされておらず、「現在も有効」を表現する場合は9999-12-31のような遠い将来の日付を終端値とする運用でカバーする必要があります。

https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types

PostgreSQL

PostgreSQLにはRange Typesがあります。

代表的な組み込み型として、以下が用意されています。

  • daterange
  • tsrange
  • tstzrange
  • int4range
  • int8range
  • numrange

PostgreSQLのRange Typesは、日付・タイムスタンプだけでなく整数や数値の範囲も扱える点、さらに各Range Typeに対応するdatemultirangeのようなMultirange Type(不連続な範囲の集合)が自動的に提供される点が特徴です。

SELECT daterange('2026-01-01', '2026-04-01', '[)');

また、境界の包含・除外を[](含む)や()(含まない)で自由に組み合わせられます。SnowflakeのPERIODは[begin, end)の半開区間に固定されており、この点はPostgreSQLの方が柔軟です。

https://www.postgresql.org/docs/current/rangetypes.html

事前準備

検証用スキーマを作成

まず、検証用のデータベースとスキーマを作成します。環境に合わせて、既存のデータベース・スキーマを利用しても問題ありません。

CREATE OR REPLACE DATABASE PERIOD_SAMPLE_DB;

CREATE OR REPLACE SCHEMA PERIOD_SAMPLE_DB.PUBLIC;

USE DATABASE PERIOD_SAMPLE_DB;
USE SCHEMA PUBLIC;

契約・料金プラン履歴テーブルを作成

顧客ごとの料金プラン履歴を管理するテーブルを作成します。従来であれば、VALID_FROMVALID_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)と表示されていれば、テーブル作成は完了です。

2026-09-16_05h57_16

試してみた

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_BEGINPERIOD_ENDを使うと、従来のVALID_FROMVALID_TOカラムに近い形で開始日・終了日を個別に取り出せます。

2026-09-16_05h59_22

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)は取得されません。

2026-09-16_05h59_59

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日は前の期間には含まれず、次の期間の開始値としてのみ含まれます。この挙動のおかげで、期間を隙間なく隣接させた料金プランの切り替えを、境界値の重複なしに管理できます。

2026-09-16_06h01_06

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)が返ります。

2026-09-16_06h04_07

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 = TRUEIS_OVERLAPPED = FALSEとなります。

一方、意図的に不正データを入れた顧客8では、以下のようにIS_ADJACENT = FALSE・IS_OVERLAPPED = TRUE`になり、期間の重複が検出できます。

2026-09-16_06h06_26

このように、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;

2026-09-16_06h12_35

このように、契約期間だけでなく、勤務時間や設備の稼働時間のような時刻単位の範囲管理にもPERIODを利用できることが確認できました。

最後に

SnowflakeのPERIODデータ型を使って、顧客の料金プラン履歴を管理することを試してみました。

今回のような料金プランの履歴管理は、開始日・終了日を個別カラムで管理する従来の方法でも実装できます。一方で、PERIODを使うと期間同士の重複・包含関係を専用の関数で表現できるため、BETWEENや不等号を組み合わせた自前のロジックを書かずに済み、クエリの可読性が上がると感じました。

特に、SCD Type 2の履歴管理、契約・料金プランの有効期間管理、キャンペーン期間と契約期間の照合、勤務時間・設備稼働時間の管理などのユースケースでは効果を発揮しそうです。

一方で、2026年9月16日時点ではVARIANT内での利用、Icebergテーブルでの利用などに制限があります。「現在も有効」を表す期間には9999-12-31のような終端値を使う必要があるため、実際に導入する際は、アプリケーションやデータ連携処理側でこの終端値のルールをあらかじめ決めておく必要がありそうです。

うまく使えれば便利なデータ型だと思います、ぜひご検討ください。


Snowflake Community Awards ファイナリストに選出されました

DevelopersIO で Snowflake 記事を執筆している かわばた が、Snowflake Community Awards「RISING COMMUNITY LEADER OF THE YEAR」部門・APJ枠のファイナリストに選ばれました。
最終選考の30%はコミュニティ投票です。記事がお役に立っていたようでしたら、9月15日(火)までにぜひ一票お願いします。フォームの「(4 of 6) RISING COMMUNITY LEADER OF THE YEAR」で Tomohiro Kawabata | Classmethod, Japan を選択、2分ほどで完了します。

投票フォームを開く


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事