19 KiB
slug | title | description | keywords | |||
---|---|---|---|---|---|---|
/ja/materialized-view | Materialized View | Materialized Viewを使用してクエリを高速化する方法 |
|
Materialized View
Materialized Viewを使用すると、計算のコストをクエリ時から挿入時に移行し、SELECT
クエリを高速化できます。
Postgresなどのトランザクショナルデータベースとは異なり、ClickHouseのMaterialized Viewは、データがテーブルに挿入される際にクエリを実行するトリガーに過ぎません。このクエリの結果は、2つ目の「ターゲット」テーブルに挿入されます。さらに多くの行が挿入されると、その結果がターゲットテーブルに送られ、中間結果が更新およびマージされます。このマージされた結果は、元のデータ全体に対してクエリを実行したときと同等です。
Materialized Viewの主な動機は、ターゲットテーブルに挿入される結果が、行に対する集計、フィルタリング、または変換の結果を表していることです。これらの結果は、しばしば元のデータの小さな表現(集計の場合は部分的なスケッチ)になります。これにより、ターゲットテーブルから結果を読み取るためのクエリが単純であるため、元のデータに対して同じ計算を行った場合よりもクエリ時間が短くなることが保証され、計算(したがってクエリの遅延)がクエリ時から挿入時に移行されます。
ClickHouseのMaterialized Viewは、ベースにしたテーブルにデータが流入するとリアルタイムで更新され、継続的に更新され続けるインデックスのように機能します。これは、多くのデータベースにおいてMaterialized Viewがクエリの静的なスナップショットであり、新しくリフレッシュする必要があること(ClickHouseのリフレッシャブルMaterialized Viewと似ています)とは対照的です。
<img src={require('./images/materialized-view-diagram.png').default}
class='image'
alt='Materialized view diagram'
style={{width: '500px'}} />
例
1日の投稿に対するアップ投票とダウン投票の数を取得したいとします。
ClickHouseのtoStartOfDay
関数のおかげで、これはかなり簡単なクエリです:
SELECT toStartOfDay(CreationDate) AS day,
countIf(VoteTypeId = 2) AS UpVotes,
countIf(VoteTypeId = 3) AS DownVotes
FROM votes
GROUP BY day
ORDER BY day ASC
LIMIT 10
┌─────────────────day─┬─UpVotes─┬─DownVotes─┐
│ 2008-07-31 00:00:00 │ 6 │ 0 │
│ 2008-08-01 00:00:00 │ 182 │ 50 │
│ 2008-08-02 00:00:00 │ 436 │ 107 │
│ 2008-08-03 00:00:00 │ 564 │ 100 │
│ 2008-08-04 00:00:00 │ 1306 │ 259 │
│ 2008-08-05 00:00:00 │ 1368 │ 269 │
│ 2008-08-06 00:00:00 │ 1701 │ 211 │
│ 2008-08-07 00:00:00 │ 1544 │ 211 │
│ 2008-08-08 00:00:00 │ 1241 │ 212 │
│ 2008-08-09 00:00:00 │ 576 │ 46 │
└─────────────────────┴─────────┴───────────┘
10 rows in set. Elapsed: 0.133 sec. Processed 238.98 million rows, 2.15 GB (1.79 billion rows/s., 16.14 GB/s.)
Peak memory usage: 363.22 MiB.
このクエリは、すでにClickHouseのおかげで高速ですが、さらに改善できるでしょうか?
Materialized Viewを使用してこれを挿入時に計算したい場合、結果を受け取るテーブルが必要です。このテーブルは、1日につき1行だけを保持する必要があります。既存の日のデータが更新される場合、他のカラムは既存の日の行にマージされるべきです。このインクリメンタルな状態のマージを行うためには、他のカラムの部分的な状態を保持する必要があります。
これは、ClickHouseにおける特別なエンジンタイプを必要とします: SummingMergeTree。これは、全ての数値カラムの合計値を含む1つの行に置き換えます。以下のテーブルは、同じ日付を持つ行をマージし、数値カラムを合計します:
CREATE TABLE up_down_votes_per_day
(
`Day` Date,
`UpVotes` UInt32,
`DownVotes` UInt32
)
ENGINE = SummingMergeTree
ORDER BY Day
Materialized Viewを示すために、votesテーブルが空であり、まだデータを受け取っていないと仮定します。我々のMaterialized Viewは、votes
に挿入されたデータに対して上記のSELECT
を実行し、その結果をup_down_votes_per_day
に送ります:
CREATE MATERIALIZED VIEW up_down_votes_per_day_mv TO up_down_votes_per_day AS
SELECT toStartOfDay(CreationDate)::Date AS Day,
countIf(VoteTypeId = 2) AS UpVotes,
countIf(VoteTypeId = 3) AS DownVotes
FROM votes
GROUP BY Day
ここでのTO
句は重要で、結果が送られる場所、すなわちup_down_votes_per_day
を示しています。
以前の挿入からvotesテーブルを再度ポピュレートできます:
INSERT INTO votes SELECT toUInt32(Id) AS Id, toInt32(PostId) AS PostId, VoteTypeId, CreationDate, UserId, BountyAmount
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/votes.parquet')
0 rows in set. Elapsed: 111.964 sec. Processed 477.97 million rows, 3.89 GB (4.27 million rows/s., 34.71 MB/s.)
Peak memory usage: 283.49 MiB.
完了後、up_down_votes_per_day
のサイズを確認できます - 1日につき1行になるはずです:
SELECT count()
FROM up_down_votes_per_day
FINAL
┌─count()─┐
│ 5723 │
└─────────┘
ここで、votes
における238百万行から、このクエリの結果を保存することによって5000に行数を効果的に減少させました。ここでの鍵は、votes
テーブルに新しい投票が挿入されるたびに、新しい値がその日に対してup_down_votes_per_day
に送られ、それらはバックグラウンドで自動的に非同期でマージされることです - 1日につき1行のみを保持します。up_down_votes_per_day
は常に小さく、最新の状態に保たれます。
行のマージが非同期で行われるため、ユーザーがクエリする際に1日あたり複数の投票が存在する可能性があります。クエリ時に未結合の行を確実にマージするために、以下の2つのオプションがあります:
- テーブル名に
FINAL
修飾子を使用する。これは上記のカウントクエリで行いました。 - 最終テーブルで使用した並び替えキー(例:
CreationDate
)で集約し、メトリックを合計します。これは通常、より効率的で柔軟ですが(テーブルが他のことにも使用できます)、前者は一部のクエリにとっては単純かもしれません。以下で両方を示します:
SELECT
Day,
UpVotes,
DownVotes
FROM up_down_votes_per_day
FINAL
ORDER BY Day ASC
LIMIT 10
10 rows in set. Elapsed: 0.004 sec. Processed 8.97 thousand rows, 89.68 KB (2.09 million rows/s., 20.89 MB/s.)
Peak memory usage: 289.75 KiB.
SELECT Day, sum(UpVotes) AS UpVotes, sum(DownVotes) AS DownVotes
FROM up_down_votes_per_day
GROUP BY Day
ORDER BY Day ASC
LIMIT 10
┌────────Day─┬─UpVotes─┬─DownVotes─┐
│ 2008-07-31 │ 6 │ 0 │
│ 2008-08-01 │ 182 │ 50 │
│ 2008-08-02 │ 436 │ 107 │
│ 2008-08-03 │ 564 │ 100 │
│ 2008-08-04 │ 1306 │ 259 │
│ 2008-08-05 │ 1368 │ 269 │
│ 2008-08-06 │ 1701 │ 211 │
│ 2008-08-07 │ 1544 │ 211 │
│ 2008-08-08 │ 1241 │ 212 │
│ 2008-08-09 │ 576 │ 46 │
└────────────┴─────────┴───────────┘
10 rows in set. Elapsed: 0.010 sec. Processed 8.97 thousand rows, 89.68 KB (907.32 thousand rows/s., 9.07 MB/s.)
Peak memory usage: 567.61 KiB.
これにより、クエリ時間を0.133秒から0.004秒 - 25倍以上の改善になりました!
より複雑な例
上記の例では、Dailyの合計値を2つ取り扱うためにMaterialized Viewを使用しています。合計は、部分的な状態を維持するための最も簡単な集計形式です - 新しい値が到着したときに既存の値に追加するだけで済みます。しかし、ClickHouseのMaterialized Viewは、あらゆるタイプの集計に使用することができます。
たとえば、投稿の統計を各日数に対して計算したいとしましょう: Score
の99.9パーセンタイルとCommentCount
の平均です。この計算のためのクエリは以下のようになるかもしれません:
SELECT
toStartOfDay(CreationDate) AS Day,
quantile(0.999)(Score) AS Score_99th,
avg(CommentCount) AS AvgCommentCount
FROM posts
GROUP BY Day
ORDER BY Day DESC
LIMIT 10
┌─────────────────Day─┬────────Score_99th─┬────AvgCommentCount─┐
1. │ 2024-03-31 00:00:00 │ 5.23700000000008 │ 1.3429811866859624 │
2. │ 2024-03-30 00:00:00 │ 5 │ 1.3097158891616976 │
3. │ 2024-03-29 00:00:00 │ 5.78899999999976 │ 1.2827635327635327 │
4. │ 2024-03-28 00:00:00 │ 7 │ 1.277746158224246 │
5. │ 2024-03-27 00:00:00 │ 5.738999999999578 │ 1.2113264918282023 │
6. │ 2024-03-26 00:00:00 │ 6 │ 1.3097536945812809 │
7. │ 2024-03-25 00:00:00 │ 6 │ 1.2836721018539201 │
8. │ 2024-03-24 00:00:00 │ 5.278999999999996 │ 1.2931667891256429 │
9. │ 2024-03-23 00:00:00 │ 6.253000000000156 │ 1.334061135371179 │
10. │ 2024-03-22 00:00:00 │ 9.310999999999694 │ 1.2388059701492538 │
└─────────────────────┴───────────────────┴────────────────────┘
10 rows in set. Elapsed: 0.113 sec. Processed 59.82 million rows, 777.65 MB (528.48 million rows/s., 6.87 GB/s.)
Peak memory usage: 658.84 MiB.
前述のように、postsテーブルに新しい投稿が挿入されるとき、上記のクエリを実行するMaterialized Viewを作成できます。
例の目的で、S3からの投稿データのロードを避けるために、posts
と同じスキーマを持つ重複テーブルposts_null
を作成します。ただし、このテーブルはデータを保存せず、Materialized Viewが行を挿入する際に使用されます。データの保存を防ぐために、Null
テーブルエンジンタイプを使用できます。
CREATE TABLE posts_null AS posts ENGINE = Null
Nullテーブルエンジンは強力な最適化です - /dev/null
のようなものです。我々のMaterialized Viewは、挿入時にposts_null
テーブルが行を受け取ると統計を計算して格納します - ただのトリガーに過ぎません。ただし、生データは保存されません。実際には、元の投稿を保存する必要があるかもしれませんが、このアプローチは集計を計算しながら生データのストレージオーバーヘッドを回避するために使用できます。
それゆえ、Materialized Viewは以下のようになります:
CREATE MATERIALIZED VIEW post_stats_mv TO post_stats_per_day AS
SELECT toStartOfDay(CreationDate) AS Day,
quantileState(0.999)(Score) AS Score_quantiles,
avgState(CommentCount) AS AvgCommentCount
FROM posts_null
GROUP BY Day
注: ここでは、集約関数の末尾にState
サフィックスを付けています。これにより、関数の集約状態が返され、最終的な結果ではなく、他の状態とマージするための追加情報を持つ部分状態が含まれます。たとえば平均の場合、これはカウントとカラムの合計を含みます。
部分的な集計状態は正しい結果を計算するために必要です。たとえば、平均を計算する場合、サブレンジの平均を単純に平均化することは誤った結果をもたらします。
次に、このビューのターゲットテーブルpost_stats_per_day
を作成し、これらの部分的集計状態を保存します:
CREATE TABLE post_stats_per_day
(
`Day` Date,
`Score_quantiles` AggregateFunction(quantile(0.999), Int32),
`AvgCommentCount` AggregateFunction(avg, UInt8)
)
ENGINE = AggregatingMergeTree
ORDER BY Day
以前は、カウントを保存するためのSummingMergeTree
が十分でしたが、他の関数にはより高度なエンジンタイプが必要です: AggregatingMergeTree
。
ClickHouseに集約状態が保存されることを確実に知らせるために、Score_quantiles
とAvgCommentCount
をAggregateFunction
タイプとして定義し、部分的状態とソースカラムのタイプの関数ソースを指定します。SummingMergeTree
のように、同じORDER BY
キー値を持つ行がマージされます(この例のDay
)
Materialized Viewを経由してpost_stats_per_day
をポピュレートするには、posts
からposts_null
にすべての行を挿入するだけです:
INSERT INTO posts_null SELECT * FROM posts
0 rows in set. Elapsed: 13.329 sec. Processed 119.64 million rows, 76.99 GB (8.98 million rows/s., 5.78 GB/s.)
プロダクションでは、おそらくMaterialized Viewを
posts
テーブルにアタッチすることになります。ここでは、nullテーブルを示すためにposts_null
を使用しました。
最終クエリは、関数にMerge
サフィックスを使用する必要があります(カラムが部分的な集計状態を格納しているため):
SELECT
Day,
quantileMerge(0.999)(Score_quantiles),
avgMerge(AvgCommentCount)
FROM post_stats_per_day
GROUP BY Day
ORDER BY Day DESC
LIMIT 10
ここでは、FINAL
を使用する代わりにGROUP BY
を使用します。
他の用途
上記では主に、データの部分集計をインクリメンタルに更新する目的でMaterialized Viewを使用し、クエリ時間から挿入時間へと計算を移行することに焦点を当てています。この一般的な使用ケースを超え、Materialized Viewには他の多くの用途があります。
フィルタリングと変換
場合によっては、挿入時に行とカラムのサブセットのみを挿入したい場合があります。この場合、posts_null
テーブルは挿入を受け、posts
テーブルへの挿入前にSELECT
クエリで行をフィルタリングできます。たとえば、posts
テーブルのTags
カラムを変換したいとします。これはタグ名のパイプ区切りリストを含んでいます。これらを配列に変換することで、個々のタグ値による集計がより簡単になります。
この変換は、
INSERT INTO SELECT
を実行するときにも実行できます。Materialized ViewはこのロジックをClickHouse DDLにカプセル化し、INSERT
をシンプルに保ちながら、新しい行に変換を適用できます。
この変換のためのMaterialized Viewは以下のようになります:
CREATE MATERIALIZED VIEW posts_mv TO posts AS
SELECT * EXCEPT Tags, arrayFilter(t -> (t != ''), splitByChar('|', Tags)) as Tags FROM posts_null
参照テーブル
ユーザーはClickHouseの順序付けキーを選択する際に、そのフィルタリングや集計句で頻繁に使用されるカラムを選択する必要があります。しかし、これは単一のカラムセットでカプセル化できない、より多様なアクセスパターンを持つシナリオにおいて制約を与える可能性があります。たとえば、次のcomments
テーブルを考えてみましょう:
CREATE TABLE comments
(
`Id` UInt32,
`PostId` UInt32,
`Score` UInt16,
`Text` String,
`CreationDate` DateTime64(3, 'UTC'),
`UserId` Int32,
`UserDisplayName` LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY PostId
0 rows in set. Elapsed: 46.357 sec. Processed 90.38 million rows, 11.14 GB (1.95 million rows/s., 240.22 MB/s.)
ここでの順序付けキーは、PostId
でフィルタリングするクエリに対してテーブルを最適化しています。
特定のUserId
でフィルタリングし、平均Score
を計算したい状況を考えてみましょう:
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
┌──────────avg(Score)─┐
1. │ 0.18181818181818182 │
└─────────────────────┘
1 row in set. Elapsed: 0.778 sec. Processed 90.38 million rows, 361.59 MB (116.16 million rows/s., 464.74 MB/s.)
Peak memory usage: 217.08 MiB.
高速です(データはClickHouseにとって小さい)が、プロセスされた行数 - 90.38百万からフルテーブルスキャンを必要とすることがわかります。より大きなデータセットに対して、フィルタリングカラムUserId
のための順序付けキー値PostId
を参照するMaterialized Viewを使用して、このクエリを効率的に加速することができます。これらの値は効率的な参照に使用できます。
この例では、Materialized Viewはとてもシンプルで、挿入時にcomments
からPostId
とUserId
のみを選択します。これらの結果は、UserId
で並び替えられたcomments_posts_users
テーブルに送られます。以下でcomments
テーブルのnullバージョンを作成し、このビューとcomments_posts_users
テーブルをポピュレートするために使用します:
CREATE TABLE comments_posts_users (
PostId UInt32,
UserId Int32
) ENGINE = MergeTree ORDER BY UserId
CREATE TABLE comments_null AS comments
ENGINE = Null
CREATE MATERIALIZED VIEW comments_posts_users_mv TO comments_posts_users AS
SELECT PostId, UserId FROM comments_null
INSERT INTO comments_null SELECT * FROM comments
0 rows in set. Elapsed: 5.163 sec. Processed 90.38 million rows, 17.25 GB (17.51 million rows/s., 3.34 GB/s.)
このビューをサブクエリで使用して前のクエリを加速できます:
SELECT avg(Score)
FROM comments
WHERE PostId IN (
SELECT PostId
FROM comments_posts_users
WHERE UserId = 8592047
) AND UserId = 8592047
┌──────────avg(Score)─┐
1. │ 0.18181818181818182 │
└─────────────────────┘
1 row in set. Elapsed: 0.012 sec. Processed 88.61 thousand rows, 771.37 KB (7.09 million rows/s., 61.73 MB/s.)
チェイン化
Materialized Viewはチェイン化可能で、複雑なワークフローを確立できます。実際の例については、このブログ投稿をお勧めします。