株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
このシリーズについて
「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心で解消していくシリーズです。
- 第1回:ACIDの「C(一貫性)」、説明できますか?
- 第2回:トランザクション分離レベル ―「READ COMMITTED」で結局なにが読めるの?
- 第3回:ロックとデッドロック ―「誰が」「何を」「どこまで」ロックしている?
- 第4回:「スキーマ」って結局なに? ― 同じ単語が文脈で別物を指している
- 第5回:正規化 ―「第3正規形まで」の「まで」って何?
- 第6回:インデックス ― 貼ったのに効かないのはなぜ?
- 第7回:実行計画 ― EXPLAINの出力、どこを見る?
- 第8回:N+1問題 ― なぜ「1回」で済まないのか
- 第9回:コネクションプール ― プールサイズは何を基準に決める?
まず自己診断
「この集計クエリ、毎回書くの面倒だからビューにしとくね」——よくある会話です。では:
ビューにすると、そのクエリは速くなりますか?
なりません。1ミリ秒も。 ここで「あれ?」と思ったなら、この記事の対象読者です。
そして「速くしたいならマテリアライズドビュー」と言われても、なぜそっちは速いのか・何を失うのかまで説明できる人は多くありません。整理します。
結論:この1枚
両者の違いは、「SELECT文を保存する」のか「SELECT文の結果を保存する」のか、それだけです。
| ビュー | マテリアライズドビュー | |
|---|---|---|
| 保存されるもの | SELECT文 | 実行結果(実データ) |
| 参照時の処理 | 毎回そのSELECTを実行 | 保存済みの表を読むだけ |
| 鮮度 | 常に最新 | REFRESHした時点のまま |
| 速度 | 元のクエリと同じ | 桁違いに速い |
| ディスク使用 | ほぼゼロ | 結果ぶん消費 |
「最新か、速いか」のトレードオフ。これが本質です。
以下、PostgreSQL 16.14(Docker postgres:16) で顧客5万件・注文500万件(orders は363MB)を使った実測です。
「ビューにすると速くなる」を実測で解体
地域別の売上を集計する、それなりに重いクエリを用意しました。これをビューにします。
CREATE VIEW v_region_sales AS
SELECT c.region, count(*) AS order_count, sum(o.amount) AS total_amount,
avg(o.amount)::numeric(10,2) AS avg_amount
FROM orders o JOIN customers c ON c.id = o.customer_id
GROUP BY c.region;
① 元のクエリを直接実行した実行計画(抜粋):
Finalize GroupAggregate (cost=81427.91..81428.99 rows=4 width=37)
-> Hash Join (cost=1444.00..59594.51 rows=2083330 width=9)
-> Parallel Seq Scan on orders o (cost=0.00..52681.30 rows=2083330 width=8)
Execution Time: 1083.785 ms
② ビュー経由(SELECT * FROM v_region_sales):
Finalize GroupAggregate (cost=81427.91..81428.99 rows=4 width=37)
-> Hash Join (cost=1444.00..59594.51 rows=2083330 width=9)
-> Parallel Seq Scan on orders o (cost=0.00..52681.30 rows=2083330 width=8)
Execution Time: 1187.876 ms
コストの数字が小数点まで完全に同一です。500万行のスキャンも集計もそのまま行われています。実行時間の差(1083ms と1187ms)は測定ごとのブレで、有意差ではありません。
ビューは「名前を付けただけ」。参照するたびに中身のSELECTが展開されて実行されます。だから速くなりようがありません。
ビューの価値は速度ではなく、複雑なSQLに名前を付けて隠すこと・見せたい列だけを見せることにあります。
マテリアライズドビューの実測
同じ定義でマテリアライズドビューを作ります。
CREATE MATERIALIZED VIEW mv_region_sales AS
SELECT c.region, count(*) AS order_count, ... (定義はビューと同じ);
作成に 919.713 ms かかりました。これはこの時点で1回集計を実行し、結果を保存したからです。参照してみます。
Seq Scan on mv_region_sales (cost=0.00..18.80 rows=880 width=64)
Execution Time: 0.016 ms
1083 ms → 0.016 ms。 500万行の集計が消え、4行の小さな表を読むだけになりました。ディスク使用量も確認すると、性格の違いがはっきりします。
ビュー v_region_sales : 0 bytes
マテビュー mv_region_sales: 24 kB
ベース表 orders : 363 MB
代償:鮮度が止まる
ここが本題です。ベーステーブルに10万件追加してみます。
INSERT INTO orders (customer_id, amount, created_at)
SELECT 4, 100, now() FROM generate_series(1,100000);
追加前と追加後で、両者を比べます(east地域の注文件数)。
| ビュー | マテリアライズドビュー | |
|---|---|---|
| INSERT前 | 1,250,000 | 1,250,000 |
| INSERT後 | 1,350,000 | 1,250,000 ← 古いまま |
マテリアライズドビューは自動で更新されません。 明示的にREFRESHして初めて追いつきます。
REFRESH MATERIALIZED VIEW mv_region_sales; -- Time: 1400.441 ms
REFRESH後は両者とも 1,350,000 で一致しました。当然ながら、REFRESHには集計をやり直すぶんの時間(今回は約1.4秒)がかかります。
つまりマテリアライズドビューを使うとは、「どれくらい古いデータなら許せるか」を決めることとほぼ同義です。REFRESHを1時間おきにするなら、最大1時間古い数字がユーザーに見えます。
なお通常の
REFRESHは完了までそのマテビューへの参照をブロックします。REFRESH MATERIALIZED VIEW CONCURRENTLYを使えば参照を止めずに更新できますが、一意インデックスが必須です。実際に付けずに実行するとこう怒られます。ERROR: cannot refresh materialized view "public.mv_region_sales" concurrently HINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.
エンジン差:MySQLにマテリアライズドビューはない
ここも歯がゆさポイントです。MySQLにマテリアライズドビューは存在しません。 公式FAQに明記されています。
A.6.5. Does MySQL have materialized views?
No.
MySQL 8.0でも、最新の9.7のマニュアルでも回答は同じ「No」です。CREATE VIEW(通常のビュー)は使えますが、CREATE MATERIALIZED VIEW に相当する構文はありません。
ではMySQLで重い集計を高速化したいときはどうするか。集計結果を入れる普通のテーブルを自分で作り、バッチやイベントスケジューラで定期的に作り直す——これが定石です。やっていることはマテリアライズドビューと同じで、PostgreSQLが機能として持っているものを、MySQLでは手作業で組むという違いになります。
で、どっちをいつ使う?
| 使う場面 | 選ぶもの |
|---|---|
| 複雑なJOINに名前を付けて再利用したい | ビュー |
| 特定の列だけ見せたい(権限の窓口にする) | ビュー |
| ダッシュボードの重い集計を毎回走らせたくない | マテリアライズドビュー |
| 日次・時間単位の鮮度で構わないレポート | マテリアライズドビュー |
| 口座残高・在庫数など、常に正確でないと困る値 | どちらでもなく、クエリ側を改善する |
第4回で扱った3層スキーマを覚えているでしょうか。利用者ごとの見え方を定義する「外部スキーマ」の実装が、まさにビューです。ビューは高速化の道具ではなく、設計上の「見せ方」を表現する道具——そう捉えると位置づけがはっきりします。
まとめ:1行で説明するなら
「ビューはSELECT文に名前を付けたもので、参照のたびに中身が展開されて実行されます。だから常に最新ですが、速くはなりません。マテリアライズドビューは実行結果そのものを保存するので桁違いに速い代わりに、REFRESHするまでデータが古いままです。最新が要るならビュー、速度が要って鮮度に妥協できるならマテリアライズドビュー。なおMySQLに後者はないので、集計テーブルを自作します」
冒頭の「ビューにしとくね」に戻ります。それは整理のためであって、速くはならない。 ここが言えるかどうかが分かれ目です。
次回:最終回
第11回:レプリケーションとラグ ——「書いた直後に読めないのはなぜ?」を図解します。実は今回の「鮮度が遅れる」話と、同じ構造の問題が待っています。
(シリーズ全11回の予定は第1回に掲載しています)
参考
- PostgreSQL 16 Documentation - CREATE VIEW
- PostgreSQL 16 Documentation - CREATE MATERIALIZED VIEW
- PostgreSQL 16 Documentation - REFRESH MATERIALIZED VIEW
- PostgreSQL 16 Documentation - Materialized Views
- MySQL 8.0 Reference Manual - A.6 FAQ: Views
- MySQL 8.0 Reference Manual - CREATE VIEW Statement
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!