接手一套糟糕的 PostgreSQL 数据库:从止损到治理

2026-09-02 41 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

接手遗留数据库,往往意味着面对不合理的编码、几百列的大表、缺失的索引、没有约束的数据,以及没人敢碰的配置。它们通常不是某个人单独造成的,而是赶工、团队缺少 DBA、系统自然膨胀,或者架构决策脱离实际维护需求共同留下的结果。

真正重要的不是追责,而是建立一条可验证、可持续的修复路径:先了解谁在痛苦,再检查数据库的真实状态,最后用小项目逐步降低风险。

先停止恐慌,也停止甩锅

“坏数据库”通常不是突然出现的。一个原本只服务于简单报表的表,可能逐渐承载了交易、搜索、审计和导入任务;一个当时看起来合理的设计,在业务扩大后却变得难以维护。也有些系统受到“架构疾病”的影响:设计者相信自己找到了完美方案,于是忽略了团队熟悉的实践和长期维护成本。

无论原因是什么,追究历史责任都不会修复当前的慢查询或错误数据。更有用的心态是把危机转化为改进权限。只要系统还在运行,就可以从风险最高、收益最明确的地方开始。

可以把目标分成三类:

  • 恢复可见性:知道数据库里有什么、谁在使用、哪些操作最痛。
  • 降低即时风险:补上备份、监控、关键索引和必要约束。
  • 建立长期治理:把高可用、灾难恢复、维护和变更流程自动化。

不要一开始就承诺“重写数据库”。重写通常意味着更大的迁移风险,也容易让团队在几个月内看不到任何可交付成果。

先问人,再问数据库

业务人员、应用开发者和运维人员通常都知道哪里最难受,只是没有稳定的反馈入口。可以让他们描述具体事件:哪个页面慢、哪个报表经常失败、哪类数据需要人工修正、什么故障最担心再次发生。

听取需求时要警惕 XY 问题。有人可能会直接要求“给这张表做分区”或“把数据接入 Kafka”,但这些只是他们想到的解决方案,不一定是实际目标。

例如:

  • “我们需要分区”可能真正要解决的是历史数据清理太慢。
  • “我们需要 Kafka”可能真正要解决的是应用无法可靠获知数据变化。
  • “这个查询必须加缓存”可能真正要解决的是缺失索引或错误的查询条件。

把请求改写成可衡量的问题会更容易推进:查询 p95 延迟从 8 秒降到 1 秒以内;每天的数据修正从 200 条降到 20 条以内;恢复演练能够在 30 分钟内完成。

检查四个层面

1. Schema:结构是否表达了业务规则

使用 pg_dump 导出结构,再配合 pgAdmin 或 DBeaver 浏览表、索引、视图、序列和权限。重点关注:

  • 数据库编码和排序规则是否符合应用与数据要求。
  • 是否存在一张拥有数百列、实际上来自电子表格的表。
  • 主键、外键、唯一约束和非空约束是否缺失。
  • 索引是否覆盖真实查询,而不是只覆盖设计者猜测的场景。
  • 字段类型是否合理,例如用文本存储时间、金额或状态。
  • 权限是否过于宽松,应用账号是否拥有不必要的管理权限。

没有约束并不代表系统更灵活,通常只是把一致性检查推迟到应用代码、人工脚本或事故处理中。

2. Data:数据本身是否可信

结构看起来正常,不代表数据可靠。可以通过探索性查询寻找重复值、空值、孤儿记录和异常分布。例如,下面的 SQL 假设存在 orders 表,其中包含 customer_idstatuscreated_at 字段:

-- 查找客户标识为空的订单
SELECT count(*) AS missing_customer_id
FROM orders
WHERE customer_id IS NULL;

-- 检查订单状态的实际取值
SELECT status, count(*) AS row_count
FROM orders
GROUP BY status
ORDER BY row_count DESC;

-- 查找同一客户在同一分钟内创建的大量订单,辅助识别异常写入
SELECT customer_id,
       date_trunc('minute', created_at) AS minute,
       count(*) AS row_count
FROM orders
GROUP BY customer_id, date_trunc('minute', created_at)
HAVING count(*) > 100
ORDER BY row_count DESC;

这些查询只是示例,运行前需要把表名和字段名替换为实际 schema。不要直接根据一次查询结果修改生产数据。先确认业务含义,统计影响范围,并准备回滚方案。

3. Configuration:配置是否与负载匹配

检查连接数、内存相关设置、日志、检查点、WAL、归档和 autovacuum 配置。配置不能脱离工作负载讨论:交易系统、分析系统和批处理数据库需要关注的指标不同。

可以先查看关键参数和当前活动连接:

SHOW server_encoding;
SHOW max_connections;
SHOW shared_buffers;
SHOW log_min_duration_statement;
SHOW autovacuum;

SELECT pid,
       usename,
       application_name,
       state,
       wait_event_type,
       wait_event,
       query_start,
       left(query, 160) AS query_sample
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
ORDER BY query_start NULLS LAST;

生产环境中修改参数前,应确认 PostgreSQL 版本、参数是否需要重启,以及变更对连接池、内存和故障恢复的影响。

4. Behavior:系统实际发生了什么

日志能告诉你错误和慢请求,pg_stat_activity 能显示当前会话,pg_stat_statements 则适合找出一段时间内消耗最多的查询。启用扩展的方式取决于版本和部署环境,下面是一个常见的查询示例:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT calls,
       round(total_exec_time::numeric, 2) AS total_ms,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       rows,
       left(query, 180) AS query_sample
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

这份结果适合帮助排序工作,不代表每条查询都应该立即重写。高总耗时可能来自调用次数很多,也可能来自单次执行极慢;需要结合 calls、平均耗时、返回行数和业务优先级判断。

用小项目推进修复

一次只处理一个变化,并为它定义成功标准。一个可执行的修复项目可以长这样:

项目:降低订单列表查询延迟

当前指标:p95 = 8.4 秒
目标指标:p95 <= 1.5 秒
范围:只处理 orders 列表查询,不改订单写入流程
步骤:
1. 记录当前查询计划和基线指标
2. 在测试环境建立候选索引
3. 使用接近生产规模的数据压测
4. 观察写入开销、锁等待和磁盘增长
5. 分批发布并保留回滚方案
6. 发布后对比 p50、p95、错误率和数据库负载

索引示例可以这样验证,假设查询按客户和创建时间筛选:

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_created
ON orders (customer_id, created_at DESC);

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE customer_id = 42
ORDER BY created_at DESC
LIMIT 50;

CREATE INDEX CONCURRENTLY 可以减少对生产写入的阻塞,但会消耗额外资源,也不能放在事务块中。索引不是免费优化:它会增加磁盘占用,并让写入和维护成本上升。发布前后都要比较查询性能与写入影响。

把一次性修复变成团队能力

数据库治理不能依赖某个“英雄”长期手工操作。完成局部修复后,应把经验沉淀到可重复的流程中:

  • 定期执行备份,并验证备份确实能够恢复。
  • 为高可用和灾难恢复制定目标,至少明确 RPO、RTO 和演练频率。
  • 监控锁等待、连接数、缓存命中、WAL、复制延迟、表膨胀和 autovacuum 状态。
  • 对 schema 变更建立评审、迁移、监测和回滚流程。
  • 把索引、约束、命名、权限和数据保留规则写成团队指南。
  • 使用脚本或基础设施代码自动化重复维护任务。

不要等数据丢失后才准备备份,等宕机后才设计高可用,等表出现严重膨胀后才调整 autovacuum,也不要等云账单失控后才分析资源使用。提前规划的成本通常可以控制,而事故中的选择空间会迅速缩小。

一份适合落地的检查清单

  • [ ] 收集业务、开发和运维人员最痛的三个问题。
  • [ ] 导出 schema,盘点表、索引、约束、权限和编码。
  • [ ] 用探索性 SQL 检查空值、重复值、孤儿记录和异常分布。
  • [ ] 查看日志、pg_stat_activitypg_stat_statements
  • [ ] 确认备份策略,并完成一次恢复验证。
  • [ ] 为每项修复记录基线、目标、影响和回滚方式。
  • [ ] 一次发布一个可观察的小变化。
  • [ ] 将有效做法写入文档并纳入自动化。

接手糟糕的数据库并不意味着必须立即推倒重来。更稳妥的路径是把未知变成数据,把抱怨变成指标,把大问题拆成小项目,再把解决方案交给整个团队维护。数据库会继续演化,但团队不必继续重复同样的历史错误。


相关推荐