Skip to content

psql 变量与 \gset ​

psql 不只是「SQL 的回显器」,它本身是一门带变量、条件分支与命令替换的小型脚本语言。变量(variable)是这门语言的核心构件:你可以用 \set 定义变量、在 SQL 与元命令里用 :var 引用变量、用 \gset 把查询结果反向写进变量,再用 \if 根据变量走不同分支。把这几块拼起来,psql 就能完成过去必须写 Python/Shell 才能做的参数化运维脚本——而且全程在数据库客户端内闭环。

这一页会系统讲清楚 psql 变量的三层语义:赋值(\set/\unset/\gset)、引用(:var/:'var'/:"var" 三种插值形态)与求值(\if 条件块、反引号命令替换)。其中最重要、也最容易被误解的一条规则是:psql 变量是客户端的文本替换,发生在 SQL 发送到服务器之前——它与服务端预处理的 $1 参数是两套完全不同的机制,混淆两者正是 SQL 注入风险的源头。

掌握本页内容后,你将能把重复的巡检、迁移、批量维护操作收敛成一份份可复用、可传参的 .sql 脚本;上一页《psql 输出格式》控制结果「长什么样」,这一页控制 SQL「用什么参数跑」。

前置知识

建议先阅读:psql 输出格式(\pset)

基础概念 ​

psql 变量本质上是一张 名字 → 字符串 的键值表,由 psql 客户端维护:

  • 所有值都是字符串,没有数字、布尔类型之分(\set 时可以写 1、on、'hello world',最终都以文本形态存储);
  • 一部分变量是预定义变量(如 VERSION、DBNAME、ON_ERROR_STOP),由 psql 自动赋值并影响 psql 自身行为,你可以读它们,多数也可以改;
  • 用户自定义变量与预定义变量共用一个命名空间,通常用大写保留 psql 的、小写放自己的,避免冲突。
text
\set 赋值 ──► psql 变量表 ──► :var / :'var' / :"var" 插值 ──► 发送到服务器的最终 SQL
                ▲                                        │
                └────────── \gset 把查询结果写回 ──────────┘

三个必须建立的心智模型:

  1. 插值是文本替换。:var 会把变量的字符内容原样「粘贴」进 SQL 文本里,粘贴发生在 psql 这一端。服务器看到的永远是替换后的完整 SQL。
  2. 与服务端参数是两回事。扩展查询协议的 $1(或驱动里的 ?、%s、?1)由服务器解析 SQL 后再绑定值,值永远不会改变语句结构。psql 变量插值则发生在解析之前,因此有注入风险,必须用 :'var' 做安全引用。
  3. 作用域是会话。变量活在单个 psql 会话里,退出即消失;持久化靠 .psqlrc 或脚本开头的一串 \set。
术语英文说明
变量赋值\set / \unset定义或清除一个变量
变量插值interpolation把变量内容替换进输入文本的过程
安全引用quoted interpolation:'var'/:"var" 生成带转义的 SQL 字面量/标识符
结果捕获\gset把单行查询结果的各列写进同名变量
条件块\if/\elif/\else/\endif按变量值控制脚本分支
命令替换backtick substitution把 shell 命令输出注入 psql 输入

语法与用法 ​

赋值与查看:

sql
\set                       -- 列出所有变量
\set tag migration_42      -- 定义 tag,值为 migration_42
\set threshold 1000        -- 值为字符串 "1000"
\set greeting 'hello psql' -- 值含空格,用单引号包住
\echo :tag                 -- 输出:migration_42
\unset tag                 -- 清除变量
\warn :threshold           -- 输出到 stderr

三种插值形态:

sql
\set user_input "O'Brien"

-- 1) :var 原样粘贴 —— 有注入风险,仅用于受信内容
SELECT * FROM t WHERE note = :user_input;      -- 语法错误甚至注入

-- 2) :'var' 安全引用为 SQL 字面量(自动转义引号)
SELECT * FROM t WHERE note = :'user_input';    -- WHERE note = 'O''Brien'

-- 3) :"var" 安全引用为 SQL 标识符(用于表名/列名)
SELECT * FROM :"schema_name".:"table_name";

把查询结果写进变量:

sql
SELECT count(*) AS n_live, max(created_at) AS latest FROM orders \gset
\echo 订单数 :n_live,最新订单 :latest

-- 带前缀,避免与已有变量重名
SELECT setting AS val FROM pg_settings WHERE name = 'shared_buffers' \gset sb_
\echo shared_buffers = :sb_val

条件分支与延时:

sql
\if :n_live > 0
\echo 表里有数据,跳过初始化
\else
\echo 表为空,执行初始化
\i init_data.sql
\endif

\sleep 2s     -- 暂停 2 秒,单位支持 s/ms/min

命令行传参(脚本化的标准入口):

bash
psql -X -v ON_ERROR_STOP=1 -v dbname=appdb -v min_id=100 -f cleanup.sql
sql
-- cleanup.sql 内部
\echo 开始清理 :dbname 中 id > :min_id 的记录
DELETE FROM logs WHERE id > :min_id;

详细说明 ​

\set 的取值规则与陷阱 ​

\set 名 值1 值2 … 会把多个值用空格拼接成一个字符串存入。规则与常见坑:

sql
\set a 123            -- "123"
\set b 1 + 2          -- "1 + 2"(整体拼接,不是求 3!)
\set c '1 + 2'        -- 同样是字符串 "1 + 2"
\set d                -- 空字符串(注意:不是 unset)
  • psql 变量没有算术运算。\set b 1 + 2 得到字符串 "1 + 2";\if 条件块里有有限的数值比较能力,但通用计算请交给 SQL(SELECT 1+2 \gset)或 shell。
  • 单引号内的内容可以包含空格,且引号不保留在值里;想在值里放单引号用 \'。
  • 变量名区分大小写,只能由字母、数字、下划线组成。
  • \set 不带参数时是「列出所有变量」,手滑把名字漏了会刷一屏。
  • \echo 输出到 stdout,\warn 输出到 stderr——写脚本时把诊断信息用 \warn,别污染结果流。

三种插值形态与 :: 的冲突规避 ​

:var、:'var'、:"var" 是本页最重要的三个记号,语义必须分清:

形态生成结果典型用途安全性
:var变量内容原样粘贴数值、枚举等受信内容无转义,有注入风险
:'var'带转义的单引号 SQL 字面量任何字符串值的比较、插入安全(防注入)
:"var"带转义的双引号 SQL 标识符动态表名、列名、schema 名安全(防注入)

一个贴心的细节:psql 对 :: 有特判——连续两个冒号会被识别为 PostgreSQL 的类型转换而不做插值:

sql
\set col_name amount
SELECT :col_name::numeric(12,2) FROM payments;  -- ::numeric 是类型转换,不会报“变量 col_name: 找不到”

此外,:'var' 的转义遵循 PostgreSQL 字面量规则,遇到值里本身有单引号会翻倍转义(O'Brien → 'O''Brien'),配合 standard_conforming_strings 默认行为可以正确处理反斜杠。

\gset:让查询结果反向注入变量 ​

\gset 是变量体系里最有生产力的一环:它执行当前查询缓冲区,并要求结果恰好一行,把这一行的每一列写进与列同名的变量:

sql
SELECT current_setting('shared_buffers') AS shared_buffers,
       pg_size_pretty(pg_database_size(current_database())) AS db_size,
       (SELECT count(*) FROM pg_stat_activity WHERE state = 'active') AS active_cnt
\gset

\echo 缓冲=:shared_buffers 库大小=:db_size 活跃=:active_cnt

关键行为与建议:

  • 零行:不赋值,变量保持原样;多行:psql 会给出 "more than one row returned for \gset" 类告警,以最后一行为准——务必用聚合、LIMIT 1 或主键条件保证单行;
  • 列名即变量名,因此用 AS 给列起一个好名字比什么都重要;
  • 前缀机制:\gset prefix_ 会让所有变量名带上前缀,常用于给特定主题的变量分组(如 sb_、st_),避免覆盖同名变量;
  • 与 \gset 并列的还有 \gdesc(只查看当前查询的列结构而不执行)与 \aset(对缓冲区里的多条语句分别捕获各自结果集的最后一行,逐条写入变量)。\aset 在多语句脚本里取「每一步的关键返回值」很好用,细节以官方文档为准。

注意 \gset 本身不产生输出(不打印表格),查询结果被「吃」进变量里——这正是它适合做「取数 → 判断 → 分支」链路的原因。

预定义变量全景 ​

psql 内置了一组变量,读懂它们能让你少写很多查询。按用途分组:

连接与环境信息(只读,由连接自动赋值)

变量内容示例说明
DBNAMEappdb当前连接的数据库名
USERapp_rw当前连接角色
HOSTdb1.prod.internal连接主机(套接字连接时为空)
PORT5432端口
ENCODINGUTF8客户端编码
VERSIONPostgreSQL 18.0 …完整版本串
VERSION_NAME18.0短版本号
VERSION_NUM180000数值版本,用于 \if 做版本判断

执行状态反馈(每次 SQL 后自动更新)

变量说明
ROW_COUNT上一条 SQL 影响或返回的行数(元命令不更新它)
LASTOID最后一次 INSERT 返回的 OID;现行版本普通表没有 OID,恒为 0,仅兼容保留
SQLSTATE上一条 SQL 的 SQLSTATE 码,成功时为 00000
LAST_ERROR_MESSAGE最近一次错误的消息文本
LAST_ERROR_SQLSTATE最近一次错误的 SQLSTATE

行为开关(可读可写,改变 psql 行为)

变量默认说明
ON_ERROR_STOP未设(遇错继续)设为 on 后遇错立即退出,脚本化必开
AUTOCOMMITon自动提交开关,下一页详述
ON_ERROR_ROLLBACK未设交互模式下出错自动回滚到 SAVEPOINT
ECHOnonequeries/errors/all,对应 -e/-b/-a
VERBOSITYdefault错误详细程度:default/verbose/terse
SHOW_CONTEXTerrors错误上下文(CONTEXT 行)显示策略
FETCH_COUNT未设设为正数后 SELECT 分块取回(大结果集省内存)
QUIEToff抑制欢迎与提示信息
SINGLELINEoff分号回车即执行,忽略换行排版
SINGLESTEPoff每条语句执行前询问(单步调试脚本)
HISTCONTROL/HISTFILE/HISTSIZE—命令历史策略
IGNOREEOF未设设为 on 后 Ctrl+D 不退出
PROMPT1/PROMPT2/PROMPT3见下三级提示符模板

其中 ROW_COUNT 与 SQLSTATE 组合可以做出「执行后立即体检」的脚本模式:

sql
DELETE FROM expired_sessions;
\if :SQLSTATE = 00000
\warn 清理完成,删除 :ROW_COUNT 行
\else
\warn 清理失败::LAST_ERROR_MESSAGE
\endif

PROMPT1/2/3:提示符模板 ​

提示符变量支持 % 转义,常用的有:%n 用户名、%/ 数据库名、%M 主机、%> 端口、%#(超级用户显示 #,普通用户 >)、%R(续行提示符状态字符)、%~(把 ~ 替换为基础目录)。

sql
\set PROMPT1 '%n@%M:%>/ %~ %# '
\set PROMPT2 '%R '        -- 续行(如未闭合的引号/括号)
\set PROMPT3 '>> '        -- COPY FROM STDIN 的输入提示

多实例、多环境来回切换时,把「是谁、在哪、连的哪个库」写进提示符,是防止在生产库误操作最廉价有效的手段。

\if 条件块:psql 的分支语法 ​

\if 表达式 / \elif 表达式 / \else / \endif 必须成对闭合,可嵌套。求值规则:

  • 支持布尔字面量 true/false、整数与浮点数、未加引号的变量名;
  • 支持比较与逻辑运算符 =、<>、<、>、<=、>=、and、or、not 与括号;
  • 未定义变量按 false 处理——这是脚本里判断「参数是否传入」的标准手法;
  • 表达式先经过变量插值再求值,因此 :'var' 也可用于先取值再比较;
  • 不支持函数调用与任意 SQL。复杂判断请先用 \gset 从 SQL 里拿结论,再对结论变量做 \if。
sql
-- 判断“参数是否传入”的惯用法
\if :{"target_table"}?
\echo target_table 已定义
\else
\warn 用法: psql -v target_table=xxx -f script.sql
\quit
\endif

:{name}? 是「变量是否已定义」的专用记号(返回 true/false),配合 \quit 可实现缺参即退出的健壮脚本入口。整块条件里未命中的分支会被 psql 跳过解析,因此可以在未命中分支里写「当前版本会报错」的 SQL——这是做版本兼容迁移脚本的利器。

\sleep 与节奏控制 ​

\sleep 数字 [单位] 让 psql 暂停,单位 s(默认)/ms/min,数字本身可以用变量:

sql
\sleep 500ms
\set interval 3
\sleep :interval s

典型用途是等待外部条件:切换后等备库追平、批量删除分批之间留缓冲、重试循环里退避。注意 \sleep 只在客户端等待,期间不持有任何数据库锁,但连接保持打开——长时间的轮询等待脚本要注意 tcp_keepalives 与防火墙的空闲断连。

SQL 中使用变量的边界:客户端插值 ≠ 服务端参数 ​

这一节值得单独强调,因为它关系到安全底线。

psql 插值的完整链路:

text
:var 替换 ──► 得到完整 SQL 文本 ──► 发送到服务器 ──► 服务器解析并执行

服务端参数(扩展查询协议)的链路:

text
带 $1 占位符的语句模板 + 参数值 ──► 分别发送 ──► 服务器解析模板 ──► 绑定参数执行

前者在解析之前拼接文本,值如果含恶意 SQL 片段('; DROP TABLE t; --)会成为语句的一部分;后者值永远只是值。由此得出使用准则:

sql
-- 危险:外部输入经 :var 直接进入 SQL
\set name "'; DROP TABLE users; --"
SELECT * FROM t WHERE name = :name;        -- 灾难

-- 安全::'var' 生成正确转义的字面量
SELECT * FROM t WHERE name = :'name';      -- WHERE name = '''; DROP TABLE users; --'

-- 更稳的做法:值走 :'var',标识符走 :"var",纯数值才允许 :var
SELECT * FROM :"schema".orders WHERE id = :order_id AND region = :'region';

给团队写共享脚本时的铁律:凡是可能来自 -v 命令行、环境变量、反引号命令输出、文件内容的值,一律 :'var'/:"var" 引用;:var 只用于你自己写死的常量与数值。如果脚本要交给驱动/程序执行,请改用驱动端的参数化 API(后续语言篇均会覆盖),不要用字符串拼接造 SQL。

变量与 \watch、\gexec、\gdesc 的协同 ​

变量不是孤立的,它与 psql 其他元命令组合出很多高效用法。

与 \watch 组合做轮询。\watch 重复执行当前查询缓冲区,间隔参数同样经过变量插值——故障排查时想把 10s 轮询临时调成 2s,改一个变量即可:

sql
\set interval 2
SELECT now() AS tick,
       count(*) FILTER (WHERE state = 'active') AS active,
       count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting
FROM pg_stat_activity
\watch :interval

Ctrl+C 中断后查询缓冲区仍在,可以直接改 SQL 继续 \watch,不必重新敲整条语句。

与 \gexec 组合做「结果驱动执行」。\gexec 把查询结果的每一行当作 SQL 去执行,行内容经常由变量参与生成:

sql
\set target_schema 'archive'
SELECT format('ANALYZE %I.%I;', table_schema, table_name)
FROM information_schema.tables
WHERE table_schema = :'target_schema' \gexec

format('%I') 对标识符做安全包装,与 quote_ident() 等价。这里 'archive' 是脚本里写死的常量,即使引用形态松一点也不会出事——但保持 :'var' 的一致习惯更稳。

与 \gdesc 组合做「先看结构再决定」。\gdesc 列出当前查询缓冲的列名与类型但不执行——脚本里可以先确认列名,再决定 \gset 后引用哪个变量:

sql
SELECT relname, n_live_tup FROM pg_stat_user_tables LIMIT 1 \gdesc

三个命令的共同规则:插值都发生在发送之前。无论结果去向是屏幕、变量、文件还是再次执行,服务器收到的都已是替换完成的最终文本。

反引号命令替换与 \o 生成脚本 ​

psql 输入里反引号包裹的内容会交给 shell 执行,命令的 stdout 替换回输入文本(对元命令参数与 \set 生效):

sql
\set today `date +%F`
\echo 今天是 :today

\set dump_file /tmp/backup_:today.sql
\o :dump_file

反引号 + 变量组合是「让 psql 脚本感知外部世界」的通道:读取主机名、日期、文件列表,甚至调用 curl 拿配置。安全提醒与上一节相同——反引号输出属于「外部内容」,进入 SQL 前必须 :'var'。

反过来,psql 也能生产脚本:用输出控制把查询结果写成一个 SQL 文件,再用 \i 执行它( \g file + \i file,或一步到位的 \gexec——后者在下一页事务篇里与事务配合讲解):

sql
\pset tuples_only on
\pset format unaligned
\o /tmp/reindex.sql
SELECT 'REINDEX TABLE CONCURRENTLY ' || quote_ident(schemaname) || '.' || quote_ident(tablename) || ';'
FROM pg_tables WHERE schemaname = 'public';
\o
\pset tuples_only off
\pset format aligned

\i /tmp/reindex.sql

注意生成端用了 quote_ident() 对标识符做安全包装——「生成 SQL 的 SQL」同样要防注入。

环境变量:脚本的另一个配置入口 ​

psql 的行为除了变量,还受一组环境变量控制,写脚本时两者配合:

环境变量作用脚本用法
PGHOST/PGPORT/PGDATABASE/PGUSER连接目标缺省值调度脚本按环境导出,psql 命令行保持干净
PGPASSWORD密码仅临时排查用;生产走 .pgpass 或证书认证
PSQLRC.psqlrc 的替代路径让批处理跳过个人配置:PSQLRC=/dev/null
PSQL_PAGER分页器批处理里置空,避免输出卡在 pager
PGAPPNAMEapplication_name让 pg_stat_activity 能溯源到具体脚本

一个容易被忽略的细节:cron / CI 里跑 psql 时,环境与交互 shell 不同——个人 .psqlrc 可能不存在或行为不一致(交互增强类 \set 全部消失),PSQL_PAGER 没有终端可交互时会挂住管道。批处理的标准姿势:

bash
#!/usr/bin/env bash
set -euo pipefail
export PSQLRC=/dev/null PSQL_PAGER= PGAPPNAME=nightly-cleanup
psql -X -v ON_ERROR_STOP=1 -d appdb -f /opt/scripts/cleanup.sql

-X(跳过 .psqlrc)与 PSQLRC=/dev/null 二选一即可,同时上是双保险;PSQL_PAGER= 置空是批处理不挂起的常见保命符。环境变量与 -v 变量的分工:环境变量管「连接与客户端行为」,-v 管「业务参数」,各司其职。

变量驱动的管理脚本:组织模式 ​

把前文拼图组合起来,一份生产级 psql 管理脚本通常长这样:

sql
-- cleanup.sql —— 可传参、可安全退出、可观察的管理脚本骨架
\set ON_ERROR_STOP on
\warn === 清理脚本开始,目标库 :DBNAME @ :HOST ===

-- 1. 缺参即退出
\if :{"retention_days"}?
\else
\warn 缺少参数 retention_days,例如: psql -v retention_days=90 -f cleanup.sql
\quit
\endif

-- 2. 从数据库取关键事实
SELECT count(*) AS n_expired FROM sessions WHERE updated_at < now() - (:'retention_days' || ' days')::interval \gset

-- 3. 按事实分支
\if :n_expired = 0
\warn 没有过期会话,无事可做
\quit
\endif

-- 4. 执行动作并汇报
DELETE FROM sessions WHERE updated_at < now() - (:'retention_days' || ' days')::interval;
\warn 已删除 :ROW_COUNT 行

配合 shell 的调度:

bash
psql -X -d appdb -v retention_days=90 -f cleanup.sql

这个骨架包含了参数校验、事实获取、条件分支、执行汇报四段,几乎所有运维脚本都能套用。

插值出错的排查与诊断 ​

变量脚本最常见的三类报错与对应排查路径:

现象原因处置
unterminated quoted string 或语法错落在变量位置用了 :var 而值含引号/特殊字符改用 :'var'
syntax error at or near ":"变量未定义,插值不发生、原文残留\set 查看变量表;引用前用 :{name}? 判存在性
值带着引号进了库用了 :'var' 又在 SQL 里手工包了一层引号:'var' 自带引号,外面不要再包

调试三招,由轻到重:

  1. \set 不带参数列出全部变量,确认拼写与当前值;
  2. \echo 试插值:拿不准的引用先 \echo :'val' 看替换结果,再放进 SQL;
  3. ECHO=queries 复盘:psql -a(或 \set ECHO queries)把每条发往服务器的最终 SQL 打印出来,插值结果一目了然——这是定位「服务器到底收到了什么」的终极手段。
sql
\set ECHO queries
\set name O'Brien
SELECT * FROM t WHERE name = :'name';
-- 屏幕先回显:SELECT * FROM t WHERE name = 'O''Brien'; 再输出结果

写生成型脚本(\gexec、\o + \i 两段式)时第三招尤其重要:先确认生成的 SQL 长什么样,再放它真跑。

实战示例 ​

示例一:\gset 捕获单值并在后续语句复用 ​

场景:先把备份表名、时间窗算进变量,再执行归档。

sql
\set ON_ERROR_STOP on

SELECT to_char(now() - interval '1 month', 'YYYY_MM') AS suffix,
       date_trunc('month', now() - interval '1 month') AS month_start,
       date_trunc('month', now()) AS month_end
\gset

\echo 归档区间::month_start ~ :month_end

CREATE TABLE orders_:suffix (LIKE orders INCLUDING ALL);
INSERT INTO orders_:suffix SELECT * FROM orders WHERE created_at >= :'month_start' AND created_at < :'month_end';
DELETE FROM orders WHERE created_at >= :'month_start' AND created_at < :'month_end';

\echo 归档完成,主表剩余 :ROW_COUNT 行数据被清理

注意 orders_:suffix 这种「标识符拼接」只在变量值完全受信时可用;若表名来自外部输入,应改用 :"suffix" 形态并接受引号包裹的标识符。

示例二:\if 做版本与对象存在性兼容 ​

sql
SELECT EXISTS (SELECT 1 FROM pg_attribute
               WHERE attrelid = 'orders'::regclass AND attname = 'tenant_id') AS has_tenant \gset

\if :has_tenant
SELECT count(DISTINCT tenant_id) AS tenants FROM orders \gset
\warn 多租户模式,共 :tenants 个租户
\else
\warn 单租户模式
\endif

迁移脚本常用这个模式探测目标库结构,在旧新版本上跑同一份脚本而互不报错。

示例三:变量 + 反引号 + 生成脚本批量维护 ​

sql
-- 对超过 500 MB 的表批量 VACUUM ANALYZE(示例演示生成与执行两段式)
\pset tuples_only on
\pset format unaligned
\o /tmp/vacuum_big.sql
SELECT 'VACUUM (ANALYZE) ' || quote_ident(n.nspname) || '.' || quote_ident(c.relname) || ';'
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
  AND pg_total_relation_size(c.oid) > :min_bytes
  AND n.nspname NOT IN ('pg_catalog', 'information_schema');
\o
\pset tuples_only off
\pset format aligned

\i /tmp/vacuum_big.sql
bash
psql -X -d appdb -v min_bytes=524288000 -f vacuum_driver.sql

示例四:交互式日常效率工具(配合 .psqlrc) ​

text
-- ~/.psqlrc 追加
\set slow 'SELECT pid, now()-query_start AS dur, wait_event_type, left(query,60) AS q FROM pg_stat_activity WHERE state <> ''idle'' ORDER BY dur DESC LIMIT 10;'
\set dbstat 'SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size, numbackends FROM pg_stat_database ORDER BY pg_database_size(datname) DESC;'
\set idx 'SELECT indexrelid::regclass AS idx, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan DESC LIMIT 20;'

之后在任意会话里 :slow、:dbstat、:idx 一键出结果;这是上一页 .psqlrc 技巧与本页变量插值的合流。

示例五:带退避的等待循环(\i 自递归) ​

psql 没有 while 循环,「查询事实 → \gset → \if 分支 → \sleep → 重跑脚本」的组合可以搭出轮询骨架。把下面存成 wait_replica.sql:

sql
-- wait_replica.sql:等待备库回放追平
SELECT CASE
         WHEN pg_is_in_recovery() AND pg_last_wal_replay_lsn() IS NOT NULL THEN 'ok'
         ELSE 'waiting'
       END AS state \gset

\if :state = ok
\echo 备库回放正常
\else
\warn 尚未追平,10 秒后重试
\sleep 10s
\i wait_replica.sql
\endif
bash
psql -X -d appdb -f wait_replica.sql

\i 自递归是 psql 脚本实现循环的标准姿势。两个护栏必须加:一是退出条件(本例的 \if 判断,缺了就是死循环),二是最大重试次数——传参 max_retry,每轮 \gset 递减后判断,归零即 \quit。更工程化的做法是把循环交给外层 shell 或调度系统,psql 只负责单次探测、以退出码表达结果,组合性更好。

注意事项 ​

插值发生在客户端,是文本替换。 永远不要把 psql 变量插值当成参数化查询。外部来源的值一律 :'var'/:"var",否则存在 SQL 注入风险——这是本页最重要的安全结论。

变量没有类型。 \set x 1000 存的是字符串 "1000",比较与运算都要小心:\if :x > 999 可以工作(数值比较),但 \set y :x + 1 得到的是 "1000 + 1" 字符串。做算术请走 SQL \gset。

\gset 必须单行。 返回多行会有告警且结果不可靠,返回零行不赋值。写 \gset 前先确认查询被聚合/限定到一行;列名就是变量名,AS 别名要起得规范。

\if/\endif 必须成对。 忘写 \endif 时 psql 会在文件结尾报错;嵌套时要像代码缩进一样对齐书写。另外 \if 分支里被跳过的内容不做语法解析,可以放版本专属 SQL,但也不能依赖「跳过分支」来「定义变量」。

LASTOID 基本已成历史。 现行版本普通表不含 OID,LASTOID 恒为 0。需要「刚插入行的标识」请用 INSERT ... RETURNING id 配合 \gset,这是唯一可靠的方式。

变量不会自动持久化。 会话结束即丢;要跨会话复用,写进 .psqlrc(个人默认值)或脚本开头(任务参数)。两者都不要放敏感信息(密码等)——命令行 -v 与 .psqlrc 内容在 ps/文件层面可见。

\echo 与查询输出混流。 \echo/\warn 的内容与 SELECT 结果走同一路输出(\o 重定向时都会进文件)。脚本产数据时,诊断信息用 \warn 并考虑单独重定向 stderr。

\quit 只退出当前会话。 它不是「结束脚本」,而是结束整个 psql 进程;在 -f 脚本里 \quit 会带上已执行部分的退出码语义,用于「缺参早退」正合适,但别在事务中途 \quit 留下未决事务。

递归 \i 实现循环必须带退出条件。 \i 自引用是 psql 唯一的循环手段,缺退出条件就是死循环;用重试计数变量在 \if 里控制,或把循环交给外层 shell、psql 只跑单次探测并以退出码表达结果。

最佳实践 ​

  1. 外部值一律 :'var',标识符一律 :"var",受信数值才用 :var。 把这条写成团队脚本的评审红线,能杜绝绝大多数 psql 脚本注入面。
  2. 脚本开头固定三件套:\set ON_ERROR_STOP on + 缺参检测(\if :{...}? + \quit)+ \warn 起始横幅。 三行代码换来「参数错了立即停、日志可追溯」。
  3. 用 -v 传参而不是改脚本。 同一份脚本对多套环境复用:psql -v env=prod -f task.sql,脚本内只按 :env 分支。
  4. 取数用 \gset、判断用 \if、动作再执行。 把「先查事实、再决策」的顺序固化下来,脚本才会从「一把梭」变成可控工具。
  5. 给 \gset 的列起规范别名。 列名即变量名,统一小写下划线命名(如 row_cnt、max_ts),避免与 psql 预定义变量或其他业务变量撞名;有冲突风险时用前缀 \gset p_。
  6. 用 RETURNING + \gset 拿自增主键。 INSERT ... RETURNING id \gset 后用 :id 继续后续语句,这是 psql 脚本里做「主从表写入」的干净做法。
  7. 复杂逻辑下沉到 SQL,psql 只做控制流。 计算交给 SELECT ... \gset,分支留给 \if;psql 表达式能力有限,别硬写。
  8. 多环境脚本用 SQLSTATE/ROW_COUNT 做执行体检。 每个关键动作后检查 :SQLSTATE 与 :ROW_COUNT 并 \warn 汇报,出问题时日志里能直接定位到第几步。
  9. 配合 FETCH_COUNT 处理大结果集。 \set FETCH_COUNT 1000 让 SELECT 分批取回,交互查大表时内存平稳、首行更快返回。
  10. 版本兼容用 VERSION_NUM 与对象探测双保险。 \if :VERSION_NUM >= 170000 管版本特性开关,pg_attribute/pg_class 探测管具体对象差异。
  11. 生成 SQL 的 SQL 必须用 quote_ident()/quote_literal()。 「二段式脚本」(生成端 + 执行端)两端都要有防注入意识。
  12. 把高频巡检 SQL 固化为 .psqlrc 变量。 :slow、:locks 这类短词长命令,是 psql 交互效率提升回报率最高的习惯。
  13. 批处理一律显式接管环境:-X、PSQL_PAGER=、PGAPPNAME。 别让个人配置与分页器在 cron/CI 里制造「在本机好好的、上调度就挂」的玄学问题;再配 ON_ERROR_STOP=1 让退出码可信。

下一节

继续学习:psql 事务控制(AUTOCOMMIT)

参考链接 ​