アプリケーションにおいて高負荷になりやすい処理の一つにレポート・データ集計があります。
リアルタイム性が求められる場合もあれば、ある時点までに集計されたデータを見れればOKなど、要件はさまざまですが、一般的にはデータベースに対して複雑なクエリや複数テーブルに対しての結合を行う必要があります。そのためデータ量に依存しやすく、リリース当初は問題なくレポートが見れていたのに、サービスの運用期間が長くなるとレポートが見えなくなったという話をよく聞きます。
この問題への対処方法の1つとして、データのキャッシュ化が挙げられます。
リアルタイム性は失われてしまいますが、ある時点までの集計がされているデータを用意しておけば、都度、集計を行う必要がないため結果を表示するまでの時間を大幅に短縮することができます。
MATERIALIZED VIEWによるキャッシュ化
キャッシュ化をアプリケーション側で実装しても良いですが、実はデータベースにはMATERIALIZED VIEW
(マテリアライズドビュー)という、SQLの実行結果をキャッシュする機能が提供されています。通常のビューとは異なり、SQLの実行結果を保持してくれるため、あたかも1つのテーブルのように扱うことができます。
PostgreSQLにおけるマテリアライズドビューはビューのようにルールシステムを使用しますが、あたかもテーブルであるかのような形態で結果を保持します。
しかしながら名前に「ビュー」が含まれていることからも分かるように通常のテーブルとは異なります。
マテリアライズドビューは直接の更新(INSERT・UPDATE・DELETE)はできずREFRESH
による最新状態への更新のみがサポートされています。
マテリアライズドビューの定義
ユーザーが投稿した記事の一覧を管理する3つのテーブルを用意しました。
user_posts
は中間テーブルであり、頻繁にusers
とposts
を結合したデータを参照したいです。
SELECT user_posts.user_id, user_posts.post_id, users.name AS user_name, posts.title AS post_title, posts.body AS post_body FROM user_posts JOIN users ON user_posts.user_id = users.user_id JOIN posts ON user_posts.post_id = posts.post_id ;
このSQLの実行結果をキャッシュ化するために、マテリアライズドビューを作成してみます。
CREATE MATERIALIZED VIEW
を実行することで、簡単に作ることができました。
CREATE MATERIALIZED VIEW join_table AS ( SELECT user_posts.user_id, user_posts.post_id, users.name AS user_name, posts.title AS post_title, posts.body AS post_body FROM user_posts JOIN users ON user_posts.user_id = users.user_id JOIN posts ON user_posts.post_id = posts.post_id );
先ほども書いたようにテーブルではないため、当然、インデックスなどは持っていません。
postgres=# \d join_table Materialized view "public.join_table" Column | Type | Collation | Nullable | Default ------------+------------------------+-----------+----------+--------- user_id | integer | | | post_id | integer | | | user_name | character varying(30) | | | post_title | character varying(100) | | | post_body | text
差分の確認
先ほど定義したテーブルに100人のユーザー、300件の記事を作成して、1人のユーザーが3つの記事をもつ状態を用意します。 この時点では作成したマテリアライズドビューには追加されたデータは反映されておらず、明示的に更新を行う必要があります。
postgres=# SELECT COUNT(*) FROM join_table; count ------- 0 (1 row)
マテリアライズドビューを最新の状態に更新するにはREFRESH MATERIALIZED VIEW
を実行すれば良いです。
更新後に再度、データ数を確認すると300件になっていることが分かります。逆を言えば明示的な更新が行われるまでマテリアライズドビューは影響を受けないため、データ数が急増した場合などに突然、参照できなくなるということは考えにくいということです。
postgres=# REFRESH MATERIALIZED VIEW join_table; REFRESH MATERIALIZED VIEW postgres=# SELECT COUNT(*) FROM join_table; count ------- 300 (1 row)
vs. 既存のテーブルからの取得
マテリアライズドビューを定義したことで、どれだけ高速化されるのか比較してみました。
データ量の増加に伴って、通常のクエリ実行とマテリアライズドビューからのデータ取得の実行時間に差が開いていくことが分かります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/t/takamizawa46/20231224/20231224175016.png)
この結果からマテリアライズドビューの有効性が分かりました。
ただし、気をつける必要があるのはマテリアライズドビューからのデータ取得は全てSeq Scan
になるという点です。
今回のようなデータ量がどんどんと増えていくケースにはIOコストが高くなる可能性があります。
Seq Scan on join_table (cost=0.00..8.00 rows=300 width=85) (actual time=0.011..0.044 rows=300 loops=1) Planning Time: 0.291 ms Execution Time: 0.073 ms
計測後に気づいたのですが、なんとマテリアライズドビューにはインデックスが作成できるようです。
もうここまで来るとテーブルと何が違うのかよく分からなくりますね...。
マテリアライズドビューにはインデックスを設定することもできますが...
まとめ
- データベースはSQLの実行結果を保持するマテリアライズドビューという機能を提供している
- マテリアライズドビューは直接、更新(INSERT・UPDATE・DELETE)することができない
- マテリアライズドビューの更新には
REFRESH MATERIALIZED VIEW
を実行する必要がある - マテリアライズドビューは更新されるまで元テーブルに追加されたデータを反映していない
- 実際のテーブルとマテリアライズドビューからのデータ取得ではマテリアライズドビューの方が高速だった
マテリアライズドビューの特性を考えるとガッチリ定義するまでもない集計データを保持しておくのにピッタリです。
ただし、デフォルトではインデックスが定義されていないためseq scan
になってしまうため、データ量が増えていくようなSQLをマテリアライズドビュー化する際は注意が必要です。
マテリアライズドビューを上手く使いこなせば強力な武器になりそうですね。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。