❄️

Snowflakeのプルーニングとかクラスタリングって結局なんなの?

2022/12/20に公開約6,200字

本記事はSnowflake Advent Calendar 2022の11日目です。

すみません、大遅刻しました!ただ、遅刻したおかげで(?)この記事の主役となるマイクロパーティションに関する素晴らしい解説がholywater044さんによってAdvent Calendar12日目の記事として公開され、なんとなく勝手にコラボっぽくなった気がしています。そもそもマイクロパーティションとは何かを詳しく知りたいという方はぜひ以下を先にご参照ください!
https://zenn.dev/holywater044/articles/972876054c75bc

加筆・修正

2022/12/21:プルーニングのクエリプロファイル上での見え方を画像付きで加筆しました。クラスタ化のためのDDLに誤りがあったため修正しました。

はじめに

基本的にSnowflakeは何もしなくても非常に高速です。テーブルスペースやらインデックスやら分散キーやらパーティションやらを物理設計する必要はほぼありません。「もう少し性能欲しいなー」と思ったら、ウェアハウスサイズを上げれば大体のことは解決します。

しかし、ある程度巨大なデータ量を保持するテーブルであったり、厳しい性能目標を達成することが必要だったりすると、チューニングしたくなってくることもあります。

各種キャッシュの利用や、マテリアライズドビューや検索最適化(Search Optimization)等の機能もありますが、最も根本的な対処は、Snowflakeのアーキテクチャ上の大きな特徴でありパフォーマンスの根本である「マイクロパーティション」の構造を活かし、その構成を操ることで性能を向上させることです。そのためには、プルーニングとクラスタリングの仕組みをしっかりと理解することが必要です。

この記事では、マイクロパーティションの構造を活かしたプルーニングとクラスタリングの概念と仕組みを紹介します。特に新機能なわけでも、キラキラ事例なわけでもないですが、Snowflakeを使い込んできている方は改めて知っておいて損のない、基礎的だけど大事な内容です。

ただし、あまり大きくないサイズのデータであったり、実行頻度の高くないクエリの場合は、この検討をしている人件費よりささっとWHサイズを上げたほうがずっと経済的である、ということがしばしば起こり得ることも事実です。何もしなくてもある程度速いからこそSnowflakeには価値があるという面もあるため、仕組みはちゃんと理解したうえで、やりすぎには十分注意しましょう。

マイクロパーティションの構造

マイクロパーティションについての全体的な解説は前述のholywater044さんの記事を参照していただきたいのですが、プルーニング/クラスタリングの観点でいうと、以下の点が重要なポイントとなります。

  • マイクロパーティションの分割単位は「行」であること。
  • マイクロパーティションの内部では「列」ごとにデータがまとめて圧縮されていること。

Snowflakeはカラムナ型のDWHと呼ばれることもありますが、このように実は行と列のハイブリッド形式である、ということです。これにより、列方向の圧縮率が高く必要な列のみを高速でスキャンできることに加えて、行方向での絞り込みも高速化することができます。その仕組みが、プルーニングやクラスタリングです。

また、以下も非常に大きなポイントです。

  • 各マイクロパーティションの各列のメタデータが中央管理されていること。

Snowflakeのクラウドサービスレイヤでは、各マイクロパーティションのデータ量や行数はもちろん、マイクロパーティション内の「列ごと」のメタデータについても様々管理しています。特に、各列に格納されている値の最大値・最小値のメタデータは、プルーニングの大きな鍵となります。

プルーニングとは

前述のとおり、各マイクロパーティションの各カラムごとに保持している最大値・最小値などのメタデータを利用して行われるSnowflakeのパフォーマンス向上がプルーニングです。

プルーニングとは枝刈、剪定といった意味で、不要とわかっているパーティションを間引く(読みに行かない)ことで、ストレージIOを減らし、処理対象データを少なくして性能向上するための技術です。対象データが含まれるパーティションを積極的に当てに行くのではなく、絶対にないと判断できるパーティションを読み飛ばす機能というイメージですね。

例えば、取引ID(transaction_id)、顧客ID(customer_id)、店舗ID(store_id)、取引日(transaction_date)を持つ取引テーブル(transaction_table)に対し、以下のSQLを実行します。

select transaction_id, customer_id, store_id, transaction_date 
from transaction_table
where transaction_date = '2022-12-11';

Snowflakeのオプティマイザはまずメタデータを見て、このテーブルのtransaction_dateカラムの各マイクロパーティション内の最大値・最小値を探索します。具体的には、最大値が'2022-12-11'よりも大きく、かつ最小値が'2022-12-11'よりも小さいパーティションだけを抽出します。その情報をもとに実行計画が立てられ、WHが起動して実際にパーティションを読みに行く際は、対象のデータが存在するかもしれないパーティションのみにアクセスし、存在しないことが分かっているパーティションには一切IOしません。

クエリプロファイルの統計欄には以下のように、テーブル全体のパーティション数「パーティションの合計」に対し、実際にアクセスしたパーティション数「スキャン済みパーティション」が小さく表示されるはずです。

このように、メタデータを活かしてオプティマイザがアクセスを最適化してくれる機能がプルーニングです。

プルーニングは何もしなくても使える

一つのマイクロパーティションに収まる行の構成は、基本的にロードやINSERTしたデータの順番そのままで、一度に書き込んだデータがひとまとまりになります。[1]

これにより、プルーニングという機能は、ユーザが特に意識していなくても利用することができます。

例えば前述の例の取引日(transaction_date)という日付カラムを考えてみます。1日分のデータが1つのパーティション以上のデータ量であることを想定すると、毎日その日の取引分のデータをロードするようなテーブルの場合、このカラムは自然と同じ値が同じマイクロパーティションに格納され、毎日違うマイクロパーティションが生成されていくことになります。

すると、日付を指定したSQLにおいて、各マイクロパーティションの最大値・最小値の判定が行われ、指定した日以外のパーティションを読む必要がないことがわかり、プルーニングが効くことになります。(これをナチュラルクラスタリングと以前は呼んでいましたが、今はあまり言われなくなったようです。)

つまり、時系列で蓄積されるテーブルにおいて、日付、タイムスタンプ、インクリメンタルな数値などは、何もしなくてもその順番の通りに並んでパーティションが形成されていくため、自然とプルーニングが効くことになります。

プルーニングには設定という概念はなく、自動的に勝手に行ってくれるため、もちろん利用は無料です。一方で、プルーニングが効くかどうかで、性能ひいてはSnowflakeの利用コストも大きく変わってきます。

よくできた並列分散処理は、データ量に対して線形に処理量が増える、つまり時間やコストがかかっていくものです。しかしプルーニングが効くと、データ量が増えてもそのうちの一部のみを処理対象とすれば良いため、データ量と処理時間・コストが比例しません。爆発的なデータの増加に対して、コストを増加させずにデータをさばける、非常に重要な機能と言えます。

クラスタリングとは

時系列テーブルにおいてインクリメントするカラムでの絞り込みは自然とプルーニングできることはわかりましたが、絞り込みたいのは必ずしもそういったカラムだけではありません。例えば、前述の取引テーブルを参照するBIツールにおいて、常に店舗IDで絞り込みを行うというとき、ナチュラルクラスタリングだけでは常に全てのパーティションに実際にアクセスして確認しなければならなくなります。

そんなとき、任意のカラムをキーにしてパーティションの再編成を行い、プルーニングを効きやすくする機能が「クラスタリング」です。
https://docs.snowflake.com/ja/user-guide/tables-clustering-keys.html

例えば、先ほどの例のテーブルであれば、以下のクエリは店舗IDという自然にはインクリメントされないであろうカラムで絞込を行っているため、通常プルーニングは効きません。

select transaction_id, customer_id, store_id, transaction_date 
from transaction_table
where store_id = 'A001';

クエリプロファイルを見れば、すべてのパーティションをスキャンしてしまっていることがわかります。

そこで以下のようにクラスタリングキーを設定してみます。(alter tableで設定していますが、create table時点でも設定できます)

alter table transaction_table cluster by (store_id);

このDDLを実行すると、クラスタリングキーが設定されたことで、自動クラスタリングが非同期で実行され始めます。クラスタリング実行中でもテーブルには全く制約なくすべての操作を行うことができますが、裏ではソートによるマイクロパーティションファイルの作り替えと置き換えが発生しています。

クラスタリングが完了してから改めて同じクエリを投げてみると、今度はプルーニングが効いています。(クラスタリングによって総パーティション数にも若干変化が出ています)

ちなみに、このように大きなテーブルを一度にクラスタリングするとそれなりに時間がかかりますが、残念ながらクラスタリングの実行の途中経過を見ることはできません。ただし、SYSTEM$CLUSTERING_INFORMATIONSYSTEM$CLUSTERING_DEPTHという関数を使えば、クラスタリングが完了したかどうかを見ることは可能です。クラスタリングが完了するとパーティションの重複深度を示す値が一気に小さくなります。

クラスタリングの基本的な考慮点

クラスタリングはIOを最適化してフルスキャンを避けるという意味で、インデックスのような働きをするとも言えます。ただ、インデックスと異なり、メタデータのみならずデータそのものの配置を再編成しているということに大きな注意が必要です。

一つはコスト上の問題です。クラスタリングキーを設定すると、自動でクラスタリングが行われ、新しいデータが入ってきたらバックグラウンドで大量データの並び替え処理が常に発生することになります。これは大きなコストとなり得るため、そこでかかるコスト以上に参照時の性能が向上することを確認する必要があります。めったに参照されないクエリのためにこのコストをかけることは費用対効果が合いません。

もう一つは、クラスタリングキーは一つしか設定できないということです。複数の参照パターンがどちらも頻繫に発生するテーブルにおいては、そのカラムにクラスタリングキーを設定するべきか、悩ましい問題となります。複数のカラムによる複合キーを設定することはできるため、最大公約数的なキーの設定をしていくことになります。

プルーニングとクラスタリングの設計TIPS

プルーニングとクラスタリングの仕組みがわかってきたところで、いよいよ実際の設計におけるTIPSを紹介!したかったのですが・・・予想外に仕組みの説明に字数と時間を使ってしまったため、分割します。すみません・・・。

  • SnowflakeユーザコミュニティSnowVillageのオフラインイベント「BUILD.local」の準備で発覚した予想外のプルーニング挙動
  • 文字列型のクラスタリングキーはここに注意!
  • ディメンショナルモデルにおけるクラスタリングの有効な設計手法

などなど、本当はこちらを記事にするつもりだったので、そのうち必ず書きます!ご期待ください!

脚注
  1. 例外的に、小さすぎる書き込みが連続するとパーティションの大きさを整えるために既存のパーティションを置き換えることもあります。詳細は以下の記事参照。
    https://zenn.dev/indigo13love/articles/66b34577b3805f ↩︎

Discussion

ログインするとコメントできます