将 Lua 集成到 psql 客户端之后,数据库运维命令就不必局限于一条 SQL 或一段 shell 管道。Lua 可以调用 psql 的连接、查询和异步执行接口,把多个 SQL 操作组合成一个更适合人工使用的命令。
这个示例围绕 VACUUM 展开:普通模式逐表执行清理,verbose 模式显示当前表名,并定期读取 pg_stat_progress_vacuum,把清理阶段和块处理进度打印到同一行。它展示的重点不是重新实现 VACUUM,而是给 psql 增加一层可读、可扩展的交互式包装。
Lua 脚本如何组织一次 VACUUM
脚本的工作流可以拆成四步:
- 从系统目录中收集需要处理的表。
- 根据命令参数决定是否启用 verbose 模式。
- 在专用连接上异步发送
VACUUM,避免进度查询阻塞主操作。 - 在等待命令完成期间,查询
pg_stat_progress_vacuum并刷新终端输出。
这里使用独立连接非常关键。执行 VACUUM 的连接处于忙碌状态,如果同时用同一连接查询进度,查询就无法及时执行。因此脚本先创建一个连接副本,再通过另一个连接查询进度。
VACUUM 的后台进度可以通过当前后端进程号过滤。脚本执行 select pg_backend_pid() 获取连接 PID,然后使用下面的条件查找对应记录:
SELECT phase,
heap_blks_total,
heap_blks_scanned,
indexes_total,
indexes_processed
FROM pg_stat_progress_vacuum
WHERE pid = 12345;
进度视图没有记录时,通常表示当前阶段尚未产生可见进度,或者 VACUUM 已经接近完成。终端刷新逻辑需要同时处理这两种情况。
异步接口带来的差异
同步调用会一直等待服务器返回结果,代码简单,但无法在等待期间执行进度查询。异步方式则把等待过程拆开:
sendquery发送命令,不等待最终结果。consumeinput接收服务器已经返回的数据。isbusy判断当前连接是否仍在处理命令。resultwait(1)等待结果,参数可以表示等待的秒数。getresult取出最终结果,并检查 SQL 错误。
示例中还会发送一个 select pg_sleep(20)。它的作用是让同一个异步队列保持一段时间,方便观察进度刷新效果。实际脚本中可以根据客户端 API 的行为决定是否保留这一条;如果接口能够持续轮询忙碌连接,通常不需要额外的 sleep。
一个最小的轮询结构如下:
mycon:sendquery("vacuum verbose public.orders")
mycon:consumeinput()
while mycon:isbusy() do
if mycon:resultwait(1) == 0 then
local rs, err = psql.connect():exec([[
SELECT phase, heap_blks_total, heap_blks_scanned,
indexes_total, indexes_processed
FROM pg_stat_progress_vacuum
WHERE pid = 12345
]])
if err then
error(err)
end
local row = rs:fetch()
if row then
io.write(string.format(
"\rphase: %s, scanned: %d/%d, indexes: %d/%d\27[0K",
row.phase,
row.heap_blks_scanned,
row.heap_blks_total,
row.indexes_processed,
row.indexes_total
))
else
io.write("\r\27[0K")
end
io.flush()
rs:clear()
end
mycon:consumeinput()
end
local result, err = mycon:getresult()
if err then
print(err)
end
运行前需要把 12345 替换成实际连接 PID,并确认当前用户有权限读取相关统计信息。这个片段假设 Lua 已经运行在支持 psql.connect()、sendquery() 和 resultwait() 的 lua-psql 环境中。
让 verbose 输出更适合终端阅读
原始 VACUUM VERBOSE 输出通常包含大量诊断信息。脚本可以在每张表开始处理时打印一个反色标题,让用户快速定位当前对象:
print(string.format("\27[7mVACUUM %s.%s \27[27m", schema_name, table_name))
其中 \27[7m 开启终端反色显示,\27[27m 恢复默认显示。进度行使用回车符 \r 覆盖上一行,并用 \27[0K 清除行尾残留字符。这样不会为每次轮询产生一屏新日志,长时间维护任务也更容易观察。
表名来自系统目录时,脚本还需要避免直接拼接不可信标识符。可以在 SQL 中使用 quote_ident,并排除系统表:
local query = [[
SELECT quote_ident(n.nspname) AS nspname,
quote_ident(c.relname) AS relname
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname <> 'information_schema'
AND n.nspname !~ '^pg_toast'
AND c.relkind IN ('r', 's', 'n')
]]
这里的 quote_ident 解决的是标识符转义问题,不能替代权限控制。脚本仍应限制可处理的 schema 和表范围,避免把维护命令暴露成任意对象执行器。
参数解析与安全边界
示例用法是:
\luafile ~/vacuum.lua
\lua vacuum {verbose=true}
Lua 函数收到字符串参数后,将它拼接成 return ... 并交给 load 执行。这种写法允许使用 Lua 表作为轻量配置,例如 {verbose=true},但也意味着参数本身可以执行任意 Lua 代码。
如果命令只面向可信管理员,这种方式足够直接。若脚本会被多个用户调用,建议改成显式解析,只接受布尔值和有限选项:
local function parse_options(args)
local options = { verbose = false }
if not args or args == "" then
return options
end
if args == "{verbose=true}" then
options.verbose = true
return options
end
error("usage: \\lua vacuum {verbose=true}")
end
这会牺牲一部分 Lua 表语法的灵活性,但能清楚地限制命令行为。对于数据库运维工具,参数可控性通常比表达式解析的便利更重要。
采用时需要检查什么
这个方案适合需要交互式反馈、又不想为每个运维动作单独部署服务的场景。落地前建议检查以下事项:
- 使用与目标 PostgreSQL 版本匹配的进度视图字段。
- 确认客户端 API 的异步连接对象是否支持并发进度查询。
- 对 schema、表名和用户权限设置明确边界。
- 决定是否保留额外的
pg_sleep,避免无意义地延长任务。 - 在非交互终端中关闭 ANSI 控制序列,改为结构化日志。
- 为
getresult、连接关闭和查询错误增加完整处理。
Lua 进入 psql 后,客户端不再只是“发送一条 SQL 并打印结果”的工具。通过连接克隆、异步轮询和进度视图,复杂维护命令可以保持数据库原生执行能力,同时获得更清晰的操作反馈。这个模式也可以推广到索引创建、批量迁移以及其他需要持续观察状态的后台任务。