はじめに
私はデータ基盤の運用保守に携わっており、主にバッチ処理まわりの保守や追加開発を担っています。
「Snowfalkeにおいて、パフォーマンスを改善したければウェアハウスを大きくすればよい」——今回の検証では、これは当てはまりませんでした。SQL Server から Snowflake へのバッチ移行で処理時間を決めたのは「処理単位件数(DML 1回あたりに渡すデータ件数)」で、ウェアハウスサイズの差はごくわずかでした。 処理単位件数を小さくすると移行元(SQL Server)の約3倍まで遅くなり、件数を増やすと大きく短縮しました。
SQL Server などから Snowflake へバッチ移行する際、.NET(Snowflake .NET Connector)でのバッチ処理を前提に、ウェアハウスサイズと処理単位件数を振って比較した傾向を共有します。
本記事の数値は、案件の都合により正確な実測値ではなく、傾向を示す概数・相対値です(倍率・削減幅のみ)。絶対的な処理時間は環境(データ構造・ネットワーク・同時実行など)に強く依存するため、ご自身の環境では必ず計測してください。
環境・前提
- OS:Windows(コマンド例・前提は Windows 基準。macOS / Linux は公式をご確認ください)
- 言語/ドライバ:.NET(Snowflake .NET Connector)/ SQL Server 側は
SqlClient- Snowflake .NET Connector
- 実行環境:PowerShell 7.x(Snowflake .NET Connector を PowerShell から利用)。マシンに別途インストールした .NET SDK のバージョンは本バッチの動作に直接は影響しません。
- 対象:既存の SQL Server 向けバッチを Snowflake へ移行したもの
バッチ処理の概要
tsv ファイルを読み取り、ターゲットテーブルへ DML(DELETE / INSERT)を行うバッチです。元々 SqlClient で書かれていた SQL Server 用の処理を、Snowflake .NET Connector を使う処理に置き換えました。
今回扱ったデータ量(1回の取り込み):
| 項目 | 件数(概数) |
|---|---|
| tsv ファイル内のデータ(SJIS) | 約 140 万件 |
| └ DELETE 対象(主キーのみ) | 約 60 万件 |
| └ INSERT 対象 | 約 80 万件 |
DELETE / INSERT のロジック
- DELETE 対象:一時テーブルに「削除対象の主キーカラム」のデータを 配列バインドによるバルクINSERTを実行し、その一時テーブルとターゲットテーブルを主キーで内部結合 → 抽出された行を削除する。
- INSERT 対象:一時テーブルは使わず、ターゲットテーブルへ直接配列バインドによるバルクINSERTを実行する。
配列バインドとは:
SQLインジェクション対策とバルク処理を可能にする仕組みで鵜s。
ドライバがデータを内部の一時ステージ SYSTEM$BIND へ PUT(アップロード)してから取り込む処理を行うことで大量データの扱いも容易にしています。今回もクエリ履歴に、この SYSTEM$BIND ステージへ PUT しているクエリが残っていました。ステージ名そのものは公式ドキュメントでも確認でき、Python Connector の例では裏側で CREATE TEMPORARY STAGE SYSTEM$BIND ... が実行されると記載されています(Python の Batch inserts)。
検証した2つの変数
バッチには ①ウェアハウスサイズ と ②処理単位件数(DML コマンド1回あたりに渡すデータ件数)を引数として渡せるようにしてあり、この2つを振って処理時間を比較しました。
処理単位件数とは: 1回の DML(ここでは主に INSERT)でまとめて渡す行数のことです。.NET をはじめ Snowflake の各ドライバには、複数行を1回のコマンドでまとめて送る仕組み(配列バインド。公式表記では bulk insert / batch insert)があり、その「1回あたりの件数」をここでは処理単位件数と呼んでいます。公式ドキュメントも、この方法で「複数行を1バッチで挿入することで、ネットワークの往復とコンパイルを避けて性能を改善できる」と説明しています(Bind variables)。
検証結果と考察
① ウェアハウスサイズ:ほぼ効かなかった
処理単位件数を固定してウェアハウスサイズだけを変えても、処理時間の差は数分で極わずかでした。今回のデータ量(数十万〜140万件規模)の範囲では、ウェアハウスサイズは処理時間の主因ではありませんでした。
これは「ウェアハウスサイズは無意味」という話ではありません。あくまで今回のデータ量・処理内容の範囲での結果です。より大きなデータでは効いてくる可能性があります。
② 処理単位件数:ここで大きく差が出た
逆に、処理単位件数を変えると処理時間は大きく動きました(傾向)。
| 処理単位件数 | 移行元(SQL Server)比の傾向 |
|---|---|
| 1,000 件 | オーバーヘッドがかさみ 約3倍遅い |
| 1 万件 | 1,000件から**約80%**短縮 |
| 10 万件 | さらに**約20%**短縮(1万件比) |
件数を増やすほど速くなりますが、1,000件→1万件の効きに比べ、1万件→10万件の上積みは小さく、効果は逓減していきました。

処理単位件数と処理時間の関係(SQL Server を 1.0 とした相対イメージ)。1,000件→1万件は大きく短縮するが、1万件→10万件の上積みは小さい。
どの処理が短縮されたのか(内訳)
処理時間を「Snowflake 内部の処理」と「それ以外」に分けて見ると、差が出たのはほぼ前者でした。
- Snowflake 内部の処理(.NET ドライバで SQL コマンドを発行してから、結果がバッチ側に返ってくるまで)… ここが大きく短縮。処理単位件数の効果はほぼここに集中。
- 通信時間 … 短縮(件数をまとめることで往復回数が減るため)。
- tsv ファイル読み込み処理 … ほぼ変化なし(処理単位件数とは独立)。
つまり、処理単位件数を小さくすると DML の発行〜応答の往復が何度も発生し、その積み重ね(オーバーヘッド) が処理時間を押し上げていた、という理解です。件数をまとめることで往復回数が減り、オーバーヘッドが圧縮されたと考えられます。これは、配列バインドが「ネットワークの往復とコンパイルを避ける」という公式の説明とも整合します。
オーバーヘッドのもう一つの側面:小さなマイクロパーティションの統合コスト
Snowflake サポートからは、もう一つの要因として**「小さなマイクロパーティションの統合(再編成)コスト」**も挙げられました。
Snowflake はテーブルのデータを「マイクロパーティション」(圧縮前で 50〜500MB 程度の格納単位)に自動分割し、各パーティションのメタデータ(最小・最大値や件数など)でスキャン対象を枝刈り(プルーニング)します(Micro-partitions & Data Clustering)。
ここで処理単位件数が小さい(例:1,000件)INSERT を繰り返すと、1回あたりに生成されるマイクロパーティションが非常に小さくなります。Snowflake はテーブルの読み取り効率を保つため、こうした小さなマイクロパーティションを**自動的に統合(再編成)**します。
INSERT を繰り返すほど、統合されていない小さなパーティションが増えていくため、後続の INSERT の実行時間が累積的に上昇します。そして自動統合が走って小さなパーティションが整理されると、実行時間が初回と同程度までリセットされ、また少しずつ上昇していく——というのこぎり状の波が、今回の性能テストで確認できました。この挙動は Snowflake サポートにも確認しています。
この波は処理単位件数を大きくしても現れますが、1回に生成されるパーティションが大きく、累積する回数も少ないため、上昇幅もリセットの振れ幅もずっと小さくなります。処理単位件数を大きくしておくほど、この統合コストの影響を抑えられる——これも「件数をまとめると速くなる」理由のひとつです。
処理単位件数は大きいほどよい、わけではない
処理単位件数を大きくすればよい、と単純化はできません。 1回あたりの件数を増やすほどバッチサーバーのメモリ消費が増えるため、メモリの監視が必要です。実務では「速さ」と「メモリ・安定性」のバランスで処理単位件数を決めることになります。
メモリ監視の注意点(.NET): .NET で扱うオブジェクトはマネージドヒープに確保されます。ヒープもプロセスメモリの一部なので OS のメモリ使用量(タスクマネージャーのワーキングセット等)に「まったく出ない」わけではありませんが、正確には追えません。GC は不要になったオブジェクトを回収しても解放した領域をすぐ OS に返さないことがあり、ワーキングセットはヒープの予約分や断片化も含むためです。実際のヒープ使用量を見るには、dotnet-counters の GC Heap Size(System.Runtime)など .NET 専用の計測手段を使うのが確実です(.NET のメモリ管理)。
検証方法と限界
数値の読み方の前提として、今回の計測方法と、性能テストとしての限界を補足しておきます。性能比較の基本は、①変数を1つずつ振る(今回はウェアハウスサイズと処理単位件数を分けて比較)、②同じ条件で複数回まわして中央値などで代表させる、③ウォームアップ(初回のキャッシュ未構築・初期化)を分けて考える、④マシン・ネットワーク・同時実行などの環境をそろえる、の4点です。図にすると、次のような「1変数ずつ・複数回まわす」ループになります。
今回は時間的制約から、②の「同一条件を複数回(例:10回)繰り返してばらつきを平準化する」ところまではできていません。 そのため本記事の数値は、平均や p95 のような統計値ではなく、傾向をつかむための概数・相対値である点にご留意ください(環境差・同時実行・キャッシュ状態などの影響を平準化しきれていません)。
まとめ・学び
- 今回の私の条件では、Snowflake バッチの処理時間を決めたのは ウェアハウスサイズではなく処理単位件数 だった。
- 処理単位件数が小さいと DML 発行〜応答の往復オーバーヘッド がかさみ、移行元の約3倍まで遅くなった。件数をまとめると大きく短縮できたが、効果は逓減する。
- オーバーヘッドの中身は、Snowflake サポートによれば主に2つ。①DML 1回ごとのサイクル(コンパイル→ロック取得→実行→メタデータ更新→コミット)の繰り返しと、②小さなマイクロパーティションの自動統合コストの累積。どちらも「件数をまとめて DML 回数を減らす」ことで効いてくる。
- 速さだけを見て件数を上げると メモリ を圧迫する。速さと安定性のトレードオフで決める。
- 「クラウドだからウェアハウスを上げれば速くなる」とは限らない。まず計測し、効くパラメータを見極める ことが大事だと再認識した。
参考リンク(公式ドキュメント)
-
Bind variables(配列バインド=bulk insert / batch insert、
COPY等の推奨にも言及) — 配列バインドは「複数行を1バッチで挿入し、ネットワーク往復とコンパイルを避けて性能を改善できる」と明記。 - Snowflake .NET Connector — Querying Data(Bind array variables) — .NET ドライバでの配列バインドの書き方。
-
Using the Python Connector(Batch inserts の自動ステージング) — 配列バインドが裏側で一時ステージ
SYSTEM$BINDを作って取り込む挙動の公式記述(CREATE TEMPORARY STAGE SYSTEM$BIND ...)。「値の数が閾値を超えると自動でステージ経由」と説明されるが、実際の発動条件はコネクタ・バージョン依存。 - Micro-partitions & Data Clustering(マイクロパーティションとメタデータ) — テーブルが自動でマイクロパーティションに分割され、メタデータでプルーニングされる仕組み。
-
Load data into Snowflake(推奨されるデータロード方法の入口) — 配列バインドより推奨されるロード方法(
COPY等)の入口。 - Warehouse considerations(ウェアハウスサイズと性能の考え方) — 「大きいほど速いとは限らない」「ロード時間にウェアハウスサイズはほとんど影響しない(ファイルが非常に大きい場合を除く)」と明記。
-
.NET のメモリ管理(マネージドヒープと監視) — GC は解放領域をすぐ OS に返さないことがあり、実ヒープ量は
dotnet-counters等で見るのが確実。