临时表与非日志表(TEMPORARY / UNLOGGED)
不是所有表都需要"永久 + 可靠"。做报表中间计算时,数据用完即弃;做可重建的缓存时,丢了也无所谓。PostgreSQL 为这两类场景提供了 临时表(TEMPORARY TABLE) 与 非日志表(UNLOGGED TABLE),它们以牺牲持久性换取写入性能与清理便利。本节讲清三者的差异、代价与连接池下的陷阱。
前置知识
阅读本节前,建议先了解:CREATE TABLE AS 与 SELECT INTO
基础概念
三种表的定位差异,是本节的核心对照表:
| 维度 | 永久表(普通表) | 临时表(TEMPORARY) | 非日志表(UNLOGGED) |
|---|---|---|---|
| 写入 WAL | 是 | 否(仅少量元数据) | 否(仅少量元数据) |
| 崩溃后数据 | 完好 | 会话结束即消失 | 数据被清空(表结构保留) |
| 是否可复制到备库 | 是 | 否 | 是(但备库上数据为空) |
| 可见范围 | 所有会话(受权限约束) | 仅创建它的会话 | 所有会话 |
| 生命周期 | 永久 | 会话结束 / 事务结束(取决于 ON COMMIT) | 永久 |
| 存放位置 | 数据库目录 | 临时模式 pg_temp_N | 数据库目录 |
| 写入性能 | 基准 | 最快(无 WAL) | 快(无 WAL) |
| 适用场景 | 业务数据 | 会话内中间结果 | 可重建的缓存、日志落库前的暂存 |
几个关键概念:
- 临时表(Temporary Table):名字空间独立,多个会话可以各建一张同名临时表而互不冲突。它存在于
pg_temp_<N>这个会话私有的模式里,search_path会自动优先解析到它。 ON COMMIT三种行为:PRESERVE ROWS(默认,数据保留到会话结束)、DELETE ROWS(每次事务提交即清空)、DROP(事务提交即删表)。这是临时表最实用也最常被忽略的选项。- 非日志表(Unlogged Table):表结构会写入 WAL 并在备库上重建,但数据不写 WAL。因此备库上这张表始终为空,崩溃恢复时(包括备库提升为主库)数据会被全部清空。
temp_buffers:临时表专用缓冲区,默认 8MB,按会话设置。它独立于shared_buffers。pg_temp_N模式:PostgreSQL 自动为每个会话分配,编号动态变化,因此不能硬编码。
PostgreSQL 中还有第三类"快但更危险"的选择——UNLOGGED + 关闭 synchronous_commit,以及完全不落盘的内存表变体。本节只覆盖前两类实际可用的方案。
语法与用法
-- ① 临时表
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } ] TABLE [ IF NOT EXISTS ] table_name (
...
) [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ]
[ WITH ( storage_parameter [= value] [, ... ] ) ]
[ TABLESPACE tablespace_name ]
-- ② 非日志表
CREATE UNLOGGED TABLE [ IF NOT EXISTS ] table_name (
...
)
-- ③ 状态切换
ALTER TABLE table_name SET LOGGED;
ALTER TABLE table_name SET UNLOGGED;GLOBAL 与 LOCAL 都是语法噪音——PostgreSQL 中两者完全等价,且临时表永远是"会话局部"的(与 SQL 标准的理解不同)。官方建议直接忽略这两个关键字。
-- 临时表:会话结束自动删除
CREATE TEMP TABLE tmp_orders (
id bigint,
amount numeric(12,2)
);CREATE TABLE-- 事务提交即清空(适合"每事务重置的累加器")
CREATE TEMP TABLE tmp_counter (
sku text PRIMARY KEY,
qty integer NOT NULL DEFAULT 0
) ON COMMIT DELETE ROWS;
-- 事务结束即删表(适合一次性流程)
CREATE TEMP TABLE tmp_once (
v text
) ON COMMIT DROP;-- 非日志表:永久存在,但崩溃后数据清空
CREATE UNLOGGED TABLE session_cache (
cache_key text PRIMARY KEY,
cache_val jsonb NOT NULL,
expires_at timestamptz NOT NULL
);CREATE TABLE详细说明
临时表:命名空间与可见性
临时表的数据对其他会话完全不可见,这是它与普通表最本质的差别:
-- 会话 A
CREATE TEMP TABLE tmp_demo (x int);
INSERT INTO tmp_demo VALUES (1), (2);
-- 查询当前会话能看到的所有 schema
SELECT current_schemas(true); current_schemas
--------------------------------------------------
{pg_catalog,pg_temp_3,public}
(1 row)pg_temp_3 就是本会话的临时模式,它排在 public 前面,因此同名对象会被临时表"遮蔽":
-- 危险!临时表会遮蔽同名永久表
CREATE TABLE metrics (id int, value numeric); -- 永久表
CREATE TEMP TABLE metrics (id int); -- 临时表,遮蔽前者
SELECT * FROM metrics; -- 访问的是临时表!
INSERT INTO metrics VALUES (1); -- 写入临时表!-- 想明确访问永久表,必须写全限定名
SELECT * FROM public.metrics;这也是连接池环境下最危险的一类 bug:临时表随会话存在,而连接池会把物理连接复用给不同的逻辑请求。
-- 查看当前会话的临时表
SELECT n.nspname AS schema, c.relname AS temp_table, pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname LIKE 'pg_temp%' AND c.relkind = 'r';ON COMMIT 三种行为
-- ① PRESERVE ROWS(默认):数据保留到会话结束
CREATE TEMP TABLE t_preserve (x int) ON COMMIT PRESERVE ROWS;
BEGIN;
INSERT INTO t_preserve VALUES (1);
COMMIT;
SELECT count(*) FROM t_preserve; -- 1,事务提交后数据还在 count
-------
1
(1 row)-- ② DELETE ROWS:每次提交后清空
CREATE TEMP TABLE t_delete (x int) ON COMMIT DELETE ROWS;
BEGIN;
INSERT INTO t_delete VALUES (1);
COMMIT;
SELECT count(*) FROM t_delete; -- 0,已被清空 count
-------
0
(1 row)-- ③ DROP:提交后连表一起删
CREATE TEMP TABLE t_drop (x int) ON COMMIT DROP;
BEGIN;
INSERT INTO t_drop VALUES (1);
COMMIT;
SELECT count(*) FROM t_drop; -- 报错:表不存在ERROR: relation "t_drop" does not exist注意 ON COMMIT DROP 只在事务块内才体现差别:如果不在 BEGIN 里,CREATE TEMP TABLE ... ON COMMIT DROP 会在语句自己的隐式事务提交时立刻把表删掉,后续无法使用。它只适合"在事务内建表、用完即删"的场景。
ON COMMIT | 数据何时消失 | 表何时消失 | 典型用途 |
|---|---|---|---|
PRESERVE ROWS(默认) | 会话结束 | 会话结束 | 会话内多次复用 |
DELETE ROWS | 每次事务提交 | 会话结束 | 事务级累加器、批处理暂存 |
DROP | — | 事务提交时 | 事务内一次性的中间表 |
非日志表:崩溃后数据清空
UNLOGGED 的收益来自"跳过 WAL 写入",代价是放弃崩溃恢复能力:
-- 建一张非日志表并写入数据
CREATE UNLOGGED TABLE ul_demo (id int, payload text);
INSERT INTO ul_demo SELECT g, repeat('x', 100) FROM generate_series(1, 100000) g;CREATE TABLE
INSERT 0 100000-- 查看表的持久化属性
SELECT relname, relpersistence,
CASE relpersistence WHEN 'p' THEN '永久(permanent)'
WHEN 'u' THEN '非日志(unlogged)'
WHEN 't' THEN '临时(temporary)' END AS kind
FROM pg_class WHERE relname IN ('ul_demo', 'tmp_demo'); relname | relpersistence | kind
----------+----------------+--------------------
ul_demo | u | 非日志(unlogged)
(1 row)relpersistence 的取值是判断表类型的可靠依据:p = permanent、u = unlogged、t = temporary。
崩溃后的实际后果:非日志表在崩溃恢复(或流复制备库提升)时会被整体清空。PostgreSQL 会在恢复阶段检测到非日志表的 init fork 与实际数据不一致,直接丢弃整张表的数据。
-- 模拟崩溃的等效验证:查看非日志表的 init fork 存在性
SELECT pg_relation_filepath('ul_demo') AS main_file; main_file
------------------------
base/16385/24813
(1 row)-- 对比 WAL 写入量:UNLOGGED 几乎不写 WAL
SELECT pg_current_wal_lsn() AS before_lsn \gset
INSERT INTO ul_demo SELECT g, repeat('y', 100) FROM generate_series(1, 100000) g;
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'before_lsn')) AS wal_for_unlogged;INSERT 0 100000
wal_for_unlogged
------------------
4680 kB
(1 row)对比永久表写入同样数据量时会产生的 WAL 量(通常为数据量的 1.5~2 倍),UNLOGGED 的收益非常可观。但收益的前提是"数据可重建"——如果丢了要能从其他地方重新算出来,才适合用。
类型转换:SET LOGGED / SET UNLOGGED
两种属性之间可以互相转换,但都要重写整表、且持 ACCESS EXCLUSIVE 锁:
-- 永久表 → 非日志表(重写整表)
ALTER TABLE big_table SET UNLOGGED;
-- 非日志表 → 永久表(重写整表,补齐 WAL)
ALTER TABLE big_table SET LOGGED;-- 临时表不能改属性
ALTER TABLE tmp_demo SET LOGGED;ERROR: cannot change logged status of table "tmp_demo"
DETAIL: It is temporary.转换过程中会重写整表,这与 ALTER COLUMN TYPE 的代价同级,大表上必须放到维护窗口。
临时表与 temp_buffers
临时表的数据默认走 temp_buffers(会话级,默认 8MB),不参与 shared_buffers 的争用。这意味着临时表不会污染共享缓存——这是它除了"省 WAL"外的第二个重要优点。
-- 查看与调整当前会话的临时缓冲区
SHOW temp_buffers; temp_buffers
--------------
8MB
(1 row)-- 临时表前调大(必须是会话中第一条语句之前,或至少在首次使用临时表前)
SET temp_buffers = '64MB';-- 查看临时表占用的磁盘空间(临时文件位置由 temp_tablespaces 决定)
SHOW temp_tablespaces;若 temp_tablespaces 为空,临时表与排序临时文件会写到默认表空间(pg_default),与业务数据抢同一块磁盘。生产环境通常单独挂一块盘给临时数据:
-- 建一个专门的临时表空间(需要超级用户)
CREATE TABLESPACE ts_temp LOCATION '/data/pg_tmp';
ALTER SYSTEM SET temp_tablespaces = 'ts_temp';
SELECT pg_reload_conf();连接池环境下的陷阱
这是临时表在生产上翻车最多的地方。连接池(pgbouncer 的 transaction / statement 池化模式、或应用侧连接池)会把同一个物理连接先后交给不同逻辑请求,于是:
| 陷阱 | 现象 | 缓解方式 |
|---|---|---|
| 临时表"残留" | 上一个请求建的临时表,被下一个请求看到并使用 | 用 ON COMMIT DROP / DELETE ROWS,或每次显式 DROP TABLE IF EXISTS |
| 临时表遮蔽永久表 | 逻辑请求误查到同名临时表,数据"要少一部分" | 关键查询写全限定名 public.t |
temp_buffers 设置丢失 | SET 只对当前会话有效,换连接后失效 | 在连接重置脚本中设置 |
| 临时表目录膨胀 | 会话不结束则临时表不释放,长期连接池连接越积越多 | 连接归还前执行 DISCARD ALL |
pg_temp 注入 | 恶意会话通过同名临时表劫持函数内的表引用 | 函数内使用 SET search_path = pg_catalog, pg_temp 或全限定名 |
-- 连接归还前的标准清理动作
DISCARD ALL; -- 等价于 RESET ALL + DEALLOCATE ALL + CLOSE ALL + UNLISTEN * + DISCARD PLANS + DISCARD TEMP-- 用 DROP TEMP 清理当前会话所有临时表
DROP TABLE IF EXISTS tmp_a, tmp_b;实战示例
场景:为报表系统做一个"当日销售汇总"流程,要求——① 中间计算用临时表,会话结束自动清理;② 高频读取的汇率换算结果用非日志表做缓存;③ 验证三者在 WAL、可见性、崩溃行为上的差异。
第 1 步:准备基础表
DROP TABLE IF EXISTS orders CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS session_cache;
DROP TABLE IF EXISTS ul_wal_demo;
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users (id),
amount numeric(12,2) NOT NULL,
status text NOT NULL DEFAULT 'paid',
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO users (email) SELECT 'u' || g || '@example.com' FROM generate_series(1, 50) g;
INSERT INTO orders (user_id, amount, status, created_at)
SELECT (g % 50) + 1, (random() * 500 + 5)::numeric(12,2), 'paid', now() - (g % 30) * interval '1 day'
FROM generate_series(1, 30000) g;CREATE TABLE
CREATE TABLE
INSERT 0 50
INSERT 0 30000第 2 步:用临时表做会话内中间计算
-- 每事务重置的累加器
CREATE TEMP TABLE tmp_user_amount (
user_id bigint PRIMARY KEY,
total numeric(16,2) NOT NULL DEFAULT 0,
cnt integer NOT NULL DEFAULT 0
) ON COMMIT DELETE ROWS;
-- 一次填充
INSERT INTO tmp_user_amount (user_id, total, cnt)
SELECT user_id, sum(amount), count(*)
FROM orders WHERE status = 'paid'
GROUP BY user_id;
-- 关联查询:拿到用户维度汇总
SELECT t.user_id, u.email, t.cnt, t.total
FROM tmp_user_amount t
JOIN users u ON u.id = t.user_id
ORDER BY t.total DESC
LIMIT 5;INSERT 0 50
user_id | email | cnt | total
---------+--------------------+------+------------
17 | u17@example.com | 60 | 15x
...
(5 rows)-- 确认临时表位于会话私有 schema 中
SELECT n.nspname AS schema, c.relname, c.relpersistence
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = 'tmp_user_amount'; schema | relname | relpersistence
--------------+------------------+----------------
pg_temp_3 | tmp_user_amount | t
(1 row)第 3 步:验证 ON COMMIT DELETE ROWS 的效果
BEGIN;
INSERT INTO tmp_user_amount (user_id, total, cnt) VALUES (999, 1, 1);
SELECT count(*) AS inside_tx FROM tmp_user_amount WHERE user_id = 999;
COMMIT; count
-------
1
(1 row)
COMMIT-- 提交后自动清空
SELECT count(*) AS after_commit FROM tmp_user_amount; after_commit
--------------
0
(1 row)第 4 步:临时表与永久表同名遮蔽演示
-- 建一张永久表
CREATE TABLE metrics (id bigint, note text);
INSERT INTO metrics VALUES (1, '永久表的数据');
-- 同会话内建同名临时表
CREATE TEMP TABLE metrics (id bigint);
-- 不带模式名的查询命中的是临时表
SELECT count(*) AS from_which FROM metrics;CREATE TABLE
INSERT 0 1
CREATE TABLE
from_which
------------
0
(1 row)-- 想访问永久表必须写全限定名
SELECT * FROM public.metrics; id | note
----+------------------
1 | 永久表的数据
(1 row)第 5 步:用非日志表做缓存
CREATE UNLOGGED TABLE session_cache (
cache_key text PRIMARY KEY,
cache_val jsonb NOT NULL,
expires_at timestamptz NOT NULL DEFAULT now() + interval '1 hour'
);
INSERT INTO session_cache (cache_key, cache_val)
VALUES
('rate:USD:CNY', '{"rate": 7.12}'),
('rate:EUR:CNY', '{"rate": 7.78}')
ON CONFLICT (cache_key) DO UPDATE SET cache_val = EXCLUDED.cache_val;
SELECT cache_key, cache_val FROM session_cache ORDER BY cache_key;CREATE TABLE
INSERT 0 2
cache_key | cache_val
---------------+--------------------
rate:EUR:CNY | {"rate": 7.78}
rate:USD:CNY | {"rate": 7.12}
(2 rows)第 6 步:对比 WAL 写入量
-- 构造两张同结构的表,一张永久、一张非日志
CREATE TABLE logged_demo (id int, payload text);
CREATE UNLOGGED TABLE unlogged_demo (id int, payload text);
-- 先记下 LSN
SELECT pg_current_wal_lsn() AS lsn_before \gsetCREATE TABLE
CREATE TABLE-- 往永久表写入 10 万行
INSERT INTO logged_demo SELECT g, repeat('x', 100) FROM generate_series(1, 100000) g;
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'lsn_before')) AS wal_logged;INSERT 0 100000
wal_logged
------------
18 MB
(1 row)-- 往非日志表写入同样 10 万行
SELECT pg_current_wal_lsn() AS lsn_before2 \gset
INSERT INTO unlogged_demo SELECT g, repeat('x', 100) FROM generate_series(1, 100000) g;
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'lsn_before2')) AS wal_unlogged;INSERT 0 100000
wal_unlogged
--------------
4704 kB
(1 row)以上数值为典型量级示意(100 字节/行、10 万行),实际结果随
wal_level、full_page_writes、页填充率与压缩设置而变。结论是稳定的:非日志表的 WAL 量通常只有永久表的三分之一到五分之一(差额来自行数据本身不写 WAL,仅剩元数据与检查点记录)。
第 7 步:属性互转的代价
-- 非日志 → 永久:重写整表并补齐 WAL
ALTER TABLE unlogged_demo SET LOGGED;
SELECT relname, relpersistence FROM pg_class WHERE relname IN ('logged_demo', 'unlogged_demo');ALTER TABLE
relname | relpersistence
---------------+----------------
logged_demo | p
unlogged_demo | p
(2 rows)-- 永久 → 非日志:同样重写
ALTER TABLE logged_demo SET UNLOGGED;ALTER TABLE第 8 步:清理
DROP TABLE IF EXISTS logged_demo, unlogged_demo, session_cache, metrics;
DROP TABLE IF EXISTS orders, users CASCADE;
DROP TABLE IF EXISTS tmp_user_amount;DROP TABLE
DROP TABLE
DROP TABLE注意事项
- 临时表在连接池下会跨请求"残留"。 这是生产事故的高发区,必须显式清理或使用
ON COMMIT DROP。
-- 危险!下一个逻辑请求可能看到这些数据
CREATE TEMP TABLE stage (id int);
-- 安全:或用 ON COMMIT DROP,或在归还连接前 DISCARD TEMP
DROP TABLE IF EXISTS stage;- 临时表会遮蔽同名永久表。 关键查询务必用全限定名。
-- 危险!实际查的是临时表
SELECT count(*) FROM metrics;
-- 正确
SELECT count(*) FROM public.metrics;ON COMMIT DROP在非事务块中会立即删表。
-- 表立刻消失,后续无法使用
CREATE TEMP TABLE t (x int) ON COMMIT DROP;
SELECT * FROM t;ERROR: relation "t" does not exist- 临时表不支持
SET LOGGED。
-- 错误!
ALTER TABLE tmp_demo SET LOGGED;ERROR: cannot change logged status of table "tmp_demo"
DETAIL: It is temporary.- 非日志表在崩溃或备库提升后数据全部丢失。 只放可重建的数据。
-- 危险!订单这类不可重建的业务数据放进非日志表
CREATE UNLOGGED TABLE orders (...); -- 崩溃后订单全没了- 非日志表在流复制备库上始终为空。 不要用它做"主备一致性"相关的逻辑。
-- 备库上查询非日志表会得到 0 行
SELECT count(*) FROM session_cache; -- 0SET LOGGED/SET UNLOGGED会重写整表并持ACCESS EXCLUSIVE锁。 大表上必须在维护窗口执行。
-- 大表转换前先确认磁盘空间
SELECT pg_size_pretty(pg_total_relation_size('big_table')) AS need_space;
ALTER TABLE big_table SET UNLOGGED;- 临时表不参与
shared_buffers,走temp_buffers。 默认 8MB 对复杂分析偏小。
-- 在会话开始时调大
SET temp_buffers = '128MB';temp_buffers必须在首次使用临时表/排序之前设置,否则本会话不生效。临时表默认与主数据共用一个磁盘。 生产环境应设置独立的
temp_tablespaces。
CREATE TABLESPACE ts_temp LOCATION '/data/pg_tmp';
ALTER SYSTEM SET temp_tablespaces = 'ts_temp';- 临时表不支持某些特性:不能建外键引用、不能创建其他对象引用它、不支持逻辑复制。
-- 错误!foreign key constraints cannot be used with temporary tables
ALTER TABLE tmp_orders ADD FOREIGN KEY (id) REFERENCES public.orders (id);pg_temp_N的编号不能硬编码。 它是会话级动态分配,不同会话编号不同。
-- 错误!编号会变
SELECT * FROM pg_temp_3.tmp_demo;
-- 正确:用 pg_temp 别名(每个会话都指向自己的临时模式)
SELECT * FROM pg_temp.tmp_demo;临时表上有
ON COMMIT DELETE ROWS时,事务内的自增/序列不会重置。UNLOGGED表可以建索引、约束、触发器,功能与永久表一致,仅持久化属性不同。DISCARD ALL会清理所有临时表与所有会话状态,是连接归还前的标准动作。
DISCARD ALL;- 逻辑复制不支持无主键表,也不复制非日志表的定义(仅复制了结构时数据为空)。
最佳实践
- 临时表一律显式指定
ON COMMIT,让生命周期一目了然,不依赖默认值。
-- 会话内多次复用
CREATE TEMP TABLE tmp_base (...) ON COMMIT PRESERVE ROWS;
-- 事务级暂存
CREATE TEMP TABLE tmp_stage (...) ON COMMIT DELETE ROWS;
-- 事务内一次性
CREATE TEMP TABLE tmp_once (...) ON COMMIT DROP;- ETL 中间表统一用
CREATE TEMP TABLE ... AS,省去手动建表与清理。
CREATE TEMP TABLE tmp_stage AS
SELECT id, user_id, amount FROM orders WHERE status = 'paid';- 关键查询写全限定名
public.表名,彻底规避临时表遮蔽风险。
SELECT count(*) FROM public.orders WHERE status = 'paid';- 连接池配置
DISCARD ALL为连接归还动作,防止临时表与search_path污染。
-- 应用侧在归还连接前执行
DISCARD ALL;- 非日志表只用于可重建的缓存/中间数据,并在应用层实现"缓存失效即重建"逻辑。
CREATE UNLOGGED TABLE rate_cache (
pair text PRIMARY KEY,
rate numeric(12,6) NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);- 为临时数据单独建表空间,避免影响主数据的 IO。
CREATE TABLESPACE ts_temp LOCATION '/data/pg_tmp';
SET temp_tablespaces = 'ts_temp';- 临时表会话开始时调大
temp_buffers,尤其是复杂分析类会话。
SET temp_buffers = '128MB';不要用临时表传递"跨请求"状态。 那是应用缓存该做的事,用数据库临时表做会在连接池下出错。
把
UNLOGGED表纳入备份检查清单:pg_dump会导出其结构,但数据在崩溃后为空,恢复时要有重建脚本。上线前用
pg_class.relpersistence复核,确认表的持久化属性符合设计。
SELECT relname, relpersistence
FROM pg_class
WHERE relnamespace = 'public'::regnamespace AND relkind = 'r'
ORDER BY relpersistence, relname;临时表不适合放大量数据。 需要处理百万级中间结果时,优先用
UNLOGGED+ 显式清理,或直接改进查询(避免物化中间结果)。DROP TABLE IF EXISTS与CREATE TEMP TABLE组合使用,让脚本在连接复用时也能安全执行。
DROP TABLE IF EXISTS tmp_stage;
CREATE TEMP TABLE tmp_stage AS SELECT ...;下一节
继续学习:注释(COMMENT)与其他对象管理