CSV 日志分析
csvlog 是 PostgreSQL 内置的结构化日志格式:把 log_destination 设为 csvlog,收集进程就会把每条日志写成一个固定 26 列的 CSV 行。比起肉眼难读的 stderr 前缀拼串,CSV 把「时间、用户、数据库、错误级别、SQLSTATE、消息、语句原文」拆成了可以直接用 SQL 查询的字段——用数据库的能力分析数据库的日志,这正是本页的主题。
CSV 日志的价值在「分析」二字:错误趋势按天聚合、哪个用户在报哪类错、死锁事件的完整现场、连接建立/断开的审计流水,这些需求都能落到一条 SQL 上。配套的读写工具有两件:file_fdw 外部表(把日志文件挂成一张表直接查)与 COPY(把日志灌进分析表慢慢算)。
本页先讲清 26 列的完整结构(这是所有分析的基础),再给出外部表搭建与一组拿来即用的分析 SQL。更现代的 jsonlog(PG15+)字段与本格式完全一致,只是载体换成 JSON,细节在下一页对比展开。
前置知识
建议先阅读:日志轮转与清理
基础概念
启用 CSV 日志只需两行配置(收集进程必须已开启):
logging_collector = on # 修改需重启
log_destination = 'csvlog' # 可与其他去向并列,如 'stderr,csvlog'
log_filename = 'postgresql-%Y-%m-%d.log'文件形态要点:
- 文件名自动加
.csv后缀:当log_destination含csvlog而log_filename不以.csv结尾时,收集进程会自动追加.csv。上例实际产出postgresql-2026-09-30.csv。 - 每条消息恰好一行:消息内含换行(如带
CONTEXT的错误)时按 CSV 规则用引号包裹、双引号转义,物理上仍是一行。这保证了COPY与外部表读取的行对齐。 - 多去向并存:
'stderr,csvlog'会同时产出.log与.csv两套文件,过渡期常用。
为什么是 CSV 而不是「更好看的格式」
| 格式 | 人读 | 机器处理 | 字段固定 | 依赖 |
|---|---|---|---|---|
| stderr 文本 | 中(依赖 log_line_prefix 配置) | 差(正则解析) | 无 | 无 |
| csvlog | 差(列多行宽) | 优(SQL/任意 CSV 工具) | 26 列固定 | 收集进程 |
| jsonlog | 中 | 优(JSON 生态原生) | 同 26 字段 | 收集进程,PG15+ |
csvlog 是「零外部依赖的机器可读格式」:不需要 PG15、不需要日志平台,一台数据库加一个 file_fdw 就能开查。
语法与用法
方式一:file_fdw 外部表(推荐)
把日志目录下的 CSV 文件挂成外部表,查询即时生效:
-- 1) 创建扩展与服务器(首次)
CREATE EXTENSION IF NOT EXISTS file_fdw;
CREATE SERVER pglog_server FOREIGN DATA WRAPPER file_fdw;
-- 2) 建外部表,location 指向日志文件
CREATE FOREIGN TABLE pglog (
log_time timestamp(3) with time zone,
user_name text,
database_name text,
process_id integer,
connection_from text,
session_id text,
session_line_num bigint,
command_tag text,
session_start_time timestamp with time zone,
virtual_transaction_id text,
transaction_id bigint,
error_severity text,
sql_state_code text,
message text,
detail text,
hint text,
internal_query text,
internal_query_pos integer,
context text,
query text,
query_pos integer,
location text,
application_name text,
backend_type text,
leader_pid integer,
query_id bigint
) SERVER pglog_server
OPTIONS ( filename '/var/lib/postgresql/18/main/log/postgresql-2026-09-30.csv', format 'csv' );
-- 3) 直接查询
SELECT error_severity, count(*)
FROM pglog
GROUP BY 1;外部表的权限体系照常生效:给监控账号 GRANT SELECT ON pglog 即可,无需操作系统权限。
方式二:COPY 灌入分析表
-- 建与 csvlog 同构的分析表(加业务时间戳)
CREATE TABLE csvlog_stage (LIKE pglog INCLUDING ALL) WITH (fillfactor = 90);
-- 批量导入
COPY csvlog_stage
FROM '/var/lib/postgresql/18/main/log/postgresql-2026-09-30.csv'
WITH (FORMAT csv);外部表适合「即席查询」;COPY 入表适合长期分析(加索引、分区、与监控系统联动)。
详细说明
26 列字段全览
以下是官方定义的 csvlog 完整列结构(按顺序):
| 序号 | 列名 | 类型 | 含义 |
|---|---|---|---|
| 1 | log_time | timestamp(3) with time zone | 消息记录时间(毫秒精度) |
| 2 | user_name | text | 发出消息的用户名 |
| 3 | database_name | text | 数据库名 |
| 4 | process_id | integer | 进程号(对应 pg_stat_activity.pid) |
| 5 | connection_from | text | 客户端 IP 与端口(本地连接为空) |
| 6 | session_id | text | 会话 ID(等同 %c 的输出) |
| 7 | session_line_num | bigint | 会话内的日志行号 |
| 8 | command_tag | text | 命令标签(如 SELECT、UPDATE) |
| 9 | session_start_time | timestamp with time zone | 会话(进程)启动时间 |
| 10 | virtual_transaction_id | text | 虚拟事务 ID,形如 3/4821 |
| 11 | transaction_id | bigint | 顶层事务 ID(无事务为空) |
| 12 | error_severity | text | 消息级别:LOG / ERROR / WARNING / FATAL / PANIC 等 |
| 13 | sql_state_code | text | SQLSTATE 错误码,如 40P01(死锁)、22012(除零) |
| 14 | message | text | 主消息文本 |
| 15 | detail | text | 详细信息(如死锁的等待环描述) |
| 16 | hint | text | 提示信息 |
| 17 | internal_query | text | 内部语句(如触发的约束校验 SQL) |
| 18 | internal_query_pos | integer | 内部语句中出错的位置 |
| 19 | context | text | 调用上下文(函数/触发器调用链) |
| 20 | query | text | 出错的语句原文(受 log_min_error_statement 控制) |
| 21 | query_pos | integer | 语句中出错的位置 |
| 22 | location | text | 内核源码位置(log_error_verbosity = verbose 时) |
| 23 | application_name | text | application_name |
| 24 | backend_type | text | 后端类型(client backend、autovacuum worker 等) |
| 25 | leader_pid | integer | 并行组 leader 的 PID(PG13+,无则为空) |
| 26 | query_id | bigint | 查询 ID(PG14+,需内核计算 query id) |
版本差异:PG12 及更早为 24 列;PG13 增加 leader_pid;PG14 增加 query_id。用 file_fdw 建外部表时,列数必须与日志版本匹配,否则解析错位。
关键列的解读要点
session_id+session_line_num:日志世界的「会话主键」。同一连接产生的所有行(连接、错误、断开)共享session_id,行号保证顺序,是会话级追踪的完整方案。error_severity:LOG级行承载检查点、慢语句、autovacuum 等运行信息;ERROR以上是故障分析的主料。sql_state_code:机器分诊的最佳依据。常见码值如23505(唯一约束冲突)、40P01(死锁)、57P01(admin shutdown)、42501(权限不足)——按码聚合比按消息文本聚合稳定得多。virtual_transaction_id/transaction_id:与pg_locks、死锁日志中的事务标识直接对应,方便把日志证据与锁视图互相印证。backend_type:过滤业务日志与后台进程日志的开关(autovacuum 的日志量不小)。query列的可见性:受log_min_error_statement控制,低于阈值不落语句原文;敏感场景的合规评估见上一页。
用 SQL 分析日志的三条主线
-- 主线一:错误总览(级别 × 时间)
SELECT date_trunc('hour', log_time) AS hr,
error_severity,
count(*)
FROM pglog
WHERE log_time >= current_date
GROUP BY 1, 2
ORDER BY 1, 3 DESC;
-- 主线二:SQLSTATE 分诊(哪类错误最多)
SELECT sql_state_code,
count(*) AS cnt,
max(message) AS sample_message
FROM pglog
WHERE error_severity IN ('ERROR', 'FATAL', 'PANIC')
GROUP BY 1
ORDER BY cnt DESC;
-- 主线三:会话追踪(一条连接的完整轨迹)
SELECT session_line_num,
log_time,
error_severity,
sql_state_code,
message,
left(query, 60) AS query_head
FROM pglog
WHERE session_id = '6858ca.1234'
ORDER BY session_line_num;死锁与 FATAL 事件的取证
CSV 日志把死锁的 DETAIL(等待环描述)完整保留成独立列,检索极其方便:
-- 死锁事件清单(SQLSTATE 40P01)
SELECT log_time,
database_name,
user_name,
process_id,
message,
detail
FROM pglog
WHERE sql_state_code = '40P01'
ORDER BY log_time;
-- FATAL 事件(认证失败、连接被杀等)
SELECT log_time,
user_name,
connection_from,
sql_state_code,
message
FROM pglog
WHERE error_severity = 'FATAL'
ORDER BY log_time DESC
LIMIT 50;28P01(密码错误)与 28000(认证失败)在 FATAL 行里高频出现时,就是暴力破解尝试的信号,应联动安全告警。
与锁监控、会话视图的交叉印证
CSV 日志的最大优势是「事后」价值:pg_stat_activity 与 pg_locks 是实时快照,事后无从查起,而日志留了全部现场。交叉印证的典型链路:
- 日志中锁定一条错误行,取其
process_id与session_id。 - 若该进程仍存活,直接
SELECT * FROM pg_stat_activity WHERE pid = <process_id>看现状。 - 死锁的
detail列列出各进程在等哪个事务、被谁阻塞——与pg_locks的 transactionid 视图同一套语言(见《pg_locks 锁监控》与《死锁检测机制》)。
pgBadger 等离线分析工具
社区工具 pgBadger 对 csvlog 支持良好,一条命令产出 HTML 报告(Top 查询、错误分布、时间线、连接画像):
pgbadger /var/lib/postgresql/18/main/log/postgresql-2026-09-30.csv -o report.html它解决了「日志里慢语句的聚合分析」:配合 log_min_duration_statement 开启的慢语句日志,pgBadger 能给出比手写 SQL 丰富得多的报表。轻量即席分析用本页的 SQL,定期全量报告交给 pgBadger,两者不冲突。
版本差异与迁移注意
- 列结构跨版本变化:升级大版本后,外部表与解析脚本要同步调整列清单(PG13 加
leader_pid、PG14 加query_id)。 transaction_id的类型:列类型在文档中固定为bigint,但老版本日志中的事务 ID 语义(32 位 xid)与新版本存在差异,做跨版本日志合并分析时注意口径。- 多去向并存时的体积翻倍:
'stderr,csvlog'会让日志体量接近翻倍,轮转与清理容量按双份估算(见《日志轮转与清理》)。
26 列在不同消息类型里的取值形态
同一套列,不同类型的消息填充的列不同。掌握「哪类消息看哪几列」能大幅加快分析:
| 消息类型 | 关键列 | 典型形态 |
|---|---|---|
连接建立(connection authorized) | session_id、user_name、connection_from、application_name | error_severity = LOG,message 含用户与库名 |
连接断开(disconnection) | session_id、session_start_time | message 含会话时长 |
| 检查点 / 运行信息 | backend_type(checkpointer 等) | LOG 级,command_tag 为空 |
慢语句(duration: X ms) | message、query | LOG 级,语句文本拼在 message 里 |
| SQL 错误 | sql_state_code、message、query | error_severity = ERROR |
| 死锁 | sql_state_code = 40P01、detail | detail 列含完整等待环 |
| 认证失败 | error_severity = FATAL、connection_from | SQLSTATE 28P01 / 28000 |
| autovacuum 行为 | backend_type = autovacuum worker | message 形如 automatic vacuum of table ... |
其中「慢语句行」值得单独说明:它由 log_min_duration_statement 触发,耗时与语句文本合并在 message 列里,格式固定为 duration: 123.456 ms statement: SELECT ...,多行语句同样按 CSV 规则转义为物理单行。
慢语句日志行的提取
慢语句的「耗时」要从 message 里用正则提取,这是 csvlog 分析里最常用的技巧:
-- 提取耗时并排序:当日最慢语句 Top 10
SELECT (regexp_match(message, 'duration: ([0-9.]+) ms'))[1]::numeric AS duration_ms,
log_time,
user_name,
left(regexp_replace(message, '^duration: [0-9.]+ ms statement: ', ''), 80) AS statement_head
FROM pglog
WHERE error_severity = 'LOG'
AND message LIKE 'duration: %statement: %'
ORDER BY duration_ms DESC
LIMIT 10;-- 慢语句的小时分布与平均耗时
SELECT date_trunc('hour', log_time) AS hr,
count(*) AS slow_count,
round(avg((regexp_match(message, 'duration: ([0-9.]+) ms'))[1]::numeric), 1) AS avg_ms
FROM pglog
WHERE message LIKE 'duration: %statement: %'
GROUP BY 1
ORDER BY 1;边界提醒:这里提取的是「逐条流水」,与 pg_stat_statements 的聚合视角互补——前者能还原单次慢执行的时间点与参数值,后者回答长期排名。两者的字段对应关系见下一节。
与 pg_stat_statements、pg_stat_activity 的字段对照
csvlog 的列与监控视图的字段有一套稳定的对应关系,交叉取证全靠它:
| csvlog 列 | 对应视图字段 | 说明 |
|---|---|---|
process_id | pg_stat_activity.pid | 同一进程号体系 |
session_id | (仅日志侧)log_line_prefix 的 %c | 实时视图不暴露会话 ID,只能靠日志串轨迹 |
virtual_transaction_id | pg_locks.virtualtransaction | 形如 4/1921 |
transaction_id | pg_stat_activity.backend_xid | 顶层事务号 |
query_id | pg_stat_activity.query_id ↔ pg_stat_statements.queryid(PG14+) | 三方联动的关键键 |
application_name / user_name / database_name | 同名字段 | 画像维度 |
error_severity / sql_state_code | (无对应) | 日志独有,错误分诊主键 |
拿到一条慢语句日志后,若其 query_id 非空,直接去 pg_stat_statements 查该模板的历史画像(平均耗时、调用次数、块读),一次就能判断「这次慢是偶发还是常态」。
历史文件管理与分区分析表
「COPY 入表」做大之后,管理动作要跟上分区与索引:
-- 月度分区分析表(按天导入,只保留高频分析列)
CREATE TABLE csvlog_history (
log_time timestamp(3) with time zone NOT NULL,
user_name text, database_name text, process_id integer,
error_severity text, sql_state_code text, message text,
query text, application_name text, backend_type text,
session_id text
) PARTITION BY RANGE (log_time);
CREATE TABLE csvlog_history_2026_09
PARTITION OF csvlog_history
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
-- 常用查询路径的索引
CREATE INDEX ON csvlog_history (log_time);
CREATE INDEX ON csvlog_history (sql_state_code);# 每日导入昨日文件(FROM PROGRAM 免去文件名调度难题;后端身份运行可读 0600 文件)
psql -c "COPY csvlog_history (log_time, user_name, database_name, process_id,
error_severity, sql_state_code, message, query, application_name, backend_type, session_id)
FROM PROGRAM 'cat /var/lib/postgresql/18/main/log/postgresql-$(date -d yesterday +\\%F).csv'
WITH (FORMAT csv);"说明:只导入高频分析的列可以显著缩小表体积,完整 26 列的原始证据交给冷归档;分区按月(量大按天),过期分区 DROP PARTITION 秒级清理。
解析性能与工具选择
csvlog 解析的技术选型按数据量分级:
| 场景 | 工具 | 理由 |
|---|---|---|
| 单文件即席查询 | file_fdw 外部表 | 零搬运,SQL 即查 |
| 长期分析 | COPY 入分区表 | 可索引、可聚合、可关联业务数据 |
| 脚本轻量统计 | Python csv 模块 | 正确处理引号内换行与逗号 |
| 定期全量报告 | pgBadger | 开箱即用的报表 |
| 文本流式监控 | tail -f + 简单 grep | 仅限「按行匹配」的场景 |
性能要点:数 GB 的单日 CSV 用外部表全扫一遍要数十秒,高频看板必须走「入表 + 索引」;正则提取慢语句耗时是 CPU 密集操作,先按 message LIKE 'duration: %' 缩小结果集再提取;pgBadger 支持 --jobs 并行解析大文件。
实战示例
示例一:搭建日志外部表并跑通第一个分析
-- 前置:file_fdw 已安装(跨库可用,见 file_fdw 专题页)
CREATE EXTENSION IF NOT EXISTS file_fdw;
-- 服务器对象只需建一次
CREATE SERVER IF NOT EXISTS pglog_server FOREIGN DATA WRAPPER file_fdw;
-- 外部表(26 列,与上表一致)
CREATE FOREIGN TABLE pglog (
log_time timestamp(3) with time zone, user_name text, database_name text,
process_id integer, connection_from text, session_id text,
session_line_num bigint, command_tag text, session_start_time timestamptz,
virtual_transaction_id text, transaction_id bigint, error_severity text,
sql_state_code text, message text, detail text, hint text,
internal_query text, internal_query_pos integer, context text,
query text, query_pos integer, location text, application_name text,
backend_type text, leader_pid integer, query_id bigint
) SERVER pglog_server
OPTIONS ( filename '/var/lib/postgresql/18/main/log/postgresql-2026-09-30.csv',
format 'csv' );
SELECT count(*), min(log_time), max(log_time) FROM pglog; count | min | max
--------+----------------------------+----------------------------
104283 | 2026-09-30 00:00:01.012+08 | 2026-09-30 10:42:33.981+08
(1 row)示例二:每日错误看板
-- 错误分布:按小时 × SQLSTATE
SELECT date_trunc('hour', log_time) AS hr,
sql_state_code,
count(*) AS cnt,
max(left(message, 50)) AS sample_msg
FROM pglog
WHERE error_severity = 'ERROR'
GROUP BY 1, 2
HAVING count(*) > 5
ORDER BY hr, cnt DESC; hr | sql_state_code | cnt | sample_msg
---------------------+----------------+-----+----------------------------------
2026-09-30 02:00:00 | 23505 | 412 | duplicate key value violates ...
2026-09-30 04:00:00 | 40P01 | 3 | deadlock detected
2026-09-30 09:00:00 | 42501 | 18 | permission denied for table ...
(3 rows)读法:凌晨的 23505 高峰指向批量任务的重复键问题(应用层缺陷);40P01 出现即触发死锁排查;42501 集中在上午提示权限变更或代码发布引入的越权访问。
示例三:连接审计流水
配合 log_connections = on 与 log_disconnections = on,CSV 日志即审计流水:
-- 连接建立/断开流水(LOG 级消息,按 message 前缀过滤最稳)
SELECT log_time,
session_id,
user_name,
connection_from,
message
FROM pglog
WHERE message LIKE 'connection authorized%'
OR message LIKE 'disconnection%'
ORDER BY log_time DESC
LIMIT 20;-- 按来源统计当日连接数(连接风暴取证)
SELECT connection_from,
user_name,
count(*) FILTER (WHERE message LIKE 'connection authorized%') AS connects,
count(*) FILTER (WHERE message LIKE 'disconnection%') AS disconnects
FROM pglog
WHERE log_time >= current_date
AND (message LIKE 'connection authorized%' OR message LIKE 'disconnection%')
GROUP BY 1, 2
ORDER BY connects DESC;connects 与 disconnects 的差值即「当日净增连接」,长期为正的来源就是连接泄漏嫌疑对象;审计留存策略见《log_connections / log_disconnections》一页。
示例四:慢语句 Top 榜(当日)
-- 当日最慢的 10 条语句(含耗时与语句头)
SELECT round(d.duration_ms, 1) AS ms,
d.log_time,
d.user_name,
left(d.statement_head, 70) AS statement_head
FROM (
SELECT (regexp_match(message, 'duration: ([0-9.]+) ms'))[1]::numeric AS duration_ms,
log_time, user_name,
regexp_replace(message, '^duration: [0-9.]+ ms statement: ', '') AS statement_head
FROM pglog
WHERE log_time >= current_date
AND message LIKE 'duration: %statement: %'
) d
ORDER BY d.duration_ms DESC
LIMIT 10; ms | log_time | user_name | statement_head
-----------+-----------------------------+-----------+---------------------------------------------------
18422.334 | 2026-09-30 02:14:07.882+08 | etl_user | SELECT ... FROM events JOIN ...
4210.101 | 2026-09-30 09:41:22.101+08 | app | SELECT * FROM orders WHERE ...
(2 rows)-- 对照历史画像:这条语句平时也这么慢吗(PG14+,query_id 关联)
SELECT calls, round(mean_exec_time::numeric, 1) AS mean_ms, round(max_exec_time::numeric, 1) AS max_ms
FROM pg_stat_statements
WHERE query LIKE 'SELECT ... FROM events JOIN%';两条解读路径:凌晨的 ETL 慢语句若与历史均值接近,属于「已知的重查询」,走优化排期;上午那条远超历史 max_exec_time 的,才是当日需要现场介入的异常。
示例五:bash 巡检(Python 解析版)
引号内换行让裸文本工具不可靠,轻量巡检用 Python 的 csv 模块最稳:
#!/usr/bin/env bash
# csvlog_hourly.sh —— 最近一小时的错误分诊摘要
LOG=/var/lib/postgresql/18/main/log/postgresql-$(date +%F).csv
python3 - "$LOG" <<'EOF'
import csv, sys, collections
import datetime as dt
one_hour_ago = dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=1)
cnt = collections.Counter()
sample = {}
with open(sys.argv[1], newline='') as f:
for row in csv.reader(f):
if len(row) < 14:
continue
try:
t = dt.datetime.fromisoformat(row[0])
except ValueError:
continue
if t < one_hour_ago:
continue
sev, code, msg = row[11], row[12], row[13]
if sev in ('ERROR', 'FATAL', 'PANIC'):
cnt[(sev, code)] += 1
sample.setdefault((sev, code), msg[:80])
for (sev, code), n in cnt.most_common(10):
print(f"{sev:6} {code or '-':6} {n:6} {sample[(sev, code)]}")
EOFERROR 23505 412 duplicate key value violates unique constraint ...
FATAL 28P01 18 password authentication failed for user ...
ERROR 40P01 3 deadlock detected脚本要点:csv.reader 天然处理引号转义;列下标按 26 列顺序硬编码(error_severity 是第 12 列、sql_state_code 是第 13 列),跨版本使用时同步核对列清单。
示例六:pgBadger 批量与增量报告
csvlog 是 pgBadger 支持最好的格式,--jobs 并行 + 增量模式适合大日志量:
# 一次性全量报告:8 并行解析当日 CSV
pgbadger -j 8 --prefix '' -o report-$(date +%F).html \
/var/lib/postgresql/18/main/log/postgresql-2026-09-30.csv
# 增量模式:按天滚动生成,聚合出周/月视图(-I 开启增量,-O 指定输出目录)
0 6 * * * pgbadger -I -O /var/www/pgbadger \
/var/lib/postgresql/18/main/log/postgresql-$(date -d yesterday +\%F).csv说明:pgBadger 能自动识别标准 csvlog 的列结构,--prefix '' 仅用于明确「无自定义前缀」(csvlog 默认即可识别,显式写出是防御配置漂移);增量模式下 -O 目录里会出现按周期的汇总文件,pgbadger --rebuild 可在数据缺失时重建聚合。报告覆盖 Top 查询、锁等待、检查点与会话分布,与本文的 SQL 分析互补——SQL 侧做精确取证,pgBadger 侧做全景速览。
示例七:错误趋势周报(一条 SQL)
把当周每天的 SQLSTATE 分布拉成趋势,接邮件或看板都行:
SELECT log_time::date AS day,
sql_state_code,
count(*) AS cnt,
min(left(message, 50)) AS sample_msg
FROM pglog
WHERE log_time >= current_date - interval '7 days'
AND error_severity IN ('ERROR', 'FATAL')
GROUP BY 1, 2
ORDER BY 1 DESC, cnt DESC; day | sql_state_code | cnt | sample_msg
------------+----------------+------+---------------------------------------------
2026-09-30 | 23505 | 412 | duplicate key value violates unique ...
2026-09-30 | 28P01 | 18 | password authentication failed for ...
2026-09-29 | 23505 | 398 | duplicate key value violates unique ...
(3 rows)进阶:外层套窗口函数算「环比」,cnt - lag(cnt) OVER (PARTITION BY sql_state_code ORDER BY day),把环比突增(比如 +200%)的 SQLSTATE 单独标红——同一错误类别的异常增长比绝对计数更能暴露新故障。
注意事项
列数必须与版本匹配。 外部表少了列(老版本日志新列)会导致整行解析失败;升级大版本后第一时间核对 csvlog 列清单。
单行可能非常宽。 带 CONTEXT、QUERY 的大错误一行可达数 MB,外部表扫描与 COPY 都要吃内存与 I/O;log_error_verbosity 的取舍要考虑这一层。
query 列不是默认总有值。 只有达到 log_min_error_statement 阈值的错误才带语句原文;以为「日志里必有 SQL」的分析脚本要处理空值。
CSV 文件不可在写入中重命名。 外部表按文件路径读取,正在写入的当天文件可以查,但被 logrotate 改名的文件会瞬间「消失」——分析历史文件时用改名后的稳定路径,或先 COPY 入表。
权限是两层。 外部表只解决数据库内的权限;文件系统层面日志文件属 postgres 用户、0600 权限,file_fdw 读取以后端进程身份进行,与运行用户一致,无需额外系统权限,但不要把日志目录权限放宽。
大文件查询有成本。 数 GB 的单日 CSV 全扫一遍不便宜;高频分析应该走「COPY 入表 + 索引/分区」,外部表留给即席查询。
多行消息在物理上仍是一行。 不要用 grep 按行切分处理 csvlog——引号内的换行会骗过 naive 文本工具。解析交给 SQL、Python csv 模块或成熟工具。
最佳实践
log_destination = 'csvlog'作为生产默认。 它以零外部依赖提供了机器可读日志,分析门槛从「正则表达式」降到「SELECT」。- 用
file_fdw外部表做即席分析,用 COPY 入表做长期分析。 前者零搬运,后者可建索引、可关联业务数据;两条路径共享同一份 26 列结构定义。 - 分析按
sql_state_code聚合而不是按消息文本。 错误码稳定、文本会随版本与参数变化;基于文本的告警在升级后最容易失效。 - 死锁、FATAL、checksum 类事件设为即时告警。 这些是低频高危事件,等日报就来不及了;
40P01、28P01暴增、57P03都有明确含义。 - 把会话 ID 纳入排障话术。 应用报障时先拿时间与用户过滤出
session_id,再拉整条会话轨迹,比「看日志找感觉」快一个量级。 - 与 pg_stat_statements、pg_stat_activity 形成三视角。 日志看「发生了什么」,实时视图看「正在发生什么」,扩展统计看「长期谁最重」——三者的语言(pid、queryid、session_id)在 csvlog 里都有对应列。
- 定期报告交给 pgBadger,临时取证用 SQL。 不要为已有工具能做的事情手写脚本,也不要指望 pgBadger 替你做业务定制分析。
- 外部表定义进版本库,升级时同步改列。 csvlog 列清单是版本敏感物,与数据库版本一起管理。
- 日志分析账号用只读权限 + 独立外部表。 控制能查哪些日志文件(外部表按文件粒度授权),避免所有人在所有文件上无差别查询。
- 大版本升级后重验解析链路。 列增删、文件命名、轮转行为都可能有变,升级清单里加一项「跑通示例一的外部表查询」。
下一节
继续学习:JSON 日志(jsonlog)