Snowflakeの3種類のキャッシュについてお勉強してみた #SnowflakeDB
大阪オフィスの玉井です。2021年にAWSの大阪リージョンができるらしいっすね。物理的なロケーションが気になるところ。
仮想ウェアハウスを使わないでSnowflakeを使うには
Snowflakeはストレージとコンピューティングが分離したアーキテクチャとなっています。
テーブルに対してクエリを実行する時、Snowflakeではコンピュート…すなわち仮想ウェアハウスが動きます。仮想ウェアハウスには色々な種類(というかサイズ)があったりするのですが、ここで重要なのは、Snowflakeの利用費は(基本的に)仮想ウェアハウスが動いた時に加算されるということです。
逆に言うと、仮想ウェアハウスを使わなければ、Snowflakeの費用はめちゃくちゃ抑える事ができます。っていうかほとんどかからないでしょう。しかし、大体の人は下記のように思うのではないでしょうか。
「でも、クエリを実行するのが仮想ウェアハウスである限り、仮想ウェアハウスを動かすことは避けられないのでは?」
実は、Snowflakeは、キャッシュを使いこなせば、仮想ウェアハウスの動作を最小限に抑えつつ利用することができます(完全に使わなくするのは厳しいッスけど)。もちろんパフォーマンスも向上します。
Snowflakeのキャッシュは独特なものもあるので、今回はSnowflakeの3種類のキャッシュをご紹介したいと思います。
Snowflakeの3種類のキャッシュ
QUERY RESULT CACHE
計算を必要とせずに再利用できるクエリの(結果の)キャッシュです。
保存内容
クエリの結果
使用されるタイミング
テーブルの内容が変更されておらず(クエリに関連するマイクロパーティションに影響が無い場合は変更が可能)、クエリが同一の場合、使用されます。
使用できるユーザー
クエリが実行された同じロールのすべてのユーザー。
同じロールであれば、セッションをまたいでもキャッシュが使われます。
保存場所
クラウドサービスレイヤー。Snowflakeがよしなに管理するため、ユーザー側が意識する必要はありません。
保存期間
24時間。
これはクエリが再利用されるたびにカウントし直しとなります。つまり、再利用されずに24時間まで保存されるということです。
メリットなど
仮想ウェアハウスを介さないため、早い上に費用がかかりません。ですので、例えば複雑な計算を伴うクエリや半構造化データを扱うクエリは、このキャッシュが効くように調整すれば非常に効力を発揮できると思います。同一テーブル同一クエリである必要があるので、例えばBIツールで作った静的なダッシュボード等に適しています。
やってみた
Snowflakeに元々あるサンプルデータベースを使用します。Snowflakeにログインして、仮想ウェアハウスを動かしていない状態(サスペンド)で、lineitemというテーブルに下記のクエリを実行し、Query Profileを見てみます。
下記のクエリを実行します。
SELECT l_returnflag, l_linestatus, SUM(l_quantity) AS sum_qty, SUM(l_extendedprice) AS sum_base_price, SUM(l_extendedprice * (l_discount)) AS sum_disc_price, SUM(l_extendedprice * (l_discount) * (1+l_tax)) AS sum_charge, AVG(l_quantity) AS avg_qty, AVG(l_extendedprice) AS avg_price, AVG(l_discount) AS avg_disc, COUNT(*) AS count_order FROM lineitem WHERE l_shipdate <= dateadd(day, 90, to_date('1998-12-01')) GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;
当然ながら、キャッシュは全く使われていません。
仮想ウェアハウスをサスペンドし、上記クエリをもう一度実行します。
キャッシュが使用されましたね。同一テーブル同一クエリなので、Resultキャッシュが使用されました。
METADATA CACHE
Snowflakeはテーブル等に関する情報をメタデータとして自動的に収集・管理しています。そのメタデータをキャッシュとして使うことで、クエリの高速化を図ります。
保存内容
- テーブル単位
- 行数
- サイズ
- バージョン
- カラム単位(マイクロパーティション)
- MIN値、MAX値
- DISTINCTした数
- NULLの数
使用されるタイミング
そのクエリの結果が、メタデータに保存されている内容だけで答えられる場合(賄える場合)。クエリのオプティマイザの判断により自動的に使用されます。
使用できるユーザー
誰でも
保存場所
クラウドサービスレイヤー。Snowflakeがよしなに管理するため、ユーザー側が意識する必要はありません。
保存期間
永続(継続的に更新され続ける)
メリットなど
こちらも、仮想ウェアハウスを介さないため、早い上に費用がかかりません。最小値や最大値など、メタデータキャッシュに格納されているような値を求める場合で効くことになります。後は、INFORMATION SCHEMA
をクエリする場合もメタデータキャッシュが効きます。
やってみた
Snowflakeに元々あるサンプルデータベースを使用します。下記のクエリを実行し、Query Profileを見てみます。
SELECT MIN(l_orderkey), MAX(l_orderkey), COUNT(*) FROM lineitem;
仮想ウェアハウスは使用されず、メタデータキャッシュのみでクエリが実行されたことがわかります。
DATA CACHE
保存内容
クエリ結果のファイルヘッダとカラムデータ。クエリの結果ではなくデータ。
クエリが実行された時に上記データをローカル(仮想ウェアハウスのSSD)に保存します。
使用されるタイミング
テーブルのデータが変更されておらず、同じデータの一部または全部を使用するクエリが、同じ仮想ウェアハウスで実行された時。
仮想ウェアハウスは、クエリを実行する時、まずローカル(仮想ウェアハウスのSSD)で利用可能なデータを読み取り、その後ストレージ側からデータを読み取ります。
使用できるユーザー
同じ仮想ウェアハウスを実行したユーザー。
保存場所
仮想ウェアハウス(が動作しているインスタンスのSSD)
保存期間
その仮想ウェアハウスが生きている間。
SSDが満杯になると、参照されていない時間が最も長いデータ(LRU)から消去されていきます。
メリットなど
同一仮想ウェアハウスで同様のクエリをグループ化して実行すると、データキャッシュの再利用を最大化でき、パフォーマンスとコストを最適化することができます。
ただ、注意点があります。まず、仮想ウェアハウスを中断すると、キャッシュはクリアされます。仮想ウェアハウスを中断することでコストを抑える事と、パフォーマンスを向上させるためにキャッシュを維持するこ事は、トレードオフの関係にあります。
また、実行中の仮想ウェアハウスのサイズを小さくすると、サーバーから仮想ウェアハウス(の機能)が切り離されます。こうなると、サーバーに関連付けられているキャッシュが削除されます。つまり、この場合においても、仮想ウェアハウスをサイズダウンしてコストを抑える事とキャッシュを維持する事はトレードオフの関係となります。
このあたりはクエリの要件によると思うので、都度判断していくしかないでしょう。
やってみた
データキャッシュだけが効くように、QUERY RESULTキャッシュをOFFにします。
ALTER SESSION SET USE_CACHED_RESULT = FALSE
下記のクエリを実行します。この時、仮想ウェアハウスはサスペンド状態ですので、クエリ実行と同時に立ち上がります。
SELECT l_returnflag, l_linestatus, SUM(l_quantity) AS sum_qty, SUM(l_extendedprice) AS sum_base_price, SUM(l_extendedprice * (l_discount)) AS sum_disc_price, SUM(l_extendedprice * (l_discount) * (1+l_tax)) AS sum_charge, AVG(l_quantity) AS avg_qty, AVG(l_extendedprice) AS avg_price, AVG(l_discount) AS avg_disc, COUNT(*) as count_order FROM lineitem WHERE l_shipdate <= dateadd(day, 90, to_date('1998-12-01')) GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;
Query Profileを見てます。キャッシュは使用されていません。仮想ウェアハウスがサスペンド状態からの再開(コールドスタート的な)なので、そもそもキャッシュは存在しません。
次に下記クエリを実行します。
SELECT l_returnflag, l_linestatus, SUM(l_quantity) AS sum_qty, SUM(l_extendedprice) AS sum_base_price, SUM(l_extendedprice * (l_discount)) AS sum_disc_price, SUM(l_extendedprice * (l_discount) * (1+l_tax)) AS sum_charge, AVG(l_quantity) AS avg_qty, AVG(l_extendedprice) AS avg_price, AVG(l_discount) AS avg_disc, COUNT(*) as count_order FROM lineitem WHERE l_shipdate <= dateadd(day, 90, to_date('1998-12-01')) AND l_extendedprice <= 20000 GROUP BY l_returnflag, l_linestatus ORDER BY l_returnflag, l_linestatus;
キャッシュ使用率が100%になりました。このクエリは1回目のクエリと似たパターンのため、データキャッシュを使用することができました(WHERE句は若干異なるが、SELECT句で指定しているカラムは同じだったり)。
おわりに
それぞれのキャッシュの特徴と使われるタイミングを抑えておくと、安くて早いクエリの実行が可能となります。