📋
概念 #データ設計 #マテリアライズドビュー #PostgreSQL #パフォーマンス #CQRS #DDIA 📚 データ志向アプリケーション設計(DDIA)

マテリアライズドビュー

クエリ結果をディスクに保存して高速に返すマテリアライズドビューの仕組みとリフレッシュ戦略。インクリメンタル更新・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();

通常ビューとの使い分け

観点通常のビューマテリアライズドビュー
常に最新❌(リフレッシュが必要)
読み取り速度遅い(毎回計算)速い(保存済み)
ストレージ不要必要
インデックス不可可能
更新の複雑さなしリフレッシュ戦略が必要
適した用途複雑なクエリの簡略化重い集計・ダッシュボード

関連概念

出典・参考文献

  • 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. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー
  34. 34. 🔵コンシステントハッシング
  35. 35. 📋マテリアライズドビュー
  36. 36. 📡DBモニタリングとオブザーバビリティ
  37. 37. 🔐データプライバシーとCrypto-Shredding
  38. 38. ✂️垂直分割(Vertical Partitioning)
  39. 39. 🪟ウィンドウ関数
  40. 40. 🧲ベクトルDBとpgvector
  41. 41. 🔧dbtとデータ変換パイプライン
  42. 42. 📬ジョブキューの設計
  43. 43. 📐正規化理論(1NF〜BCNF)
  44. 44. ☁️クラウドDBサービスの設計思想と選択基準
  45. 45. 🗺️地理空間データとPostGIS
  46. 46. 🔑DBセキュリティと権限管理
  47. 47. 🏔️Lakehouse(Apache Iceberg / Delta Lake)
  48. 48. 📜データコントラクト
  49. 49. 🔭OpenTelemetryとDBトレーシング

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 3