❄️

Snowflake: 複合クラスタリングキーの選定テクニック

2024/12/20に公開

この記事は Snowflake Advent Calendar 2024 Series 2 の 20 日目の記事になります。

Intro

Snowflake では、クラスタリングキーに複数の列・式を指定することができます。

2 つ以上の列・式が指定された、いわゆる「複合クラスタリングキー」について、ドキュメントでも下記のようなプラクティスが説明されています。

https://docs.snowflake.com/ja/user-guide/tables-clustering-keys#strategies-for-selecting-clustering-keys

単一のクラスタリングキーには、1つ以上の列または式を含めることができます。ほとんどのテーブルでは、Snowflakeはキーごとに最大3または4列(または式)を推奨しています。3〜4を超える列を追加すると、利益よりもコストが増加する傾向があります。

テーブルに複数列のクラスタリングキーを定義する場合は、 CLUSTER BY 句で指定される列の順序が重要です。原則として、Snowflakeは列を 最低 カーディナリティから 最高 カーディナリティに並べることを推奨しています。一般に、低いカーディナリティ列の前に高いカーディナリティ列を配置すると、後者の列でのクラスタリングの有効性が低下します。

この記事では、

  • これらのプラクティスがどのような意味を持っているか?
  • これ以外に意識すべき点があるか?

という点について、Snowflake の Performance Engineer として、日々ユーザのクラスタリングキー設定/変更の提案などをしている経験からお話します。

複合クラスタリングキーがどのように動作するか

まず前提として、複合クラスタリングキーを指定した場合に、どのように動作するかを説明します。

例えば (a, b, c, ...) というクラスタリングキーが指定されているとします。このとき、クラスタリングは、

  1. a の順にソート
  2. 同じ a の値を持つ各行群を b の順にソート
  3. 同じ (a, b) ペアの値を持つ各行群を c の順にソート
  4. ...

という形で処理されます。

下記は実際に試した例となります。

create or replace table t1 (a int, b int, c int)
cluster by (a, b, c) as
select seq4()%2, seq4()%3, seq4()%4
from table(generator(rowcount => 12));

select * from t1;
/*
A	B	C
0	0	0
0	0	2
0	1	0
0	1	2
0	2	0
0	2	2
1	0	1
1	0	3
1	1	1
1	1	3
1	2	1
1	2	3
*/

この例をみると、

  • a は最左のクラスタリングキーなのでテーブル全体で昇順にソートされている
  • b は 2 番目のクラスタリングキーなので、各 a 値 (0, 1) の行群の中で昇順にソートされている
  • c は 3 番目のクラスタリングキーなので、各 (a, b) 値 ((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)) の行群の中で昇順にソートされている

という形となり、前述の説明の通りになっていることがわかります。

以降のセクションでは、この動作の理解を前提として解説していきます。

ドキュメントに書かれたプラクティスを理解する

指定する式の順序のプラクティス

ドキュメントでは、下記のように説明されています。

https://docs.snowflake.com/ja/user-guide/tables-clustering-keys#strategies-for-selecting-clustering-keys

テーブルに複数列のクラスタリングキーを定義する場合は、 CLUSTER BY 句で指定される列の順序が重要です。原則として、Snowflakeは列を 最低 カーディナリティから 最高 カーディナリティに並べることを推奨しています。一般に、低いカーディナリティ列の前に高いカーディナリティ列を配置すると、後者の列でのクラスタリングの有効性が低下します。

では、なぜカーディナリティが低い (値の種類の数が少ない) 列からカーディナリティが高い (値の種類が多い) 列に向けて並べたほうがよいのかというと、カーディナリティが低い列の値が複数パーティションに分散する余地がなくなるからです。


例えば、is_activecreation_dt の両方でクラスタリングしたい、というケースがあり、

  • テーブルのパーティション数は 1,000 (p_1, ..., p_{1000})
  • is_active は 2 種類の値 (a_1, a_2)
  • creation_dt は 10,000 種類の値 (b_1, b_2, ..., b_{10000})
  • 各列の値は一様分布

という条件とすると、(is_active, creation_dt) の場合:

  • a_1[p_1,p_{500}]
    • b_1, ... b_{20}p_1
    • b_{21}, ..., b_{40}p_2
    • ...
    • b_{9981}, ..., b_{10000}p_{500}
  • a_2[p_{501}, p_{1000}]
    • b_1, ..., b_{20}p_{501}
    • b_{21}, ..., b_{40}p_{502}
    • ...
    • b_{9981}, ..., b_{10000}p_{1000}

といった分布になるため、

  • is_active = ? は 500/1000 = 50%
  • creation_dt = ? は (1/1000)*2 = 0.2%

のスキャンに抑えることができます。


一方で (creation_dt, is_active) の場合:

  • b_1, ..., b_{10}p_1
    • a_1, a_2p_1
  • b_{11}, ..., b_{20}p_2
    • a_1, a_2p_2
  • ...
  • b_{9991}, ..., b_{10000}p_{1000}
    • a_1, a_2p_{1000}

という分布になるため、すべてのパーティションが全種類の is_active 値を持つことになります。

したがって、

  • creation_dt = ? は 1/1000 = 0.1%
  • is_active = ? は 100%

という形となり、is_active をクラスタリングキーに指定する意味がなくなってしまいます。

すなわち「カーディナリティの低い列から高い列に向けて並べる」というプラクティスは「2 個目以降のクラスタリングキーでプルーニングが発生するようにする」ために必要なものであることがわかります。

指定する式の数のプラクティス

ドキュメントでは、下記のように説明されています。

https://docs.snowflake.com/ja/user-guide/tables-clustering-keys#strategies-for-selecting-clustering-keys

単一のクラスタリングキーには、1つ以上の列または式を含めることができます。ほとんどのテーブルでは、Snowflakeはキーごとに最大3または4列(または式)を推奨しています。3〜4を超える列を追加すると、利益よりもコストが増加する傾向があります。

このプラクティスは文字通り、

  • Snowflakeはキーごとに最大3または4列(または式)を推奨しています
  • 3〜4を超える列を追加すると、利益よりもコストが増加する傾向があります

という話をしており、「3〜4 列であれば複合クラスタリングキーは効いてくれる」とも「5 列以上でなければコストがベネフィットを上回ることはない」とも言っていません

実際に、経験上多くのケースで、3 番目のクラスタリングキーはほぼ有効ではありません。しかし、3〜4 番目のクラスタリングキーは常に有効ではないかというとそんなことはなく、

  • 前段の列/式のカーディナリティ
  • テーブルのパーティション数

に依存します。


例えば、(is_active, is_married, age, creation_dt) のようなクラスタリングキーを設定しているとして、

  • テーブルのパーティション数は 2,000 (p_1, ..., p_{2000})
  • is_active は 2 種類の値 (a_1, a_2)
  • is_married も 2 種類の値 (b_1, b_2)
  • age は 100 種類の値 (c_1, c_2, ..., c_{100})
  • creation_dt は 10,000 種類の値 (d_1, d_2, ..., d_{10000})
  • 各列の値は一様分布

とすると、

  • a_1[p_1,p_{1000}], a_2[p_{1001},p_{2000}]
  • b_1[p_1,p_{500}], [p_{1001},p_{1500}], b_2[p_{501},p_{1000}], [p_{1501},p_{2000}]
  • c_1[p_1,p_5], [p_{501},p_{505}], [p_{1001},p_{1005}], [p_{1501},p_{1505}], c_2 → ...,
    • c_{100}[p_{496},p_{500}], [p_{996},p_{1000}], [p_{1496},p_{1500}], [p_{1996},p_{2000}]
  • d_1p_1, p_6, ..., p_{1996}, d_2 → ...,
    • d_{10000}p_5, p_{10}, ..., p_{2000}

という分布になるので、

  • is_active = ? は 1000/2000 = 50%
  • is_married = ? は 1000/2000 = 50%
  • age = ? は 4/2000 = 0.2%
  • creation_dt = ? は 400/2000 = 20%

となり、すべての列でプルーニングが効いていることがわかります。


しかし、同条件のデータが 200 パーティションだった場合:

  • a_1[p_1,p_{100}], a_2[p_{101},p_{200}]
  • b_1[p_1,p_{50}], [p_{101},p_{150}], b_2[p_{51},p_{100}], [p_{151},p_{200}]
  • c_1, c_2p_1, p_{51}, p_{101}, p_{151}, ...,
    • c_{99}, c_{100}p_{50}, p_{100}, p_{150}, p_{200}
  • d_1, ..., d_{10000}p_1, p_2, ..., p_{199}, p_{200}

という分布になるため、

  • is_active = ? は 100/200 = 50%
  • is_married = ? は 100/200 = 50%
  • age = ? は 4/200 = 2%
  • creation_dt = ? は 100%

となり、age でのプルーニング効率が 10 倍低下した上に、creation_dt でのプルーニングはまったく効かなくなっています。


したがって、複合クラスタリングキーがちゃんと効いているかどうかは、「n 番目のクラスタリングキーだから効かない」「カーディナリティがいくつだから効かない」といった単純なものではなく、多段ソートを意識した上で、パーティション数に対して n 段目の分布でも絞り込みができるかという点を考えてあげる必要があります。

また、クラスタリングキーは一度設定したら不変なものではなく、データ量やカーディナリティの変化に応じて、都度再検討する必要がありそうです。

カーディナリティの低い列でのクラスタリング

下記ドキュメントに書かれているように、カーディナリティが低すぎる (boolean とか) 列をクラスタリングキーにすると、プルーニング効率があまり高くなく、プルーニングによるパフォーマンス改善効果がクラスタリングコストを下回る可能性が高くなります。

https://docs.snowflake.com/ja/user-guide/tables-clustering-keys#strategies-for-selecting-clustering-keys

列/式内の個別の値の数(つまり、カーディナリティ)は、クラスタリングキーとして選択する重要な側面です。以下を含むクラスタリングキーを選択することが重要です:
・テーブルで効果的なプルーニングを可能にするために十分な数の個別の値。
・Snowflakeが同じマイクロパーティション内の行を効果的にグループ化できるようにするための十分に少数の個別の値。
ブール値のみを含む IS_NEW_CUSTOMER という名前の列のように、カーディナリティが非常に低い列は、最小限のプルーニングのみを生成する可能性があります。

しかし、上記ドキュメントでも明確に否定されていないように、必ずしもカーディナリティの低い列でのクラスタリングが悪いわけではなく、効果がある局面もあります。

偏った値の分布

例えば、下記のような「何らかのジョブの履歴を記録する」ためのテーブルについて考えます (これが適切なテーブルデザインかどうかについては一旦置いておきます):

create or replace table job_status_history (
    id int,
    start_time timestamp_ntz,
    end_time timestamp_ntz,
    is_failed boolean
);

もしエラーレートが 0.1% だったとしたら、is_failed の値の分布は false 99.9% に対して true 0.1% になります。

ここで is_failed = true のようなフィルタを使うクエリが多数存在する場合、カーディナリティが 2 しかない is_failed でクラスタリングする価値が生まれてきます。

create or replace table job_status_history (
    id int,
    start_time timestamp_ntz,
    end_time timestamp_ntz,
    is_failed boolean
)
cluster by (is_failed) as
select
    seq4(),
    dateadd(second, seq4(), '2024-01-01') start_time,
    dateadd(second, abs(random()%100), start_time),
    iff(seq4()%1000 = 0, true, false)
from table(generator(rowcount => 100000000));

select system$clustering_information('job_status_history');
-- "total_partition_count" : 128

explain
select * from job_status_history
where is_failed;
/*
			GlobalStats				128	1	11883008
1	0		Result			JOB_STATUS_HISTORY.ID, JOB_STATUS_HISTORY.START_TIME, JOB_STATUS_HISTORY.END_TIME, JOB_STATUS_HISTORY.IS_FAILED			
1	1	[0]	Filter			JOB_STATUS_HISTORY.IS_FAILED			
1	2	[1]	TableScan	YOSHI_DB.PUBLIC.JOB_STATUS_HISTORY		ID, START_TIME, END_TIME, IS_FAILED	128	1	11883008
*/

後段のクラスタリングキーに与える影響

カーディナリティが低いクラスタリングキーは、後続のクラスタリングキーのプルーニング効率が十分に高ければ、与える影響が小さくなります。

例えば、「指定する式の順序のプラクティス」セクションで使った、下記のようなテーブルについて考えます。

  • テーブルのパーティション数は 1,000 (p_1, ..., p_{1000})
  • is_active は 2 種類の値 (a_1, a_2)
  • creation_dt は 10,000 種類の値 (b_1, b_2, ..., b_{10000})
  • 各列の値は一様分布

このとき、クラスタリングキーが (creation_dt) だったとすると、creation_dt = ? のプルーニング効率は 1/1000 で 0.1% となります。

それに対して、(is_active, creation_dt) だったときの creation_dt = ? のプルーニング効率は、すでに計算している通り 2/1000 で 0.2% となります。

これらのテーブルスキャンの実行時刻はほぼ変わらないため、is_active を入れても入れなくても creation_dt = ? を含むクエリのパフォーマンスには、おそらくほとんど影響を与えません。

そのため、このケースはシンプルに「is_active = ? で 50% のプルーニング効率を得る」ということについて、追加のクラスタリングコストを考慮してお得かどうかを考えればよいことになります。

また、追加のクラスタリングコストについても、複合カーディナリティがたかだか倍になる程度で、すでに自動クラスタリングが動いている状況であれば、急激に跳ね上がることはなさそうです。

そのため、クラスタリングキーのないテーブルに導入するときは慎重に判断する必要がありそうですが、すでに存在していてるクラスタリングキーの前段として置くのであれば、もう少し気軽に検討できそうです。

プラスアルファで意識すべきこと

クラスタリングキー同士の相関性

今までの例では、各列の各値が一様に分布しており、また非相関 (すべての a_k に対して、各 b_k を持つ行がすべて存在する) であることが前提となっていました。

しかし現実では、各列の値は相関性を持っていることがあり、複合クラスタリングキーのカーディナリティを考えるにあたり重要なファクターとなります。


例えば、下記のようなテーブルについて考えます。

create or replace table t1 (c1 int, c2 int)
cluster by (c1, c2) as
select abs(random())%100 c1, c1*1000 + abs(random())%100
from table(generator(rowcount => 100000000));

c1[0,99]c2[0,99], [100,199], ..., [10000,10099] のような範囲を取り、それぞれのカーディナリティは 100 と 10,000 になります。

select count(distinct c1), count(distinct c2)
from t1;
-- 100  10000

このとき、このテーブルのパーティション数は 128 個あります。

select system$clustering_information('t1');
/*
{
  "cluster_by_keys" : "LINEAR(c1, c2)",
...
  "total_partition_count" : 128,
...
*/

ここで、各列のパーティションの分布を、前セクションのように一様分布で考えると、

  • c1: 100 種類の値に対して 128 パーティション
    • 1 種類の値あたり 1-2 パーティション
  • c2: 10,000 種類の値に対して、各 1-2 パーティション
    • c1 の 100 種類の値それぞれに対して、特定の c2 値を持つパーティションが 1 個存在する (合計 100 パーティション)

という形になるので、

  • c1 = ? は 1〜2/128 = 0.8〜1.6%
  • c2 = ? は 100/128 = 78%

となり、c2 でのプルーニング効率が悪くなってしまうように見えます。

しかし、実際には c2 = 100c1 + n \enspace (n < 100) と強い相関性があり、c1 の各値の行は 100 種類の c2 値しか持っていません。

この分布を前提に、再度プルーニング効率を考えると、

  • c1: 100 種類の値に対して 128 パーティション
    • 1 種類の値あたり 1-2 パーティション
  • c2: 100 種類の値に対して、各 1-2 パーティション
    • c1 の 100 種類の値全体に対して、特定の c2 値を持つパーティションが 1 個存在する (合計 1 パーティション)

という形になるため、

  • c1 = ? は 1〜2/128 = 0.8〜1.6%
  • c2 = ? は 1/128 = 0.8%

となり、どちらも高いプルーニング効率が得られることが想定されます。

実際にプルーニングの状況を見てみると、計算通りプルーニングされていることが確認できます。

explain
select * from t1
where c1 = 50;
/*
			GlobalStats				128	2	2146816
1	0		Result			T1.C1, T1.C2			
1	1	[0]	Filter			T1.C1 = 50			
1	2	[1]	TableScan	YOSHI_DB.PUBLIC.T1		C1, C2	128	2	2146816
*/

explain
select * from t1
where c2 = 5050;
/*
			GlobalStats				128	1	1060352
1	0		Result			T1.C1, T1.C2			
1	1	[0]	Filter			T1.C2 = 5050			
1	2	[1]	TableScan	YOSHI_DB.PUBLIC.T1		C1, C2	128	1	1060352
*/

というわけで、プルーニング効いててよかったね…という話になるかというとそう簡単な話でもありません。そもそも c2 でのクラスタリングは必要でしょうか

create or replace table t2 (c1 int, c2 int) 
cluster by (c1) as
select * from t1;

explain
select * from t2
where c1 = 50;
/*
			GlobalStats				56	2	1780224
1	0		Result			T2.C1, T2.C2			
1	1	[0]	Filter			T2.C1 = 50			
1	2	[1]	TableScan	YOSHI_DB.PUBLIC.T2		C1, C2	56	2	1780224
*/

explain
select * from t2
where c2 = 5050;
/*
			GlobalStats				56	2	1780224
1	0		Result			T2.C1, T2.C2			
1	1	[0]	Filter			T2.C2 = 5050			
1	2	[1]	TableScan	YOSHI_DB.PUBLIC.T2		C1, C2	56	2	1780224
*/

なんだかパーティション数が減りましたが、クラスタリングキーが (c1) だけでもプルーニング効率としてはほぼ変わっていません。

これは c1c2 に強い相関性があるため、c1 でデータがソートされている場合、自然と c2 でもソートされているのに近い状態となるため、クラスタリングキーに c2 が含まれていなくても c2 = ? でのプルーニングが強く効く状態になっています。

したがって、

  • 片方から片方を直接導出できるような強い相関性がある (例: UNIX epoch と日時文字列、年収と源泉徴収税額、税抜価格と税込価格など)
  • 意味的に強い相関性がある (例: 連番ユーザ ID とユーザ作成日時、都市名と商品価格など)

などのケースでは、相関性にまかせて片方の列のみをクラスタリングキーに指定するのも、クラスタリングコストを下げる意味では有効なプラクティスになります。

クラスタリングキーは、あくまでも「どのようにデータをソート・分割するかの動作の指定」でしかなく、実際のプルーニングは「動作の結果」に対して発生するので、どのようなメタデータ (最小値/最大値) が生成されそうかを想像したり概算したりしながら、クラスタリングキーを選定するとよいでしょう。

各段のプルーニング効率のトレードオフ

例えば、下記のようなテーブルについて考えます。

create or replace table t1 (c1 int, c2 int)
cluster by (c1, c2) as
select random()%500000, random()%500000
from table(generator(rowcount => 100000000));

select count(distinct c1), count(distinct c2)
from t1;
-- 999999	999999

select system$clustering_information('t1');
-- "total_partition_count" : 128,

このテーブルは、128 パーティションに対して、クラスタリングキーのカーディナリティがそれぞれ 999,999 となっており、1 段目のプルーニングが効きそうですが、2 段目のプルーニングはまったく効かず、また自動クラスタリングのコストが高くなっています。

そのため、下記のプラクティスに沿って、TRUNC で有効桁数を詰めていこうと思います。

https://docs.snowflake.com/ja/user-guide/tables-clustering-keys#strategies-for-selecting-clustering-keys

Tip
一般に、列(または式)のカーディナリティが高い場合、その列でのクラスタリングの維持はより高価になります。
一意のキーでのクラスタリングのコストは、特にそのテーブルの主な使用例ではないポイントルックアップの場合、そのキーでのクラスタリングの利点を上回る場合があります。
カーディナリティが非常に高い列をクラスタリングキーとして使用する場合は、個別の値の数を減らすために、キーを列ではなく列の式として定義することをSnowflakeはお勧めします。式は、各パーティションの最小値と最大値でプルーニングが有効になるように、列の元の順序を保持する必要があります。
...
別の例として、 TRUNC 関数とスケールの負の値(例: TRUNC(123456789, -5))を使用して、数値をより少ない有効桁数に切り捨てることができます。

有効桁数をそれぞれ 3, 4, 5 桁切り詰めた場合のカーディナリティは、以下のようになります。

select
    count(distinct trunc(c1, -3)),
    count(distinct trunc(c2, -3))
from t1;
-- 999	999

select
    count(distinct trunc(c1, -4)),
    count(distinct trunc(c2, -4))
from t1;
-- 99	99

select
    count(distinct trunc(c1, -5)),
    count(distinct trunc(c2, -5))
from t1;
-- 9   9

これらについて、前のセクションと同じように等価条件でのプルーニング効率を考えると、

TRUNC(?, -3):

  • c1 = ? → カーディナリティがパーティション数を上回っているので 1/128 = 0.8%
  • c2 = ? → 100% (プルーニングなし)

TRUNC(?, -4):

  • c1 = ? → 各値が 1-2 パーティションに配置されるので 1〜2/128 = 0.8〜1.6%
  • c2 = ? → 各値を含み得るパーティションが各 TRUNC(c1, -4) 値に対して 1 つずつ存在するので 99/128 = 77%

TRUNC(?, -5):

  • c1 = ? → 128/9 = 14.222 より、各値が 14-15 パーティションに配置されるので 14〜15/128 = 11〜12%
  • c2 = ? → 14/9 = 1.555, 15/9 = 1.666 より、各値を含み得るパーティションが各 TRUNC(c1, -5) 値に対して 1-2 個ずつ存在するので 1〜2/9 = 11〜22%

という形となります。

ここで「TRUNC(?, -4)TRUNC(?, -5) のどちらにするか?」という判断点が発生します。

一般的には、c1 のスキャンパーティション数の差異よりも、c2 の差異のほうがかなり大きいため、c1 のプルーニング効率を多少下げても、TRUNC(?, -5) のほうがよさそうです。
また「1-2 パーティションでも 14-15 パーティションでもスキャン時間はそんなに変わらないけど、14-15 パーティションと 99 パーティションは変わる」という考え方もできます。

しかし、最終的に重要なのは「ビジネスロジック/ワークロードにとってどちらが重要か?」という部分で、「c1 はパフォーマンスクリティカルなクエリで使われるけど、c2 はそうでもないから多少プルーニングされてくれればいい」みたいな要件であれば、TRUNC(?, -4) を検討する価値があります。

TRUNC(?, -3) は単一クラスタリングキーにしたほうがよいです。

最も重要な判断基準

ここまでいろいろと細かいプラクティスの話をしてきましたが、最後に「適切なクラスタリングキーを選定するにあたっての判断基準」の話をします。

まず、よくある判断基準としては、

  • クラスタリングによる消費クレジット軽減分が追加のクラスタリングコストを上回っているか
  • 多くのクエリの WHERE 句で使用されている列か
  • average_overlap/average_depth が低くなっているか

などがあると思いますが、これらはあくまでもヒントであり判断材料の一つであって、これらをもって「適切なクラスタリングキーではない」と判断するのは誤っているケースがあります。

適切なクラスタリングキーを選定するにあたり、最も重要なポイントは「そのクラスタリングキーがビジネスにとってプラスになるか」です。

実際に 1 つずつ考えてみましょう。


まず

  • クラスタリングによる消費クレジット軽減分が追加のクラスタリングコストを上回っているか

ですが、そもそもクラスタリングの目的は「パフォーマンスの改善」であって、消費クレジットの軽減はパフォーマンス改善の副産物でしかありません。

また、必要なパフォーマンスを得るために、たとえリニアにスケールしなかったとしても、大きいウェアハウスサイズを使って札束で殴ることがあるわけです。

つまり、もしそのクエリのパフォーマンスターゲットがビジネスにとって重要なものなのであれば、追加のクラスタリングコストが上回っていたとしても、クラスタリングする価値が生まれます。

クラスタリングキー選定時にクラスタリングコストを考慮し、また TRUNC などで適切なカーディナリティのコントロールを行うことで不要なクラスタリングコストを削ることは重要ですが、それ自体が絶対的な採用可否の判断基準にはなりません。


次に

  • 多くのクエリの WHERE 句で使用されている列か

ですが、重要なのは「そのクラスタリングキーがビジネスにとってプラスになるか」になるか、つまりその大多数のクエリがビジネスにとって重要なものかがポイントになります。

例えば、ワークロード全体の 95% のクエリの WHERE 句で使用されている列があったとして、これをクラスタリングキーに使用することで全体の消費クレジットを抑えることはできそうですが、一方でこの 95% のクエリがビジネスにとって重要なクエリかどうかとは別問題です。

残りの 5% のクエリがパフォーマンスターゲットを達成することが、ビジネスの継続にとって重要なのであれば、95% を無視して 5% を狙ったクラスタリングキーにするのも、正しい判断になります。

また消費クレジットにフォーカスしたとしても、95% の元々軽量なクエリと 5% の長時間かかるクエリという構成であれば、5% に注力したほうがトータルの消費クレジット削減が大きくなる可能性があります。

そのため、単純な多寡や割合ではなく、

  • どのクエリがビジネスにとって重要か?
  • どのクエリがワークロード全体のパフォーマンスに与える影響が大きいか?

などを考慮して、チューニングターゲットを決定する必要があります。


最後に

  • average_overlap/average_depth が低くなっているか

ですが、これは「自動クラスタリングの状態があまり効率的でない」といった状態を検知するのには有用な指標になりますが、クラスタリングキー選定時はそれほど気にする必要がない指標になります。

例えば、1,000 パーティションのテーブルに boolean 型のクラスタリングキーを指定すると、おそらく average_depth が 500 とかになります。

しかし、重要なのは「そのクラスタリングキーを設定することによるテーブルスキャンの 50% 削減」であり、この効果がクリティカルなのであれば、当該指標は無視してでも設定するべきです。

Conclusion

長くなってしまいましたが、まとめると、

  • クラスタリング後のメタデータを想像/概算する
  • パーティション数とカーディナリティを意識して選定する
  • クラスタリングキーはパフォーマンスが改善するかどうかで決める
  • クラスタリングキーはビジネスにとって重要なパフォーマンスターゲットかどうかで決める

といったことが重要なポイントになります。

一見複雑そうですが、慣れてくると概算とプルーニング結果がぴったりあって気持ちよかったりするので、ぜひ試してみてください。

Discussion