📋
マテリアライズドビュー
クエリ結果をディスクに保存して高速に返すマテリアライズドビューの仕組みとリフレッシュ戦略。インクリメンタル更新・TimescaleDBのContinuous Aggregates・CQRSのRead Sideとの関係を理解する
定義
マテリアライズドビュー(Materialized View):クエリの結果をディスクに物理的に保存したビュー。通常のビュー(論理ビュー)がクエリのたびにSQLを実行するのに対し、マテリアライズドビューは事前計算済みの結果を返す。
通常のビュー:
SELECT * FROM order_summary; ← 実行のたびにJOINと集計が走る
マテリアライズドビュー:
SELECT * FROM order_summary; ← 保存済みの結果を返す(高速)
ただし最新でない可能性がある(リフレッシュが必要)
PostgreSQLでの作成
-- マテリアライズドビューの作成
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT
DATE_TRUNC('month', created_at) AS month,
customer_id,
SUM(total_amount) AS revenue,
COUNT(*) AS order_count
FROM orders
WHERE status = 'completed'
GROUP BY 1, 2;
-- インデックスを張れる(通常のテーブルと同じ)
CREATE INDEX ON monthly_revenue (customer_id);
CREATE INDEX ON monthly_revenue (month DESC);
-- クエリ(事前計算済みなので高速)
SELECT * FROM monthly_revenue
WHERE customer_id = 1
ORDER BY month DESC;
リフレッシュ戦略
完全リフレッシュ(Full Refresh)
-- ビュー全体を再計算して置き換える
REFRESH MATERIALIZED VIEW monthly_revenue;
-- → リフレッシュ中にビューがロック → 読み取り不可
-- CONCURRENTLY: ロックなしでリフレッシュ(ユニークインデックスが必要)
CREATE UNIQUE INDEX ON monthly_revenue (month, customer_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue;
-- → リフレッシュ中も読み取り可能(古いデータが返る)
問題:テーブルが大きいほどリフレッシュに時間がかかる。毎分リフレッシュは現実的でない。
スケジュール設定
-- pg_cronを使って定期リフレッシュ
SELECT cron.schedule(
'refresh-monthly-revenue',
'0 * * * *', -- 毎時0分
'REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue'
);
インクリメンタル更新の実装
PostgreSQLはインクリメンタル更新をネイティブサポートしていないが、トリガーで実現できる。
-- 注文が追加・更新されたとき集計を更新するトリガー
CREATE OR REPLACE FUNCTION update_monthly_revenue()
RETURNS TRIGGER AS $$
DECLARE
v_month DATE := DATE_TRUNC('month', NEW.created_at);
BEGIN
-- upsert で差分だけ更新
INSERT INTO monthly_revenue (month, customer_id, revenue, order_count)
VALUES (v_month, NEW.customer_id, NEW.total_amount, 1)
ON CONFLICT (month, customer_id) DO UPDATE SET
revenue = monthly_revenue.revenue + EXCLUDED.revenue,
order_count = monthly_revenue.order_count + 1;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_revenue
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.status = 'completed')
EXECUTE FUNCTION update_monthly_revenue();
注意:DELETE・UPDATEへの対応が複雑になる。CRDTのPN-Counterの発想と似ている。
TimescaleDBのContinuous Aggregates
タイムシリーズDBで触れたTimescaleDBは、インクリメンタル更新をネイティブサポートする。
-- 新しいデータが追加されるたびに差分だけ更新
CREATE MATERIALIZED VIEW hourly_sensor_avg
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
sensor_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp
FROM sensor_readings
GROUP BY bucket, sensor_id;
-- リフレッシュポリシー(直近2時間を自動更新)
SELECT add_continuous_aggregate_policy('hourly_sensor_avg',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);
マテリアライズドビューとCQRS
CQRSのRead Sideとマテリアライズドビューは同じ考え方。
CQRS の Read Side:
書き込みイベントに基づいて読み取り専用のモデルを維持
→ アプリレベルの実装
マテリアライズドビュー:
書き込みに基づいてDBが読み取り専用のモデルを維持
→ DBレベルの実装
どちらを選ぶか:
シンプルな集計 → マテリアライズドビュー
複雑なビジネスロジック → アプリ側のCQRS
リアルタイム性が必要 → ストリーム処理 + マテリアライズドビュー
ダッシュボードへの実践的な適用
-- ダッシュボード用の事前計算テーブル(マテリアライズドビュー的な役割)
CREATE TABLE dashboard_stats (
tenant_id UUID PRIMARY KEY,
total_users INT,
active_users_30d INT,
total_revenue DECIMAL(15,2),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 夜間バッチで全件更新
INSERT INTO dashboard_stats (tenant_id, total_users, active_users_30d, total_revenue)
SELECT
tenant_id,
COUNT(*),
COUNT(*) FILTER (WHERE last_active_at > NOW() - INTERVAL '30 days'),
SUM(lifetime_revenue)
FROM users
GROUP BY tenant_id
ON CONFLICT (tenant_id) DO UPDATE SET
total_users = EXCLUDED.total_users,
active_users_30d = EXCLUDED.active_users_30d,
total_revenue = EXCLUDED.total_revenue,
updated_at = NOW();
通常ビューとの使い分け
| 観点 | 通常のビュー | マテリアライズドビュー |
|---|---|---|
| 常に最新 | ✅ | ❌(リフレッシュが必要) |
| 読み取り速度 | 遅い(毎回計算) | 速い(保存済み) |
| ストレージ | 不要 | 必要 |
| インデックス | 不可 | 可能 |
| 更新の複雑さ | なし | リフレッシュ戦略が必要 |
| 適した用途 | 複雑なクエリの簡略化 | 重い集計・ダッシュボード |
関連概念
- → CQRS(アプリレベルのRead Side設計)
- → ストレージとインデックス(物理的なデータ保存との関係)
- → タイムシリーズDB(Continuous Aggregatesの詳細)
- → バッチ処理(夜間バッチでの一括リフレッシュ)
出典・参考文献
- PostgreSQL Documentation, “Materialized Views” — postgresql.org/docs/current/rules-materializedviews.html
- Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 3
- TimescaleDB Documentation, “Continuous Aggregates” — docs.timescale.com
- 1. 🗄️データ志向アプリケーション設計:概要
- 2. 🧩データモデルとクエリ言語
- 3. 💾ストレージエンジンとインデックス
- 4. 🔁レプリケーション
- 5. 🍕パーティショニング(シャーディング)
- 6. 🔒トランザクションとACID
- 7. ⚡分散システムの本質的な問題
- 8. 🤝一貫性と分散合意
- 9. 📦バッチ処理
- 10. 🌊ストリーム処理
- 11. 📋エンコーディングとスキーマ進化
- 12. 🔗Sagaパターンと分散トランザクション
- 13. 🏗️データシステムの統合設計
- 14. 📸MVCC(多版型同時実行制御)
- 15. 📊列指向ストレージとOLAP設計
- 16. 🕰️ベクタークロックと因果順序
- 17. 🔀CRDT(競合なし複製データ型)
- 18. 🔍クエリオプティマイザーと実行計画
- 19. ⚡キャッシュ戦略とRedis設計
- 20. 🔎全文検索と転置インデックス
- 21. 🌐NewSQL(分散ACIDデータベース)
- 22. 📝WALと論理レプリケーション
- 23. 🔌コネクションプーリング
- 24. 🚧ゼロダウンタイムマイグレーション
- 25. 🆔分散ID生成
- 26. 🔄N+1問題とDataLoaderパターン
- 27. 📈タイムシリーズDB
- 28. 🛡️Row Level Security(行レベルセキュリティ)
- 29. 📤Outboxパターン(トランザクショナルアウトボックス)
- 30. 💾DBバックアップとPITR
- 31. ⚠️データベース設計アンチパターン
- 32. 🕸️グラフDB深掘り
- 33. 🔋バックプレッシャーとサーキットブレーカー
- 34. 🔵コンシステントハッシング
- 35. 📋マテリアライズドビュー
- 36. 📡DBモニタリングとオブザーバビリティ
- 37. 🔐データプライバシーとCrypto-Shredding
- 38. ✂️垂直分割(Vertical Partitioning)
- 39. 🪟ウィンドウ関数
- 40. 🧲ベクトルDBとpgvector
- 41. 🔧dbtとデータ変換パイプライン
- 42. 📬ジョブキューの設計
- 43. 📐正規化理論(1NF〜BCNF)
- 44. ☁️クラウドDBサービスの設計思想と選択基準
- 45. 🗺️地理空間データとPostGIS
- 46. 🔑DBセキュリティと権限管理
- 47. 🏔️Lakehouse(Apache Iceberg / Delta Lake)
- 48. 📜データコントラクト
- 49. 🔭OpenTelemetryとDBトレーシング
出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 3