常见陷阱与最佳实践
全手册陷阱汇总 + 高危坑深讲 + 上线自检清单。基线 PostgreSQL 18(2026-10 实测最新补丁版 18.6,兼顾 14–17);每条陷阱后标注展开章节,这里只给"现象 → 后果 → 一招制敌"。
一、陷阱汇总表
安装与配置
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
listen_addresses 忘改(默认 localhost) |
远程连接死活不通,还以为是防火墙 | 改成 * 或指定地址后 reload,再配 pg_hba |
00-安装与配置.md |
Docker 数据目录挂到宿主机容器层(忘了 -v) |
容器一删库没;放 overlayfs 上写性能也差 | 命名卷或独立盘挂载 /var/lib/postgresql/data |
00-安装与配置.md |
改 postgresql.conf 不 reload/restart |
参数看似改了实际没生效 | SELECT pg_reload_conf();;分不清哪些要重启就查 pg_settings.context |
00-安装与配置.md |
shared_buffers 拉到内存 80% |
OS page cache 被挤饿,整体更慢还加剧 OOM 风险 | 25% 左右起步,够不着别硬凑 | 00-安装与配置.md |
max_connections 盲目调大 |
每连接一个进程,几千连接把机器压成筛子 | 应用侧收口 + PgBouncer,而不是抬上限 | 01-架构与核心概念.md |
| pg_hba 把宽松规则放前面 | 首条命中制,后面的严格规则全成摆设 | 严格在前宽松在后,改完用 pg_hba_file_rules 视图核对 |
00-安装与配置.md |
用 trust 图省事 |
本机任意进程免密登录数据库 | 开发容器也别用,一律 scram-sha-256 | 12-安全与权限管理.md |
类型与 DDL
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
timestamp 不带时区 |
跨国/夏令时/应用与库时区不一致,时间线集体漂移 | 军规:一律 timestamptz |
02-数据类型与DDL.md |
| 随机 UUID v4 做大表主键 | B-tree 写入打散全树、缓存命中率崩、索引膨胀 | 时序场景改 uuidv7()(PG 18)或 BIGSERIAL/IDENTITY |
02-数据类型与DDL.md |
用 serial 忘了它是旧惯用法 |
sequence 与列的归属/权限行为差异在迁移时咬人 | 新表一律 GENERATED ... AS IDENTITY |
02-数据类型与DDL.md |
float 存钱 |
0.1+0.2 的经典误差进账单 | numeric(12,2) |
02-数据类型与DDL.md |
char(n) 存变长文本 |
尾部补空格,比较和展示全是惊喜 | text(需要约束再 varchar(n)) |
02-数据类型与DDL.md |
亿行大表直接 ADD COLUMN ... NOT NULL / ALTER COLUMN type |
全表重写 + ACCESS EXCLUSIVE 长锁,站点瘫痪 | 先 nullable 再回填;NOT NULL 用 CHECK 证明配方(PG 12 起);大改走逻辑复制迁移 | 02-数据类型与DDL.md |
DROP TABLE ... CASCADE 顺手一敲 |
依赖它的视图、外键全被连坐删除 | 先不带 CASCADE 看报错清单再决定 | 02-数据类型与DDL.md |
| enum 加值 + prepared statement 缓存 | 老会话用新值报错,重启才好的玄学 | 低频变更可接受;高频状态列干脆用 text + CHECK | 02-数据类型与DDL.md |
| 对生成列直接 UPDATE | 报错 0…generated column | 改它依赖的源列 | 02-数据类型与DDL.md |
查询写法
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| LEFT JOIN 右表过滤条件写进 WHERE | 静默退化成 INNER JOIN,“丢了的数据去哪了"查三小时 | 右表条件放 ON,判丢失放 WHERE | 03-SQL查询实战.md |
NOT IN (子查询) 且子查询可能出 NULL |
整条查询返回空集(三值逻辑) | 一律改 NOT EXISTS |
03-SQL查询实战.md |
BETWEEN 配时间戳上界 |
边界秒/毫秒漏行或多行 | >= AND < 左闭右开 |
03-SQL查询实战.md |
对列套函数:WHERE date_trunc(...)/::date |
索引报废全表扫 | 改写为列裸露的区间谓词 | 04-索引实战.md |
| OFFSET 深分页 | offset=1000000 仍要扫过再丢弃,越翻越慢 | keyset(行比较 + ORDER BY 带唯一 tie-breaker) | 03-SQL查询实战.md |
DISTINCT ON 的 ORDER BY 与目标不一致 |
每组取到的"第一行"不符合预期 | DISTINCT ON 列必须是 ORDER BY 前缀 | 03-SQL查询实战.md |
字符串 || 遇 NULL |
整串变 NULL | CONCAT() 自动忽略 NULL |
03-SQL查询实战.md |
| 数据修改型 CTE 里"改完再读同一张表” | 语句级快照看不到 CTE 的改动;副作用顺序不保证 | 一律从 RETURNING 取数 |
03-SQL查询实战.md |
| ON CONFLICT 不带冲突目标 / 猜约束名 | 语法报错;部分索引上的唯一约束要带 WHERE | 明确 (列) 或 ON CONSTRAINT 名 |
03-SQL查询实战.md |
索引
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 低基数列(bool/枚举)单列索引 | 优化器压根不走,白付维护费 | 组合进复合索引或用部分索引 | 04-索引实战.md |
软删除表所有索引不带 WHERE deleted_at IS NULL |
“已删除"行永远占索引,越跑越肥 | 部分索引只索引活行 | 04-索引实战.md |
CREATE INDEX CONCURRENTLY 失败就完事 |
留下 INVALID 索引占空间还拖累写 | 查 pg_index.indinvalid,DROP 后重建 |
04-索引实战.md |
SSD 不降 random_page_cost(默认 4.0) |
优化器嫌弃索引扫、偏爱 seq scan | SSD 集群调到 1.1–1.2 | 09-查询优化与EXPLAIN.md |
| LIKE ‘%xx%’ 想靠 B-tree | 前导通配无解 | pg_trgm + GIN;自然语言上全文检索 | 04-索引实战.md |
| BRIN 建在会被随机更新的列 | 块摘要持续加宽,索引失效 | BRIN 只配"只追加不重排"的时序大表 | 04-索引实战.md |
| FK 引用列不建索引 | 级联删改/反查全表扫 | 引用列显式建 B-tree | 04-索引实战.md |
| 迷信 index-only scan 的 INCLUDE | vacuum 跟不上时 VM 位没置,照样回表 | 先治 autovacuum 再谈覆盖收益 | 06-MVCC与Vacuum.md |
事务与锁
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
DDL 不带 lock_timeout |
ALTER 排队拿 ACCESS EXCLUSIVE,把后面所有 SELECT 堵成全站事故 | SET lock_timeout='5s' + 重试窗口执行 |
05-事务与锁.md |
| 以为 REPEATABLE READ 能防写偏斜 | PG 的 RR 只是快照隔离,双窗口/预约类业务照样穿 | 关键冲突点 SELECT ... FOR UPDATE,或上 SERIALIZABLE |
05-事务与锁.md |
| 上 SERIALIZABLE 却不处理 40001 | 偶发"序列化失败"报错被当数据库坏了 | 捕获 40001/40P01 整事务重试 | 05-事务与锁.md |
| 任务队列用状态 CAS 逐行抢占 | 热点页高并发空转;FOR UPDATE 不带 SKIP LOCKED 则 worker 排队 | 领取一律 FOR UPDATE SKIP LOCKED |
05-事务与锁.md |
| 多行更新不加统一排序 | 两个事务以不同顺序拿行锁 → 死锁 | ORDER BY id 固定加锁顺序 |
05-事务与锁.md |
idle in transaction 挂满连接池 |
连接占着、事务 pin 着 xmin,vacuum 和池双输 | idle_in_transaction_session_timeout + 代码审查 |
05-事务与锁.md |
synchronous_commit=off 当成"性能开关” |
崩溃丢最近最多若干提交 | 只在可容忍小窗口的场景开 | 05-事务与锁.md |
| 大事务一次 UPDATE 亿行 | 锁持有久、膨胀风暴、失败全回滚 | 分批 + 每批独立事务 | 06-MVCC与Vacuum.md |
MVCC 与 Vacuum
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 关 autovacuum “怕它抢 IO” | 死元组堆积、索引肥大、最后 xid 回绕被强制停机清理 | 永远开着,按表调参 | 06-MVCC与Vacuum.md |
| 亿行表默认 autovacuum_vacuum_scale_factor=0.2 | 改 20% 才动手 = 两千万死元组才清理 | 大表按表调小(0.01–0.05)+ 调小 threshold | 06-MVCC与Vacuum.md |
把 VACUUM FULL 当日常 |
重写整表 + ACCESS EXCLUSIVE 锁,大表等于停站 | 日常 VACUUM 即可;真要瘦身上 REINDEX CONCURRENTLY/分区重建 | 06-MVCC与Vacuum.md |
| 不看事务 ID age | 逼近回绕线时数据库拒写才慌 | 监控 age(datfrozenxid),告警线远早于官方红线 |
06-MVCC与Vacuum.md |
| 复制槽遗留不管 | 主库 WAL 被无限保留,盘满宕机 | max_slot_wal_keep_size 兜底 + 定期清 inactive 槽 |
10-复制与高可用.md |
高级 SQL 与分区
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 窗口帧想当然按 ROWS | 默认是 RANGE,并列行全算进累计——“对不上的合计” | 显式写帧,懂 RANGE/ROWS/GROUPS 差异 | 07-高级SQL.md |
| 递归 CTE 不防环 | 脏数据成环时无限递归直到报错/挂死 | path 数组或 PG 14+ 的 CYCLE 子句 |
07-高级SQL.md |
| 分区表想要"全局唯一约束" | PG 唯一约束必须包含分区键,做不到 | 业务层去重或单表方案,接受现状 | 07-高级SQL.md |
| 大 jsonb 对象高频局部更新 | 每次整值重写,写放大 + TOAST 膨胀双杀 | 核心字段拆列;jsonb 放稀疏只增不改的属性 | 07-高级SQL.md |
| 中文全文检索指望开箱即用 | 内置 regconfig 不分中文词 | zhparser/pg_jieba 等扩展,或上层 ES(见 elasticsearch 手册) | 07-高级SQL.md |
Go 客户端(pgx)
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| pgx 的 prepared 语句过 PgBouncer transaction 模式 | could not open ... prepared statement 类报错 |
PgBouncer ≥1.21 开 max_prepared_statements,或降 session 模式 |
09-查询优化与EXPLAIN.md |
| 循环里逐行 Exec 插十万行 | 网络往返打满,导入慢一个数量级 | CopyFrom / SendBatch |
08-Go客户端pgx.md |
Query 不传带超时的 context |
慢查询吊死请求 goroutine | 每个调用都传 ctx |
08-Go客户端pgx.md |
| pgxpool 默认 MaxConns=4 当终值 | 高并发下池排队成为隐形限流 | 按负载显式调,同时与 PG 端 max_connections 对齐 | 08-Go客户端pgx.md |
| numeric 列扫进 float64 | 钱进了浮点 | pgtype.Numeric(或扫进 [16]byte/string 再转) |
08-Go客户端pgx.md |
| 抄 2023 年前的 pgx 教程 | API 名早变了(v5.11 源码无 ForRows/GetStatuses 等) | 以 pkg.go.dev 与源码为准 | 08-Go客户端pgx.md |
复制与高可用
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 拿复制当备份 | 误删/勒索/坏 DDL 秒级同步到所有副本 | 备份+归档独立于复制,异地一份 | 11-备份恢复与运维.md |
以为 async 复制 + synchronous_commit=on 就不丢数据 |
on 只保证到主库 WAL;主库盘坏仍可能丢 | RPO=0 需要同步档 + quorum 设计 | 10-复制与高可用.md |
不用槽也不用 wal_keep_size |
备库掉线片刻,WAL 已被回收、复制断裂 | 至少 wal_keep_size 垫底,讲究就用槽 |
10-复制与高可用.md |
| 逻辑复制当全量同步用 | 不搬 DDL、序列、大对象、截断 | 只当 DML 管道;序列切换时手工 setval | 10-复制与高可用.md |
| 读写分离不看一致性 | 刚写完读备库读到旧值,用户以为"没保存" | 写后读走主库/等待 replay LSN | 10-复制与高可用.md |
备份恢复与运维
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 从没演练过恢复 | 备份文件躺在对象存储里"薛定谔可用" | 定期全量恢复演练 + pg_restore --list 抽检 |
11-备份恢复与运维.md |
| 只 pg_dump 不备 globals | 新实例恢复后角色/授权全缺 | pg_dumpall --globals-only 一并备 |
11-备份恢复与运维.md |
| 建库时关了数据校验和 | 静默页腐化多年后爆雷 | PG 18 默认已开;老集群补开要停机 | 11-备份恢复与运维.md |
从不看 log_min_duration_statement(默认关) |
慢查询事故无现场可查 | 生产设 250ms–1s,配 auto_explain | 11-备份恢复与运维.md |
| K8s 里拿默认 StorageClass 跑 PG | 网络盘抖动/丢失拖垮数据库 | 本地盘/高性能卷 + 备份进应用层,见 k8s 手册 | 11-备份恢复与运维.md |
安全与权限
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 应用连库用超级用户 | 注入/误操作的爆炸半径 = 整个实例 | 应用角色 DML-only,DDL 迁移角色分离 | 12-安全与权限管理.md |
| 新表忘了重新 GRANT | “为什么应用查不到刚建的表” | ALTER DEFAULT PRIVILEGES 预设 |
12-安全与权限管理.md |
RLS 忘了 FORCE ROW LEVEL SECURITY |
表 owner 绕过策略,隔离形同虚设 | 策略 + FORCE + 应用角色非 owner | 12-安全与权限管理.md |
| 动态标识符(表名/列名/排序方向)拼字符串 | 占位符替不了标识符,照样注入 | 应用层白名单 + format('%I'/'%L') |
12-安全与权限管理.md |
| md5 口令不迁移 | 官方已弃用(18 起设置即告警),离线可破 | password_encryption=scram-sha-256 + 重设口令 |
12-安全与权限管理.md |
DSN 不写 sslmode=verify-full |
prefer 模式下中间人降级明文 | 生产显式 require/verify-full | 12-安全与权限管理.md |
服务端编程(函数与存储过程)
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
把包着 now() 的函数标成 IMMUTABLE |
规划期被折叠成常量,表达式索引/生成列静默存旧值,改函数也不重算 | 波动性是对优化器的承诺:依赖时间标 STABLE,永远同值才 IMMUTABLE | 13-函数与存储过程.md |
| SECURITY DEFINER 不固定 search_path | 配合 CVE-2018-1058 式投毒,函数体里未限定的名字命中攻击者对象,属主权限变特洛伊木马 | SET search_path='' + 函数体全 schema 限定 + EXECUTE 收紧 |
13-函数与存储过程.md |
| 以为过程内 COMMIT 百无禁忌 | 事务控制只在顶级 CALL 可行:包在事务块里、DEFINER 过程、带 SET 子句的过程都直接报错 | 批量脚本让调用方不套事务;踩到限制就换回应用层分批 | 13-函数与存储过程.md |
| CREATE OR REPLACE 当万能替换 | 返回类型/OUT 列改不了;引用旧函数的视图、触发器要重建 | 把签名当 API 契约;要换就 drop 重建,进迁移脚本 | 13-函数与存储过程.md |
| WHEN OTHERS 一把吞 | 异常被静默吃掉,数据漏了且日志里查无此事 | 按具体条件名捕获;GET STACKED DIAGNOSTICS 记完再抛 |
13-函数与存储过程.md |
触发器
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
| 百万行导入挂着行级触发器 | 每行一次函数调用的税,导入慢出倍数 | 导入期 DISABLE TRIGGER,或改写 AFTER STATEMENT + 过渡表批量化 | 14-触发器.md |
| 以为 UPDATE OF 等于"值变了" | SET 清单提及该列就触发,SET x = x 照样触发 |
判"真变了"用 OLD.col IS DISTINCT FROM NEW.col |
14-触发器.md |
| 同一事件多触发器靠创建顺序兜底 | PG 的执行顺序是触发器名字母序,与创建顺序无关 | trg_01_/trg_02_ 前缀显式控制 |
14-触发器.md |
session_replication_role=replica 全程开着搬数据 |
外键检查连同触发器被一起跳过,事后"约束没违反/审计缺段"难查 | 只在完整性有外部保证时用,用完立刻还原 origin | 14-触发器.md |
| 以为事件触发器能授权给普通治理角色 | 只有超级用户可创建(18 文档现文) | 治理类事件触发器由迁移/DBA 角色安装,定义进版本管理 | 14-触发器.md |
扩展与生态
| 陷阱 | 后果 | 对策 | 详见 |
|---|---|---|---|
Timescaledb/pg_cron/pgAudit 没进 shared_preload_libraries |
CREATE EXTENSION 报错,或装上后后台任务静默不跑 | ALTER SYSTEM 设 preload 并重启;改值时警惕覆盖掉原有列表 | 15-扩展与生态.md |
大版本升级后不 ALTER EXTENSION UPDATE |
二进制是新版本、库内对象还停在旧版本,行为漂移埋进暗处 | 升级清单里逐扩展过一遍 UPDATE | 15-扩展与生态.md |
DROP EXTENSION ... CASCADE 顺手一敲 |
依赖该扩展的视图、生成列连坐被删 | 先不带 CASCADE 看报错清单再决定 | 15-扩展与生态.md |
| untrusted 语言(plpython3u 等)随手装 | 函数可读写服务器文件系统、开网络连接,注入面从 SQL 挪进宿主语言 | 公司环境原则上不装;要 Python 生态先评估应用层方案 | 15-扩展与生态.md |
| 扩展组合"全都要" | 每个都是攻击面 + 升级债,出事故时排障面翻倍 | 准入三问(谁维护/要不要 preload/挂了能否绕过),装前走变更评审 | 15-扩展与生态.md |
二、七个高危坑
1. 随机 UUID 主键:一张表自己把自己挤胖
uuid v4 毫无顺序,插入把 B-tree 全树搅花:每个叶子页都不满又都占缓存,缓存命中率随数据量线性下滑,索引体积明显大于同规模 BIGSERIAL。PG 18 的 uuidv7() 把时间戳放进 UUID 高位,写回落到右侧热点页,局部性与自增主键相当——需要 UUID 形态(多节点生成、对外暴露)就用 v7;纯内部主键,bigint GENERATED ALWAYS AS IDENTITY 仍是体积与速度的双重最优。存量 v4 大表不建议停机重排,新表开始止损。
2. OFFSET 深分页:翻页越深越接近全表扫
LIMIT 20 OFFSET 1000000 并不是"跳过"一百万行——执行器老老实实取出并丢弃它们,代价线性上涨,还伴随 MVCC 下更多死元组扫描。keyset 分页把"上一页最后一行"变成谓词:WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 20,配 (created_at, id) 复合索引后每页恒定成本。下拉无限滚动、导出任务、队列消费全部适用。
3. 一条 DDL 堵死全站:ACCESS EXCLUSIVE 排队风暴
锁矩阵里只有一档能拦住普通 SELECT——ACCESS EXCLUSIVE。ALTER TABLE 这类语句先拿到锁再执行;执行期间的排队会级联阻塞其后所有语句:一条"看起来只改元数据"的 DDL,配上一次长时间回填,就是全站超时的事故报告。标准防线三件套:SET lock_timeout = '5s'(拿不到就放弃,不排队)、业务低峰执行、失败重试;真正的大改动走"nullable 新列 + 分批回填 + 改名切换"或逻辑复制迁移(见 10-复制与高可用.md)。
4. 隔离级别的两个"想当然"
想当然之一:开了 REPEATABLE READ 就高枕无忧——PG 的 RR 是快照隔离,两个并发事务各自"看到旧值、写不同行"的写偏斜它防不住(双医生值班/双窗口超卖,见 05-事务与锁.md)。想当然之二:上了 SERIALIZABLE 就一劳永逸——SSI 的代价是冲突时返回 40001 序列化失败,应用没有重试逻辑就等于把数据库正确性转嫁成用户可见报错。正确姿势:关键冲突要么 FOR UPDATE 折叠成单点,要么 SERIALIZABLE + 整事务指数退避重试。
5. 长事务与遗留槽:不动声色的双料炸弹
一个忘 COMMIT 的交互事务、一个没人消费的复制槽,都会把 xmin horizon 或 WAL 保留点钉在原地:前者让 autovacuum 全域失效(死元组越积越多、索引膨胀、最后逼近回绕强制清理),后者让 pg_wal 只增不减直到盘满。两者在监控上都是"安静的"——必须显式配告警:pg_stat_activity 中 xact_start 超阈值、pg_replication_slots 的 restart_lsn 落后水位、age(datfrozenxid) 曲线。防线参数:idle_in_transaction_session_timeout、max_slot_wal_keep_size。
6. 复制不是备份,没演练的备份不算备份
复制忠实同步一切,包括 DELETE、DROP、以及勒索 SQL;延迟删除(recovery_min_apply_delay)只能算缓解手段。真正的安全垫是:定时全量(pg_dump 或 basebackup)+ WAL 归档 + 可组合的 PITR,三者都放在复制够不着的地方(对象存储/异地)。并且每季度真的恢复一遍到临时实例——恢复路径从未走通的备份,等价于没有备份。
7. SECURITY DEFINER 不固定 search_path:库内提权机器
DEFINER 让函数体以属主的权限执行——设计意图是让小权限角色借用一次精确的高权限操作。但函数体的 SQL 在调用者的会话环境里解析:对象名沿调用会话的 search_path 查找,只要函数体里有不带 schema 限定的名字,而攻击者能控制自己会话的 search_path(或在可写 schema 里预埋同名对象——15 之前的 public 人人可建,正是 CVE-2018-1058 的土壤),高权限函数就会打在假对象上。官方给出的安全写法就三件事:定义时固定 SET search_path(文档示例 SET search_path = admin, pg_temp,pg_temp 垫底)、函数体内只写完全限定名、EXECUTE 精确授权。存量排查一行搞定:SELECT oid::regproc, provolatile, prosecdef FROM pg_proc WHERE prosecdef;——每个 DEFINER 都值得过一遍(写法模板见 13-函数与存储过程.md,投毒背景见 12-安全与权限管理.md)。
三、最佳实践清单
设计期
- 时间一律
timestamptz;钱一律numeric;文本一律text。 - 主键:内部表 BIGINT IDENTITY,多节点/对外用
uuidv7()(PG 18+)。 - 软删除配部分索引;状态列配部分索引或组合索引,别为 bool 单开索引。
- 每张表标配
created_at/updated_at(updated_at 用触发器或应用维护)。 - FK 列显式建索引;级联删除慎设 CASCADE。
编码期
- SQL 一律参数化;动态标识符走白名单 +
format('%I')。 - 事务保持最短;捕获 40001/40P01 并整事务重试;杜绝 idle in transaction。
- 队列消费用
FOR UPDATE SKIP LOCKED;翻页用 keyset;导入用 COPY/Batch。 - Go 端每个 db 调用传 context;numeric 不扫 float64;pgx 以当前版本文档为准。
RETURNING取代"写完再查";批量写用 CTE/unnest 折成单语句。- 服务端逻辑按变更频率分工:常变的业务规则留在 Go,数据不变量(状态机守卫、updated_at、审计)落到触发器/约束;写的每个函数显式声明波动性、对象名带 schema。
运维期
- autovacuum 永远开着;亿行级大表单独调参(scale_factor、threshold、cost)。
- 监控四件套:磁盘(含 pg_wal 水位)、事务回绕 age、长事务/遗留槽、复制延迟。
- 备份三件套:全量 + WAL 归档 + 定期恢复演练;globals 单独备。
- 慢查询:
log_min_duration_statement+ pg_stat_statements + auto_explain 三管齐下。 - 变更剧本化:DDL 带 lock_timeout;CONCURRENTLY 类操作验证 INVALID 残留。
- 函数、触发器、事件触发器、扩展一律进版本化管理;热表触发器备一份过渡表批量版和"导入期禁用"预案;大版本升级清单含逐扩展
ALTER EXTENSION UPDATE。
安全期
- 角色三分:应用(DML)/迁移(DDL)/报表(只读);生产 DSN 永不含超级用户。
ALTER DEFAULT PRIVILEGES预设授权;新集群(15+)之外记得补收 public schema。- RLS 多租户三件套:策略 + FORCE + 应用角色非 owner。
- scram-sha-256 起步,md5 存量尽快轮换;连接串
sslmode=verify-full。
四、上线自检清单
-
wal_level、max_connections、shared_buffers、work_mem、random_page_cost按机型确认过(默认值是测试用的,不是生产用的) - pg_hba 逐条 review:无 trust、无宽段地址、无 md5 存量口令
-
log_min_duration_statement、log_checkpoints、log_lock_waits、log_temp_files已开;pg_stat_statements 已装 - initdb 数据校验和已开(新集群默认;老集群确认状态)
- autovacuum 参数按最大表尺寸核算过,不是默认 0.2 一把梭
-
idle_in_transaction_session_timeout、max_slot_wal_keep_size、statement_timeout(按业务)已设 - 备份计划:全量周期 + WAL 归档 + 保留策略 + 最近一次恢复演练日期
- 复制拓扑:槽/保留策略、同步档位与 RPO 对齐,备库延迟有告警
- 大表变更剧本含 lock_timeout 与回滚方案;CONCURRENTLY 失败有处置预案
- 应用角色最小权限;DDL 账号与运行账号分离
- 存量 SECURITY DEFINER 函数逐个核对:固定 search_path + 体内 schema 限定 + EXECUTE 收紧
- 扩展清单过一遍准入三问(谁维护/要不要 preload/出事能否绕过);preload 类扩展的
shared_preload_libraries现值留档 - 关键业务已验证:隔离级别语义符合预期(尤其 RR 不防写偏斜)、错误码 40001/40P01 有重试
- 翻页/导出/队列三类接口确认无深 OFFSET、无逐行往返
- 监控面板含:容量、事务回绕 age、死元组、复制延迟、连接水位、慢查询 TopN
- PG 大版本计划:18.x 小版本按月跟进;升级路线(pg_upgrade / 逻辑复制)演练过
- Go/客户端依赖钉住版本(pgx v5.11.0 基线),驱动行为文档与手册版本一致
五、调试速查表
| 症状 | 第一反应 | 工具/查询 | 详见 |
|---|---|---|---|
| 连不上 | 看 listen_addresses → pg_hba → 防火墙/安全组 | psql 报错码即知卡在哪层 |
00-安装与配置.md |
| 认证失败 | 用户/库存在性与 pg_hba 首条命中 | SELECT * FROM pg_hba_file_rules; |
12-安全与权限管理.md |
| 查询突然变慢 | 统计过期?计划回归?锁等待? | pg_stat_activity 看 wait_event → EXPLAIN (ANALYZE, BUFFERS) |
09-查询优化与EXPLAIN.md |
| 查询卡在不动 | 谁堵谁 | SELECT pg_blocking_pids(pid); |
05-事务与锁.md |
| 表"变大但数据没多" | 死元组/索引膨胀 | pg_stat_user_tables.n_dead_tup、pgstattuple |
06-MVCC与Vacuum.md |
| 全库读写报错 | 是不是回绕保护/盘满 | age(datfrozenxid);pg_wal 目录大小 |
06-MVCC与Vacuum.md |
| 备库延迟大 | 回放竞争/热查询冲突/WAL 网络 | pg_stat_replication flush vs replay LSN |
10-复制与高可用.md |
| 应用报 40001/40P01 | 正常信号:并发冲突,缺的是重试 | 错误码见 05-事务与锁.md 处置节 |
05-事务与锁.md |
| 导入慢 | 逐行 INSERT?索引太多?触发器税?vacuum 风暴? | 改 CopyFrom;导入期临时建索引/禁触发器 | 08-Go客户端pgx.md、14-触发器.md |
| 函数/表达式索引结果"过期" | IMMUTABLE 错标 + 常量折叠/计划缓存复用 | pg_get_functiondef 看波动性;EXPLAIN 看折叠点 |
13-函数与存储过程.md |
| 触发器"没生效" | DISABLE 状态、replica 会话、WHEN/UPDATE OF 没命中 | pg_trigger.tgenabled;SHOW session_replication_role |
14-触发器.md |
| “重启就好了” | 别急,先留现场 | pg_stat_activity 快照 + plan 导出再重启 |
09-查询优化与EXPLAIN.md |
实战建议
这本手册读到这里,最值钱的习惯其实是三条:
- 参数默认值只对测试负责。 上线前把
pg_settings里 source 为 default 的关键项过一遍——shared_buffers、work_mem、random_page_cost、autovacuum 组、日志组、超时组,每个都问一句"这台机器这个业务该是多少"。 - 把 MVCC 的账算在应用头上。 死元组、长事务、深分页、无效索引、uuid 打散写入,本质都是"你写的 SQL 在存储层留下的脚印"。会看
pg_stat_user_tables、pg_stat_activity、EXPLAIN 这三个视图,等于有了数据库的体检报告。 - 备份和权限按最坏一天设计。 复制会同步灾难,所以要有独立备份并演练恢复;注入会放大权限,所以角色要最小化并锁 TLS。出事时的差距,取决于你平安时做过哪件"无聊"的事。