BigQueryのSQLいろいろ (2) 文字列型 | Wantedly Engineer Blog
BigQueryのSQLについて、ドキュメントを読んだり実験したりしながら挙動を解き明かしていこうと思います。第2回は文字列型についてです。文字列型と関連する型本稿では以下を扱います。文字列型は...
https://www.wantedly.com/companies/wantedly/post_articles/1010315
Photo by Behnam Norouzi on Unsplash
BigQueryのSQLについて、ドキュメントを読んだり実験したりしながら挙動を解き明かしていこうと思います。第3回は日付・時刻型についてです。
TIMESTAMP型は、地球上で共有される同時性における特定の瞬間をあらわします。ただし、計算の都合上うるう秒は除外されているため、UTCやTAIとの秒単位での対応関係は曖昧です。
TIMESTAMPが表現できる範囲は、遡及グレゴリオ暦の紀元後1年の頭 (ちょうどを含む) から10000年の頭 (ちょうどを含まない) までの9999年間です。閏日の扱いの差により、ユリウス暦基準では1年1月3日からしか扱えません。
TIMESTAMPの精度はマイクロ秒です。
うるう秒は除外されているため、TIMESTAMP型において1日は正確に86400秒です。これはUNIXタイムスタンプの計算方法と同じです。そのため、TIMESTAMP型が表せる範囲は正確に315537897600000000マイクロ秒の範囲です。
TIMESTAMPを表記する際は、RFC3339形式のオフセットつき日付時刻で書かれることが多いです。しかし、実際にTIMESTAMPが保持しているのは瞬間であり、タイムゾーンやオフセットの情報はパース時に破棄されています。
SELECT
-- true
TIMESTAMP '2025-01-02T12:34:56+09:00' =
TIMESTAMP '2025-01-02T03:34:56Z'うるう秒が除外されているため、RFC3339形式のパース時に正のうるう秒は通常の秒に変換されます。
SELECT
-- true
TIMESTAMP '2017-01-01T08:59:60+09:00' =
TIMESTAMP '2017-01-01T09:00:00+09:00'DATETIME型は、名前の通り、日付と時刻の組み合わせを表します。意味的には、時刻を瞬間として解釈するのに必要な情報(地点)が指定されていないため、あくまでもオフセットに対しての相対的な時間概念を表すものとして解釈されます。
ただし、意味的な扱いが異なるだけで、扱える情報の量はTIMESTAMP型とぴったり一致します。UTCを指定してDATETIMEとTIMESTAMPを相互変換するとき、1対1対応します。
実際、以下のように、DATETIMEでも閏秒の情報は捨てられます。
SELECT
-- true
DATETIME '2016-12-31T23:59:60' =
DATETIME '2017-01-01T00:00:00'DATE型は日付を表します。DATETIMEの日付部分と一致します。
なお、日付もまたタイムゾーンに依存した概念であることに注意が必要です。日本の7時において、アメリカではまだ前の日です。
TIME型は1日の間の時刻を表します。DATETIMEの時刻部分と一致します。
TIMEは巡回的な型です。つまり、1秒ずつ足していくといつかは元の場所に戻ってきます。この性質に対して、BigQueryは以下のような実装になっています。
INTERVAL型は2つの日付時刻の間の差を表すデータです。しかしその実態はやや複雑です。
INTERVAL は以下の3つの要素からなります。
それぞれの要素は独立であり、3次元のベクトルのように振舞います。大きさの制限もそれぞれの要素で独立に適用されます。
表示上は、INTERVALの3要素はさらに個別の桁に分解されます。3要素ごとに符号は独立です。
いずれかの要素が0でも全ての要素を文字列化します。できた文字列は空白で連結します。
そのため、「1年」や「1時間」などの単位はあくまで表示上のものであり、それぞれ「12ヶ月」や「3600000000000ナノ秒」と同じものとして扱われます。
INTERVAL はベクトルのように振る舞うため、加減算やスカラー倍の計算を行うことができます。
SELECT
-- 0-11 22 0:0:0.000000033
INTERVAL '0-10 20 0:0:0.000000030' YEAR TO SECOND +
INTERVAL '0-1 2 0:0:0.000000003' YEAR TO SECOND,
-- 0-2 4 0:0:0.000000006'
INTERVAL '0-1 2 0:0:0.000000003' YEAR TO SECOND * 2INTERVALの同値性 (=, !=, IN, NOT IN, GROUP BY など) や大小関係 (<, <=, >, >=, BETWEEN, ORDER BY など) を比較するときの挙動はかなり特殊です。こうしたときは以下のように線形の量に射影して比較を行います。
そのため、内容が異なっていても、比較で同じ結果になるINTERVAL値が存在します。
SELECT INTERVAL 1 MONTH = INTERVAL 30 DAY -- trueしかし、これらは同じように振る舞うわけではありません。
SELECT
-- 1
EXTRACT(MONTH FROM INTERVAL 1 MONTH),
-- 0
EXTRACT(MONTH FROM INTERVAL 30 DAY)INTERVALの要素を取り出すには EXTRACT を使います。これはINTERVALの表示のときに使われるアルゴリズムと概ね同様で、以下のように動作します。
逆に、INTERVALを要素から構成するには MAKE_INTERVAL を使います。このとき、year や month など項別の引数で境界検査を行うのではなく、月数/日数/時間のそれぞれの要素まで計算してから境界検査を行うようです。
また、 JUSTIFY_DAYS, JUSTIFY_HOURS, JUSTIFY_INTERVAL はINTERVAL値の正規化に使えます。これはINTERVAL値の比較と同じく、1ヶ月=30日、1日=24時間 の単位でくり上げるものです。このとき上位の要素と下位の要素の符号は揃えられます。
SELECT
-- 0-0 11 12:0:0
JUSTIFY_INTERVAL(INTERVAL '0-0 10 36:0:0' YEAR TO SECOND),
-- 0-0 11 12:0:0
JUSTIFY_INTERVAL(INTERVAL '0-0 13 -36:0:0' YEAR TO SECOND),
-- 0-0 -11 -12:0:0
JUSTIFY_INTERVAL(INTERVAL '0-0 -10 -36:0:0' YEAR TO SECOND),
-- 0-0 -11 -12:0:0
JUSTIFY_INTERVAL(INTERVAL '0-0 -13 36:0:0' YEAR TO SECOND)日付・時刻の差分は以下のように動作します。
SELECT
-- 0-0 0 9529:1:1
TIMESTAMP '2002-02-02T02:02:02Z' - TIMESTAMP '2001-01-01T01:01:01Z',
-- 0-0 397 1:1:1
DATETIME '2002-02-02T02:02:02' - DATETIME '2001-01-01T01:01:01',
-- 0-0 396 22:58:59
DATETIME '2002-02-02T01:01:01' - DATETIME '2001-01-01T02:02:02',
-- 0-0 -396 -22:58:59
DATETIME '2001-01-01T02:02:02' - DATETIME '2002-02-02T01:01:01',
-- 0-0 -397 -1:1:1
DATETIME '2001-01-01T01:01:01' - DATETIME '2002-02-02T02:02:02',
-- 0-0 397 0:0:0
DATE '2002-02-02' - DATE '2001-01-01',
-- 0-0 0 1:1:1
TIME '02:02:02' - TIME '01:01:01'TIMESTAMPに対するINTERVAL操作は、月数要素がない場合にのみ行えます。1日=24時間として時間を加減算します。
DATETIMEに対するINTERVAL操作は月数要素をサポートしますが、この場合には月によって日数が異なることが問題になります。このような場合は以下の手順で処理します。
SELECT
DATETIME '2025-01-31T00:00:00' + INTERVAL 1 MONTH,
DATETIME '2024-02-29T00:00:00' + INTERVAL 1 YEAR,
DATETIME '2025-01-31T00:00:00' + INTERVAL '0-1 -1 0:0:0' YEAR TO SECOND,
DATETIME '2025-01-31T00:00:00' + INTERVAL '0-1 1 0:0:0' YEAR TO SECOND,
DATETIME '2025-01-30T00:00:00' + INTERVAL '0-1 -1 0:0:0' YEAR TO SECOND,
DATETIME '2025-01-30T00:00:00' + INTERVAL '0-1 1 0:0:0' YEAR TO SECONDDATEに対するINTERVAL操作は、DATETIMEに変換してから実行されます。
TIMEとINTERVALを足す操作はありません。
TIMESTAMP_ADD などの関数は一般のINTERVALを受け取る演算ではありませんが、基本的にはここに挙げたものと同様に動作すると考えられます。
RANGE 型は半開区間をあらわす型で、端点として2つの値をとります。
RANGE はジェネリック型ですが、以下の3種類のインスタンス化のみをサポートします。
一般論としては、区間データ型はを整数などの他のデータ型に対しても使えることが多いです。BigQueryではこの一般論が成り立たず、RANGE型は日付・時刻関連の処理にのみ利用することができます。
RANGE型は、下端が閉(端点を含む)、上端が開(端点を含まない)であるような非空な区間のみを扱えます。空区間は扱えません。
SELECT RANGE<DATE> '[2025-01-01, 2025-01-01)' -- エラー (空区間)いっぽう、非有界な区間を扱うことはできます。これは表記上は UNBOUNDED と記載され、端点の値としては NULL が用いられます。
SELECT
RANGE<DATE> '[2025-01-01, 2025-01-02)', -- 有界
RANGE<DATE> '[2025-01-01, UNBOUNDED)', -- 上に非有界
RANGE<DATE> '[UNBOUNDED, 2025-01-02)', -- 下に非有界
RANGE<DATE> '[UNBOUNDED, UNBOUNDED)' -- 全体集合RANGE は端点の情報を持っているため、同じ長さでも位置によって異なる区間を表します。いっぽう、 INTERVAL は長さの情報のみを持ちます。
なお、数学的には「区間 (interval)」と呼ぶ場合、ここでいう RANGE 相当のものを指します。用語の混乱に注意が必要です。
RANGEの構成には RANGE<T> リテラルまたは RANGE<T> 関数を使い、分解には RANGE_START / RANGE_END を使います。
代数的な演算としては以下が提供されています。
区間から配列を生成する方法として以下も提供されています。
RANGE の同値性 (=, !=, IN, NOT IN, GROUP BY など) は、端点の同値性で表されます。非有界の場合は端点が NULL (=UNBOUNDED) になる可能性がありますが、この場合でも同一なRANGE同士は等しいです。
RANGE の大小関係 (<, <=, >, >=, ORDER BY, BETWEEN など) は、始点と終点の辞書順で表されます。このとき始点のUNBOUNDEDは最小値、終点のUNBOUNDEDは最大値として扱われます。
SELECT * FROM UNNEST([
-- この順にソートされる
RANGE<DATE> '[UNBOUNDED, 2025-01-01)',
RANGE<DATE> '[UNBOUNDED, 2025-01-02)',
RANGE<DATE> '[UNBOUNDED, 2025-01-03)',
RANGE<DATE> '[UNBOUNDED, 2025-01-04)',
RANGE<DATE> '[UNBOUNDED, UNBOUNDED)',
RANGE<DATE> '[2025-01-01, 2025-01-02)',
RANGE<DATE> '[2025-01-01, 2025-01-03)',
RANGE<DATE> '[2025-01-01, 2025-01-04)',
RANGE<DATE> '[2025-01-01, UNBOUNDED)',
RANGE<DATE> '[2025-01-02, 2025-01-03)',
RANGE<DATE> '[2025-01-02, 2025-01-04)',
RANGE<DATE> '[2025-01-02, UNBOUNDED)',
RANGE<DATE> '[2025-01-03, 2025-01-04)',
RANGE<DATE> '[2025-01-03, UNBOUNDED)',
RANGE<DATE> '[2025-01-04, UNBOUNDED)'
]) AS x
ORDER BY x DESCBigQueryの以下の型について、細部の振舞いに注意しながら紹介しました。