PostgreSQL 这些年越来越像数据库里的“六边形战士”:事务完善、扩展丰富、JSON 支持强,很多新项目直接把它当默认关系库。不少人从安装开始就卡住,不是装不上,而是装完不知道下一步该干什么。这篇文章按我实际部署项目的顺序来写:先解决版本选择和安装方式,再把 Windows、Linux、Docker 三条路线跑通,然后讲配置、日常操作、高阶实战和性能排查。适合刚接触 PostgreSQL 的开发者,也适合已经会用 MySQL、想转过来的工程师。最值得关注的不是某一条命令,而是安装到生产环境之间那几步最容易踩坑的环节:认证配置、备份恢复、慢查询定位。
1. 先定版本和安装方式,别急着敲命令
1.1 下载哪个版本:稳定优先,别追最新
搜索热词里天天有人问“PostgreSQL 下载哪个版本”。我的建议很简单:新项目用官方当前大版本往后退一到两个版本,老项目跟着系统包管理器稳定版走。PostgreSQL 的版本策略不是 LTS 那种严格逻辑,每个大版本都有长期支持窗口,所以不用追最新。
如果你要在生产环境用,选已经发布一段时间、社区踩坑资料多的版本更省心。比如 PostgreSQL 16 是目前很多团队已经在生产跑过的版本,资料齐全,遇到问题能搜到大量案例。PostgreSQL 17 的功能更新值得了解,但如果不是非用新特性不可,没必要当第一批升级的人。
这里有个判断标准:去官方下载页看 EOL 时间表,选剩余支持期最长的稳定版。还有一个更简单的办法,看你的系统包管理器默认提供哪个版本——Ubuntu 官方源、CentOS 的 dnf 源打包的版本通常都经过测试,直接拿来学习完全够用。
1.2 四种安装方式对比
不同环境适合不同方式,先看清差异再动手。
| 安装方式 | 适合场景 | 优点 | 要注意的问题 |
|---|---|---|---|
| 官方安装包/安装器 | Windows、macOS 新手 | 图形界面,包含 pgAdmin,省心 | 需要手动加到 PATH,防火墙和端口可能冲突 |
| 包管理器(apt/yum) | Linux 服务器 | 和系统集成好,systemd 托管 | 数据目录位置要确认,注意权限 |
| Docker | 开发环境、临时测试、CI | 干净隔离,起停快 | 容器删除后数据会丢,必须挂载数据卷 |
| 源码编译 | 特殊平台、离线环境、自定义编译参数 | 版本可控,支持定制 | 编译时间长,依赖多,升级维护成本高 |
离线安装是另一个常见需求。生产内网环境经常不能访问外网,这种情况不要现场编译,先在能联网的机器上把 deb 或 rpm 包下载齐全,再批量安装。如果内网机器架构一致,这比源码编译快得多,也不容易漏依赖。
1.3 我的推荐路径
我的建议路径是这样:
- 本地学习首选官方安装器或 Docker。Windows 上直接双击安装包,下一步下一步,装完用 pgAdmin 连一下就算入门了。
- 测试服务器首选 Docker,一条命令起服务,环境不会污染宿主机。
- 生产环境如果是 Debian/Ubuntu 系,用 apt 源安装;是 RHEL 系,用 dnf 或先下载 rpm 离线包。
- 真到了需要源码编译的场景,一定是已经确认包管理器版本不满足需求,比如需要特定编译参数、特定补丁或者特殊平台支持。不是为了“显得高端”去编译。
注意:不管选哪种方式,装完第一件事不是写业务代码,而是确认服务状态、数据目录和端口监听正常。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把 PostgreSQL 跑起来:Windows、Linux、Docker 三条路线
2.1 Windows 安装后先确认三件事
Windows 上用官方安装器最省事。下载对应版本的 exe,安装过程中会让你设置超级用户 postgres 的密码、默认端口 5432 和 locale。这些信息建议记下来,很多新手第一坑就是密码忘了或者端口被占用。
装完先确认三件事:
- 服务是否启动。打开服务管理器,找到 postgresql 相关服务,状态应该是“正在运行”。
- psql 是否在环境变量里。正常情况下安装器会帮你加 PATH,可以在 PowerShell 里执行
psql --version验证。如果提示找不到命令,去安装目录的 bin 下手动调用,或者把路径加进环境变量。 - 能否连接。执行
psql -U postgres -h localhost,输入密码后能看到提示符。如果报用户名认证失败,先检查 pg_hba.conf 里的认证方式。
2.2 Ubuntu 上用 apt 安装并管理服务
Ubuntu 安装命令非常简单:
bash复制sudo apt update
sudo apt install postgresql postgresql-contrib
装完之后注意几点。包管理器安装不会把数据目录放在用户目录,而是放在系统数据目录下。服务名一般是 postgresql,可以用 systemctl status postgresql 查看。默认情况下监听 127.0.0.1,外部机器连不上,这是安全设计而不是故障。
Ubuntu 下有一个特殊问题:安装后默认有一个 postgres 系统用户,你当前用户不一定能直接连数据库。一般做法是切换到 postgres 用户再操作:
bash复制sudo -u postgres psql
这个命令会以 postgres 系统用户的身份进入 psql,不需要密码。很多初次接触的人在这里卡住,因为他们直接敲 psql 然后要求输入密码,却不知道默认用的是操作系统用户认证。
如果想让当前用户直接管理数据库,可以创建同名数据库用户,或者创建一个与系统用户名相同的数据库角色。理解了认证机制,后面配置 pg_hba.conf 就顺了。
2.3 Docker 安装要注意数据卷,不是跑起来就行
Docker 方式最简洁,但也是最容易丢数据的方式。先看基础命令:
bash复制docker run -d \
--name postgres-test \
-e POSTGRES_PASSWORD=yourpassword \
-e POSTGRES_USER=app_user \
-e POSTGRES_DB=app_db \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16
几个环境变量要理解清楚:POSTGRES_USER 决定初始超级用户,POSTGRES_PASSWORD 是它的密码,POSTGRES_DB 会在初始化时自动创建。如果不设置 POSTGRES_DB,默认会创建一个与 POSTGRES_USER 同名的库。
-v pgdata:/var/lib/postgresql/data 这行是关键。它把容器内的数据目录映射到宿主机的一个命名卷。没有这一行,容器一删,所有数据就没了。不要因为只是本地测试就懒得挂卷,养成好习惯之后到生产环境才不会漏。
第一次启动后,执行 docker logs postgres-test 能看到初始化日志。连接测试可以用:
bash复制docker exec -it postgres-test psql -U app_user -d app_db
生产环境用 Docker 跑 PostgreSQL 还要额外考虑网络、TLS、备份目录和资源限制,这里不展开。开发环境至少做到挂卷和日志检查。
2.4 验证安装成功的统一标准
不管你用哪种方式安装,统一做三件事验证:
- 服务状态正常。systemd 环境用
systemctl status,Docker 用docker ps,Windows 看服务管理器。 - 命令行能连上。
psql -U xxx -d xxx -c "select version();"有输出。 - 能创建表并插入数据。建一张临时表,写一行数据,查询回来,再删掉。
这三件事跑通,说明安装、认证、文件权限和数据目录都正常。之后才值得进入配置和业务开发。
3. 连接、认证和库表权限:配置错了,后面全是坑
3.1 postgresql.conf 里的核心参数
PostgreSQL 的主要配置都在 postgresql.conf 里。用 SHOW 命令可以快速查看当前值:
sql复制SHOW listen_addresses;
SHOW max_connections;
SHOW shared_buffers;
常见需要修改的参数:
| 参数 | 默认含义 | 什么时候改 |
|---|---|---|
| listen_addresses | 监听地址,默认 localhost | 需要远程连接时改为 * 或具体网卡 IP |
| port | 监听端口,默认 5432 | 端口冲突或安全需求时改 |
| max_connections | 最大连接数,默认 100 | 应用并发高或连接池不够时调大,但要结合内存 |
| shared_buffers | 共享缓冲内存 | 数据库内存调优的核心,一般建议不超过物理内存四分之一 |
| log_min_duration_statement | 慢查询日志阈值 | 排查慢查询时设置为 500 到 1000 毫秒 |
改动后要不要重启要看参数类型。shared_buffers 需要重启,log_min_duration_statement 这类 SIGHUP 参数只需要 reload。改完用 SELECT pg_reload_conf(); 或 systemctl reload postgresql 生效。
3.2 pg_hba.conf 控制谁能登录
pg_hba.conf 是访问控制的命门。它的每一行都是一条规则,从上往下匹配,匹配到就用对应认证方式,不再往下看。新手最容易在这里犯错。
一个常见问题是“我安在服务器上,本地 psql 能连,远程连不上”。这通常有两层原因:listen_addresses 没有放开,以及 pg_hba.conf 里没有对应客户端的 host 规则。
推荐先看现有的规则结构:
conf复制# 格式: type database user address method
local all postgres peer
host all all 127.0.0.1/32 scram-sha-256
host all all 0.0.0.0/0 scram-sha-256
最后一条 0.0.0.0/0 表示所有 IPv4 地址都可以尝试连接数据库,认证方式是密码。实际生产环境不要直接写 0.0.0.0/0,应该写具体网段,比如 10.0.0.0/8 或者某个客户端的 IP,减少暴露面。
认证方式也有讲究。md5 已经过时,新版本默认推荐 scram-sha-256。如果客户端工具太老不支持 scram,才考虑兼容方案。trust 这种方式只建议在一次性容器的本地调试里用,任何联网环境都不要开。
注意:修改 pg_hba.conf 之后需要 reload,不是重启。执行
SELECT pg_reload_conf();就可以。
3.3 创建用户、数据库和最小权限分配
开发环境中很多人习惯直接用 postgres 超级用户跑业务。个人学习可以,但只要是多人协作或接近生产,都建议按最小权限拆。
先建独立用户,再建数据库并指定归属:
sql复制CREATE USER app_user WITH PASSWORD 'strong_password';
CREATE DATABASE app_db OWNER app_user;
然后给业务用户授权:
sql复制GRANT ALL PRIVILEGES ON DATABASE app_db TO app_user;
GRANT ALL ON SCHEMA public TO app_user;
实际操作里,owner 就是这个库的 owner,后续表默认归它管,开发和运维都很顺。只读账号则只需要 SELECT 权限:
sql复制CREATE USER readonly_user WITH PASSWORD 'readonly_password';
GRANT CONNECT ON DATABASE app_db TO readonly_user;
GRANT USAGE ON SCHEMA public TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
这里注意 PostgreSQL 的权限模型和 MySQL 差别:数据库、schema、表、列是分层的,授权粒度更细。刚开始容易漏掉 schema 的 USAGE 权限,导致用户建了表却打不开。
3.4 日志先开起来,排错才有依据
很多人出了问题才去看日志,结果发现日志根本没开或者级别太低。最小可用配置:
conf复制logging_collector = on
log_directory = 'log'
log_min_duration_statement = 500
log_connections = on
log_disconnections = on
log_connections 和 log_disconnections 在排查连接数满、频繁断连时非常有用。log_min_duration_statement 会把执行超过 500 毫秒的 SQL 记录一条,慢查询刚有苗头就能看到。
日志路径通常在数据目录下,或者用 log_directory 指定。不同包管理器的默认位置不一样,用 SHOW log_directory; 查询最靠谱。
4. 日常操作:建表、导入导出、索引和查询分析
4.1 建表时把数据类型选对,比后期优化省事
PostgreSQL 的数据类型比 MySQL 丰富,选对了能省很多事情。常用选择:
- 数值用 integer、bigint、numeric。金额不要用 float,用 numeric(18,2),避免浮点误差。
- 字符串短文本用 varchar(n),文本内容较长用 text。PostgreSQL 里 text 和 varchar 性能差别不大,主要看业务约束。
- 时间用 timestamp with time zone,也就是 timestamptz。存时间尽量带时区,避免不同时区服务器读出来的值不一致。
- 布尔值用 boolean,原生支持 true/false,不用再造 0/1 字段。
- 枚举类型 enum 可以用,但后续加值麻烦,变更多的情况下更推荐查数据表。
- JSON 数据用 jsonb,后面会单独讲。
建表示例:
sql复制CREATE TABLE orders (
id bigserial PRIMARY KEY,
order_no varchar(32) UNIQUE NOT NULL,
customer_id integer NOT NULL,
amount numeric(18,2) NOT NULL DEFAULT 0,
status varchar(16) NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
bigserial 是自增主键的常见写法,等价于序列。order_no 如果业务上必须唯一,加唯一约束比在应用里判断稳。
4.2 psql 常用命令,能覆盖八成日常操作
psql 是 PostgreSQL 自带的交互式命令行,很多人在图形工具里点点点,遇到服务器环境就懵。把常用元命令记住,大部分日常操作不用再装工具:
| 命令 | 作用 |
|---|---|
\l |
列出数据库 |
\c dbname |
切换数据库 |
\dt |
查看当前 schema 下的表 |
\d table_name |
查看表结构、索引和约束 |
\x |
扩展显示,行内容太长时用 |
\dn |
查看 schema |
\du |
查看角色 |
\i file.sql |
执行 SQL 文件 |
\timing |
打开每条 SQL 的耗时显示 |
\q |
退出 |
日常我会把 \timing on 开着,执行查询的时候心里有个底:这条到底要多久。排查慢查询时,它比猜快得多。
一条查询结果列太多时,先 \x 再跑 SQL,结果会变成键值对形式,看起来更清楚。
4.3 备份恢复用 pg_dump 和 pg_restore
备份是数据库使用里最不能省的一环。pg_dump 做逻辑备份,适合单库或指定 schema:
bash复制pg_dump -U app_user -h localhost -d app_db -F c -f app_db.dump
-F c 是自定义格式,后面 pg_restore 才能灵活选择是否恢复整库、是否跳过某些对象。恢复命令:
bash复制pg_restore -U app_user -h localhost -d app_db -c app_db.dump
-c 表示先删除已有对象,再重建。注意目标数据库 app_db 要先创建好,pg_restore 不会替你建库。
还有一种纯 SQL 格式:
bash复制pg_dump -U app_user -d app_db > backup.sql
psql -U app_user -d app_db < backup.sql
SQL 格式适合小库、迁移和跨大版本场景,缺点是恢复时不够灵活。物理备份 pg_basebackup 适合整机备份和搭建 standby,逻辑备份恢复粒度更细。两个概念要分清。
备份验证这件事多说一句:备份文件生成成功不代表能恢复。定期挑一台空机器做一次完整恢复演练,比任何备份参数都有说服力。
4.4 用 EXPLAIN ANALYZE 判断查询到底慢在哪
很多“查询慢”的问题其实不是数据库的问题,是索引缺失、数据分布差或者 SQL 写法导致全表扫描。用 EXPLAIN ANALYZE 能直接看到执行计划:
sql复制EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 123;
重点看这几个信息:
- Seq Scan 说明走了全表扫描。表小可以接受,表大就要关注。
- Index Scan / Bitmap Heap Scan 说明用了索引。
- 每个节点后的 actual time 是实际耗时。
- rows 和 actual rows 差别大,说明优化器对数据量的估算不准确。
- Buffers 显示读了多少缓冲页,能辅助判断内存命中率。
我一般不会只看有没有走索引,还会看预估行数和实际行数的偏差。偏差大,通常是因为 WHERE 条件里用了函数包装列、字段类型不匹配,或者统计信息没更新。先 ANALYZE table_name; 再重新看执行计划,是成本最低的调整。
5. 高阶实战:窗口函数、CTE、JSONB 和分区表
5.1 窗口函数:排名、累计、环比一次说清
窗口函数是高阶查询里最值得掌握的一类。它和 GROUP BY 的区别是:GROUP BY 把多行合并成一行,窗口函数不减少行数,每一行还能保留原始明细,同时拿到分组统计结果。
排名需求直接用 row_number、rank、dense_rank:
sql复制SELECT
customer_id,
order_no,
amount,
row_number() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders;
这个查询会按 customer_id 分组,在每组内按创建时间倒序编号。取每组最新一条时,外面套一层子查询过滤 rn = 1 即可。
累计和环比也是高频需求:
sql复制SELECT
day,
amount,
sum(amount) OVER (ORDER BY day) AS cumulative_amount
FROM daily_sales;
lag 可以取上一行的值,用来计算环比变化:
sql复制SELECT
day,
amount,
amount - lag(amount) OVER (ORDER BY day) AS day_diff
FROM daily_sales;
窗口函数用起来不复杂,关键是记住两个点:PARTITION BY 决定分组维度,ORDER BY 决定窗口内排序。这两个维度搞清楚了,大部分窗口函数都能写。
5.2 CTE 让复杂查询从嵌套地狱变成顺序阅读
CTE(Common Table Expression)用 WITH 开头,把复杂的子查询拆成一段一段,每段有名字。复杂查询可读性提升非常明显。
sql复制WITH recent_orders AS (
SELECT customer_id, sum(amount) AS total
FROM orders
WHERE created_at >= now() - interval '30 days'
GROUP BY customer_id
),
top_customers AS (
SELECT customer_id, total,
row_number() OVER (ORDER BY total DESC) AS rn
FROM recent_orders
)
SELECT c.customer_name, t.total
FROM top_customers t
JOIN customers c ON c.id = t.customer_id
WHERE t.rn <= 10;
CTE 在优化器里未必一定比子查询快,但可读性和可维护性高很多。团队协作时,别人接手你的 SQL 不需要一层一层往外拆括号,这比微小的性能差异重要。
另外 CTE 可以用递归,处理树形结构、菜单、地区层级这类数据:
sql复制WITH RECURSIVE org_tree AS (
SELECT id, parent_id, name, 1 AS depth
FROM departments WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, d.name, t.depth + 1
FROM departments d JOIN org_tree t ON d.parent_id = t.id
)
SELECT * FROM org_tree;
递归 CTE 解决“展开整棵树”的问题非常顺手,比应用层多次查库高效。
5.3 JSONB 不是存字符串,要会用操作符和 GIN 索引
PostgreSQL 的 JSONB 不能当普通的 TEXT 字段用。它内部是二进制存储,支持检索、更新和索引。
基本操作符:
sql复制-- 创建表
CREATE TABLE events (
id bigserial PRIMARY KEY,
payload jsonb NOT NULL
);
-- 插入
INSERT INTO events (payload) VALUES
('{"event": "click", "user": 123, "tag": ["a", "b"]}');
-- 查询某个字段
SELECT payload->>'event' FROM events;
-- 按 JSON 字段过滤
SELECT * FROM events WHERE payload->>'user' = '123';
-- 判断是否包含某个键
SELECT * FROM events WHERE payload ? 'tag';
-- 判断数组包含某个元素
SELECT * FROM events WHERE payload @> '{"tag": ["a"]}';
jsonb 和 json 的区别是:jsonb 不保留键顺序、存储时做去重和类型归类,查询性能更好,也支持索引。绝大多数场景选 jsonb 就对了。
高频查询的 JSON 字段要建 GIN 索引:
sql复制CREATE INDEX idx_events_payload ON events USING GIN (payload);
GIN 索引对 @> 这种包含查询非常有效。建了索引之后,用 EXPLAIN ANALYZE 确认真的走到 Bitmap Index Scan,否则等于白建。
JSONB 适合存灵活结构、半结构化和不适合强行建模的字段,但不适合当主数据模型。核心业务字段尽量用明确列,灵活字段用 jsonb,两者结合才是正解。
5.4 分区表:量大了之后最值得做的改造
数据量到千万、亿级别之后,单表查询和删除都会变慢。分区表把一张大表物理上拆成多个子表,优化器只扫描需要的分区。
按时间的范围分区是最常见场景:
sql复制CREATE TABLE orders_part (
id bigserial,
created_at timestamptz NOT NULL,
amount numeric(18,2)
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2025_01 PARTITION OF orders_part
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE orders_2025_02 PARTITION OF orders_part
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
分区键的类型很关键。时间字段用 timestamptz 时,分区边界要和插入值的时区对齐,否则会匹配不到分区直接报错。这里的典型坑:客户端传的是字符串时间,时区转换后落到的分区间和预想不一致。
分区还有一个日常维护动作:新月份要提前建分区,旧分区可以 detach 后归档。
sql复制ALTER TABLE orders_part DETACH PARTITION orders_2024_12;
DETACH 之后这张子表变成独立表,可以单独备份或迁移。这个能力非常适合冷热数据分离。
不是所有表都要分区。几百万行的表通常没必要,分区反而是管理负担。判断标准是:查询经常带分区键过滤、数据持续增长、删除和归档需求明确、单表已经影响维护操作。
5.5 事务隔离级别和锁:并发写正确性靠这里
PostgreSQL 的默认隔离级别是 Read Committed,大多数业务在这个级别下没问题。需要更高隔离时可以用 Repeatable Read 或 Serializable。
sql复制BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET status = 'paid' WHERE id = 1;
COMMIT;
FOR UPDATE 是行级锁,防止两个事务同时更新同一行。UPDATE 本身就带行锁,但先 SELECT 再 UPDATE 的场景里,需要在 SELECT 阶段就加锁,否则两个事务可能读到同一个旧状态。
锁冲突是最常见的并发问题之一。当一个事务持锁不提交,其他事务会卡在等待锁。排查时查 pg_locks 和 pg_stat_activity:
sql复制SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active' AND wait_event_type IS NOT NULL;
如果看到大量 idle in transaction,说明有连接开了事务没提交,这会把锁一直攥着,拖垮整个系统。找到 pid 后,确认业务逻辑正常,可以终止它:
sql复制SELECT pg_terminate_backend(pid);
能提交就提交,不能提交才考虑终止。这个操作在生产环境要非常谨慎,先通知相关人员确认事务不重要。
6. 性能调优和问题排查:不靠赌,靠日志和统计视图
6.1 慢查询日志先开,默认关着等于白跑
性能问题排查的第一步永远是先有数据。PostgreSQL 默认不开慢查询日志,你不开就不知道自己库里哪些 SQL 慢。
配置项:
conf复制log_min_duration_statement = 500
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_line_prefix 把这行日志加上时间、进程号、用户、数据库和来源 IP,定位问题时一眼就知道是哪个连接干的。
设置之后 reload 即可生效:
sql复制SELECT pg_reload_conf();
开慢查询日志不会吃掉太多性能,500 毫秒以上的 SQL 才记录,正常业务不会刷屏。先开起来,看一周,再决定要不要调低阈值。
6.2 解析 EXPLAIN ANALYZE 的关键字段
前面讲了 EXPLAIN 的基础用法,这里说更实用的判断顺序:
- 看有没有 Seq Scan 出现在大表上。
- 看 estimated rows 和 actual rows 的偏差。
- 看 Sort 和 Hash 节点的内存消耗。
- 看有没有 Nested Loop 循环次数过多。
- 看返回行数和扫描行数的比例,判断选择性。
一条典型的慢查询解释:
sql复制EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 123 AND created_at > '2025-01-01';
如果执行计划走了 Index Scan on orders_pkey,但实际条件列不是主键,说明优化器用错了索引。如果走了 Bitmap Index Scan on idx_orders_customer_id,后面还有一个 Bitmap Heap Scan 和 Recheck Cond,说明回表数量大,可以考虑覆盖索引。
覆盖索引写法:
sql复制CREATE INDEX idx_orders_customer_created ON orders(customer_id, created_at);
把 WHERE 和 ORDER BY 里常用的列组合进一个复合索引,可以减少回表。不是索引越多越好,每个索引都会拖慢写入。一般规则:查询多写少,适当加索引;写入频繁,索引要克制。
6.3 连接数、锁和死锁的排查顺序
遇到系统变卡,很多人第一反应是“数据库挂了”,其实多数是连接数打满、锁等待或慢查询堆积。
排查顺序我一般是:
- 先看连接数情况。
select count(*) from pg_stat_activity;如果接近 max_connections,再按 state 分组看哪种状态占多数。 - 区分 active 和 idle in transaction。idle in transaction 大量堆积,通常是应用层事务未提交,不是数据库问题。
- 看 wait_event_type。如果是 Lock,查 pg_locks 找持锁来源;如果是 IO,看磁盘和 WAL 写入;如果是 Client,说明在等客户端。
- 看慢查询日志。如果日志里有大量超过阈值的 SQL,优先处理这些 SQL,而不是改全局参数。
- 最后才考虑调 shared_buffers、max_connections 等全局参数。
连接数不满也卡的情况,常见原因是某个查询占用了大量 CPU 或内存。用 top 看进程资源,结合 pg_stat_activity 的 query 列定位到 PID。
死锁日志会出现在服务日志里,报错信息一般是 deadlock detected。死锁本质是多个事务加锁顺序不一致,比如事务 A 先锁表 X 再锁表 Y,事务 B 先锁 Y 再锁 X。解法是统一应用层的加锁顺序,或者减少长事务。
6.4 参数调整的边界:先改能确认收益的项
调参不要上来就把 shared_buffers 拉到内存的一半。shared_buffers 太高反而可能浪费内存,而且和操作系统缓存搭配效果未必最好。一般建议不超过物理内存的四分之一,具体值要靠观察命中率和 workload 来定。
work_mem 是另一个容易被“拍脑袋调大”的参数。它控制单个排序和哈希操作的内存,不是全局池子,每个排序都可能分配一份。调大 work_mem 能加速大排序,但并发高时内存可能成倍涨。判断标准是 EXPLAIN 里的 Sort Method 是否显示 external merge,如果是,说明排序落盘了,可以考虑调大;如果没有落盘,调大收益有限。
max_connections 默认值 100 对很多业务其实够用。应用层更推荐引入连接池,把连接复用起来,而不是把 max_connections 调到几千。连接数越大,管理开销越大,锁竞争也越明显。
总结一下调参原则:先定位到具体 SQL 和等待事件,再改对应参数。没有证据之前改全局配置,基本靠赌。
7. 生产落地清单:备份、监控、升级和持续学习
7.1 备份策略要分两类:逻辑备份和物理备份
生产环境的备份不能只有一个 pg_dump 定时任务。一个合理的策略至少包含两层:
物理层用 pg_basebackup 做基础备份,配合 WAL 归档实现时间点恢复(PITR)。崩溃后可以恢复到任意时间点。逻辑层用 pg_dump 定期导出一份 SQL 或自定义格式,用于单表恢复和跨版本迁移。
wal_level 要设置成 replica 或更高,才能支持 WAL 归档。配置:
conf复制wal_level = replica
archive_mode = on
archive_command = 'cp %p /backup/wal/%f'
archive_command 在生产里通常换成备份工具的命令,而不是简单 cp。这块配置复杂,建议先在一台测试机上完整模拟一次“删库、用备份恢复、确认数据完整”的演练。
备份验证的标准:备份文件能列出、能恢复、恢复后的库能跑核心业务查询。做不到这三条,备份等于摆设。
7.2 监控视图和常用管理查询
PostgreSQL 自带的统计视图足够日常监控用,不一定要先上第三方工具。
sql复制-- 当前活动连接
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- 慢查询实时查看
SELECT pid, now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '5 seconds';
-- 表大小排行
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;
-- 数据库大小
SELECT datname, pg_size_pretty(pg_database_size(datname))
FROM pg_database;
pg_stat_statements 扩展是慢 SQL 统计的利器。启用它:
sql复制CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
然后在 postgresql.conf 里把它加到 shared_preload_libraries,重启后它会持续跟踪所有 SQL 的执行次数、总耗时、平均耗时。查最耗时的 SQL,只要一条语句:
sql复制SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
这个扩展值得在每套环境里都开,尤其是生产环境。它给出的排序结果,能直接告诉你该优化哪条 SQL。
7.3 大版本升级用 pg_upgrade,小版本直接平替
PostgreSQL 的大版本升级不能直接把数据目录拿过去用。官方提供 pg_upgrade 工具来做这个事,流程大致是:新版本二进制、旧数据目录、运行 pg_upgrade 检查、执行升级、测试服务。
升级前必须做的事:
- 完整备份,包括物理备份和 pg_dump 逻辑备份,双保险。
- 确认所有扩展在目标版本有对应版本。
- 记录原配置,尤其是 shared_preload_libraries、max_connections、locale。
- 在测试环境先升一遍,完整跑业务测试。
- 生产升级选业务低峰期,准备回滚方案。
不升级的理由也有:如果当前版本还在支持期内,没有迫切的 bug 或安全需求,可以先稳定跑着。数据库升级不像应用升级,影响面大,没有明确收益时不要盲动。
小版本升级(比如 16.1 到 16.3)通常只是二进制替换,数据目录格式没变。用系统包管理器直接更新即可,但也建议看 release notes 确认没有变更。
7.4 从基础到高级的学习路径
最后给一条实用学习路径,按我自己的经验排序:
- 先装好环境,把 psql 基础命令刷熟。能建库、建表、插数据、查数据,就算入门。
- 把 pg_hba.conf 和 postgresql.conf 的常用参数过一遍,理解认证和连接机制。
- 写业务查询时刻意用窗口函数和 CTE,替换原来习惯的子查询写法。
- 把 EXPLAIN ANALYZE 变成每次慢查询的固定动作,慢慢积累执行计划阅读经验。
- 尝试用 JSONB 存一些灵活结构,体会半结构化数据在关系库里的处理方式。
- 接触备份恢复,完整做一次备份演练。
- 再往后,可以了解复制、读写分离、连接池、扩展开发这些方向。
新版生态还有一个变化:很多开发者开始用 MCP Server、各种数据库 Skill 让 AI 编程助手直接连接 PostgreSQL 查结构和数据。这件事对日常开发效率有帮助,但使用时要特别控制权限:给只读账号,不暴露生产数据,返回结果里如果有敏感字段要做脱敏。工具可以提高效率,数据库权限和数据处理规则仍然要靠人来把握。
PostgreSQL 的功能面非常广,一篇文章不可能覆盖全部。真正有价值的是把安装、配置、操作、性能和备份这条链路逐个打通。先把单库跑稳,再谈复制和高可用;先把慢查询看明白,再调参数;先把备份恢复演练做扎实,再上线业务。
