第4章 パフォーマンス最適化・クエリ・変換(Performance & Transformation, 21%)
🎯 この節の学習目標
Snowflake は JSON・Avro・ORC・Parquet・XML といった半構造化データを、リレーショナルなテーブルの中で第一級のデータとして扱えます。その受け皿となるのが次の3つのデータ型です。
| データ型 | 格納できるもの | 例 |
|---|---|---|
| VARIANT | あらゆる型の値(オブジェクト・配列・数値・文字列・NULL など何でも)を格納できる汎用型 | JSONドキュメント全体 |
| OBJECT | キーと値のペアの集合(キーは文字列、値は VARIANT) | {"name": "abc", "age": 30} |
| ARRAY | 値の順序付きリスト(要素は VARIANT) | [1, 2, 3] |
実務・試験でもっとも登場するのは VARIANT です。JSON をまるごと1列に格納でき、スキーマを事前に定義せず、読むときに構造を解釈する schema-on-read のアプローチを実現します。重要な制限として、VARIANT 列に格納できる1つの値のサイズ上限は最大128MB(非圧縮データ)です(内部的なオーバーヘッドにより、実際に格納できるサイズはこれよりやや小さくなる場合があります)。これを超える巨大な JSON はロード時に分割する必要があります。なお、この上限は以前は16MB(圧縮後)でしたが、その後128MBへ拡大されました。試験では旧仕様の「16MB」が選択肢に登場する可能性もあるため、「現在は128MB(非圧縮)」と整理して覚えておきましょう。
また、Snowflake は VARIANT に格納された半構造化データの内部で、頻出するパスを自動的に列のように格納(サブカラム化)します。このおかげで、VARIANT 内のフィールドへのクエリでも列指向ストレージやプルーニングの恩恵をある程度受けられ、性能が大きく劣化しません。すべてのパスが同じ型で安定しているデータほど、この自動列化が効きやすくなります。
半構造化データをテーブルに入れる主な方法を整理します。
💡 具体例:文字列からの変換と COPY でのロード
-- 文字列を VARIANT に変換する
CREATE TABLE events (v VARIANT);
INSERT INTO events
SELECT PARSE_JSON('{"user": "u1", "items": [{"sku": "A", "qty": 2}]}');
-- ステージ上の JSON ファイルを COPY INTO でロードする
-- ファイルが [ {...}, {...}, ... ] という外側配列のとき、
-- STRIP_OUTER_ARRAY = TRUE で外側配列を外し、要素ごとに1行としてロードする
COPY INTO events
FROM @json_stage
FILE_FORMAT = (TYPE = 'JSON' STRIP_OUTER_ARRAY = TRUE);
STRIP_OUTER_ARRAY を指定しないと、外側配列全体が1行(1つの巨大な VARIANT)としてロードされ、128MB上限に抵触しやすくなります。「配列を行に分割してロードするオプションはどれか」という形で問われる論点です。
Parquet や Avro のようにスキーマ情報をファイル自身が持つ形式では、INFER_SCHEMA 関数でステージ上のファイルから列定義を検出し、その結果を使って CREATE TABLE ... USING TEMPLATE で構造化テーブルを自動作成することもできます。「VARIANT の1列に入れて後で解釈する」か「最初から列に展開して入れる」かを選べる、と理解しておきましょう。
VARIANT 内の値には、コロン(:)とドット・角括弧のパス記法でアクセスします。取り出した値は VARIANT 型のままなので、比較や表示のために :: で目的の型にキャストするのが基本形です。
-- v = {"user": {"name": "u1"}, "items": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 1}]}
SELECT
v:user.name::STRING AS user_name, -- ネストは ドット でたどる
v:items[0].sku::STRING AS first_sku, -- 配列は [インデックス](0始まり)
v:items[0].qty::NUMBER AS first_qty,
GET_PATH(v, 'user.name') AS same_thing -- 関数形式でも同じパスを取れる
FROM events;
「1行の中の配列」を「複数の行」に変換するのがテーブル関数 FLATTEN です。元のテーブルと組み合わせるときは LATERAL キーワードとともに使い、行ごとにその行の配列を展開して結合します。
-- items 配列の要素を1行ずつに展開する
SELECT
v:user.name::STRING AS user_name,
f.value:sku::STRING AS sku,
f.value:qty::NUMBER AS qty,
f.index AS item_index
FROM events,
LATERAL FLATTEN(input => v:items) f;
-- 配列が空/NULLの行も残したい場合は OUTER => TRUE
SELECT v:user.name::STRING, f.value
FROM events,
LATERAL FLATTEN(input => v:items, OUTER => TRUE) f;
FLATTEN の出力には、展開後の要素が入る value 列のほか、配列内の位置を示す index、キー名(オブジェクト展開時)の key、パスを示す path などの列が含まれます。既定では配列が空または NULL の行は結果から消えますが、OUTER => TRUE を指定すると、その行も value が NULL の行として残ります(外部結合と同じ発想です)。
図:JSON → VARIANT列 → FLATTEN → リレーショナル表への典型的な流れ
逆に、リレーショナルなデータから半構造化データを組み立てる関数も揃っています。アンロード(3-6)で JSON 形式に書き出す前段などで使います。
| 関数 | 働き |
|---|---|
| OBJECT_CONSTRUCT(k1, v1, k2, v2, ...) | キーと値のペアから OBJECT を組み立てる |
| ARRAY_AGG(列) | グループ内の値を集約して ARRAY にまとめる(集計関数) |
| TO_JSON(variant) | VARIANT/OBJECT/ARRAY を JSON文字列に変換する(PARSE_JSON の逆方向) |
💡 具体例:行データからJSONを組み立てる
-- 注文明細を、注文ごとに1つのJSONオブジェクトへまとめる
SELECT
order_id,
TO_JSON(
OBJECT_CONSTRUCT(
'order_id', order_id,
'skus', ARRAY_AGG(sku)
)
) AS order_json
FROM order_items
GROUP BY order_id;
-- 結果例: {"order_id": 1001, "skus": ["A", "B"]}
「FLATTEN は 分解、OBJECT_CONSTRUCT / ARRAY_AGG は 組み立て」という対の関係で覚えると整理しやすくなります。
✅ この節のまとめ
問1. VARIANT データ型の説明として正しいものはどれか。
正解:B
VARIANTはあらゆる型の値を格納できる汎用型で、上限は最大128MB(非圧縮。以前は16MBでしたが拡大されました)です。Aはオブジェクトのみを格納するOBJECT型の説明に近く、VARIANTには配列も格納できます。Cは誤りで、VARIANTは構造を読むときに解釈する schema-on-read を実現します。Dは誤りで、JSONに限らずAvro・ORC・Parquet・XMLなど半構造化形式全般のデータを格納できます。
問2. ステージ上のJSONファイルが「[ {...}, {...}, {...} ]」のように外側が配列になっている。配列の各要素を1行ずつとしてロードするために、JSONファイルフォーマットで指定すべきオプションはどれか。
正解:A
STRIP_OUTER_ARRAY = TRUE は外側の配列を取り除き、要素ごとに1行としてロードします。指定しないと配列全体が1つのVARIANT値(1行)になり、128MB上限にも抵触しやすくなります。BのOUTER => TRUEはFLATTENテーブル関数のオプションで、ロードのオプションではありません。CのINFER_SCHEMAはオプションではなく、Parquet等からスキーマを検出する関数です。Dのようなオプションは存在しません。
問3. VARIANT列 v に {"customer": {"id": 42}, "tags": ["a", "b"]} が入っている。customer の id を数値として取り出す式として正しいものはどれか。
正解:B
VARIANT列から最初のキーへはコロン(v:customer)、その先のネストはドット(.id)でたどり、::NUMBERでキャストします。Aは最初の区切りがドットになっており、列名とパスの区切りとして正しくありません。Cのキャストは「AS 型」ではなく「::型」(またはCAST関数)で書きます。DのFLATTENは配列やオブジェクトを行に展開するテーブル関数で、単一値の取り出しには使いません。
問4. LATERAL FLATTEN(input => v:items) を使ったクエリで、items 配列が空の行が結果から消えてしまう。空配列の行も value を NULL として結果に残すための修正はどれか。
正解:A
FLATTENは既定(OUTER => FALSE)では展開する要素がない行を結果から除外します。OUTER => TRUE を指定すると、外部結合と同様に、空配列やNULLの行も value = NULL の行として保持されます。BのSTRIP_OUTER_ARRAYはCOPY時のファイルフォーマットオプションであり、FLATTENの引数ではありません。CのINNERというキーワードの使い方はありません。Dは逆で、NULL行を除外する条件であり、行を残す目的に反します。