JSONやネストしたログをそのままSnowflakeに貯め、必要なときにSQLで取り出す——それを支えるのがVARIANT型です。本記事では、PARSE_JSONによる取り込みから、ドット/ブラケット記法での要素アクセス、LATERAL FLATTENによる配列・オブジェクトの展開、そして自動カラム化を踏まえた格納・パフォーマンス設計までを、AWS上での実運用を前提に整理します。
01なぜ VARIANT が要るのか
アプリのイベントログ、SaaSのWebhook、IoTのテレメトリ、外部APIのレスポンス——現場に入ってくるデータの多くは、行と列にきれいに収まらないJSONやネストした構造を持っています。これらを扱うたびにスキーマを固定し、項目が増減するたびにテーブルを作り替えるのは現実的ではありません。SnowflakeのVARIANT型は、こうした半構造化データを「まず原本のまま格納し、後からSQLで必要な形に取り出す」というスキーマ・オン・リード的なアプローチを可能にします。取り込み時点で構造を決め切らなくてよいため、仕様変更に強く、データを失いません。
02VARIANT・OBJECT・ARRAY と格納の基本
Snowflakeの半構造化型は三つあります。VARIANTは任意のJSON値(数値・文字列・真偽・オブジェクト・配列)を1カラムに保持できる汎用型、OBJECTはキーと値のマップ、ARRAYは順序付きの値の並びです。実務ではまずVARIANTで受け、必要に応じてOBJECT/ARRAYとして扱うのが素直です。1つのVARIANT値のサイズ上限は、長らく16MB前後が目安とされていましたが、2025年の最大サイズ拡張(BCR)により既定で最大128MBまで扱えるようになりました(内部オーバーヘッドがあるため実効値はやや小さくなります)。とはいえ、上限に近い巨大な文書を1行に詰めるのは更新コストの面で不利になりがちなので、大きすぎる塊は分割を検討します。
AWS上での取り込みは、S3を外部ステージとして登録し、COPY INTOやSnowpipe(S3イベント通知をトリガにした継続ロード)で行うのが定番です。ストレージ統合(Storage Integration)にIAMロールを紐づければアクセスキーを埋め込まずに済み、PrivateLinkやKMS暗号化と組み合わせて閉域・暗号化された経路でロードできます。
03JSONをVARIANTにする:PARSE_JSON と TRY_PARSE_JSON
文字列として入ってきたJSONをVARIANTに変換する基本関数がPARSE_JSONです。PARSE_JSON('{"id":42}') のように渡すと、解釈済みのVARIANTが返ります。ここで重要なのが、壊れたJSONに対する振る舞いです。PARSE_JSONは不正な入力に対してクエリ全体をエラーにしますが、TRY_PARSE_JSONは解析に失敗した行でエラーを投げずにNULLを返します。外部ソースのデータは一定の割合で壊れているのが常なので、取り込みパイプラインではTRY_PARSE_JSONで受けて、NULLになった行だけを隔離テーブルへ退避する設計が堅牢です。
PARSE_JSONのままだとバッチ全体が落ちると分かったため、本番ではTRY_PARSE_JSONと失敗行の隔離に切り替え、取り込みを止めない構成にしています。04要素にアクセスする:ドット記法・ブラケット記法・型キャスト
VARIANTの中身は二通りの記法でたどれます。ドット記法は col:level1.level2 のようにコロンとドットでパスを指定します。キーに空白や記号が含まれる場合や動的に組み立てたい場合は、ブラケット記法 col['level1']['level2'](シングルクオート)が便利です。配列要素は col:tags[0] や col:customer[0].name のように0始まりの添字で取り出します。ここで注意したいのが大文字小文字で、カラム名は大文字小文字を区別しませんが、JSONの要素名(キー)は区別します。col:Id と col:id は別物です。
パスをたどって得られる値は既定でVARIANTのままなので、比較・集計・結合に使うときは目的の型へキャストします。col:id::number、col:name::string のように::で型付けするのが定石です(CASTやTRY_CASTも使えます)。文字列としてキャストして取り出すと、後述するJSON null(文字列としての"null")を通常のNULLへ寄せられる副次効果もあります。
05配列とネストを展開する:LATERAL FLATTEN
1行のVARIANTに配列が入っているとき、その各要素を「行」に展開したい場面は非常に多くあります。ここで使うのが表関数FLATTENです。FLATTENはVARIANT・OBJECT・ARRAYを受け取り、要素ごとに1行を生成するラテラルビュー(インラインの相関ビュー)を返します。出力は固定の列で構成され、SEQ(入力レコードの連番)・KEY(オブジェクトのキー)・PATH(要素へのパス)・INDEX(配列の添字)・VALUE(要素の値)・THIS(展開対象そのもの)が得られます。
ベーステーブルの列と相関させて展開するためにLATERALと組み合わせ、SELECT ... FROM src, LATERAL FLATTEN(input => src.data:tags) f のように書きます。ネストが深い場合はFLATTENを連鎖させ、外側のVALUEをさらにFLATTENします。空配列やNULLの親行を残したいときはOUTER => TRUE(左結合のように親行を保持)、階層すべてを一気に展開したいときはRECURSIVE => TRUE、対象をオブジェクト/配列に絞るときはMODEを指定します。
06格納とパフォーマンスの考え方:自動カラム化と落とし穴
「全部VARIANTに入れておけば楽」というのは半分正解です。Snowflakeは、VARIANTに入ったデータから、アクセスしやすいパスを可能な範囲で自動的にサブカラムとしてカラム化し、内部的に列指向で保持します。値の型やキー構成が安定していれば、VARIANT経由でも型付き列に近い性能とプルーニングが効きます。逆に、行ごとに型が混在していたり、キーが可変で膨大だったりすると自動抽出が効きにくく、クエリのたびに構造全体を読むことになり遅くなります。
特に注意すべきは非ネイティブ値です。JSONの日付やタイムスタンプはVARIANT内では文字列として格納されるため、範囲検索でパーティションプルーニングが効きにくく、スキャン量・コストが膨らみます。もう一つがJSON null と SQL NULL の違いで、VARIANT内のJSON nullは文字列としての"null"で保持され、通常のNULLとは区別されます。件数や条件が合わないときの定番の原因です。TO_VARCHARで文字列化するとJSON nullは通常のNULLへ寄り、IS_NULL_VALUEでJSON null自体を判定できます。
col:ordered_at::timestamp_ntzで専用のTIMESTAMP列へ抽出し、フィルタとクラスタリングをそこへ寄せることで、スキャン量とコストを未然に抑えています。07設計パターン:RAWはVARIANT、分析は型付き列
結論として、実務では役割で層を分けるのが安定します。RAW層は、S3から届いた原本をVARIANTのまま着地させ、監査・再処理に備えて欠損なく保持します。変換層でFLATTENや::キャストにより必要な項目を型付き列へ展開し、分析層では型付きの相対列としてBIやMLへ供給します。変換の実行基盤には、依存関係と増分更新を自動で管理するDynamic Tablesを使うと、スケジュールとメンテナンスを簡素化できます。
この分離により、「原本を失わない柔軟さ」と「型付き列の速さ・安さ」を両立できます。どこまでをVARIANTのまま持ち、どこから型付き列へ落とすかは、クエリの当たり方(頻繁にフィルタ・結合する項目か)で決めるのが実践的です。頻繁に条件やキーに使う項目ほど早めに型付き列へ、めったに触らない付帯情報はVARIANTに残す——この線引きが、Snowflakeで半構造化データを扱いこなす勘所です。
参考情報(一次情報)
- Semi-structured data types | Snowflake Documentation
- Querying semi-structured data | Snowflake Documentation
- PARSE_JSON | Snowflake Documentation
- FLATTEN | Snowflake Documentation
- Considerations for semi-structured data stored in VARIANT | Snowflake Documentation
- New maximum size limits for database objects | Snowflake Documentation
半構造化データの取り込み設計からパフォーマンス最適化まで、EMWがAWS×Snowflakeの実装・運用を一気通貫でご支援しますので、お気軽にご相談ください。
相談する