CREATE / DROP VIEW
视图(VIEW)是 PostgreSQL 里最轻量的抽象层:它把一个命名的 SELECT 查询保存进系统目录,每次引用时再展开执行,自身不存储任何数据。本节把视图的创建与删除语法、列别名、WITH 选项、依赖关系、权限模型、ALTER VIEW 以及调试手段一次讲清,帮你在可控成本内搭起「数据访问层」。
前置知识
阅读本节前,建议先了解:基础查询(SELECT)、模式(Schema)
基础概念
视图是什么
视图的定义只有一句话:一个被命名的查询(a named query)。它不保存结果行,只保存定义文本与解析后的查询树。
SELECT * FROM my_view;
│
▼
优化器:把 my_view 的定义文本替换进来
│
▼
等价于直接执行 my_view 里那条 SELECT因此有几个必然结论:
| 事实 | 说明 |
|---|---|
| 视图不占数据空间 | 只有 pg_class 中一个元数据条目,pg_relation_size() 返回 0(或 0 字节头) |
| 视图永远读到最新数据 | 每次查询都重新执行底层 SELECT |
| 视图不会加速查询 | 它只是 SQL 文本的封装,能不能快取决于底层表与索引 |
| 视图可以嵌套 | 视图基于视图之上,但嵌套层次过深会影响可读性与优化 |
| 视图必须显式列出列 | 列名来自 SELECT 的输出列别名 |
视图 vs 表 vs 物化视图
| 对象 | 是否存数据 | 数据新鲜度 | relkind | 能否建索引 |
|---|---|---|---|---|
| 表(Table) | 是 | 实时 | r | 是 |
| 视图(View) | 否 | 实时(每次重算) | v | 否 |
| 物化视图(Matview) | 是 | 取决于刷新 | m | 是(这是关键差异) |
普通视图不能建索引。如果你的痛点是「这条视图查询太慢」,正确做法是在底层表上建索引,或者改用物化视图。
视图的本质是一条规则
在 PostgreSQL 内部,视图是用「查询重写规则」(_RETURN 规则)实现的。创建视图时,系统会自动在 pg_rewrite 里写入一条 ON SELECT DO INSTEAD 规则:
-- 查看视图背后的规则
SELECT r.rulename, pg_get_ruledef(r.oid) AS rule_def
FROM pg_rewrite r
JOIN pg_class c ON c.oid = r.ev_class
WHERE c.relname = 'v_users_simple'; rulename | rule_def
-----------+------------------------------------------------------------------------------------------
_RETURN | CREATE RULE "_RETURN" AS ON SELECT TO v_users_simple DO INSTEAD
| SELECT users.id, users.email, users.name FROM users;
(1 row)这就是为什么删除视图时会连带删除它的 _RETURN 规则。规则系统的完整机制见 CREATE RULE 规则。
视图能解决什么
| 场景 | 视图的作用 |
|---|---|
| 简化复杂查询 | 把多表 JOIN + 过滤封装成一个名字 |
| 列级权限控制 | 只暴露部分列,隐藏敏感字段 |
| 稳定接口 | 底层表结构变化时,视图保持对外接口不变 |
| 逻辑隔离 | 对外只暴露「逻辑表」,屏蔽分表/分区细节 |
| 统一口径 | 把业务过滤条件(如 is_deleted = false)固化进定义 |
语法与用法
-- 创建视图
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW name [ ( column_name [, ...] ) ]
[ WITH ( view_option_name [= view_option_value] [, ... ] ) ]
AS query
[ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
-- 修改视图属性(不含查询定义)
ALTER VIEW name RENAME TO new_name
ALTER VIEW name SET SCHEMA new_schema
ALTER VIEW name OWNER TO new_owner
ALTER VIEW name SET ( view_option_name [= view_option_value] [, ... ] )
ALTER VIEW name RESET ( view_option_name [, ... ] )
-- 删除视图
DROP VIEW [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ]WITH 选项(view options)目前只有两个:
| 选项 | 取值 | 含义 | 版本 |
|---|---|---|---|
security_barrier | boolean | 阻止下层谓词越过视图边界下推,保证「先按视图过滤、再由外层过滤」 | 9.2+ |
security_invoker | boolean | 视图按调用者权限执行(默认 false,即按定义者权限) | 15+ |
check_option | local / cascaded | 等价于在定义里写 WITH CHECK OPTION | 9.4+ |
最简可运行示例:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
name text NOT NULL,
password text NOT NULL,
is_deleted boolean NOT NULL DEFAULT false
);
CREATE VIEW v_users_simple AS
SELECT id, email, name
FROM users
WHERE is_deleted = false;CREATE TABLE
CREATE VIEW-- 像表一样查询
SELECT * FROM v_users_simple; id | email | name
----+-------+------
(0 rows)详细说明
列别名:显式、隐式与自动推导
视图的列名有三个来源,优先级从高到低:
- 视图定义里显式给出的列名列表(
CREATE VIEW v (a, b) AS ...) SELECT输出列的别名(SELECT id AS user_id ...)- 底层列名(
SELECT id ...→ 列名id)
-- 方式 1:视图级列名列表(最明确)
CREATE VIEW v_orders_a (order_id, buyer, amount) AS
SELECT id, user_id, amount FROM orders;
-- 方式 2:SELECT 内别名
CREATE VIEW v_orders_b AS
SELECT id AS order_id, user_id AS buyer, amount AS amount FROM orders;
-- 方式 3:依赖底层列名
CREATE VIEW v_orders_c AS
SELECT id, user_id, amount FROM orders;三种方式得到的列名完全相同。当输出列是表达式时,PostgreSQL 会自动推导名字:
CREATE VIEW v_order_stats AS
SELECT
user_id,
count(*) AS order_count,
sum(amount) AS total_amount
FROM orders
GROUP BY user_id;-- 查看视图的列名与类型
\d v_order_stats View "public.v_order_stats"
Column | Type | Collation | Nullable | Default
--------------+---------+-----------+----------+---------
user_id | bigint | | |
order_count | bigint | | |
total_amount | numeric | | |如果表达式没有别名,PostgreSQL 会生成 ?column? 之类的名字,这是不推荐的做法:
-- 不推荐:表达式没有别名,列名自动生成,难以引用
CREATE VIEW v_bad AS
SELECT user_id, sum(amount) FROM orders GROUP BY user_id;
\d v_bad View "public.v_bad"
Column | Type | Collation | Nullable | Default
----------+---------+-----------+----------+---------
user_id | bigint | | |
sum | numeric | | |当同名表达式出现多次时,名字会带数字后缀(sum、sum_1……),非常容易出错。永远给表达式列起别名。
WITH 选项:security_barrier
security_barrier 解决的是「谓词下推导致的安全漏洞」。设想一个视图只暴露「自己部门」的行,其中包含一个可能报错的表达式函数:
-- 定义一个会除零的过滤函数(示意)
CREATE FUNCTION safe_div(a numeric, b numeric) RETURNS numeric
LANGUAGE sql IMMUTABLE AS $$ SELECT a / b $$;如果没有 security_barrier,优化器可能把用户写在外层的 WHERE 谓词下推到视图内部先执行,从而触发本不该被执行的函数(行级安全与视图安全的核心就是这个「求值顺序」问题):
-- 带 security_barrier 的视图:先按视图条件过滤,再执行外层条件
CREATE VIEW v_secret WITH (security_barrier = true) AS
SELECT id, dept_id, salary FROM employees WHERE dept_id = current_setting('app.dept')::int;
-- 修改已有视图以开启/关闭该选项
ALTER VIEW v_secret SET (security_barrier = true);
ALTER VIEW v_secret RESET (security_barrier);代价是失去谓词下推能力,性能会下降。完整分析见 视图与性能(视图展开)。
WITH 选项:security_invoker(PG 15+)
默认情况下,视图按下「视图所有者」的权限执行(security_invoker = false)。这在「视图用于授权访问」时是优点(只需给视图 SELECT 权限),但也是安全隐患:如果视图所有者是超级用户或权限很大的角色,调用者就能通过视图读到本无权访问的数据。
PostgreSQL 15 引入了 security_invoker,让视图按调用者权限执行:
-- PostgreSQL 15+
CREATE VIEW v_employees_public
WITH (security_invoker = true) AS
SELECT id, name, dept FROM employees;
-- 对已有视图开启
ALTER VIEW v_employees_public SET (security_invoker = true);两种模式的对比:
| 模式 | 权限判定依据 | 适用场景 |
|---|---|---|
security_invoker = false(默认) | 视图所有者的权限 + 视图的 ACL | 用视图做「受控授权」入口 |
security_invoker = true(PG 15+) | 调用者自己的权限 + 底层对象 ACL | 希望底层 RLS / 权限仍然生效 |
security_invoker = true 时,调用者必须同时拥有底层表的权限,否则即便有视图权限也会报 permission denied for table employees。这是与旧行为的最大区别,迁移时容易踩到。
CREATE OR REPLACE VIEW 的限制
OR REPLACE 只能替换查询定义,且列结构必须兼容:
| 能否修改 | 说明 |
|---|---|
| 能否改查询体 | 可以 |
| 能否增加列 | 可以,但新列必须追加在末尾 |
| 能否删除列 | 不可以(除非 DROP VIEW 重建) |
| 能否改列名 | 不可以 |
| 能否改列类型 | 不可以(例如 int → text) |
| 能否改列顺序 | 不可以 |
能否改 WITH 选项 | 可以 |
CREATE VIEW v_a AS SELECT id, email FROM users;
-- 正确:在末尾追加新列
CREATE OR REPLACE VIEW v_a AS SELECT id, email, name FROM users;
-- 错误!列顺序/名字/类型改变
CREATE OR REPLACE VIEW v_a AS SELECT email, id FROM users;ERROR: cannot change name of view column "id" to "email"
HINT: Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.-- 错误!试图改变列类型
CREATE OR REPLACE VIEW v_a AS SELECT id, email, name::text AS name FROM users;ERROR: cannot change data type of view column "name" from text to text(上例在类型真的发生改变时才会报错;关键是改类型不被允许。)遇到必须改结构的情况,只能删除重建,或用 ALTER VIEW ... RENAME / SET SCHEMA 做改名与迁移。
依赖关系与 CASCADE
视图对底层对象有依赖,记录在 pg_depend 中。删除视图依赖的对象时,默认 RESTRICT 会直接报错:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL
);
CREATE VIEW v_orders AS SELECT id, user_id, amount FROM orders;
-- 尝试删除底层表
DROP TABLE orders;ERROR: cannot drop table orders because other objects depend on it
DETAIL: view v_orders depends on table orders
HINT: Use DROP ... CASCADE to drop the dependent objects too.加上 CASCADE 会静默连带删除视图,这在生产环境是危险动作:
-- 危险!会连带删掉所有依赖视图
DROP TABLE orders CASCADE;NOTICE: drop cascades to view v_orders
DROP TABLE删除视图本身同样有依赖链:
CREATE VIEW v_orders_2 AS SELECT * FROM v_orders WHERE amount > 0;
-- 删除被依赖的视图
DROP VIEW v_orders;ERROR: cannot drop view v_orders because other objects depend on it
DETAIL: view v_orders_2 depends on view v_orders
HINT: Use DROP ... CASCADE to drop the dependent objects too.查依赖关系最实用的两个手段:
-- 查这个视图依赖了哪些对象
SELECT
dependent.relname AS dependent_view_name,
dependent.relkind AS dependent_kind,
referenced.relname AS referenced_name
FROM pg_depend d
JOIN pg_rewrite r ON r.oid = d.objid
JOIN pg_class dependent ON dependent.oid = r.ev_class
JOIN pg_class referenced ON referenced.oid = d.refobjid
WHERE dependent.relname = 'v_orders'
AND referenced.oid <> dependent.oid; dependent_view_name | dependent_kind | referenced_name
---------------------+----------------+-----------------
v_orders | v | orders
(1 row)-- 用 \d+ 看视图定义,一眼看清底层对象
\d+ v_orders View "public.v_orders"
Column | Type | Collation | Nullable | Default | Storage | Description
---------+--------------+-----------+----------+---------+---------+-------------
id | bigint | | | | plain |
user_id | bigint | | | | plain |
amount | numeric(12,2)| | | | main |
View definition:
SELECT id,
user_id,
amount
FROM orders;视图的权限模型
创建视图时必须对底层对象有相应权限,但视图一旦建立,其权限判定默认基于视图所有者:
-- 给普通角色只授视图查询权限,不给底层表权限
CREATE ROLE app_reader LOGIN PASSWORD 'secret';
GRANT USAGE ON SCHEMA public TO app_reader;
GRANT SELECT ON v_users_simple TO app_reader;
-- 验证(以 app_reader 身份)
SET ROLE app_reader;
SELECT * FROM v_users_simple; -- 成功(视图所有者是超级用户/表 owner)
SELECT * FROM users; -- 失败ERROR: permission denied for table users用视图做列级权限控制是最常见的用法:把敏感列排除在外,只暴露允许的列。
-- 只暴露 id / email / name,隐藏 password
CREATE VIEW v_users_public AS
SELECT id, email, name FROM users WHERE is_deleted = false;
GRANT SELECT ON v_users_public TO app_reader;
REVOKE ALL ON users FROM app_reader;三种权限控制的对比:
| 手段 | 粒度 | 能否按行过滤 | 能否隐藏列 |
|---|---|---|---|
GRANT 列级权限 | 列 | 否 | 可以(列级 GRANT) |
| 视图 + 授权 | 列 + 行 | 可以 | 可以 |
| 行级安全(RLS) | 行 | 可以(按角色/会话) | 否 |
视图与搜索路径(search_path)
视图定义中的非限定名字在创建时按当时的 search_path 解析,解析结果被固化(存为对象 OID)。这让视图比「随时可能换 schema 的 SQL 文本」更安全,但也会在 search_path 变化时产生意外:
-- 当前 search_path = public
CREATE VIEW v_products AS SELECT id, name FROM products;
-- 之后有人创建了 reporting.products 并把 search_path 改成 reporting
SET search_path = reporting, public;
-- 视图仍然读 public.products(OID 已固化),而不是 reporting.products
SELECT * FROM v_products;但函数调用的解析规则不同:视图里的函数若未加 schema 限定,在执行时按当前 search_path 解析,这构成经典的安全隐患。
-- 危险!未限定的函数调用会被 search_path 影响
CREATE VIEW v_dangerous AS
SELECT id, upper(name) AS name_upper FROM products;推荐做法:在视图定义里给所有函数与对象加 schema 限定,或把视图放在不受用户 search_path 影响的 schema 里。
-- 推荐:schema 限定,杜绝 search_path 劫持
CREATE VIEW v_safe AS
SELECT id, pg_catalog.upper(p.name) AS name_upper
FROM public.products AS p;search_path 与 SECURITY DEFINER
视图默认按定义者权限执行,行为上类似 SECURITY DEFINER 函数。若函数名未限定,攻击者可以通过临时改写 search_path 让自己的恶意函数被解析到——这与 SECURITY DEFINER / INVOKER 里的风险完全同源。
ALTER VIEW:改名、换 schema、换 owner
ALTER VIEW 不能改查询定义(那要用 CREATE OR REPLACE VIEW),只能改属性:
-- 重命名视图
ALTER VIEW v_users_simple RENAME TO v_users_active;
-- 重命名已有的列(PG 支持)
ALTER VIEW v_users_active RENAME COLUMN name TO full_name;
-- 移到另一个 schema
CREATE SCHEMA reporting;
ALTER VIEW v_users_active SET SCHEMA reporting;
-- 改所有者
ALTER VIEW reporting.v_users_active OWNER TO app_owner;
-- 修改 / 重置 view options
ALTER VIEW reporting.v_users_active SET (security_barrier = true);
ALTER VIEW reporting.v_users_active RESET (security_barrier);重命名视图不会自动更新引用它的其他视图定义文本(它们通过 OID 引用,仍然有效),但如果别的视图的定义文本里写死了名字,pg_get_viewdef() 输出会随之更新。
用系统视图与系统表查看视图
| 查询入口 | 内容 | 特点 |
|---|---|---|
\d+ view_name | 列与定义 | 最方便的交互方式 |
\dv | 当前库所有视图 | psql 元命令 |
pg_views | 视图名、schema、owner、定义 | PostgreSQL 专有 |
information_schema.views | 标准视图信息 | 跨库可移植,含 is_updatable |
pg_get_viewdef('v'::regclass, true) | 格式化后的定义 | 可编程获取 |
pg_class.relkind = 'v' | 视图对象 | 元数据查询的通用入口 |
-- PostgreSQL 专有视图
SELECT schemaname, viewname, viewowner, definition
FROM pg_views
WHERE schemaname = 'public'
ORDER BY viewname; schemaname | viewname | viewowner | definition
------------+-------------------+-----------+-------------------------------------------------
public | v_orders | postgres | SELECT id, + +
| | | user_id, + +
| | | amount + +
| | | FROM orders;
(1 row)-- 标准视图(含可更新性标志)
SELECT table_schema, table_name, is_updatable, is_insertable_into, check_option
FROM information_schema.views
WHERE table_schema = 'public'
ORDER BY table_name; table_schema | table_name | is_updatable | is_insertable_into | check_option
--------------+--------------------+--------------+--------------------+--------------
public | v_orders | YES | YES | NONE
public | v_users_simple | YES | YES | NONE
(2 rows)-- 用 pg_class 精确定位视图对象
SELECT c.oid::regclass AS view_name, c.relkind, c.reloptions
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind = 'v'; view_name | relkind | reloptions
------------------+---------+---------------
v_orders | v |
v_users_simple | v |
(2 rows)注意 reloptions:只有设置了 WITH 选项的视图才有值,默认是空。
-- 设了 security_barrier 之后
SELECT relname, reloptions FROM pg_class WHERE relname = 'v_secret'; relname | reloptions
----------+--------------------------------
v_secret | {security_barrier=true}
(1 row)视图作为抽象层的最佳实践
视图最大的价值是给应用一个稳定的数据接口,让底层表可以自由演进。
-- 接口层:对外只暴露 v_customer_profile
CREATE VIEW v_customer_profile AS
SELECT
c.id AS customer_id,
c.name AS customer_name,
c.email,
coalesce(o.order_count, 0) AS order_count,
coalesce(o.total_amount, 0) AS total_amount
FROM customers c
LEFT JOIN (
SELECT user_id, count(*) AS order_count, sum(amount) AS total_amount
FROM orders GROUP BY user_id
) o ON o.user_id = c.id;当 orders 拆分成 orders_2025 / orders_2026 时,只需改视图定义,应用代码零改动:
CREATE OR REPLACE VIEW v_customer_profile AS
SELECT
c.id AS customer_id, c.name AS customer_name, c.email,
coalesce(o.order_count, 0) AS order_count,
coalesce(o.total_amount, 0) AS total_amount
FROM customers c
LEFT JOIN (
SELECT user_id, count(*) AS order_count, sum(amount) AS total_amount
FROM (SELECT user_id, amount FROM orders_2025
UNION ALL
SELECT user_id, amount FROM orders_2026) t
GROUP BY user_id
) o ON o.user_id = c.id;注意 CREATE OR REPLACE VIEW 要求列名、顺序、类型完全一致——上面的改写正好满足,所以可以原地替换。
实战示例
场景:为一个人力资源系统搭建「对外只暴露脱敏数据」的视图层,并实现列级权限控制。
第 1 步:建表与数据
DROP TABLE IF EXISTS employees CASCADE;
CREATE TABLE employees (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
emp_no text NOT NULL UNIQUE,
name text NOT NULL,
email text NOT NULL,
phone text NOT NULL,
salary numeric(12,2) NOT NULL,
dept_id integer NOT NULL,
hired_at date NOT NULL,
is_active boolean NOT NULL DEFAULT true
);
INSERT INTO employees (emp_no, name, email, phone, salary, dept_id, hired_at, is_active)
SELECT
'E' || lpad(g::text, 5, '0'),
'Employee ' || g,
'emp' || g || '@corp.example',
'138' || lpad((g % 100000000)::text, 8, '0'),
(6000 + (g % 20) * 1200)::numeric(12,2),
1 + (g % 5),
DATE '2018-01-01' + (g % 2000),
(g % 20 <> 0)
FROM generate_series(1, 5000) AS g;
ANALYZE employees;DROP TABLE
CREATE TABLE
INSERT 0 5000
ANALYZE第 2 步:建立三个层次的视图
-- 公共视图:隐藏薪资与手机号,仅活跃员工
CREATE VIEW v_emp_public AS
SELECT id, emp_no, name, email, dept_id, hired_at
FROM employees
WHERE is_active = true;
-- 部门视图:额外暴露手机号
CREATE VIEW v_emp_dept AS
SELECT id, emp_no, name, email, phone, dept_id, hired_at
FROM employees
WHERE is_active = true;
-- HR 视图:全部字段
CREATE VIEW v_emp_hr AS
SELECT * FROM employees;
-- 给公共视图加 security_barrier,防止外部谓词下推绕过脱敏逻辑
ALTER VIEW v_emp_public SET (security_barrier = true);第 3 步:授权
CREATE ROLE app_public LOGIN PASSWORD 'secret';
GRANT USAGE ON SCHEMA public TO app_public;
GRANT SELECT ON v_emp_public TO app_public;
REVOKE ALL ON employees FROM app_public;第 4 步:验证权限隔离
SET ROLE app_public;
-- 成功:公共视图可用
SELECT count(*) FROM v_emp_public; count
-------
4750
(1 row)-- 失败:没有底层表权限,也没有其他视图权限
SELECT salary FROM employees LIMIT 1;ERROR: permission denied for table employeesRESET ROLE;第 5 步:验证视图定义与依赖
SELECT viewname, viewowner, definition
FROM pg_views
WHERE viewname = 'v_emp_public'; viewname | viewowner | definition
----------------+-----------+-------------------------------------------------------------------------------
v_emp_public | postgres | SELECT id, + +
| | emp_no, + +
| | name, + +
| | email, + +
| | dept_id, + +
| | hired_at + +
| | FROM employees + +
| | WHERE is_active = true;
(1 row)-- 确认 reloptions 生效
SELECT relname, reloptions FROM pg_class WHERE relname = 'v_emp_public'; relname | reloptions
--------------+--------------------------------
v_emp_public | {security_barrier=true}
(1 row)第 6 步:确认视图不占数据空间
SELECT
c.relname AS object_name,
c.relkind AS kind,
pg_relation_size(c.oid) AS bytes
FROM pg_class c
WHERE c.relname IN ('employees', 'v_emp_public')
ORDER BY c.relname; object_name | kind | bytes
---------------+------+--------
employees | r | 524288
v_emp_public | v | 0
(2 rows)视图的 pg_relation_size 是 0,直观印证了「视图不存储数据」。
第 7 步:清理
DROP VIEW v_emp_public, v_emp_dept, v_emp_hr;
DROP TABLE employees;
DROP ROLE app_public;注意事项
视图不存储数据,也不加速查询。 慢视图要在底层表建索引,或改用物化视图。
sql-- 错误认知:建视图就能提速 CREATE VIEW v_slow AS SELECT ... FROM huge_table WHERE ...;视图不能建索引。
CREATE INDEX ... ON view_name会直接报错:sql-- 错误!views cannot have indexes CREATE INDEX idx_v ON v_orders (user_id);textERROR: "v_orders" is not a table or materialized viewCREATE OR REPLACE VIEW不能改列名、列顺序、列类型。 需要结构性变更时只能DROP+CREATE。DROP ... CASCADE会静默删除所有依赖视图。 生产环境优先RESTRICT(默认),先看清依赖再决定。未限定 schema 的函数名是安全隐患。 视图默认类似
SECURITY DEFINER,务必用pg_catalog./ 显式 schema 限定:sql-- 不推荐 SELECT upper(name) FROM t; -- 推荐 SELECT pg_catalog.upper(t.name) FROM public.t;security_invoker = true(PG 15+)要求调用者拥有底层对象权限。 打开后常见的「突然没权限了」就是这个原因。security_barrier会阻止谓词下推,性能可能下降。 只在确实需要保证求值顺序时开启。删除视图要显式
DROP VIEW。TRUNCATE之类对视图无效。视图列名自动推导不可依赖。 表达式列永远给别名,否则升级/改表时列名会漂移。
别在视图上做业务写入逻辑(依赖
INSTEAD OF触发器的可写视图除外,见 INSTEAD 规则)。
最佳实践
给视图显式声明列名或用别名,让接口稳定:
sqlCREATE VIEW v_report (day, order_cnt, gmv) AS SELECT date_trunc('day', created_at), count(*), sum(amount) FROM orders GROUP BY 1;视图定义里所有对象都加 schema 限定,规避
search_path劫持:sqlCREATE VIEW v AS SELECT p.id, pg_catalog.upper(p.name) FROM public.products p;用视图固化「软删除」与「租户隔离」过滤条件,避免每个查询重复写:
sqlCREATE VIEW v_active_orders AS SELECT * FROM orders WHERE is_deleted = false;视图只做「列裁剪 + 行过滤 + JOIN 组装」,不堆砌复杂逻辑,把重计算留给物化视图或应用层。
对外接口用视图,内部查询直接查表。视图是接口,不是性能工具。
改视图结构前先查依赖,避免
CASCADE误删:sqlSELECT dependent.relname FROM pg_depend d JOIN pg_rewrite r ON r.oid = d.objid JOIN pg_class dependent ON dependent.oid = r.ev_class JOIN pg_class referenced ON referenced.oid = d.refobjid WHERE referenced.relname = 'v_orders' AND dependent.oid <> referenced.oid;授权只授视图,不授底层表,把视图当成唯一的访问入口。
GRANT后记得GRANT USAGE ON SCHEMA,否则调用者连 schema 都进不去。用
pg_views/information_schema.views做视图清单审计,定期清理僵尸视图:sqlSELECT schemaname, viewname FROM pg_views ORDER BY schemaname, viewname;视图命名统一前缀
v_,让「表 / 视图 / 物化视图(mv_)」一眼可辨。
下一节
继续学习:可更新视图