工业数采平台侧的"关系型数据库"终于有了系统篇------为什么 PostgreSQL 能扛住高并发写入却不怕锁死?为什么你的 SQL 时快时慢?VACUUM 到底在干什么?本文一次性讲透。
1. 背景
1.1 为什么还需要PostgreSQL
之前已经写过 SQLite3(嵌入式 OLTP)、DuckDB(嵌入式 OLAP)、RocksDB(LSM KV 引擎)、TDengine(时序数据库)、Redis(内存 KV)、etcd(分布式协调 KV)------覆盖了"嵌入式、时序、缓存、协调"四类存储。但在真实工业数采平台里,还有一类数据始终需要通用关系型数据库:
- 设备台账、产线/工位元数据、用户与权限、工艺参数版本、告警规则配置、报表模板;
- 业务侧订单/工单/IPQC 检验记录等结构化业务数据;
- 这些数据规模中等、关系复杂、需要强一致性事务与灵活查询,时序库和 KV 都不合适。
PostgreSQL 是这个领域的开源事实标准:1986 年诞生于伯克利大学 POSTGRES 项目,BSD 许可、社区驱动 30+ 年,支撑了从个人项目到大型互联网公司的生产系统。它的设计哲学是"功能全、正确性优先、可扩展":完整 MVCC、多版本事务、WAL 崩溃恢复、丰富索引类型、JSONB、窗口函数、CTE、表分区、流复制/逻辑复制、PostGIS/pgvector 扩展......几乎每个特性都值得单独成篇。
但正如本系列其他服务端篇(Redis 源码、Kafka broker、etcd)一样,"怎么用"之外的"内部为什么"才是排障的关键:
- 为什么高并发写入时 SELECT 不会阻塞?------ MVCC 多版本机制
- 为什么跑了几年的表越来越大、查询越来越慢?------ 膨胀与 autovacuum
- 为什么 UPDATE 一条记录会"复制"一份数据?------ 元组版本链
- 为什么同样的 SQL 有时走索引有时全表扫?------ 查询规划器与统计信息
- 为什么 max_connections 飙高后整库变慢?------ 进程模型与共享内存
一句话定位 :本篇是一篇通用关系型数据库服务端机制 篇,与 TDengine 构成工业数采平台的"关系库 + 时序库"双库底座。
1.3 适用场景
- 工业数采平台的元数据、配置、业务表存储(设备台账/点位配置/告警记录/工单)
- 需要 ACID 事务、复杂 JOIN、灵活查询的中等规模结构化数据
- 需要流复制高可用 / 逻辑复制做数据分发的中型系统
2. 核心概念速览
2.1 总体架构:进程模型(不是线程模型!)
┌─────────────────────────────────────┐
│ postmaster │ 主守护进程
│ (信号管理 / 子进程派生 / 崩溃重启) │
└──────────────┬──────────────────────┘
│ fork()
┌───────────────┼───────────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌────────────▼──────────────┐
│ backend │ │ background │ │ 辅助进程组 │
│ (每个连接 │ │ writer │ │ walwriter / checkpointer │
│ 一个进程) │ │ autovacuum │ │ logical replication │
└───────────┘ └─────────────┘ └───────────────────────────┘
│ 共享内存(shared memory)
│ buffer pool / WAL buffers / lock table / catalog cache
- 每连接一进程(fork 自 postmaster),不是线程模型;连接越多进程越多,内存消耗线性增长;
- 共享内存中:shared_buffers(数据页缓冲池)、WAL buffer、锁表、统计信息、系统目录缓存;
- 崩溃时 postmaster 负责重启 backend,不需要人工干预(相比 MySQL 更"自治")。
2.2 MVCC:多版本并发控制
MVCC 是 PostgreSQL 并发模型的核心,也是"读写不互斥"的物理基础:
- 每个数据行(tuple)头携带 xmin / xmax:创建它的事务 ID、删除/更新它的事务 ID;
- UPDATE 不是就地覆盖,而是插入新版本 + 在旧版本上标记 xmax,形成版本链(t_ctid 指向下一个版本);
- 每个事务开始时会获取一个快照(当前活跃事务列表 snapshot),据此判断哪些版本"可见";
- 读不阻塞写、写不阻塞读:SELECT 永远看到自己快照下的一致视图,即使其他事务正在修改同一行。
2.3 事务隔离级别(标准四档)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能(PG 不实现,等价 RC) | 可能 | 可能 | PG 中与 RC 相同 |
| READ COMMITTED(默认) | 不可能 | 可能 | 可能 | 每条语句取新快照,同一事务内两次 SELECT 可能结果不同 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(PG 的 SSI 可防部分) | 事务内所有语句用同一快照(PG 实现为快照隔离) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | PG 用 SSI(可串行化快照隔离)检测冲突并报错 |
关键点:PG 的 REPEATABLE READ 是"快照隔离",比很多数据库(如 MySQL InnoDB 默认 RR 的 next-key lock)实现更干净------无间隙锁、无幻读锁等待,冲突靠"提交时检测"而非"加锁阻塞"。
2.4 WAL(Write-Ahead Log)与崩溃恢复
- 所有修改先写 WAL(预写日志,默认 16MB 一段,pg_wal/ 目录),再写数据页;
- 数据页由 background writer / checkpointer 异步刷盘,checkpoint 时推进 pg_control 中的恢复点;
- 崩溃恢复 = 从最近 checkpoint 重放 WAL 直到日志末尾(REDO);
- 因此掉电不丢已提交事务,未提交事务自然回滚(用 WAL 中的 CLR 或 undo 链)。
2.5 锁机制:表锁与行锁
- 表锁(8 种粒度:ACCESS SHARE → ROW SHARE → ROW EXCLUSIVE → SHARE UPDATE EXCLUSIVE → SHARE → SHARE ROW EXCLUSIVE → EXCLUSIVE → ACCESS EXCLUSIVE),DDL 与 DML 之间互相排斥关系由锁强度矩阵决定;
- 行锁 :MVCC 下 UPDATE/DELETE 通过修改行的 xmax + 事务状态判断"谁在锁",SELECT FOR UPDATE 显式加锁;行锁信息存在元组头 + 事务状态里,不需要独立锁记录表(这是与 InnoDB 的最大差异之一);
- 死锁检测:后台 deadlock_timeout(默认 1s)触发检测,检测到即回滚一方事务。
2.6 查询执行流水线
SQL 文本
→ 词法/语法解析(parser 生成 parse tree)
→ 分析/重写(analyzer + rule rewriter,视图在此展开)
→ 规划(planner:生成多种候选执行计划并估算代价)
→ 执行(executor:按计划树执行,返回结果)
- 规划器依据统计信息(pg_statistic,由 ANALYZE 收集)估算每个算子的代价(顺序扫描、索引扫描、嵌套循环、哈希连接、排序等);
- EXPLAIN (ANALYZE, BUFFERS) 可以看到真实执行计划与行数估算偏差------估算偏差大是性能问题的头号来源。
2.7 膨胀与 VACUUM
- 因为 UPDATE 是"插新版本",旧版本不能立即删除------可能有旧快照仍在读取;
- 可回收的旧版本由 VACUUM 清理;autovacuum 守护进程按阈值自动触发;
- 清理不及时 → 表膨胀(dead tuple 堆积)→ 顺序扫描变慢、索引变大;
- VACUUM FULL 可物理收缩(但锁表、重写文件),普通 VACUUM 不锁表。
2.8 复制:流复制与逻辑复制
- 流复制(物理复制):WAL 实时传输到 standby,standby 重放;支持同步/异步、级联、pg_rewind 回退;主从自动/手动切换;
- 逻辑复制(逻辑解码):发布/订阅模型,可选择性复制表/行/列,可用于数据分发、迁移、异构同步(如到 Kafka 用 pgoutput 插件解码 WAL)。
3. API 说明与速查
3.1 SQL 高频语法速查(PostgreSQL 特色)
sql
-- DDL:建表(含约束、默认值、注释)
CREATE TABLE device (
id BIGSERIAL PRIMARY KEY, -- 自增主键
code VARCHAR(32) NOT NULL UNIQUE, -- 设备编码
line_id INTEGER REFERENCES line(id), -- 外键
status SMALLINT NOT NULL DEFAULT 0, -- 状态
meta JSONB DEFAULT '{}'::jsonb, -- 灵活扩展字段
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_device_status ON device(status); -- 普通索引
CREATE INDEX idx_device_meta ON device USING gin (meta); -- JSONB GIN 索引
CREATE INDEX idx_device_line_status ON device(line_id, status); -- 复合索引
-- DML:UPSERT(ON CONFLICT 是 PG 大杀器)
INSERT INTO device(code, line_id, status)
VALUES ('CNC-001', 1, 1)
ON CONFLICT (code)
DO UPDATE SET status = EXCLUDED.status, meta = EXCLUDED.meta;
-- CTE:WITH 递归(工艺 BOM 树/组织树)
WITH RECURSIVE sub AS (
SELECT id, parent_id FROM process_tree WHERE id = 100
UNION ALL
SELECT t.id, t.parent_id FROM process_tree t JOIN sub s ON t.parent_id = s.id
)
SELECT * FROM sub;
-- 窗口函数:设备按产线排名/滚动平均
SELECT code, line_id, load,
ROW_NUMBER() OVER (PARTITION BY line_id ORDER BY load DESC) AS rn,
AVG(load) OVER (PARTITION BY code ORDER BY created_at ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) AS avg5
FROM device_metrics;
-- 分区表:按时间分区(声明式分区)
CREATE TABLE alarm_log (
id BIGSERIAL, device_id INT, msg TEXT, occurred_at TIMESTAMPTZ
) PARTITION BY RANGE (occurred_at);
CREATE TABLE alarm_log_2026_10 PARTITION OF alarm_log
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
3.2 psql 常用命令
| 命令 | 作用 |
|---|---|
| \l / \dt / \d device | 列库/表/表结构 |
| \di / \dx | 索引 / 扩展 |
| \timing | 显示语句耗时 |
| \x | 行转列显示 |
| \copy device TO 'device.csv' CSV HEADER | 客户端导出(服务器端用 COPY) |
| \password | 改密码 |
| SHOW shared_buffers; | 看配置 |
3.3 EXPLAIN 输出解读
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT * FROM device WHERE status = 1 AND line_id = 2;
QUERY PLAN
-----------------------------------------------------------------------------
Index Scan using idx_device_line_status on device (actual time=0.021..0.043 rows=12 loops=1)
Index Cond: (line_id = 2)
Filter: (status = 1) -- 索引未完全覆盖条件的过滤
Buffers: shared hit=5 read=0 -- 全部命中共享缓冲
Planning Time: 0.2 ms
Execution Time: 0.05 ms
读计划三要素:节点类型 (Seq Scan/Index Scan/Index Only Scan/Nested Loop/Hash Join/Hash Aggregate...)、rows 估算 vs actual (偏差大 → 统计信息过期或谓词不可估)、Buffers 命中情况(read=0 说明都在内存)。
3.4 备份与恢复
bash
# 逻辑备份(跨版本迁移常用)
pg_dump -h host -U user -d mydb -Fc -f mydb.dump
pg_restore -h host -U user -d newdb mydb.dump
# 物理备份(一致快照,需配合 WAL 归档)
pg_basebackup -h host -U repluser -D /backup/base -X stream
3.5 编程接口速查
Go(推荐 pgx 或 database/sql + pgx 驱动)
Go
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/mydb?pool_max_conns=20")
if err != nil { panic(err) }
defer pool.Close()
// 查询多行
rows, _ := pool.Query(ctx, "SELECT code, status FROM device WHERE line_id=$1", 2)
defer rows.Close()
for rows.Next() {
var code string; var status int16
rows.Scan(&code, &status)
}
// 批量插入(copy 协议最快)
_, err = pool.CopyFrom(ctx,
pgx.Identifier{"device"},
[]string{"code", "line_id", "status"},
pgx.CopyFromRows([][]any{{"CNC-001", 1, 1}, {"CNC-002", 1, 0}}))
}
Python(psycopg3)
python
import psycopg
from psycopg.rows import dict_row
with psycopg.connect("host=localhost dbname=mydb user=user password=pass", row_factory=dict_row) as conn:
# 自动提交模式
conn.autocommit = True
with conn.cursor() as cur:
cur.execute("SELECT code, status FROM device WHERE line_id = %s", (2,))
for row in cur.fetchall():
print(row)
# 批量执行
cur.executemany(
"INSERT INTO device(code, line_id, status) VALUES (%s, %s, %s) ON CONFLICT (code) DO UPDATE SET status = EXCLUDED.status",
[("CNC-001", 1, 1), ("CNC-002", 1, 0)])
4. 详细使用:可运行示例
4.1 Docker 快速部署
bash
docker run -d --name pg16 \
-e POSTGRES_USER=admin -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=industry \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16
psql "postgres://admin:secret@localhost:5432/industry"
生产建议:数据目录挂宿主机磁盘并监控 IO;开启 pg_stat_statements 扩展(默认自带但需创建)。
4.2 事务隔离级别演示(同一张表看差异)
sql
-- 会话 A
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT count(*) FROM device; -- 假设 100 行
-- 会话 B(另开一个 psql)
BEGIN;
UPDATE device SET status = 9 WHERE id = 1; -- 未提交
-- 会话 A 再查(RC:看不到 B 未提交修改)
SELECT count(*) FROM device; -- 仍 100 行,不阻塞
SELECT * FROM device WHERE id = 1; -- 仍旧值,读不阻塞
-- 会话 B COMMIT;
-- 会话 A 同一事务内再查(RC 取新快照 → 看到新值;RR 则仍是旧值)
这是工业数采场景排查"读到旧数据"的经典现场:RC 下长事务内多次查询可能读到不同快照,需要时改用 REPEATABLE READ。
4.3 EXPLAIN 实战:找出慢 SQL
sql
-- 先确保统计信息新鲜
ANALYZE device;
-- 比较三种写法
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM device WHERE status = 1; -- 可能 Seq Scan
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM device WHERE status = 1 AND line_id = 2; -- 复合索引
-- 检查估算偏差
EXPLAIN SELECT * FROM device WHERE created_at > now() - interval '1 hour';
-- 若 rows 估算远小于 actual,说明统计信息过期 → ANALYZE / 提高 default_statistics_target
4.4 流复制主从搭建(3 条命令级)
bash
# 主库(postgresql.conf)
wal_level = replica
max_wal_senders = 5
# pg_hba.conf 增加:host replication repluser 192.168.1.0/24 md5
# 从库:物理备份 + 配置 recovery
pg_basebackup -h 主库IP -U repluser -D /var/lib/postgresql/data -X stream -R
# -R 自动生成 standby.signal 与 primary_conninfo,启动从库即可追主
4.5 逻辑复制发布/订阅(跨库/异构分发)
sql
-- 发布端
CREATE PUBLICATION pub_device FOR TABLE device, device_metrics;
-- 订阅端
CREATE SUBSCRIPTION sub_industry
CONNECTION 'host=主库IP dbname=industry user=admin password=secret'
PUBLICATION pub_device;
工业数采场景:把关系库中的设备配置变更逻辑复制到采集网关侧缓存/或下游 Kafka 桥,实现配置热同步。
4.6 工业数采双库落地(PostgreSQL + TDengine 分工)
| 数据 | 存储 | 说明 |
|---|---|---|
| 设备台账/点位配置/告警规则/用户权限 | PostgreSQL | 关系建模、事务更新、JOIN 查询 |
| 点位时序数据(温度/负载/良率) | TDengine | 高吞吐追加写、降采样、保留策略 |
| 实时最新值/命令队列/分布式锁 | Redis | 低延迟缓存 |
| 采集消息缓冲 | Kafka | 削峰、解耦、可回溯 |
典型链路:设备 → 采集网关 → Kafka → TDengine(时序) + 消费侧同步元数据到 PostgreSQL;Web/API 层用 PostgreSQL 提供设备树、告警记录、报表;TDengine 提供趋势曲线。
5. 底层实现剖析
5.1 元组(Tuple)与版本链
堆文件中的行布局(简化):
[ PageHeader (24B) ] [ ItemId 数组 ] [ tuple 数据区 ]
HeapTupleHeader:
t_xmin : 插入此版本的事务 ID
t_xmax : 删除/更新此版本的事务 ID(0 表示有效)
t_cid : 命令 ID(同事务内区分命令)
t_ctid : 指向下一个版本(自身 = 最新版本)
t_infomask: 位标志(含 xmin/xmax 是否已提交的 hint bit)
- 可见性判断:t_xmin 在快照活跃列表中 → 不可见;t_xmax 已提交且非本事务 → 该版本被删除;沿 t_ctid 链向下找最新可见版本;
- hint bit(HEAP_XMIN_COMMITTED 等)由首次访问写入,避免每次查事务提交状态(commit log);
- 长事务/长连接会阻止 old version 清理(autovacuum 只清理所有活跃快照都不可见的版本)。
5.2 WAL 记录与 Checkpoint
- 每条修改产生 WAL record(含 XLOG 记录头:lsn、事务、资源管理器类型 + 数据);
- 关键 LSN 概念:redo(最近 checkpoint 的 WAL 位置)、flush、write、insert;
- synchronous_commit = on 时,事务提交必须等 WAL flush 到磁盘才返回成功(这就是"提交不丢"的保证);
- checkpoint 由 checkpoint_timeout(默认 5min)或 max_wal_size(默认 1GB)触发,checkpoint 后旧 WAL 可回收;
- wal_level:minimal(不可做 PITR/复制)/ replica(流复制)/ logical(逻辑复制)。
5.3 查询规划器:代价模型
- 每个算子有启动代价 + 每行代价,估算公式与 seq_page_cost=1、random_page_cost=4、cpu_tuple_cost=0.01 等参数相关;
- 索引选择依据:选择性(pg_stats 中 n_distinct/most_common_vals/histogram_bounds);
- JOIN 三策略:Nested Loop(小表驱动,无索引时 O(N×M))、Hash Join(等值连接、无索引可用)、Merge Join(已排序输入);
- random_page_cost 调低(如 SSD 设 1.1)会更倾向走索引;统计信息过期 → 计划失真 → 需要 ANALYZE/VACUUM ANALYZE。
5.4 索引:B-tree 与 GIN 一瞥
- B-tree(默认):多路平衡树,页内键值有序,支持 = < > BETWEEN LIKE '前缀%';复合索引最左前缀原则;
- GIN(倒排,JSONB/全文检索/数组用):jsonb_path_ops 更快但功能少、jsonb_ops 支持 ? ?| ?& 运算符;插入时先入 pending list,gin_pending_list_limit 满后批量合并;
- GiST/SP-GiST :空间/范围数据;BRIN:按物理块区间存 min/max,适合时间戳类大表(分区表 + BRIN 是时序类查询利器);
- 覆盖索引/Index Only Scan:(line_id, status) 包含所需列时无需回表(需要 visibility map 支持)。
5.5 autovacuum 工作逻辑
- 后台进程周期(autovacuum_naptime=1min)扫描各表,比对 pg_stat_user_tables 中 dead tuple 数与阈值(autovacuum_vacuum_threshold=50 + autovacuum_vacuum_scale_factor=0.2 的百分比);
- 触发后对目标表执行 VACUUM + ANALYZE(autovacuum_analyze_* 同套阈值);
- 大表可单独配置:ALTER TABLE device SET (autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000);
- 禁止全局关闭 autovacuum------那是生产事故的常见源头。
6. 常错点/坑(25 条)
- 隔离级别误解:RC 下同一事务多次 SELECT 结果可能不同,长事务内重复读取需 RR;误以为"事务内一定一致"是排障翻车第一原因。
- 长事务/长连接不释放:一个未提交的长事务会阻止 autovacuum 清理它的旧版本 → 表膨胀;连接池要设置 idle_in_transaction_session_timeout。
- UPDATE 造成的膨胀被忽略:高频 UPDATE 点位状态表而不 VACUUM,表无限膨胀;低基数字段频繁更新应评估"状态放 Redis + 低频落库"。
- VACUUM FULL 当普通维护:VACUUM FULL 需要 ACCESS EXCLUSIVE 锁(阻塞读写),大表会停服;日常用普通 VACUUM/autovacuum。
- N+1 查询:循环里逐行查外键 → 千行变千次查询;应 JOIN 或 IN (...) 批量。
- 统计信息过期导致计划劣化:大批量写入/删除后不 ANALYZE,规划器选错计划;autovacuum 会顺手做 ANALYZE,别关。
- 复合索引列顺序错:(line_id, status) 不能直接服务于 WHERE status=1;最左前缀原则。
- 索引列上套函数:WHERE date(created_at) = '2026-10-13' 无法走普通索引;改范围条件 created_at >= '...' AND created_at < '...' 或建表达式索引。
- random_page_cost 未按磁盘调:机械盘默认 4 合理,SSD 应降(1.1~1.5),否则规划器过度回避索引。
- shared_buffers 乱调:设得太大(> 内存 1/3)反而引发频繁 evict 与内核双缓冲;常规 25% 左右 + 交给 OS page cache 兜底。
- max_connections 开太高:每连接一个 backend 进程,内存随连接数线性增长;用连接池(pgbouncer/pgxpool)而不是狂开连接。
- ON CONFLICT 依赖唯一约束:没有唯一索引/唯一约束时 ON CONFLICT 直接报错;先建 UNIQUE。
- JSONB 当万能字段滥用:无索引 JSONB 查询走全表扫;高频过滤字段应提取为普通列建索引。
- 时区与 TIMESTAMP 混用:TIMESTAMP WITHOUT TIME ZONE 存"本地时间"易错乱;统一 TIMESTAMPTZ 存储、展示时转时区。
- text vs varchar(n):PG 中两者性能几乎无差,text 无长度上限更省心;char(n) 有 padding 坑。
- 误以为 DELETE 立即释放空间:普通 DELETE 只是标记 dead tuple,空间要 VACUUM 才回收;大清理用 TRUNCATE 或分批 + VACUUM。
- 备份只做逻辑不做 WAL 归档:pg_dump 只能还原到备份点;要时间点恢复(PITR)必须 wal_level=replica + 归档 + 定期 pg_basebackup。
- 主从切换手工操作生疏:流复制故障恢复需要 pg_ctl promote/pg_rewind;生产应演练切换流程。
- CHECKPOINT 手工执行被当作优化:手工 checkpoint 只是把脏页提前刷盘,不能提升性能,反而造成 IO 尖峰。
- seq_scan 出现在"应该走索引"的查询:先看估算偏差、再看 enable_seqscan 是否被关闭、再看统计信息。
- LIMIT/OFFSET 深分页慢:OFFSET 1000000 仍扫描前面所有行;用 (id, created_at) 游标式分页(keyset pagination)。
- COUNT(*) 大表慢:PG 无"常量维护的行数",需 EXPLAIN 看是否走 index-only;真高频计数用物化视图/触发器计数表。
- 连接字符串密码明文/权限过大:pg_hba.conf 别开 trust;最小权限账号;pgpass 或环境变量管理凭据。
- 逻辑复制忘记 wal_level=logical:逻辑复制需要 logical 级别 WAL,且主库重启后复制槽继续推进(pg_replication_slots 要监控,防 WAL 撑爆磁盘)。
- default_statistics_target 太低:大表/多值分布字段估算差,可针对列 ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 1000。
7. 性能优化清单(10 条)
- 连接池化:应用侧 pgxpool/GORM 连接池 + 必要时刻 pgbouncer;连接数控制在 20~100 量级。
- 索引精修:只建查询真正用的索引(每多一个索引多一份写放大);复合索引按"等值在前、范围在后"排。
- 表分区:大日志表按时间范围分区,配合 BRIN 索引;分区裁剪避免跨区扫描。
- 统计信息新鲜:大量 DML 后触发 ANALYZE;大表调 autovacuum_* 参数。
- 写放大治理:低频更新的配置表放心走关系模型;高频点位状态走 Redis/TDengine,避免 UPDATE 风暴。
- 批量 DML:COPY / CopyFrom / 多值 INSERT 比逐行插入快一个数量级。
- WAL 与磁盘:WAL 放独立 SSD 或 IOPS 高的盘;synchronous_commit=off 只用于可容忍亚秒丢失的批处理会话。
- 查询重写:用 CTE 避免多次扫同一表;EXISTS 替代 IN 大子查询;FILTER 子句做多条件聚合。
- 只读副本分流:报表/大查询走 standby(流复制只读副本),主库专注写入。
- 监控闭环:pg_stat_statements + pg_stat_activity 抓慢 SQL 与长事务 + Prometheus postgres_exporter + 告警(连接数/膨胀率/复制延迟)。
8. 工业数采落地建议
关系库承担"平台侧业务数据",与 TDengine(时序数据面)、Redis(实时面)、Kafka(消息面)形成完整数据底座:
- 设备树与配置:line → device → point 三表关系建模,点位配置 JSONB 存扩展属性;配置变更走 ON CONFLICT UPSERT;
- 告警记录:按月分区大表 + (device_id, occurred_at) 复合索引,报表按窗口函数聚合;
- IPQC/工单:事务保证"检验记录 + 判定结果 + 设备状态"一致性更新;
- 配置热同步:逻辑复制把设备配置变更同步到边缘网关侧缓存;
- 连接规范:Go 侧 pgxpool + idle_in_transaction_session_timeout + 指标接入 Prometheus。
9. 总结(五要点)
- 模型:进程模型 + 共享内存,每连接一 backend;MVCC 让读写互不阻塞,这是并发能力的根。
- 正确性:WAL 预写日志保证崩溃不丢已提交事务;四档隔离级别中 PG 用快照隔离实现 RR/SERIALIZABLE。
- 性能真相:SQL 快慢由规划器基于统计信息决定;"EXPLAIN + 统计信息新鲜 + 索引匹配"是调优三板斧。
- 运维底线:别关 autovacuum、别让长事务悬挂、备份必须配 WAL 归档、连接用池化。
- 生态定位:与 TDengine 构成工业数采平台"关系库 + 时序库"双底座,与 Redis/Kafka 组成完整数据链路。
10. FAQ(10 条速查)
- PG 和 MySQL 怎么选? PG 功能全(索引/JSONB/CTE/分区/逻辑复制)、更重正确性;MySQL 生态成熟、运维资料多、分库分表工具链丰富。团队熟悉度 + 功能需求是决定性因素。
- 为什么 SELECT 和 UPDATE 互不阻塞? MVCC:UPDATE 生成新版本,SELECT 读自己快照下的旧版本,物理上互不干扰。
- 表膨胀了怎么办? 先查 pg_stat_user_tables.n_dead_tup 确认;等 autovacuum 或手动 VACUUM;物理收缩才用 VACUUM FULL(需停写窗口)。
- 查询一会儿快一会儿慢? 大概率是统计信息/缓存状态/连接池波动;用 EXPLAIN (ANALYZE, BUFFERS) 对比、看 pg_stat_statements 平均耗时。
- 怎么快速定位慢 SQL? pg_stat_statements 按 total_time 倒序;pg_stat_activity 查当前执行与等待事件。
- 连接数够吗? 看 pg_stat_activity 的 active/idle 分布与 max_connections;优先连接池而不是调大 max_connections。
- 主从同步落后怎么查? pg_stat_replication 的 write_lag/flush_lag/replay_lag;同步复制时还需 synchronous_standby_names 配置。
- 能不能把时序数据也放 PG? 可以(分区 + BRIN),但海量高频写入的吞吐/保留策略/降采样不如专用时序库;工业数采建议按双库分工。
- JSONB 和普通列哪个好? 扩展性强、schema 灵活用 JSONB(配 GIN);高频查询/约束/外键字段用普通列。
- 备份策略最低要求? wal_level=replica + 每日 pg_basebackup + 连续 WAL 归档(archive_mode=on),可支持任意时间点恢复;至少每周验证一次恢复演练。