PostgreSQL: PostgreSQLのビューとマテリアライズドビュー
最終更新:2026-08-26
1. 学習目標
- CREATE VIEW / ALTER VIEW / DROP VIEW
- 更新可能ビュー(単純なビューはINSERT/UPDATE/DELETEをサポート)
- WITH CHECK OPTIONで行がビューから逃れるのを防止
- マテリアライズドビュー(MATERIALIZED VIEW)——PostgreSQLの特長
- REFRESH MATERIALIZED VIEW / CONCURRENTLY
- ビュー、マテリアライズドビュー、一時テーブルの選択
2. ストーリー
AliceはECプラットフォームのデータエンジニアです。運用チームは毎日8時に売上集計レポートを確認します。このレポートは5つのテーブルを結合し、300万件の注文レコードを集計するため、クエリに2時間かかります。
上司は「レポートを即座に返せないか?」と言いました。
Aliceはクエリをマテリアライズドビューとして定義し、毎日午前4時に自動更新するようにしました。事前計算されたレポートのクエリは2時間から3秒に短縮されました。ただし、マテリアライズドビューのデータは��アルタイムではありません。これが通常のビューとマテリアライズドビューのトレードオフです。
3. 概念: 通常のビュー
(1) CREATE VIEW構文
CREATE [OR REPLACE] VIEW view_name [(column_aliases)] AS
SELECT ...;
ビューは保存されたクエリ定義であり、データは保存されません。ビューをクエリするたびに、PostgreSQLはビュー定義を展開(書き換え)して基になるクエリを実行します。
| プロパティ | 説明 |
|---|---|
| 保存内容 | クエリテキストのみ |
| データ鮮度 | リアルタイム、常に最新のベーステーブルデータを反映 |
| パフォーマンス | 基になるクエリを直接実行するのと同じ |
| 使用容量 | ほぼなし |
▶ サンプル: 売上集計ビューの作成
CREATE VIEW v_daily_sales AS
SELECT
order_date,
region,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_value
FROM orders
GROUP BY order_date, region;
Output:
count
-------
5
(1 row)
▶ サンプル: ビューのクエリ
SELECT * FROM v_daily_sales
WHERE order_date >= '2025-01-01'
ORDER BY total_revenue DESC;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
基になるSELECTを直接書くのと同等です。PostgreSQLが自動的に展開します。
(2) ALTER VIEWとDROP VIEW
| 操作 | 構文 | 説明 |
|---|---|---|
| 名前変更 | ALTER VIEW v RENAME TO v_new |
ビュー名を変更 |
| デフォルトカラム設定 | ALTER VIEW v ALTER COLUMN c SET DEFAULT d |
カラムデフォルトを変更 |
| 所有者設定 | ALTER VIEW v OWNER TO role |
所有者を変更 |
| 削除 | DROP VIEW [IF EXISTS] v [CASCADE] |
CASCADEで依存ビューも削除 |
▶ サンプル: ビューの変更と削除
ALTER VIEW v_daily_sales RENAME TO v_daily_summary;
DROP VIEW IF EXISTS v_daily_summary CASCADE;
Output:
-- SQL statement executed successfully
(3) ビュー展開の仕組み
flowchart LR
A["SELECT * FROM v_daily_sales"] --> B["書き換え<br/>ビュー定義を展開"]
B --> C["SELECT order_date, region, COUNT(*)...<br/>FROM orders<br/>GROUP BY ..."]
C --> D[オプティマイザ]
D --> E[実行]
style B fill:#fff9c4
style D fill:#c8e6c9
4. 概念: 更新可能ビュー
(1) どのビューが更新可��か
PostgreSQLでは、以下の条件をすべて満たす単純なビューが自動的に更新可能(INSERT / UPDATE / DELETEをサポート)になります。
| 条件 | 説明 |
|---|---|
| FROMが単一のベーステーブルのみ | JOINなし |
| GROUP BY / HAVINGなし | 集計なし |
| DISTINCTなし | 重複除去なし |
| ウィンドウ関数なし | OVERなし |
| 集合演算なし | UNION / INTERSECT / EXCEPTなし |
| SELECTカラムがベーステーブルのカラム | 式/計算カラムなし |
▶ サンプル: 更新可能ビュー
CREATE VIEW v_active_users AS
SELECT user_id, name, email, status
FROM users
WHERE status = 'active';
Output:
CREATE TABLE
UPDATE v_active_users SET name = 'Alice Wang' WHERE user_id = 1;
DELETE FROM v_active_users WHERE user_id = 99;
INSERT INTO v_active_users (user_id, name, email, status)
VALUES (101, 'Charlie', 'charlie@example.com', 'active');
(2) WITH CHECK OPTION
デフォルトでは、ビューを通じて更新/挿入された行はビューの範囲から「逃れる」ことができます(例: statusを'inactive'に変更すると、その行はビューに表示されなくなる)。WITH CHECK OPTIONはそのような逃避を禁止します。
| オプション | 動作 |
|---|---|
| CHECK OPTIONなし | 逃避許可。更新された行がビューに表示されなくなる可能性あり |
| WITH CHECK OPTION | 逃避禁止。更新された行はビュー条件を満たす必要あり |
| WITH CASCADED CHECK OPTION | 現在のビュー+依存ビューのチェック(再帰的) |
| WITH LOCAL CHECK OPTION | 現在のビューの条件のみチェック |
▶ サンプル: WITH CHECK OPTIONで逃避防止
CREATE VIEW v_active_users_strict AS
SELECT user_id, name, email, status
FROM users
WHERE status = 'active'
WITH CHECK OPTION;
UPDATE v_active_users_strict SET status = 'inactive' WHERE user_id = 1;
ERROR: new row violates check option for view "v_active_users_strict"
DETAIL: Failing row contains (1, ..., inactive).
▶ サンプル: 更新可能ビューに挿入してからクエリ
INSERT INTO v_active_users (user_id, name, email, status)
VALUES (200, 'Bob', 'bob@example.com', 'active');
SELECT * FROM v_active_users WHERE user_id = 200;
user_id | name | email | status
---------+------+--------------------+--------
200 | Bob | bob@example.com | active
5. 概念: マテリアライズドビュー
(1) MATERIALIZED VIEWの概要
マテリアライズドビューはクエリ結果を実際にディスクに保存します。クエリは事前計算されたデータを直接読み取り、基になるクエリを再実行しません。これはPostgreSQLの特長です。
| プロパティ | 通常のビュー | マテリアライズドビュー |
|---|---|---|
| 保存内容 | クエリテキスト | クエリテキスト+結果データ |
| データ鮮度 | リアルタイム | リフレッシュ時のみ更新 |
| クエリパフォーマンス | 基になるクエリと同じ | 極めて高速(事前計算済みを読み取り) |
| 使用容量 | ほぼなし | 結果セットと同じサイズ |
| 更新可能性 | 単純なビューは可能 | 直接DML不可 |
| リフレッシュ方法 | 不要 | REFRESH MATERIALIZED VIEW |
▶ サンプル: マテリアライズドビューの作成
CREATE MATERIALIZED VIEW mv_monthly_sales AS
SELECT
DATE_TRUNC('month', order_date)::date AS month,
region,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_value
FROM orders
GROUP BY DATE_TRUNC('month', order_date), region
WITH DATA;
Output:
count
-------
5
(1 row)
▶ サンプル: マテリアライズドビューのクエリ
SELECT * FROM mv_monthly_sales
WHERE month >= '2025-01-01'
ORDER BY total_revenue DESC;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
ミリ秒単位で返ります。データが既に事前計算されて保存されているためです。
(2) REFRESH
| 構文 | 動作 | ロック | 速度 |
|---|---|---|---|
REFRESH MATERIALIZED VIEW mv |
完全リフレッシュ、全データを置換 | 排他ロック取得、読み取りブ��ック | 遅い |
REFRESH MATERIALIZED VIEW CONCURRENTLY mv |
増分リフレッシュ(一意インデックス必要) | 読み取りブロックなし | 高速 |
CONCURRENTLYはPostgreSQLの特長です。リフレッシュ中もビューがクエリ可能で、ビジネスがブロックされません。
▶ サンプル: 完全リフレッシュ
REFRESH MATERIALIZED VIEW mv_monthly_sales;
Output:
-- SQL statement executed successfully
リフレッシュ中、このマテリアライズドビューへのすべてのSELECTがブロックされます。
▶ サンプル: CONCURRENTLY増分リフレッシュ
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales;
Output:
-- SQL statement executed successfully
前提条件: マテリアライズドビューに少なくとも1つの一意インデックスが必要です。
CREATE UNIQUE INDEX idx_mv_monthly_sales_pk
ON mv_monthly_sales (month, region);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales;
(3) WITH DATA と WITH NO DATA
| オプション | 動作 |
|---|---|
| WITH DATA | 作成時に即座にデータ投入(デフォルト) |
| WITH NO DATA | 作成時は投入しない。最初のクエリ前にREFRESHが必要 |
▶ サンプル: 遅延投入
CREATE MATERIALIZED VIEW mv_expensive_report AS
SELECT ... FROM ... WITH NO DATA;
REFRESH MATERIALIZED VIEW mv_expensive_report;
Output:
CREATE TABLE
▶ サンプル: 自動スケジュールリフレッシュ(pg_cron拡張機能)
SELECT cron.schedule(
'refresh_monthly_sales',
'0 4 * * *',
$$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales$$
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
毎日午前4時に自動的にリフレッシュされます。
6. ビュー vs マテリアライズドビュー vs 一時テーブル
| 観点 | 通常のビュー | マテリアライズドビュー | 一時テーブル |
|---|---|---|---|
| クエリ定義を保存 | はい | はい | いいえ |
| データを保存 | いいえ | はい | はい |
| データ鮮度 | リアルタイム | 手動リフレッシュ | 手動メンテナンス |
| クエリパフォーマンス | 基になるクエリに依存 | 極めて高速 | 高速 |
| クロスセッション永続性 | はい | はい | いいえ(セッション終了で消滅) |
| インデックスサポート | いいえ(ベーステーブルインデックス使用) | はい | はい |
| DMLサポート | 単純なビューは更新可能 | いいえ | はい |
| 典型的なシナリオ | クエリの簡略化 | レポート事前計算 | 一時的な中間結果 |
classDiagram
class View {
+クエリテキストを保存
+リアルタイムデータ
+単純なビューはDML可能
+WITH CHECK OPTION
}
class MaterializedView {
+クエリテキスト+データを保存
+REFRESHでリフレッシュ
+CONCURRENTLY
+インデックス可能
+WITH DATA/NO DATA
}
class TempTable {
+データのみ保存
+セッション終了で消滅
+完全にDML可能
+インデックス可能
}
View <|-- MaterializedView : extends
MaterializedView ..|> TempTable : 類似パフォーマンス
7. ビュー管理のベストプラクティス
(1) 命名規則
| 種別 | 推奨プレフィックス | 例 |
|---|---|---|
| 通常のビュー | v_ |
v_active_users |
| マテリアライズドビュー | mv_ |
mv_monthly_sales |
| 一時テーブル | tmp_ |
tmp_import_data |
(2) ビューの依存関係とセキュリティ
| 操作 | リスク | 解決策 |
|---|---|---|
| ベーステーブル削除 | ビューが無効になる | DROP TABLE CASCADEで依存ビューを自動削除 |
| ベーステーブルカラム変更 | ビューがエラーになる可能性 | CREATE OR REPLACE VIEWで定▶を更新 |
| 権限制御 | ビューでカラム可視性を制限可能 | GRANT SELECT ON view TO role |
▶ サンプル: ビューによるカラムレベル権限
CREATE VIEW v_user_public AS
SELECT user_id, name
FROM users;
GRANT SELECT ON v_user_public TO reporter_role;
REVOKE SELECT ON users FROM reporter_role;
Output:
CREATE TABLE
reporter_roleはuser_idとnameのみ表示可能で、emailなどの機密カラムは見られません。
8. 総合サンプル
Aliceのレポート最適化——マテリアライズドビュー事前計算+CONCURRENTLYリフレッシュ+ビューによるクエリ簡略化。
CREATE MATERIALIZED VIEW mv_sales_report AS
SELECT
DATE_TRUNC('month', o.order_date)::date AS month,
p.category,
o.region,
COUNT(*) AS order_count,
COUNT(DISTINCT o.customer_id) AS unique_customers,
SUM(o.amount) AS total_revenue,
SUM(o.amount) FILTER (WHERE o.amount >= 50000) AS big_deal_revenue,
AVG(o.amount) AS avg_order_value,
SUM(oi.quantity * oi.unit_price) AS total_gmv
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY DATE_TRUNC('month', o.order_date), p.category, o.region
WITH DATA;
CREATE UNIQUE INDEX idx_mv_sales_report_pk
ON mv_sales_report (month, category, region);
CREATE VIEW v_sales_dashboard AS
SELECT
month,
region,
SUM(total_revenue) AS region_revenue,
SUM(order_count) AS region_orders,
SUM(unique_customers) AS region_customers
FROM mv_sales_report
GROUP BY month, region;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales_report;
9. 実行フロー
マテリアライズドビューの作成とリフレッシュの流れ。
flowchart TD
A[CREATE MATERIALIZED VIEW] --> B[基になるクエリを実行]
B --> C[結果をディスクに書き込み]
C --> D[インデックス作成可能]
D --> E[クエリが直接ディスクデータを読み取り]
E --> F{リフレッシュが必要?}
F -->|REFRESH| G[基になるクエリを再実行]
G --> H[旧データを置換]
H --> E
F -->|CONCURRENTLY| I[増分比較リフレッシュ]
I --> J[読み取りをブロックしない]
J --> E
style A fill:#e1f5fe
style E fill:#c8e6c9
style I fill:#fff9c4
| ステップ | 説明 |
|---|---|
| 作成 | 基になるクエリを実行し、結果をディスクに永続化 |
| クエリ | ディスクデータを直接読み取り、基になるクエリは実行されない |
| リフレッシュ | 基になるクエリを再実行し、旧データを置換 |
| CONCURRENTLY | 増分リフレッシュ、リフレッシュ中もクエリをブロックしない |
❓ よくある質問
📖 まとめ
- 通常のビューはクエリテキストのみを保存し、クエリ時に展開。データは常にリアルタイム
- 単純なビュー(単一テーブル、集計なし)は自動的に更新可能で、INSERT/UPDATE/DELETEをサポート
- WITH CHECK OPTIONはDMLによって行がビューの範囲から逃れるのを防止
- マテリアライズドビューはクエリ結果をディスクに保存し、極めて高速だがリアルタイムではない
- REFRESH MATERIALIZED VIEWは完全リフレッシュ。CONCURRENTLYは読み取りをブロックしない増分リフレッシュ
- CONCURRENTLYリフレッシュの前提条件: マテリアライズドビューに一意インデックスが必要
- ビューはカラムレベル権限を強制でき、機密カラムを隠蔽可能
📝 練習問題
- ⭐ 過去30日間の注文をクエリするビュー
v_recent_ordersを作成し、customer_nameとproduct_nameを含めてください。 - ⭐ ステータス = 'active'の顧客のみを表示する更新可能ビュー
v_active_customersを作成し、WITH CHECK OPTIONを追加してください。 - ⭐⭐ 日次・カテゴリ別の売上を集計するマテリアライズドビュー
mv_daily_category_salesを作成し、CONCURRENTLYリフレッシュをサポートするための一意インデックスを追加してください。 - ⭐⭐ ネストビュー構成を設計してください。マテリアライズドビューが詳細データを事前計算し、通常のビューがマテリアライズドビューの上で次元集計を行います。
- ⭐⭐⭐ 毎日午前3時に
mv_daily_category_salesをリフレッシュし、リフレッシュに5分以上かかる場合にアラートをログ記録するpg_cronスケジュールジョブを作成してください。