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 的、小写放自己的,避免冲突。
\set 赋值 ──► psql 变量表 ──► :var / :'var' / :"var" 插值 ──► 发送到服务器的最终 SQL
▲ │
└────────── \gset 把查询结果写回 ──────────┘三个必须建立的心智模型:
- 插值是文本替换。
:var会把变量的字符内容原样「粘贴」进 SQL 文本里,粘贴发生在 psql 这一端。服务器看到的永远是替换后的完整 SQL。 - 与服务端参数是两回事。扩展查询协议的
$1(或驱动里的?、%s、?1)由服务器解析 SQL 后再绑定值,值永远不会改变语句结构。psql 变量插值则发生在解析之前,因此有注入风险,必须用:'var'做安全引用。 - 作用域是会话。变量活在单个 psql 会话里,退出即消失;持久化靠
.psqlrc或脚本开头的一串\set。
| 术语 | 英文 | 说明 |
|---|---|---|
| 变量赋值 | \set / \unset | 定义或清除一个变量 |
| 变量插值 | interpolation | 把变量内容替换进输入文本的过程 |
| 安全引用 | quoted interpolation | :'var'/:"var" 生成带转义的 SQL 字面量/标识符 |
| 结果捕获 | \gset | 把单行查询结果的各列写进同名变量 |
| 条件块 | \if/\elif/\else/\endif | 按变量值控制脚本分支 |
| 命令替换 | backtick substitution | 把 shell 命令输出注入 psql 输入 |
语法与用法
赋值与查看:
\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三种插值形态:
\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";把查询结果写进变量:
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条件分支与延时:
\if :n_live > 0
\echo 表里有数据,跳过初始化
\else
\echo 表为空,执行初始化
\i init_data.sql
\endif
\sleep 2s -- 暂停 2 秒,单位支持 s/ms/min命令行传参(脚本化的标准入口):
psql -X -v ON_ERROR_STOP=1 -v dbname=appdb -v min_id=100 -f cleanup.sql-- cleanup.sql 内部
\echo 开始清理 :dbname 中 id > :min_id 的记录
DELETE FROM logs WHERE id > :min_id;详细说明
\set 的取值规则与陷阱
\set 名 值1 值2 … 会把多个值用空格拼接成一个字符串存入。规则与常见坑:
\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 的类型转换而不做插值:
\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 是变量体系里最有生产力的一环:它执行当前查询缓冲区,并要求结果恰好一行,把这一行的每一列写进与列同名的变量:
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 内置了一组变量,读懂它们能让你少写很多查询。按用途分组:
连接与环境信息(只读,由连接自动赋值)
| 变量 | 内容示例 | 说明 |
|---|---|---|
DBNAME | appdb | 当前连接的数据库名 |
USER | app_rw | 当前连接角色 |
HOST | db1.prod.internal | 连接主机(套接字连接时为空) |
PORT | 5432 | 端口 |
ENCODING | UTF8 | 客户端编码 |
VERSION | PostgreSQL 18.0 … | 完整版本串 |
VERSION_NAME | 18.0 | 短版本号 |
VERSION_NUM | 180000 | 数值版本,用于 \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 后遇错立即退出,脚本化必开 |
AUTOCOMMIT | on | 自动提交开关,下一页详述 |
ON_ERROR_ROLLBACK | 未设 | 交互模式下出错自动回滚到 SAVEPOINT |
ECHO | none | queries/errors/all,对应 -e/-b/-a |
VERBOSITY | default | 错误详细程度:default/verbose/terse |
SHOW_CONTEXT | errors | 错误上下文(CONTEXT 行)显示策略 |
FETCH_COUNT | 未设 | 设为正数后 SELECT 分块取回(大结果集省内存) |
QUIET | off | 抑制欢迎与提示信息 |
SINGLELINE | off | 分号回车即执行,忽略换行排版 |
SINGLESTEP | off | 每条语句执行前询问(单步调试脚本) |
HISTCONTROL/HISTFILE/HISTSIZE | — | 命令历史策略 |
IGNOREEOF | 未设 | 设为 on 后 Ctrl+D 不退出 |
PROMPT1/PROMPT2/PROMPT3 | 见下 | 三级提示符模板 |
其中 ROW_COUNT 与 SQLSTATE 组合可以做出「执行后立即体检」的脚本模式:
DELETE FROM expired_sessions;
\if :SQLSTATE = 00000
\warn 清理完成,删除 :ROW_COUNT 行
\else
\warn 清理失败::LAST_ERROR_MESSAGE
\endifPROMPT1/2/3:提示符模板
提示符变量支持 % 转义,常用的有:%n 用户名、%/ 数据库名、%M 主机、%> 端口、%#(超级用户显示 #,普通用户 >)、%R(续行提示符状态字符)、%~(把 ~ 替换为基础目录)。
\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。
-- 判断“参数是否传入”的惯用法
\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,数字本身可以用变量:
\sleep 500ms
\set interval 3
\sleep :interval s典型用途是等待外部条件:切换后等备库追平、批量删除分批之间留缓冲、重试循环里退避。注意 \sleep 只在客户端等待,期间不持有任何数据库锁,但连接保持打开——长时间的轮询等待脚本要注意 tcp_keepalives 与防火墙的空闲断连。
SQL 中使用变量的边界:客户端插值 ≠ 服务端参数
这一节值得单独强调,因为它关系到安全底线。
psql 插值的完整链路:
:var 替换 ──► 得到完整 SQL 文本 ──► 发送到服务器 ──► 服务器解析并执行服务端参数(扩展查询协议)的链路:
带 $1 占位符的语句模板 + 参数值 ──► 分别发送 ──► 服务器解析模板 ──► 绑定参数执行前者在解析之前拼接文本,值如果含恶意 SQL 片段('; DROP TABLE t; --)会成为语句的一部分;后者值永远只是值。由此得出使用准则:
-- 危险:外部输入经 :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,改一个变量即可:
\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 :intervalCtrl+C 中断后查询缓冲区仍在,可以直接改 SQL 继续 \watch,不必重新敲整条语句。
与 \gexec 组合做「结果驱动执行」。\gexec 把查询结果的每一行当作 SQL 去执行,行内容经常由变量参与生成:
\set target_schema 'archive'
SELECT format('ANALYZE %I.%I;', table_schema, table_name)
FROM information_schema.tables
WHERE table_schema = :'target_schema' \gexecformat('%I') 对标识符做安全包装,与 quote_ident() 等价。这里 'archive' 是脚本里写死的常量,即使引用形态松一点也不会出事——但保持 :'var' 的一致习惯更稳。
与 \gdesc 组合做「先看结构再决定」。\gdesc 列出当前查询缓冲的列名与类型但不执行——脚本里可以先确认列名,再决定 \gset 后引用哪个变量:
SELECT relname, n_live_tup FROM pg_stat_user_tables LIMIT 1 \gdesc三个命令的共同规则:插值都发生在发送之前。无论结果去向是屏幕、变量、文件还是再次执行,服务器收到的都已是替换完成的最终文本。
反引号命令替换与 \o 生成脚本
psql 输入里反引号包裹的内容会交给 shell 执行,命令的 stdout 替换回输入文本(对元命令参数与 \set 生效):
\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——后者在下一页事务篇里与事务配合讲解):
\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 |
PGAPPNAME | application_name | 让 pg_stat_activity 能溯源到具体脚本 |
一个容易被忽略的细节:cron / CI 里跑 psql 时,环境与交互 shell 不同——个人 .psqlrc 可能不存在或行为不一致(交互增强类 \set 全部消失),PSQL_PAGER 没有终端可交互时会挂住管道。批处理的标准姿势:
#!/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 管理脚本通常长这样:
-- 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 的调度:
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' 自带引号,外面不要再包 |
调试三招,由轻到重:
\set不带参数列出全部变量,确认拼写与当前值;\echo试插值:拿不准的引用先\echo :'val'看替换结果,再放进 SQL;ECHO=queries复盘:psql -a(或\set ECHO queries)把每条发往服务器的最终 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 捕获单值并在后续语句复用
场景:先把备份表名、时间窗算进变量,再执行归档。
\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 做版本与对象存在性兼容
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迁移脚本常用这个模式探测目标库结构,在旧新版本上跑同一份脚本而互不报错。
示例三:变量 + 反引号 + 生成脚本批量维护
-- 对超过 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.sqlpsql -X -d appdb -v min_bytes=524288000 -f vacuum_driver.sql示例四:交互式日常效率工具(配合 .psqlrc)
-- ~/.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:
-- 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
\endifpsql -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 只跑单次探测并以退出码表达结果。
最佳实践
- 外部值一律
:'var',标识符一律:"var",受信数值才用:var。 把这条写成团队脚本的评审红线,能杜绝绝大多数 psql 脚本注入面。 - 脚本开头固定三件套:
\set ON_ERROR_STOP on+ 缺参检测(\if :{...}?+\quit)+\warn起始横幅。 三行代码换来「参数错了立即停、日志可追溯」。 - 用
-v传参而不是改脚本。 同一份脚本对多套环境复用:psql -v env=prod -f task.sql,脚本内只按:env分支。 - 取数用 \gset、判断用 \if、动作再执行。 把「先查事实、再决策」的顺序固化下来,脚本才会从「一把梭」变成可控工具。
- 给 \gset 的列起规范别名。 列名即变量名,统一小写下划线命名(如
row_cnt、max_ts),避免与 psql 预定义变量或其他业务变量撞名;有冲突风险时用前缀\gset p_。 - 用
RETURNING+ \gset 拿自增主键。INSERT ... RETURNING id \gset后用:id继续后续语句,这是 psql 脚本里做「主从表写入」的干净做法。 - 复杂逻辑下沉到 SQL,psql 只做控制流。 计算交给
SELECT ... \gset,分支留给\if;psql 表达式能力有限,别硬写。 - 多环境脚本用 SQLSTATE/ROW_COUNT 做执行体检。 每个关键动作后检查
:SQLSTATE与:ROW_COUNT并\warn汇报,出问题时日志里能直接定位到第几步。 - 配合 FETCH_COUNT 处理大结果集。
\set FETCH_COUNT 1000让 SELECT 分批取回,交互查大表时内存平稳、首行更快返回。 - 版本兼容用 VERSION_NUM 与对象探测双保险。
\if :VERSION_NUM >= 170000管版本特性开关,pg_attribute/pg_class探测管具体对象差异。 - 生成 SQL 的 SQL 必须用 quote_ident()/quote_literal()。 「二段式脚本」(生成端 + 执行端)两端都要有防注入意识。
- 把高频巡检 SQL 固化为 .psqlrc 变量。
:slow、:locks这类短词长命令,是 psql 交互效率提升回报率最高的习惯。 - 批处理一律显式接管环境:
-X、PSQL_PAGER=、PGAPPNAME。 别让个人配置与分页器在 cron/CI 里制造「在本机好好的、上调度就挂」的玄学问题;再配ON_ERROR_STOP=1让退出码可信。
下一节