Skip to content

log_statement 语句日志 ​

在数据库安全体系里,日志是最基础、也是最容易「开着开着就失效」的一道防线。log_statement 是 PostgreSQL 内置的语句级日志开关,它决定「哪些 SQL 会被原样写进服务器日志」。相比后面要学的 pgaudit 扩展,它是零依赖、开箱即用的,几乎所有合规检查项里的「数据库操作留痕」都可以从它开始做起。

但语句日志也是一把双刃剑。它记录的是 SQL 的原始文本——这意味着用户的输入参数、甚至 CREATE ROLE ... PASSWORD 'xxx' 里的明文口令,都可能原封不动地落入日志文件。开启它的同时,你等于把数据库里发生的每一件事都誊写了一份副本,这份副本落在哪、谁能读、保留多久、会不会把磁盘写满,都必须在打开开关之前想清楚。

本页从四个取值的语义讲起,覆盖它与慢查询日志的交互、log_line_prefix 的归因配置、敏感信息泄漏风险、日志量与磁盘预算、按角色与数据库的差异化覆盖,最后给出它与 pgaudit 的定位对比。学完本页,你应该能够为任意规模的业务选出一档「既满足审计需求、又不把系统拖垮」的语句日志策略。

前置知识

建议先阅读:证书与 TLS 配置

基础概念 ​

语句日志相关的参数都集中在「错误报告与日志(Error Reporting and Logging)」配置节里。先把这一族参数的总览放在这里,后面逐一展开:

参数类型 / 默认值作用
log_statementenum,默认 none按「语句类别」记录 SQL 文本:none / ddl / mod / all
log_min_duration_statementint,默认 -1把执行时长超过阈值的语句连同文本一起记录(慢查询日志)
log_durationbool,默认 off记录每条已完成语句的执行时长(不含语句文本)
log_min_error_statementenum,默认 ERROR当语句触发指定级别以上的错误时,把出错语句的文本记入日志
log_parameter_max_lengthint,默认 -1(PG13+)限制语句日志中绑定参数值的最大长度(字节)
log_parameter_max_length_on_errorint,默认 0(PG13+)限制错误日志中绑定参数值的最大长度(字节)
log_line_prefixstring每条日志行首的元信息前缀(时间、用户、库、进程号等)
logging_collectorbool日志收集器,把 stderr 捕获到日志文件(配合 log_destination)

语句日志在日志体系中的位置 ​

PostgreSQL 的日志可以粗分为四类,log_statement 只是其中一类:

  • 连接与会话日志:谁连进来、何时断开(log_connections / log_disconnections,下一页的主角)。
  • 语句日志:执行了什么 SQL(本页的 log_statement、log_min_duration_statement)。
  • 错误日志:什么操作失败了(log_min_error_statement 以及所有以 LOG / ERROR / FATAL 级别落盘的事件)。
  • 运维事件日志:checkpoint、VACUUM、WAL 归档等后台活动(log_checkpoints 等)。

这四类共同构成审计的最小素材库。语句日志的特殊之处在于:它记录的是语义层面的内容——你能从日志里直接读懂「有人对 orders 表执行了 DELETE」,而不需要像 WAL 那样反推。

四档取值的语义边界 ​

log_statement 的四个取值是一组递增的过滤器,挡位越高、记录越多:

text
none  ⊂  ddl  ⊂  mod  ⊂  all
  • none(默认):不记录任何语句。绝大多数默认安装的数据库都处于这一档,也就是说默认情况下数据库里执行过什么 SQL,事后是无据可查的。
  • ddl:记录所有数据定义语句(CREATE / ALTER / DROP 以及同类的 DDL 命令)。结构变更是影响面最大的操作,通常也是审计要求里「必须留痕」的部分。
  • mod:在 ddl 的基础上,再记录所有数据修改语句(INSERT / UPDATE / DELETE / TRUNCATE / COPY FROM)。注意 SELECT 仍然不在其中。
  • all:记录所有语句,包括 SELECT 和各类实用命令(utility commands)。

另外有一个反直觉的细节:包含简单语法错误的语句不会被记录——即使 log_statement = all。因为语法解析在语句被识别之前就失败了,日志系统拿不到一条「完整的语句」。审计视角下,别指望语句日志能抓到注入探测时发出的那堆语法垃圾。

生效方式与修改权限 ​

这一族参数的修改权限并不相同,排障前先确认「这个参数谁有权改、怎么改才生效」:

  • log_statement / log_min_duration_statement / log_min_error_statement:超级用户级参数,普通用户即使 SET 也会被拒绝;超级用户可以在会话内临时修改(对当前会话生效),全局修改走配置文件或 ALTER SYSTEM 后 reload。
  • log_duration:普通用户即可在会话内设置。
  • log_parameter_max_length:超级用户级;log_parameter_max_length_on_error 普通用户可设。

reload 与 restart 的分界也要记住:本页所有参数都支持 reload 生效,但只对新建会话或后续语句执行生效(会话内已有 SET 覆盖的除外)。用 pg_settings 确认参数的来源与等待状态:

sql
SELECT name, setting, source, pending_restart
FROM pg_settings
WHERE name IN ('log_statement','log_min_duration_statement',
               'log_min_error_statement','log_duration');
text
            name             | setting |        source      | pending_restart
-----------------------------+---------+--------------------+-----------------
 log_duration                | off     | default            | f
 log_min_duration_statement  | 1000    | configuration file | f
 log_min_error_statement     | error   | configuration file | f
 log_statement               | ddl     | configuration file | f

source 列能直接回答「这个值是配置文件给的、ALTER SYSTEM 给的,还是某个角色/数据库覆盖给的」,pending_restart = t 则提醒你上次修改还在等重启。

与慢查询日志的分工 ​

log_min_duration_statement(慢查询日志)和 log_statement(语句日志)都能把「语句文本 + 参数」写进日志,但触发条件完全不同:

  • 前者按执行时长触发,回答的是「这条语句为什么慢」;
  • 后者按语句类别触发,回答的是「谁在什么时间执行了什么」。

它们可以同时开启,但存在一条重要的交互规则,下一节详细说明。定位上,慢查询日志是性能工具,语句日志是审计工具——用错挡位(比如想审计却开了 log_statement=all 全量记)是日志系统最常见的翻车姿势。

语法与用法 ​

基础配置 ​

语句日志相关的参数多数可以在配置文件中设置并 reload 生效,无需重启:

conf
# postgresql.conf

# 语句日志挡位:none / ddl / mod / all
log_statement = 'ddl'

# 慢查询日志阈值:0 表示记录所有语句的耗时,-1 表示关闭
# 支持 ms / s / min / h / d 单位
log_min_duration_statement = 1s

# 出错语句记录阈值:ERROR 级别及以上的错误会连同语句文本一起落盘
log_min_error_statement = 'error'

# 是否记录每条语句的纯耗时(不含语句文本)
log_duration = off

# 日志行首前缀:时间 [进程号] 用户@数据库
log_line_prefix = '%m [%p] %q%u@%d '

# 使用日志收集器把 stderr 捕获到文件(生产环境建议开启)
logging_collector = on
log_destination = 'stderr'   # 也可选 csvlog / jsonlog(PG15+)

修改后重新加载:

bash
# 方式一:直接 reload
pg_ctl reload -D /var/lib/postgresql/18/main

# 方式二:通过 SQL 触发 reload
psql -U postgres -c "SELECT pg_reload_conf();"

# 确认参数已生效
psql -U postgres -c "SHOW log_statement;"

按数据库与角色覆盖 ​

log_statement 与 log_min_duration_statement 允许按数据库、按角色设置更细的值(这些参数属于超级用户级设置,由超级用户执行 ALTER):

sql
-- 业务库把 DML 也记下来
ALTER DATABASE appdb SET log_statement = 'mod';

-- 报表角色查询量大,退回只记 DDL,避免日志洪水
ALTER ROLE bi_reader SET log_statement = 'ddl';

-- 管理角色一切留痕
ALTER ROLE dba_ops SET log_statement = 'all';

-- 报表库的慢查询阈值放宽到 5s
ALTER DATABASE reportdb SET log_min_duration_statement = '5s';

-- 查看某角色的生效值
SELECT rolname, rolconfig FROM pg_roles WHERE rolname = 'bi_reader';

按角色/数据库的设置保存在 pg_db_role_setting 目录表中,优先级高于 postgresql.conf 的全局值,可以用 SHOW 或 current_setting() 验证实际生效值。

日志长什么样 ​

开启 log_statement = 'ddl' 后执行一条 DDL,日志文件中会出现两行:先是一行携带前缀元信息(以及可选的连接来源信息),然后是语句文本行:

text
2026-10-07 10:12:33.181 CST [21831] dba@appdb LOG:  statement: ALTER TABLE orders ADD COLUMN remark text;

如果同一连接触发了错误(且级别达到 log_min_error_statement),还会附带 STATEMENT: 行:

text
2026-10-07 10:14:02.770 CST [21831] dba@appdb ERROR:  syntax error at or near "FORM" at character 15
2026-10-07 10:14:02.770 CST [21831] dba@appdb STATEMENT:  SELECT name FORM users WHERE id = 1;

验证与自检:确认挡位真的生效 ​

挡位调整后做一次端到端自检,比「看起来 reload 成功了」可靠得多:

sql
-- 1. 全局挡位
SHOW log_statement;

-- 2. 角色级覆盖是否存在(rolconfig 非空即有覆盖)
SELECT rolname, rolconfig
FROM pg_roles
WHERE rolconfig IS NOT NULL
ORDER BY rolname;
text
  rolname  |     rolconfig
-----------+--------------------
 bi_reader | {log_statement=ddl}
 ops_admin | {log_statement=all}
sql
-- 3. 用一条真实语句验证落盘
CREATE TABLE _audit_probe (id int);
DROP TABLE _audit_probe;
bash
# 日志尾部应出现两条 statement 记录
tail -n 5 /var/lib/postgresql/18/main/log/postgresql-$(date +%F).log

自检要点:验证用的会话要与目标角色一致(挡位按角色分域时,用 postgres 超级用户验证会看到完全不同的挡位);探针表名加下划线前缀便于日志检索,验证完立即清理。

详细说明 ​

ddl / mod / all 三挡到底各自抓住什么 ​

用一张真实语句清单来划边界,比读定义更直观:

语句ddlmodall
CREATE TABLE / ALTER TABLE / DROP TABLE✅✅✅
CREATE ROLE ... PASSWORD 'xxx'✅✅✅
INSERT / UPDATE / DELETE / TRUNCATE❌✅✅
COPY FROM(导入数据)❌✅✅
SELECT(纯查询)❌❌✅
EXPLAIN SELECT ...❌❌✅
PREPARE / EXECUTE(内部为 DML 时)❌✅✅
SET search_path = ...❌❌✅
含语法错误的任意语句❌❌❌

几个容易被忽视的点:

  • PREPARE / EXECUTE / EXPLAIN 这类「包裹型」命令,是否被记录取决于其内部的命令是否落在当前挡位的类别内。EXPLAIN DELETE FROM t 在 mod 挡会被记录。
  • 使用扩展查询协议(驱动普遍采用)时,语句在被 Execute 消息触发执行时记录,Bind 阶段传入的参数值会包含在日志里(字符串中的单引号会翻倍转义)。
  • log_statement = all 并不意味着「所有到达服务器的字节都被记录」——快速失败的语法错误、连接层的握手内容都不在语句日志范畴。

扩展查询协议下的参数记录 ​

现代驱动(JDBC、libpq、psycopg、node-postgres)默认使用扩展查询协议:SQL 骨架走 Parse,参数走 Bind。这带来两个后果:

  1. 日志里看到的语句是带 $1、$2 占位符的骨架 + 参数值,而不是拼接好的最终 SQL。追查问题时需要把两者拼回去。
  2. 参数值默认会被完整记录。一条 INSERT INTO logs(msg) VALUES ($1) 如果 msg 是 10 MB 的 JSON,日志里就多出 10 MB——这正是 log_parameter_max_length(PG13+)要解决的问题,后文详述。

log_min_duration_statement 的交互规则 ​

官方文档对两者并存的交互有明确说明:当 log_statement 已经记下了某条语句时,log_min_duration_statement 对这些语句无效——同一条语句不会因为「既属于已开启的类别、又执行得够慢」而被记录两次。

由此产生的工程结论是:

  • log_statement = all 一旦开启,慢查询日志事实上被「吞掉」了:所有语句都已落入日志,但日志行里不带执行耗时。想要「全量文本 + 耗时」的组合,all 挡位给不了。
  • 反过来,只开 log_min_duration_statement(不设 log_statement)是经典的性能观测模式:记录量与慢语句数量成正比,通常可控。
  • 同时要说明的是:无论由哪个参数触发,带文本的语句日志都会包含绑定参数值,默认不截断。

log_duration:只记耗时、不记文本 ​

log_duration = on 会为每一条完成的语句记录一行耗时,但不含语句文本:

text
2026-10-07 10:20:11.902 CST [21840] app@appdb LOG:  duration: 12.341 ms

它和 log_min_duration_statement = 0 的区别是:后者「超过阈值时强制把语句文本也记下来」,而前者永远只有数字。因此 log_duration 适合「想要全局耗时基线、但不想让语句文本刷爆日志」的场景——缺点也很明显:只有耗时没有语句,你只能对着进程号去猜执行了什么。生产中更常见的组合是 log_duration = off + 合理的 log_min_duration_statement 阈值。

log_min_error_statement:把出错语句送进日志 ​

默认 log_min_error_statement = error 的含义是:当一条语句产生 ERROR 及以上级别的错误时,日志会在错误详情之后追加一行 STATEMENT:,原样给出出错语句的文本。可选级别从 DEBUG5 到 PANIC,级别越低记录越多(例如设成 warning 会把产生警告的语句也记下来)。

它是排查「应用报错但没人知道发的是什么 SQL」的第一工具,也是审计的兜底:即使语句日志全关,出错的语句也会留下痕迹。需要注意它与 log_parameter_max_length_on_error(PG13+)的配合——后者为 0 时,出错语句里的绑定参数值不会被记入错误日志;设置为正数(字节)后,参数值按该长度截断记录。这是错误路径上防止参数泄漏的专用阀门。

csvlog / jsonlog 下的语句日志形态 ​

log_destination 决定这些日志行长什么样,也决定采集端要不要写解析器:

  • stderr(默认):人类可读,log_line_prefix + 消息体;跨行关联靠前缀。
  • csvlog:一行一个 CSV 记录,字段固定(时间、用户、库名、PID、连接来源、消息、详情、语句……),语句文本落在专门的列里,ETL 可直接拆列。
  • jsonlog(PG15+):一行一个 JSON 对象,字段名稳定,是日志平台(ELK / Loki / 云日志服务)的推荐形态。

同一条慢查询在 jsonlog 下的形态:

text
{"timestamp":"2026-10-07 10:52:14.033 CST","user":"app","dbname":"appdb",
 "pid":22040,"remote_host":"10.0.3.17","session_id":"689e4c1e.5618",
 "level":"LOG","message":"duration: 812.994 ms",
 "statement":"SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC;",
 "detail":"parameters: $1 = '4711'"}

工程含义:结构化格式下「谁、在哪、执行了什么、多久」天然成列,审计报表与告警规则不再依赖正则切分。注意结构化格式有自己的固定字段清单,log_line_prefix 模板主要服务于 stderr 输出;从 stderr 切到 csvlog / jsonlog 后,采集端解析器与告警正则要按新字段重新校准——格式变更属于采集链路的破坏性变更,走变更流程。

log_line_prefix:让日志可归因 ​

一条只有 SQL 文本的日志在审计上几乎无用——你不知道是谁、在哪个库、从哪台机器发出的。log_line_prefix 负责给每行日志补上元信息。常用转义符:

转义符含义
%m毫秒级时间戳(%t 为秒级,%n 为毫秒)
%u用户名
%d数据库名
%p后端进程号(PID)
%aapplication_name
%h客户端主机名或 IP
%r客户端主机:端口
%c会话 ID(可把同一会话的多行日志串起来)
%l本会话内的日志行号
%i命令标签(command tag,如 UPDATE)
%eSQLSTATE 错误码
%x事务 ID
%s会话开始时间戳
%b后端类型(PG13+,如 client backend / walsender)
%q对无会话上下文的进程,此位置之后的转义符全部省略

推荐的生产配置是 %m [%p] %q%u@%d (连接日志一页会展开说明),如果日志会被集中收集,再补上 %h 与 %c 会大幅提升跨行关联能力。注意 %q 是一个「分隔哨兵」而不是内容——不写 %q 时,无会话的后台进程(如 checkpointer)会因为取不到 %u 而输出空字段,加上 %q 可以让这类进程优雅地跳过用户/库字段。

敏感信息泄漏:密码与绑定参数 ​

语句日志会把密码写进日志

log_statement(任何非 none 挡位)记录的是 SQL 的原始文本。执行 CREATE USER app WITH PASSWORD 'S3cret!'; 时,这行 SQL 连同明文密码会被完整写入日志。PostgreSQL 不会对语句文本中的口令字面量做脱敏(它只对认证失败类错误消息做隐藏)。因此:凡是开启语句日志的实例,改密码一律走 ALTER ROLE ... PASSWORD 的交互式 \password 或应用侧参数化执行,并定期轮换日志中出现过的凭证。

三类典型的泄漏通道需要逐一封堵:

  • 口令字面量:如上,CREATE ROLE / ALTER ROLE ... PASSWORD / CREATE SERVER ... OPTIONS (password ...) 的参数都会原样落盘。规避手段是超级用户用 psql 的 \password(口令不经过 SQL 文本)或让应用永远用绑定参数。
  • 绑定参数值:扩展协议下参数默认完整记录。PG13+ 用 log_parameter_max_length 限制语句日志中参数值的最大字节数(默认 -1 即不限制),用 log_parameter_max_length_on_error(默认 0 即错误路径不记参数值)单独控制错误日志。给两者都设一个如 64 字节的温和上限,能挡住绝大多数「参数里带手机号 / 证件号 / Token」的意外泄漏。
  • 错误语句文本:log_min_error_statement 触发的 STATEMENT: 行同样可能包含敏感字面量,同样受 log_parameter_max_length_on_error 约束。

日志文件本身的访问控制也要跟上:log_file_mode(默认 0600)、日志目录属主仅限数据库运行账户、集中收集链路上的最小权限,都属于同一条防线的延伸。

把三类泄漏通道与对应阀门整理成自查表:

泄漏通道触发条件封堵阀门
口令字面量语句日志记录到 CREATE/ALTER ROLE ... PASSWORD\password 交互式改密;参数化接口;改密语句审计复查
绑定参数值语句 / 慢查询日志记录参数(默认完整)log_parameter_max_length 截断(PG13+)
错误语句参数错误触发 STATEMENT: 行记录参数log_parameter_max_length_on_error(0 = 不记参数值)
日志文件本身文件权限过宽、采集链路裸传log_file_mode = 0600、目录权限收紧、传输走 TLS

日志量评估与磁盘预算 ​

log_statement 的记录量没有理论上限,估算要靠实测。一个可操作的步骤:

  1. 用 pg_stat_statements 统计单位时间内的语句执行次数(按 calls 聚合),乘以「平均日志行长度」(可用采样日志实测,通常 100~400 字节)得到基础量。
  2. 注意执行次数和语句数的区别:同一个高频简单查询(如每秒几百次的点查)在 all 挡下会贡献几千行/分钟的日志,而它们的审计价值几乎为零。
  3. 给日志目录单独的分区或配额,避免与 pg_wal、表数据抢占同一块盘。logging_collector 的 log_directory、log_filename(按日滚动)、log_rotation_age / log_rotation_size、log_truncate_on_rotation 组成一套滚动策略。
  4. 保留期由合规要求倒推(常见 90~180 天),并且压缩归档——日志文本的压缩比通常在 10:1 以上,长期保留应归档压缩而非保留原始文件。

经验上:中小规模 OLTP 实例开 ddl 挡,日志增量通常可以忽略;开 mod 挡需要实测;开 all 挡在写入密集型业务上每天产生几十 GB 日志并不罕见。

从语句日志到 pgaudit 的演进 ​

log_statement 是「开箱即用」的语句留痕,但它有三个天花板:挡位只有三档(颗粒度到不了「某张表」)、日志行没有结构化的类别与对象字段、mod / all 挡在 OLTP 规模下日志量难以承受。当审计要求精确到对象时,就该考虑迁移到 pgaudit 扩展:

维度log_statementpgaudit 会话审计pgaudit 对象审计
粒度三挡,可下放库 / 角色八个类别,可下放库 / 角色单对象(GRANT 声明)
日志格式原始语句文本固定九段 AUDIT 格式固定九段 AUDIT 格式
前置条件无(改配置即可)预加载 + 重启 + 建扩展同左 + 审计角色
典型用途应急放量、快速留痕常态化审计(DDL / ROLE 等)核心敏感表读写

迁移不是替换关系:log_statement 依然是应急放量的首选——一条 GUC、reload 即生效,无需预加载与扩展;pgaudit 是常态化审计的形态,代价是接管语句日志参数并要求重启窗口。适用边界与迁移清单见 pgaudit 一页的实战示例。

按角色 / 数据库覆盖与日志采样 ​

全库统一挡位往往是「要么不够审计、要么日志爆炸」的两难,解决思路是把挡位下放到更细的粒度:

  • 按角色分档(见「语法与用法」一节):管理角色 all、普通业务角色 mod 或 ddl、高频只读角色 none/ddl。审计重点是「人 + 高危操作」,而不是每一行 SELECT。
  • 按数据库分档:核心业务库与日志型的边缘库区别对待。
  • 运行时窗口采样:log_min_duration_statement 属于可在运行期修改的参数,可以在可疑时段临时调低阈值(如 1s → 100ms)或临时把某角色切到 all,观察期结束再恢复。这相当于「按需提高采样率」,比长期全量记录经济得多。
  • 采集端采样:如果日志链路接入了 Fluent Bit、Vector、rsyslog 等采集器,可以在采集端做抽样或聚合(例如对相同语句骨架的重复行做计数折叠),数据库侧保持完整、分析侧控制噪音。此外,离线分析工具如 pgBadger 可以在统计报表层面消化海量日志,缓解「记了没人看」的问题。

与 pgaudit 的对比定位 ​

log_statement 与 pgaudit(下一节的主角)回答的审计问题不同,实际项目里往往是「前者兜底、后者主审」:

维度log_statementpgaudit
依赖无(内核自带)独立扩展,需 shared_preload_libraries 预加载
分类粒度ddl / mod / all 三挡粗粒度READ / WRITE / DDL / ROLE / MISC 等类别可任意组合
对象级审计不支持支持(pgaudit.role + GRANT 指定表)
记录格式普通日志行,无结构化标记带 AUDIT: 前缀的结构化字段(类别、命令、对象、语句、参数)
按角色 / 按对象细分按角色可设置挡位按角色 + 按对象可精确指定
升级维护成本零跟随大版本安装对应分支的扩展

简单结论:没有扩展安装权限、或只需要满足「DDL 留痕」这类最低要求时,log_statement 完全够用;当合规要求「按表审计、按类别出报表、审计记录可机读」时,应当引入 pgaudit,并把 log_statement 退回 none,避免同一语句被两套机制重复记录。

用 pgBadger 分析语句日志 ​

pgBadger 是 PostgreSQL 生态最常用的离线日志分析器:解析 postgresql 日志,产出 HTML 报表(Top 查询、耗时分布、错误统计、连接来源、checkpoint/VACUUM 时间线等)。它与本页的关系是「数据库侧记录 → 离线侧消化」:

bash
# 解析单日日志生成 HTML 报表
pgbadger -I -o report-2026-10-07.html \
  /var/lib/postgresql/18/main/log/postgresql-2026-10-07.log

# 多文件并行解析(大日志提速)
pgbadger -I -j 4 -o report.html postgresql-2026-10-*.log

对语句审计的实用价值:

  • Top 查询榜单:哪些语句在刷日志一目了然,是「按角色分档」调挡位的直接依据;
  • 耗时分布:配合 log_min_duration_statement 的阈值调整,验证采样挡位是否合理;
  • 错误与会话统计:log_min_error_statement 记录的错误语句会按 SQLSTATE 聚合,异常错误码的尖刺通常先于故障被发现。

两个限制要清楚:pgBadger 的慢查询统计依赖 duration 信息——log_statement = all 产生的纯语句行没有耗时,对性能报表没有贡献(再次印证「审计挡位与性能观测分开」);以及它是离线工具,实时告警仍要靠采集链路(见审计策略一页的告警设计)。

实战示例 ​

示例一:给核心库开启 DDL 审计 ​

目标:所有结构变更留痕,业务流量零感知。

conf
# postgresql.conf(或 ALTER SYSTEM SET)
log_statement = 'ddl'
log_line_prefix = '%m [%p] %q%u@%d '
log_min_error_statement = 'error'
bash
# 生效并验证
psql -U postgres -c "SELECT pg_reload_conf();"
psql -U postgres -c "SHOW log_statement;"
sql
-- 用一个普通角色做一次结构变更
CREATE TABLE audit_test (id int PRIMARY KEY);
ALTER TABLE audit_test ADD COLUMN note text;
DROP TABLE audit_test;
text
2026-10-07 10:40:01.220 CST [22011] dev@postgres LOG:  statement: CREATE TABLE audit_test (id int PRIMARY KEY);
2026-10-07 10:40:01.246 CST [22011] dev@postgres LOG:  statement: ALTER TABLE audit_test ADD COLUMN note text;
2026-10-07 10:40:05.118 CST [22011] dev@postgres LOG:  statement: DROP TABLE audit_test;

示例二:慢查询日志与语句日志的正确并存 ​

需求变更:DDL 继续全记,同时希望慢于 500ms 的语句(含 SELECT)带文本落盘。注意 log_statement = 'ddl' 不会吞掉慢查询日志——只有已被它记录的 DDL 不会再被慢日志重复记录:

conf
log_statement = 'ddl'
log_min_duration_statement = 500ms
text
-- 慢 SELECT 的日志行(由 log_min_duration_statement 触发)
2026-10-07 10:52:14.033 CST [22040] app@appdb LOG:  duration: 812.994 ms  statement:
    SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC;
2026-10-07 10:52:14.033 CST [22040] app@appdb DETAIL:  parameters: $1 = '4711'

若错误地把 log_statement 开成 all,上面这行慢查询日志就「消失」了——语句仍会记录,但没有 duration 字段,慢查询分析工具(如 pgBadger)将失去耗时数据。

示例三:按角色差异化挡位 ​

sql
-- 超级用户执行
ALTER ROLE ops_admin    SET log_statement = 'all';    -- 运维操作全留痕
ALTER ROLE app_write    SET log_statement = 'mod';    -- 业务写入留痕
ALTER ROLE app_read     SET log_statement = 'ddl';    -- 高频只读角色只记 DDL 兜底
ALTER ROLE etl_batch    SET log_statement = 'none';   -- 海量批处理角色彻底静默

-- 校验
SELECT rolname, rolconfig FROM pg_roles
WHERE rolname IN ('ops_admin','app_write','app_read','etl_batch');
text
  rolname  |              rolconfig
-----------+---------------------------------------
 ops_admin | {log_statement=all}
 app_write | {log_statement=mod}
 app_read  | {log_statement=ddl}
 etl_batch | {log_statement=none}
(4 rows)

之后各角色新建的会话将按各自挡位记录。已在运行的旧会话不受影响,需要重连后生效。

示例四:估算日志量并设置滚动与告警 ​

sql
-- 用 pg_stat_statements 粗估语句吞吐(需已启用该扩展)
SELECT calls,
       round(total_exec_time::numeric / calls, 2) AS avg_ms,
       left(query, 60) AS query_head
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;
bash
# 观察当前日志增长速度(开启新挡位后静置 10 分钟)
du -sh /var/lib/postgresql/18/main/log/

# 设置每日滚动 + 保留压缩归档(logrotate 片段示意)
ini
/var/lib/postgresql/18/main/log/postgresql-*.log {
    daily
    rotate 90
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

把 du 的结果外推到天,再乘以 1.5~2 倍安全系数,就是这条挡位下的磁盘预算;预算不通过,就回到「按角色分档」下调挡位,而不是砍保留期。

示例五:可疑时段的临时放大与回收 ​

事件背景:安全团队怀疑某内部账号在非工作时间导出数据,需要对该账号做 24 小时全语句审计,之后完全回收。

sql
-- 1. 记录基线(回收时对比)
SELECT rolname, rolconfig FROM pg_roles WHERE rolname = 'suspect_rw';

-- 2. 临时放大(超级用户执行)
ALTER ROLE suspect_rw SET log_statement = 'all';
SELECT pg_reload_conf();
bash
# 3. 观察窗口内按天检查日志量,防止意外洪水
du -sh /var/lib/postgresql/18/main/log/
sql
-- 4. 24 小时后回收
ALTER ROLE suspect_rw RESET log_statement;
SELECT pg_reload_conf();

-- 5. 确认回收干净
SELECT rolname, rolconfig FROM pg_roles WHERE rolname = 'suspect_rw';
text
  rolname   | rolconfig
------------+-----------
 suspect_rw |

运维要点:放大前记录原值——RESET 恢复的是「无角色级覆盖」,若该角色原本就有覆盖值,回收要用 ALTER ROLE ... SET 写回原值而不是 RESET;窗口结束在值班系统登记关闭,避免「临时审计」变成永久日志洪水;期间产生的日志按事件取证流程归档,作为排查结论的支撑材料。

注意事项 ​

log_statement = all 不会让 log_min_duration_statement 再记录这些语句。 两套机制对同一条语句只落一次日志,且 all 挡落的日志不带耗时。需要耗时数据的场景(性能监控、pgBadger 分析)不要依赖 all 挡。

口令字面量会被原样记录。 CREATE ROLE ... PASSWORD / ALTER ROLE ... PASSWORD 的明文口令会进入日志。审计开启期间必须把改密流程迁移到 \password 或参数化接口,并把日志文件当成「包含历史凭证的敏感文件」来管理。

语法错误的语句不会被记录。 即使 all 挡也是如此。不要把语句日志当成 WAF 或注入检测的替代品,那不是它的设计目标。

mod 挡不记录 SELECT。 如果合规要求「数据导出行为」留痕,SELECT 大结果集、COPY TO、\copy 这类操作在 mod 挡下是盲区,需要 all 或 pgaudit 的 READ 类别。

参数值默认完整落盘。 PG13 之前的版本没有 log_parameter_max_length,大参数、敏感参数一律裸奔;即使 PG13+,这两个参数默认也是「不限制」与「不记录」,需要显式设置为合理的截断长度。

log_statement 是超级用户级参数。 普通用户无法把自己的会话调成 all「给自己记日志」,也无法改掉强加给自己的挡位。这既是安全特性(防抵赖)也是运维约束(调整都要走超级用户)。

按角色 / 按数据库的设置只对新建会话生效。 ALTER ROLE ... SET 之后,长连接(包括连接池里复用的后端连接)仍沿用旧值。变更后要评估连接池的连接重建,或在维护窗口统一重启应用连接池。

日志滚动策略必须先于挡位调整。 先配置好 log_rotation_age / log_rotation_size / log_truncate_on_rotation 与外部 logrotate,再提高日志挡位。顺序颠倒的典型事故是:挡位一开,单文件膨胀数 GB,归档与传输链路被打爆。

别忘了错误语句的记录阈值。 log_min_error_statement 被调低到 warning 甚至 notice 后,日志量会陡增;反过来在 PG13+ 把 log_parameter_max_length_on_error 设为 0 可以让错误路径只记语句骨架、不记参数值,是敏感库的推荐值。

ALTER SYSTEM 与配置文件并存时的优先级要写进运维手册。 ALTER SYSTEM SET 写入 postgresql.auto.conf,加载优先级高于手工编辑的 postgresql.conf;「改了文件没生效」的一半原因是 auto.conf 里留着旧值。

日志挡位变更要走变更单并附回滚语句。 挡位、前缀、滚动策略的每次调整都影响采集与磁盘;变更单里写清「改什么、为什么、怎么回滚」,事后审计配置漂移时也有据可查。

log_line_prefix 模板只作用于 stderr 输出。 csvlog 与 jsonlog 有自己的固定字段清单(时间、用户、库、PID、来源、消息、语句……),不使用 prefix 模板。切换到结构化格式后,采集端解析与告警规则要按新字段重新校准。

log_min_duration_statement = 0 是「全量带耗时」,与 all 挡语义不同。 前者按「慢查询阈值」机制记录全部语句并携带耗时,适合临时观测窗口;后者按语句类别记录、不带耗时。两者对同一语句也只落一次日志——把 0 当成 all 的等价物来配,容易在回归时发现「怎么没有语句挡位的语义字段」。

挡位调整的告警要覆盖「日志量骤变」。 挡位被误改(比如误发的 ALTER SYSTEM)最直接的表现是日志量分钟级翻倍或归零;在日志平台对「实例级日志速率」建一条 ±3 倍告警,能把配置事故的发现时间从天级压到分钟级。

拿语句日志当审计证据,就要按证据的标准管理它。 引用语句日志满足审计要求时,确认保留期、防篡改与访问控制都达标——运维日志升级成「证据」,管理等级要同步升级,不能停在「排障用文件」的待遇上。

最佳实践 ​

  1. 把语句日志当审计工具、慢查询日志当性能工具,分开规划。 审计需要的是「确定类别必留痕」(DDL、管理操作),性能需要的是「超阈值带耗时」。两者触发条件不同,混用必然两头不讨好。
  2. 生产环境默认从 ddl 挡起步。 DDL 影响面大、发生频率低,是性价比最高的一挡。mod 与 all 必须先做日志量实测再上。
  3. 永远不要在业务高峰前临时把挡位调到 all。 日志写入是同步路径上的额外 I/O,全量记录可能显著拖慢短查询。需要临时观测时,用「降低 log_min_duration_statement 阈值」代替「提高 log_statement 挡位」。
  4. 用角色分档实现「人」维度的审计分级。 管理角色 all、业务角色 mod/ddl、批处理角色 none,让日志量与审计价值正相关,而不是与流量正相关。
  5. PG13+ 实例显式设置 log_parameter_max_length 与 log_parameter_max_length_on_error。 前者建议 64~1024 字节,后者建议保持 0 或设为极小值,从机制上限制参数值在语句日志与错误日志中的暴露面。
  6. log_line_prefix 必须包含时间、PID、用户、库四要素(%m [%p] %q%u@%d ),接入集中日志的再加 %h、%c、%i。没有归因前缀的语句日志在审计上等于没记。
  7. 改密操作一律走 \password 或参数化接口。 同时用流程约束(变更审批、事后检查日志中的 PASSWORD 关键字)兜底,防止口令字面量落盘。
  8. 给日志目录独立分区并设置滚动配额。 logging_collector 的按天滚动 + 外部压缩归档 + 保留期对齐合规要求;磁盘水位告警要覆盖日志分区。
  9. 对高频流量做采集端采样或骨架折叠。 数据库侧保持完整记录,分析侧用采集器(Fluent Bit / Vector / rsyslog)或 pgBadger 做聚合,让「日志完整」与「人能看得过来」同时成立。
  10. 当需要对象级、结构化审计时切换到 pgaudit,并把 log_statement 归位 none。 两套机制同时开启只会带来重复记录与更高的日志量,见下一节的扩展审计方案。
  11. 把语句日志纳入灾备演练与访问控制。 日志里常有业务数据影子,其读取权限、传输加密、保留销毁流程都应与「含敏感数据的副本」同等对待。
  12. 定期做「审计覆盖度自检」。 用一条已知 DDL、一条已知 mod 语句在预发环境走一遍,确认日志中出现、前缀归因正确、采集链路可达——挡位配置「看起来对」和「真的记下来」是两回事。

下一节

继续学习:log_connections / log_disconnections

参考链接 ​