PostgreSQL 关系型数据库
20 道题- 分类
- 数据库
- 题目数
- 20 道
1 PostgreSQL 的进程架构与内存结构
答案:
PostgreSQL 采用 多进程(Multi-Process)架构,每个客户端连接对应一个独立的 Backend Process,进程间通过共享内存通信。
进程模型:
graph TD
subgraph PM["Postgres Process Hierarchy"]
PMain["Postmaster
(主守护进程)"]
PMain --> SysLogger["SysLogger
日志采集"]
PMain --> BgWriter["BgWriter
后台写进程"]
PMain --> WalWriter["WalWriter
WAL 写入"]
PMain --> AutoVacuum["AutoVacuum
Worker"]
PMain --> StatsCollector["StatsCollector
统计信息"]
PMain --> Checkpointer["Checkpointer
检查点"]
PMain --> Archiver["Archiver
WAL 归档"]
PMain --> Backend1["Backend Process
Client Connection 1"]
PMain --> Backend2["Backend Process
Client Connection 2"]
PMain --> BackendN["Backend Process
Client Connection N"]
end
共享内存结构(Shared Memory):
| 区域 | 职责 | 配置参数 |
|---|---|---|
| Shared Buffers | 数据页缓存,减少磁盘 I/O | shared_buffers(物理内存 25%) |
| WAL Buffer | WAL 日志写入缓冲区 | wal_buffers(默认 16MB) |
| CLOG(Commit Log) | 事务提交状态位图 | 固定大小,不可配置 |
| Lock Space | 锁管理器状态(行锁/表锁) | max_locks_per_transaction |
| Proc Array | 活跃进程信息数组 | max_connections 决定大小 |
各辅助进程职责:
| 进程 | 职责 | 触发机制 |
|---|---|---|
| Postmaster | 监听连接、Fork Backend Process、崩溃后重启 | 守护进程常驻 |
| Checkpointer | 执行 Checkpoint,将脏页刷入磁盘 | checkpoint_timeout 或 max_wal_size |
| BgWriter | 持续将共享缓冲区中较少使用的脏页写入磁盘 | 周期性(默认 200ms) |
| WalWriter | 将 WAL Buffer 刷新到 WAL Segment | 事务提交或 WAL Buffer 满 |
| AutoVacuum Launcher | 调度 AutoVacuum Worker | autovacuum_naptime |
| Stats Collector | 收集查询与表访问统计信息 | 周期性(默认 500ms) |
| Archiver | 将已写完的 WAL Segment 复制到归档位置 | archive_command 配置 |
| SysLogger | 将 Postgres 日志重定向到文件 | logging_collector = on |
客户端连接处理流程:
1. Postmaster 监听 TCP 5432 端口
2. 客户端发起连接请求
3. Postmaster Fork 一个 Backend Process
4. Backend Process 执行客户端 SQL,直接返回结果
5. 连接关闭 → Backend Process 退出
2 PostgreSQL 的 Shared Buffers 与 OS Page Cache 协同机制
答案:
PostgreSQL 的 I/O 路径存在双层缓存:Shared Buffers(数据库内部缓存)和 OS Page Cache(操作系统文件缓存),二者协同但不重叠。
读写 I/O 路径:
graph TD
subgraph User["用户空间"]
SB["Shared Buffers
(PostgreSQL 管理)"]
WAL_BUF["WAL Buffer"]
end
subgraph Kernel["内核空间"]
PG_CACHE["OS Page Cache
(内核管理)"]
FS["File System"]
end
subgraph HW["硬件"]
DISK["磁盘 / SSD"]
end
SQL["SQL 查询"] --> SB
SB -->|"脏页刷入"| PG_CACHE
PG_CACHE -->|"writeback"| DISK
DISK -->|"预读 / 直接 I/O"| PG_CACHE
PG_CACHE -->|"缺页读取"| SB
WAL_BUF -->|"WAL 写入"| PG_CACHE
SB -->|"WAL 写入绕过 Shared Buffers"| WAL_BUF
Key Insight:
- PostgreSQL 的
shared_buffers使用 直接 I/O?不,PostgreSQL 使用 buffered I/O —— 数据先写入 OS Page Cache,再由内核刷入磁盘。但 WAL 写入通过wal_sync_method控制刷盘策略(open_datasync/fdatasync/fsync_writethrough)。 effective_cache_size用于查询规划器估算 OS Page Cache 中可用的缓存大小,非实际分配。该值不影响内存使用,仅影响查询规划器的索引选择。
Dirty Page 刷新机制对比:
| 组件 | 刷新策略 | 参数控制 |
|---|---|---|
| BgWriter | 持续扫描 Shared Buffers,将使用频率较低的脏页刷入 OS Cache | bgwriter_delay(默认 200ms) |
| Checkpointer | 周期性或 WAL 满时检查点,强制所有脏页刷入磁盘 | checkpoint_timeout(默认 5min) |
| WalWriter | WAL Buffer 达到阈值或事务提交时刷盘 | wal_writer_delay(默认 200ms) |
双层缓存常见问题:
| 问题 | 表现 | 解决方案 |
|---|---|---|
| shared_buffers 过大 | OS Page Cache 不足,导致频繁系统调用 | 控制在物理内存的 25%,不超过 8GB |
| 双缓存浪费 | PostgreSQL 写入 2 次(1 次 Shared Buffers + 1 次 OS Page Cache) | 使用 wal_sync_method 为 directio(仅 WAL) |
| 脏页积压 | BgWriter 跟不上写入速度 → Checkpointer 写入风暴 | 调大 bgwriter_lru_maxpages |
3 PostgreSQL 的 MVCC 实现原理
答案:
PostgreSQL 的 MVCC(Multi-Version Concurrency Control)通过为每个元组保留多个版本来实现读不阻塞写、写不阻塞读的并发控制机制。实现方式为 行级 Snapshot + 可见性规则,非 InnoDB 的 Undo Log 模式。
元组结构(Heap Tuple Header):
graph LR
subgraph Tuple["Heap Tuple"]
TH["Tuple Header (23 bytes)"]
Bitmap["Null Bitmap"]
UData["User Data"]
end
subgraph TH["Tuple Header"]
XMIN["xmin
创建事务 ID"]
XMAX["xmax
删除 / 锁定事务 ID"]
CID["cid
命令 ID(CommandId)"]
TVF["t_infomask
标志位"]
OID["oid
元组 OID(可选)"]
end
关键字段:
| 字段 | 大小 | 说明 |
|---|---|---|
xmin | 4 bytes | 创建该版本的事务 ID |
xmax | 4 bytes | 删除 / 锁定的事务 ID(0 表示未删除) |
cid | 4 bytes | 创建命令的 Command Counter |
t_ctid | 6 bytes | 指向当前/新版本的物理位置 (page, offset) |
t_infomask | 2 bytes | 元组状态标志(已提交/已回滚/冻结等) |
可见性判断规则(Snapshot):
Tuple 对于当前事务可见的条件:
1. xmin 已提交 ∧ (xmax == 0 ∨ xmax 未提交 ∨ xmax == 当前事务)
→ 可见(正常数据)
2. xmin 未提交 ∧ xmin == 当前事务
→ 可见(自己插入的未提交数据)
3. xmin 已提交 ∧ xmax 已提交 ∧ xmax ≠ 当前事务
→ 不可见(已被其他事务删除)
4. xmin 为冻结事务(Frozen XID)
→ 始终可见(用于 VACUUM FREEZE 后的表)
HOT(Heap-Only Tuple)更新优化:
当更新的列不影响索引且空闲空间充足时,PostgreSQL 使用 HOT 更新,新版本在同一 Page 内以 t_ctid 链式连接,无需更新索引:
graph LR
subgraph Page["Data Page"]
T1["Old Tuple
t_ctid → (0,2)"]
T2["New Tuple
t_ctid → (0,2) 自身"]
HP["Page Header
-> pd_special"]
end
T1 -.->|"单页内指针"| T2
死元组(Dead Tuple)的产生场景:
| 操作 | 产生死元组 | 说明 |
|---|---|---|
| UPDATE | 1 个死元组 | 旧版本标记为死亡 |
| DELETE | 1 个死元组 | 被删除版本标记为死亡 |
| INSERT | 无 | 新插入的为活元组 |
| SELECT … FOR UPDATE | 取决于实现 | 行锁不产生新元组版本 |
4 PostgreSQL 的事务隔离级别与 Snapshot 机制
答案:
PostgreSQL 支持 SQL 标准定义的四种事务隔离级别,但其实现核心基于 Snapshot,由 xmin/xmax 和事务状态决定每个事务能看到的元组版本。
隔离级别实现矩阵:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 |
|---|---|---|---|---|
| Read Uncommitted | 不允许(等同 RC) | 可能 | 可能 | SSI 实现,实际等价 Read Committed |
| Read Committed | 不允许 | 可能 | 可能 | 每条 SQL 获取新 Snapshot |
| Repeatable Read | 不允许 | 不允许 | 允许(PG 实现防幻读) | 整个事务使用同一 Snapshot |
| Serializable | 不允许 | 不允许 | 不允许 | Serializable Snapshot Isolation (SSI) |
Read Committed Snapshot 行为:
每条 SQL 语句开始前获取一个新的 Snapshot:
Transaction A:
BEGIN;
SELECT * FROM t WHERE id = 1; -- 获取 Snapshot S1
-- ... 等待 ...
SELECT * FROM t WHERE id = 1; -- 获取 Snapshot S2(与 S1 可能不同)
COMMIT;
Transaction B:
UPDATE t SET val = 'b' WHERE id = 1; -- 在 A 的两次查询之间
COMMIT;
A 的第一次查询看到旧值,第二次查询看到新值(不可重复读)。
Repeatable Read Snapshot 行为:
整个事务使用第一个 Snapshot:
Transaction A:
BEGIN;
SELECT * FROM t WHERE id = 1; -- 获取 Snapshot S1,整个事务使用此 Snapshot
-- ... 等待 ...
SELECT * FROM t WHERE id = 1; -- 使用同一 Snapshot S1
COMMIT;
A 的两次查询结果相同。但 A 尝试 UPDATE 或 DELETE 其他事务已修改的行时会收到 Serialization Failure 错误(可序列化冲突检测)。
Serializable 的 SSI 实现:
PostgreSQL 的 Serializable 隔离级别使用 Serializable Snapshot Isolation (SSI),在 RR 基础上检测 Read-Write Conflicts(读写冲突) 和 Write-Skew(写偏斜):
graph TD
SI["Snapshot Isolation
(T1, T2 各自 Snapshot)"]
SI --> RW["Read-Write Conflict
T1 读 → T2 写"]
SI --> WW["Write-Write Conflict
T1 写 → T2 写"]
RW --> DSSI["Dangerous Structure
(SI 中 RW 反向依赖)"]
DSSI --> C1["Commit T1"]
DSSI --> C2["Commit T2"]
C1 --> SSI_Abort["SSI 检测 → Abort T2
(Serialization Failure)"]
C2 --> SSI_Abort
SSI 各隔离级别的 Snapshot 获取时机:
| 隔离级别 | xmin 行为 | 冲突检测 |
|---|---|---|
| Read Committed | 每个 Statement 更新 xmin 范围 | 不加 SSI predicate locks |
| Repeatable Read | 事务级固定 xmin | 无 predicate locks |
| Serializable | 事务级固定 xmin | 加上 predicate locks 检测序列化冲突 |
5 PostgreSQL 的 VACUUM 与 Autovacuum 机制
答案:
VACUUM 负责回收死元组(Dead Tuple)占用空间、更新可见性映射(VM)中的标记、冻结事务 ID(Freeze),防止事务 ID 回卷(XID Wraparound)导致数据库不可用。
VACUUM 类型对比:
| 类型 | 是否回收空间给 OS | 是否阻塞读写 | 是否更新 FSM/VM | 是否冻结行 | 适用场景 |
|---|---|---|---|---|---|
| VACUUM(常规) | 否(留给本表复用) | 否(不影响并发读写) | 是 | 是 | 常规维护,定期执行 |
| VACUUM FULL | 是(收缩表文件大小) | 是(独占锁 AccessExclusiveLock) | 是 | 是 | 空间释放紧急场景(业务低峰执行) |
| VACUUM FREEZE | 否 | 否 | 是 | 强制冻结 | 防事务 ID 回卷的维护操作 |
| Autovacuum | 否 | 否(仅短暂阻塞 DDL) | 是 | 按需(autovacuum_freeze_max_age) | 自动调度,无需人工干预 |
VACUUM 执行流程:
graph TD
START["VACUUM 开始"]
START --> Scan["扫描表(从头至尾扫 Page)"]
Scan --> VM_Check["检查 VM(Visible Map)
已全可见 Page 跳过"]
VM_Check --> ScanContent["扫描 Page 内容"]
ScanContent --> DT["收集 Dead Tuple TID"]
DT --> Remove["清除索引条目
(每个索引单独扫描)"]
Remove --> Compact["整理 Page 内空间
(移动活元组)"]
Compact --> UpdateFSM["更新 FSM
(Free Space Map)"]
UpdateFSM --> UpdateVM["更新 VM
(新标记可用 Page)"]
UpdateVM --> Freeze["冻结 XID
(超过 freeze_min_age)"]
Freeze --> UpdateStats["更新 pg_class 统计"]
UpdateStats --> END["VACUUM 完成"]
Autovacuum 触发条件:
Autovacuum 的触发基于死元组数量与表大小的比例。每个 Worker 处理一个表,满足以下任一公式即触发:
触发公式:
pg_stat_user_tables.n_dead_tup > autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * pg_class.reltuples
简化:
死元组 > 50 + 0.1 * 表总行数
关键参数链:
| 参数 | 默认值 | 作用 |
|---|---|---|
autovacuum_max_workers | 3 | 同时运行的 Worker 数(每 Worker 一表) |
autovacuum_naptime | 1min | 检查需要处理的表的间隔 |
autovacuum_vacuum_threshold | 50 | 最小死元组触发数 |
autovacuum_vacuum_scale_factor | 0.1 | 表大小的 10% 死元组触发 |
autovacuum_vacuum_cost_delay | 2ms | 每次 I/O 成本后的暂停时间 |
autovacuum_vacuum_cost_limit | 200 | 每次暂停前的成本上限 |
autovacuum_freeze_max_age | 200 million | 最大未冻结事务数,接近触发防回卷 VACUUM |
事务 ID 回卷原理:
Transaction IDs 是一个 32 位环状计数器:
0 → 1 → 2 → ... → 2³²-1 → 0(开始新的环)
问题:
XID = 3 的事务看到的 XID = 2³²-3 实际上是未来的事务(因环状回卷)
→ 需要冻结(Freeze)将 XID 标记为 FrozenXID(=2),使其对任何事务都可见
防回卷策略:
1. 普通 VACUUM:冻结 age > autovacuum_freeze_min_age 的行
2. 防回卷 VACUUM:当表达到 autovacuum_freeze_max_age(默认 2 亿)
时会强制触发 VACUUM(即使 autovacuum 关闭)
3. 接近回卷时(1.5 亿 ~ 2 亿),PostgreSQL 发出 WARNING:
"database X may be shut down to avoid wraparound data loss"
6 PostgreSQL 的 B-Tree 索引与扩展索引类型
答案:
PostgreSQL 的默认索引类型是 B-Tree,此外提供 GiST、GIN、BRIN、Hash 等索引类型,覆盖从精确查询到全文搜索、空间查询到压缩索引的多样化场景。
B-Tree 索引结构:
graph TD
subgraph Root["Root Page
(metapage)"]
RP["(5,2)
(10,8)
(15,3)"]
end
subgraph Internal["Internal Pages"]
IP1["(1,5)
(3,2)"]
IP2["(6,10)
(8,4)"]
IP3["(11,15)
(13,7)"]
end
subgraph Leaf["Leaf Pages
(双向链表)"]
LP1["(1, tid1)
(2, tid2)
(3, tid3)"]
LP2["(5, tid4)
(6, tid5)
(8, tid6)"]
LP3["(10, tid7)
(11, tid8)
(13, tid9)"]
end
RP --> IP1
RP --> IP2
RP --> IP3
IP1 --> LP1
IP1 --> LP2
IP2 --> LP2
IP2 --> LP3
IP3 --> LP3
LP1 <-->|"← →"| LP2
LP2 <-->|"← →"| LP3
B-Tree 关键特性:
| 特性 | 说明 |
|---|---|
| 页大小 | 默认 8KB(BLCKSZ),可通过 --with-blocksize 编译时指定 |
| 填充因子 | fillfactor = 90(默认 90%),预留 10% 空间给 HOT 更新 |
| 分裂 | Page 满时分裂为两个 50% 填充的 Page |
| 删除 | Dead Index Tuple 由 VACUUM 回收,空间可复用 |
| 重复键 | 重复键在 Leaf Page 中连续存放,无溢出页 |
索引类型对比:
| 类型 | 算子支持 | 适用场景 | 大小特性 |
|---|---|---|---|
| B-Tree | <, <=, =, >=, >, BETWEEN, IN, IS NULL, LIKE 'abc%' | 精确匹配 + 范围查询 | 标准大小 |
| GIN(Generalized Inverted Index) | @>, <@, ?, `? | ,?&` | 全文搜索、JSONB、数组、tsvector |
| GiST(Generalized Search Tree) | `«, &<, &>, », - | -, ~=, @, ~, &&` | 空间数据、全文搜索(rank)、范围类型 |
| BRIN(Block Range Index) | <, <=, =, >=, >(同 B-Tree) | 时序数据、日志表(天然有序) | 极小(千分之一大小) |
| Hash | = | 等值查询 | 较小,不支持排序 |
| SP-GiST | `«, &<, », &>, - | -, ~=, @, ~, &&` | 空间分区、网络地址 |
各类索引适用场景详解:
-- B-Tree:默认索引,精确匹配和范围查询
CREATE INDEX idx_btree_time ON events (created_at);
SELECT * FROM events WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01';
-- BRIN:时序数据大表索引(比 B-Tree 小 100~1000 倍)
CREATE INDEX idx_brin_time ON events USING BRIN (created_at)
WITH (pages_per_range = 32);
SELECT * FROM events WHERE created_at BETWEEN '2025-01-01' AND '2025-01-02';
-- GIN:全文搜索
CREATE INDEX idx_gin_content ON documents USING GIN (to_tsvector('english', content));
-- GiST:空间查询
CREATE INDEX idx_gist_location ON locations USING GiST (coord);
SELECT * FROM locations WHERE coord <@ '((0,0),(10,10))'::box;
-- Hash:等值查询(B-Tree 也可满足,Hash 更小)
CREATE INDEX idx_hash_email ON users USING HASH (email);
索引选择决策树:
查询类型:
├── 精确匹配(=)+ 排序 → B-Tree
├── 范围查询(BETWEEN, >, <)→ B-Tree 或 BRIN(时序数据)
├── 全文搜索/JSONB/数组 → GIN
├── 空间/地理数据 → GiST 或 SP-GiST
└── 等值唯一查询(不长扫描)→ Hash
7 PostgreSQL 的查询执行流程与优化器
答案:
PostgreSQL 的查询执行流程从 SQL 文本到结果集需经过 Parser → Analyzer → Rewriter → Planner/Optimizer → Executor 五个阶段。
查询执行流程:
graph TD
SQL["SQL Query"]
SQL --> Parser["Parser
语法分析 → Parse Tree"]
Parser --> Analyzer["Analyzer
语义分析 → Query Tree"]
Analyzer --> Rewriter["Rewriter
规则重写 → Rewritten Query Tree"]
Rewriter --> Planner["Planner/Optimizer
→ Plan Tree"]
Planner --> Executor["Executor
执行 Plan → Tuple"]
Executor --> Result["Result Set"]
subgraph Optimizer["Optimizer 内部"]
P1["预处理
谓词下推 / 常量折叠"]
P2["生成候选路径
扫表方式 + 连接方式"]
P3["评估代价
基于 pg_class + pg_stats"]
P4["选择最优 Plan"]
P1 --> P2 --> P3 --> P4
end
Planner --> Optimizer
扫描方式(Scan Methods):
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Seq Scan | 顺序扫描全表 | 大表全量查询、无过滤条件 |
| Index Scan | B-Tree 索引定位 + 回表 | 选择性高(< 5%)的过滤 |
| Index Only Scan | 直接在索引列上返回,不回表 | 查询列全部在索引中 |
| Bitmap Scan | Bitmap Index Scan + Bitmap Heap Scan | 选择性中等(5%~20%) |
| Tid Scan | 按元组 ID(ctid)直接读取 | WHERE ctid = '(0,1)' |
| Subquery Scan | 子查询包装 | 子查询结果作为输入 |
| Function Scan | FROM 子句中的函数 | SELECT * FROM generate_series(1,10) |
连接方式(Join Methods):
| 方式 | 算法 | 复杂度 | 适用场景 |
|---|---|---|---|
| Nested Loop | 外层逐行匹配内层索引 | O(N × log M) | 小表驱动大表 + 内表有索引 |
| Hash Join | 构建 Hash 表后探测 | O(N + M) | 大表等值连接,无索引 |
| Merge Join | 排序后归并 | O(N log N + M log M) | 预排序数据,非等值连接 |
优化器代价模型参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
seq_page_cost | 1.0 | 顺序页读取代价(基准单位) |
random_page_cost | 4.0 | 随机页读取代价(SSD 设为 1.1) |
cpu_tuple_cost | 0.01 | 处理单行代价 |
cpu_index_tuple_cost | 0.005 | 处理索引行代价 |
cpu_operator_cost | 0.0025 | 执行单次运算符代价 |
effective_cache_size | 4GB | OS Page Cache 大小估计(影响索引选择) |
8 PostgreSQL 的执行计划解读与 EXPLAIN 使用
答案:
PostgreSQL 使用 EXPLAIN / EXPLAIN ANALYZE / EXPLAIN (BUFFERS, ANALYZE) 获取查询执行计划。
EXPLAIN 输出字段含义:
QUERY PLAN
---------------------------------------------------------------------------
Gather (cost=1000.00..15000.00 rows=5000 width=32)
Workers Planned: 2
-> Parallel Seq Scan on orders (cost=0.00..14000.00 rows=2083 width=32)
Filter: (status = 'pending')
| 字段 | 说明 |
|---|---|
cost=1000.00..15000.00 | 启动代价(1000)到总代价(15000) |
rows=5000 | 优化器估算的输出行数 |
width=32 | 输出行的平均宽度(字节) |
Workers Planned: 2 | 并行 Worker 数量 |
聚合统计字段:
Execution Time: 12.345 ms -- 总执行时间(ANALYZE 额外开销不计入)
Planning Time: 0.234 ms -- 规划时间
Buffers: shared hit=102 read=5 -- 缓存命中 102 页,磁盘读取 5 页
EXPLAIN 各选项对比:
| 选项 | 额外信息 | 是否执行 SQL | 适用场景 |
|---|---|---|---|
EXPLAIN | 估算的 Plan Tree | 否 | 快速查看计划结构 |
EXPLAIN ANALYZE | 真实执行时间、行数、Loop 次数 | 是 | 验证估算准确性 |
EXPLAIN (BUFFERS, ANALYZE) | 缓存命中率、磁盘读取数 | 是 | I/O 性能诊断 |
EXPLAIN (ANALYZE, TIMING OFF) | 关闭每次计时(减少 ANALYZE 开销) | 是 | 减少影响生产性能 |
EXPLAIN (FORMAT JSON) | JSON 格式输出 | 否 | 编程解析 |
常见索引使用问题诊断:
-- 检查预估 vs 实际行数差异
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE created_at > '2025-01-01';
-- 关键观察:rows=10000(预估)vs actual rows=500000(实际)
-- → 统计信息过旧,执行 ANALYZE
-- 检查是否有 Parallel Seq Scan 替代索引扫描
-- 如果随机页读取成本设置过高,优化器倾向 Seq Scan
SET random_page_cost = 1.1; -- SSD 环境下
执行计划异常诊断清单:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 预估行数与实际行数差异大 | 统计信息滞后 | ANALYZE table_name |
| 大量 Seq Scan 但在应该用索引时 | random_page_cost 过高(HDD 默认) | SSD 改为 1.1 |
| Nested Loop 全表扫描内表 | 连接列缺失索引 | 添加索引 |
| 排序操作 O(N²) | work_mem 不足导致磁盘排序 | 增大 work_mem |
| 多种候选 Plan 不稳定 | 参数嗅探 | 使用 plan_cache_mode = force_generic_plan |
9 PostgreSQL 的 WAL(Write-Ahead Log)机制
答案:
WAL(Write-Ahead Log)是 PostgreSQL 保证数据一致性和持久性的核心机制:在数据页写入磁盘之前,必须先确保对该数据页的修改日志已被写入持久化存储。
WAL 写入原则:
WAL Before Data(先写日志,再写数据):
1. 事务修改数据页(Shared Buffers 中)
2. 生成 REDO 日志记录 → WAL Buffer
3. 事务提交 → WalWriter 将 WAL Buffer 刷入 WAL Segment(触发 fsync)
4. Checkpointer 在 Checkpoint 后将脏数据页刷入磁盘
恢复原则:
数据库崩溃后,从最近 Checkpoint 开始重放所有 WAL
→ 保证已提交事务不丢失,未提交事务回滚
WAL Segment 结构:
| 组件 | 大小 | 说明 |
|---|---|---|
| WAL Segment | 16MB(默认) | 每个 WAL 文件 16MB |
| WAL Page | 8KB | Segment 内页大小,与 BLCKSZ 一致 |
| WAL Record | 可变 | 单条日志记录(如:一个 Page 修改) |
| WAL Record Header | 24 bytes | 记录类型、长度、检查点等信息 |
| XLOG Page Header | 20 bytes | Page 级头部,含 LSN 引用 |
| WAL Buffer | 16MB(默认) | 内存中缓存未刷入 Segment 的 WAL Record |
LSN(Log Sequence Number)的作用:
LSN 是 WAL 中的字节偏移量,用于唯一标识日志位置:
LSN = WAL Segment 偏移 + 该 Segment 内字节偏移
示例:0/16B3748 → Segment 偏移 0x16B0000, 页内偏移 0x3748
LSN 写入数据页头 pd_lsn 字段,用于:
- 恢复起点:Checkpoint 记录 WAL 重放起点 LSN
- 页级恢复:数据页头部的
pd_lsn确定哪些 WAL Record 需要重放到此页 - 复制进度:
pg_stat_replication中的sent_lsn/write_lsn/flush_lsn/replay_lsn
Checkpoint 机制:
graph LR
subgraph Timeline["Checkpoint 时间线"]
CP1["Checkpoint
T=0min"]
CP2["Checkpoint
T=5min"]
Dirty1["Dirty Page
刷入"]
CP1 --> Dirty1
Dirty1 --> T1["(写入数据文件)"]
T1 --> CP2
WAL["WAL 持续写入"]
WAL --> CP1
WAL --> CP2
end
Checkpoint 关键参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
checkpoint_timeout | 5min | 两次定时 Checkpoint 的最大间隔 |
max_wal_size | 1GB | Checkpoint 之间 WAL 最大增长量 |
min_wal_size | 80MB | 保留的最小 WAL 空间 |
checkpoint_completion_target | 0.5 | Checkpoint 完成时间占 checkpoint_timeout 的比例 |
checkpoint_warning | 30s | Checkpoint 频率超过此值记录警告 |
WAL 配置参数:
| 参数 | 说明 | 生产建议 |
|---|---|---|
wal_level | WAL 记录级别 | replica(默认)或 logical |
wal_compression | WAL 页压缩(PG15+ 优化) | on(兼容 WAL 归档和复制) |
wal_buffers | WAL 缓冲区大小 | shared_buffers 的 1/32,建议 64MB~256MB |
wal_sync_method | WAL 刷盘方法 | fdatasync(推荐)或 open_sync |
wal_writer_delay | WalWriter 刷盘频率 | 100ms~200ms |
10 PostgreSQL 的 Streaming Replication 架构
答案:
PostgreSQL 的 Streaming Replication 基于 WAL 的实时传输,Primary 以流式方式将 WAL 数据发送给 Standby,Standby 持续接收并重放 WAL。
复制架构:
graph LR
subgraph Primary["Primary Server"]
PG_P["PostgreSQL Primary"]
WAL_P["WAL
写入"]
WALSender["WAL Sender
(walsender process)"]
PG_P --> WAL_P --> WALSender
end
subgraph Standby["Standby Server"]
WALReceiver["WAL Receiver
(walreceiver process)"]
WALWrite["WAL
写入"]
Startup["Startup Process
(WAL Apply)"]
PG_S["PostgreSQL Standby
(read-only)"]
WALReceiver --> WALWrite --> Startup --> PG_S
end
WALSender -->|"TCP 流
WAL 数据"| WALReceiver
subgraph Config["复制参数"]
P1["primary_conninfo
连接 Primary 信息"]
P2["primary_slot_name
复制槽"]
P3["hot_standby = on
允许只读查询"]
P4["synchronous_standby_names
同步模式"]
end
复制角色与文件布局:
Primary:
$PGDATA/pg_wal/ -- WAL 段文件
$PGDATA/pg_hba.conf -- 允许复制连接
$PGDATA/postgresql.conf -- wal_level = replica 等参数
Standby:
$PGDATA/standby.signal -- Standby 模式标记文件(PG12+)
$PGDATA/pg_wal/ -- 接收的 WAL 段
$PGDATA/postgresql.conf -- hot_standby = on
流复制配置示例(Primary):
# postgresql.conf
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1024 # MB, 保留的 WAL 量(未使用复制槽时的兜底)
max_replication_slots = 10
hot_standby = on
# pg_hba.conf 添加复制用户
host replication replicator <standby_ip>/32 scram-sha-256
Standby 配置示例(PG12+):
# 创建基础备份
pg_basebackup -h <primary_ip> -D $PGDATA -U replicator -P --wal-method=stream
# standby.signal 文件
touch $PGDATA/standby.signal
# postgresql.conf
primary_conninfo = 'host=<primary_ip> port=5432 user=replicator password=xxx'
primary_slot_name = 'standby_1'
hot_standby = on
11 PostgreSQL 的同步复制与异步复制
答案:
PostgreSQL 支持同步复制和异步复制两种模式。异步模式下 Primary 不等待 Standby 确认;同步模式下 Primary 等待至少一个 Standby 确认 WAL 写入后才返回事务提交成功。
同步复制确认层级(synchronous_commit):
| 参数值 | Primary 行为 | 数据安全性 | 提交延迟 |
|---|---|---|---|
off | 不等待 WAL 写入本机 WAL,直接返回 | 最低(OS crash 丢失) | 最低 |
local | 等待本地 WAL 写入本地磁盘 | 标准(实例级持久化) | 低 |
remote_write | 等待本地 WAL 写入 + Standby 写入 OS Cache | 中(Standby OS crash 可能丢) | 中 |
on(默认) | 等待本地 WAL 写入 + Standby 写入磁盘 | 高(Standby 实例级持久化) | 较高 |
remote_apply | 等待本地 WAL 写入 + Standby 已应用 | 最高(Standby 可见该事务) | 最高 |
synchronous_standby_names 配置:
# 至少 ANY 1 个 Standby 同步确认
synchronous_standby_names = 'ANY 1 (*)'
# 精确指定同步 Standby(前 n 个)
synchronous_standby_names = 'FIRST 2 (standby1, standby2, standby3)'
# 不配置 = 异步复制(Standby 仅异步追赶)
同步复制对性能的影响:
事务提交延迟 = MAX(本地 WAL fsync, 远端 Standby WAL fsync RTT)
场景:
本地 fsync: ~2ms
跨 AZ (同区域): RTT ~1-3ms → 总延迟 ~3-5ms
跨区域 (同大陆): RTT ~10-30ms → 总延迟 ~12-32ms
跨洲: RTT ~150-300ms → 总延迟 ~152-302ms (不推荐)
同步复制配置权衡:
| 同步级别 | 数据可靠性 | 性能损失 | 推荐场景 |
|---|---|---|---|
| 异步 | 最多丢 ~1 个 WAL Segment (16MB) | 无 | 读多写少、分析型负载 |
| 同步(1 Standby, 同 AZ) | 1 个 Standby 确认 | +~1ms | 一般 OLTP |
| 同步(1 Standby, 跨 AZ) | AZ 级容灾 | +~3ms | 生产 OLTP,建议 |
| 同步(2 Standby, 跨 2 AZ) | 跨 AZ 容灾 | +~5ms | 金融级 |
12 PostgreSQL 的物理备份与 PITR 恢复
答案:
PostgreSQL 的物理备份与 PITR(Point-In-Time Recovery)基于 WAL 归档实现。基础备份 + 连续 WAL 归档 + 备份时间点可以恢复到任意时间点。
备份与恢复架构:
graph TD
subgraph Backup["备份体系"]
BaseBackup["pg_basebackup
全量基础备份"]
WALArchive["连续 WAL 归档
archive_command"]
end
subgraph Storage["备份存储"]
S3["S3 / GCS / Azure Blob"]
NFS["NFS / 本地挂载"]
end
subgraph Recovery["恢复"]
RestoreCmd["restore_command
从归档获取 WAL"]
PITR["recovery_target_time / XID"]
Promote["恢复完成后提升"]
end
BaseBackup --> S3
WALArchive --> S3
S3 --> RestoreCmd
NFS --> BaseBackup
NFS --> WALArchive
RestoreCmd --> PITR
PITR --> Promote
pg_basebackup 备份命令:
# 创建全量基础备份(实时流复制,不停服)
pg_basebackup -h <primary> -D /backup/base_20250801 \
-U replicator -P --wal-method=stream \
--format=tar --gzip --compress-level=9
# 增量备份(自备为基础之上,需 pgBACKrest 或 WAL-G)
WAL 归档配置:
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
archive_timeout = 60 # 强制归档间隔(秒)
PITR 恢复步骤:
# postgresql.conf (恢复实例)
restore_command = 'cp /archive/%f %p'
# recovery.conf (PG11-) 或 recovery.signal (PG12+)
recovery_target_time = '2025-08-01 14:30:00+08'
recovery_target_xid = '12345678' # 按事务 ID 恢复
recovery_target_lsn = '0/16B3748' # 按 LSN 恢复
recovery_target_inclusive = true # 是否包含目标点
全量备份方案对比:
| 工具 | 备份类型 | 增量支持 | 并行 | 压缩 | 加密 | 恢复速度 |
|---|---|---|---|---|---|---|
pg_basebackup | 物理全量 | 否 | 否 | tar+gz | 否 | 中(全量恢复) |
| pgBackRest | 物理全量/增量/差异 | 是 | 是(多线程) | zst/bz2/gz | 是(gpg) | 快(增量恢复) |
| WAL-G | 物理全量/增量 | 是 | 是 | lz4/zstd/brotli | 是(AES) | 快(增量恢复) |
| Barman | 物理全量/增量 | 是 | 否 | gz/bz2 | 是(gpg) | 中 |
验证备份可恢复性:
# pgBackRest 验证
pgbackrest --stanza=production check
# 从备份恢复到临时实例
pg_ctl -D /tmp/pg_restore start
# 验证数据完整性
pg_checksums -D /tmp/pg_restore
13 PostgreSQL 的表空间与分区表
答案:
PostgreSQL 的表空间(Tablespace)将数据库对象映射到文件系统路径。分区表(Partitioning)将大表拆分为多个物理子表。
表空间(Tablespace)管理:
-- 创建表空间
CREATE TABLESPACE fast_space LOCATION '/ssd/pg_tablespace';
CREATE TABLESPACE archive_space LOCATION '/hdd/archive_tablespace';
-- 在指定表空间创建对象
CREATE TABLE orders (...) TABLESPACE fast_space;
CREATE INDEX idx_orders_time ON orders (created_at) TABLESPACE fast_space;
-- 移动表空间
ALTER TABLE orders SET TABLESPACE archive_space;
-- 查看表空间大小
SELECT
spcname,
pg_size_pretty(pg_tablespace_size(spcname))
FROM pg_tablespace;
分区表(Table Partitioning):
PG10+ 支持声明式分区,PG11+ 支持 Hash 分区和 DEFAULT 分区,PG12+ 支持 FOR VALUES IN 列表分区,PG13+ 支持分区行触发 BEFORE/AFTER。
-- 范围分区(时序数据典型方案)
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC(10,2),
customer_id BIGINT
) PARTITION BY RANGE (created_at);
-- 创建分区子表(每月一个分区)
CREATE TABLE orders_202501 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01')
TABLESPACE fast_space;
CREATE TABLE orders_202502 PARTITION OF orders
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01')
TABLESPACE fast_space;
-- 列表分区
CREATE TABLE customers (
id BIGSERIAL,
region TEXT,
name TEXT
) PARTITION BY LIST (region);
CREATE TABLE customers_apac PARTITION OF customers
FOR VALUES IN ('CN', 'JP', 'KR');
CREATE TABLE customers_emea PARTITION OF customers
FOR VALUES IN ('DE', 'FR', 'UK');
-- Hash 分区(均匀分布)
CREATE TABLE sessions PARTITION BY HASH (user_id);
CREATE TABLE sessions_p0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);
分区维护操作:
-- 添加新分区
CREATE TABLE orders_202503 PARTITION OF orders
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
-- 分离分区(转换为独立表)
ALTER TABLE orders DETACH PARTITION orders_202501;
-- 删除分区
DROP TABLE orders_202501;
-- 手动附加分区
ALTER TABLE orders ATTACH PARTITION orders_202503
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
分区裁剪(Partition Pruning):
PostgreSQL 的查询规划器在匹配分区键过滤条件时自动裁剪无关分区:
-- 查询仅扫描 orders_202501 和 orders_202502
EXPLAIN SELECT * FROM orders
WHERE created_at BETWEEN '2025-01-15' AND '2025-02-15';
分区对比:
| 维度 | 声明式分区(Declarative) | 继承式分区(Inheritance) |
|---|---|---|
| 支持版本 | PG10+ | PG8+(旧方式) |
| 分区创建 | PARTITION OF | CREATE TABLE xxx INHERITS (parent) |
| 约束排除 | 自动 | 依赖 CHECK 约束 |
| INSERT 路由 | 自动路由到正确分区(PG12+) | 需要触发器 |
| UPDATE 跨分区 | PG11+ 支持 | 不支持 |
| 索引管理 | 支持分区索引 | 需在子表手动创建 |
| 推荐度 | 强烈推荐 | 不推荐 |
14 PostgreSQL 的锁机制与死锁检测
答案:
PostgreSQL 的锁体系分为表级锁、行级锁和**咨询锁(Advisory Lock)**三层。死锁检测通过 Deadlock Timeout 定期扫描等待图实现。
表级锁模式(从弱到强):
| 锁模式 | SQL 操作 | 冲突的锁模式 |
|---|---|---|
| ACCESS SHARE | SELECT | ACCESS EXCLUSIVE |
| ROW SHARE | SELECT FOR UPDATE/FOR SHARE | EXCLUSIVE, ACCESS EXCLUSIVE |
| ROW EXCLUSIVE | INSERT, UPDATE, DELETE | SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE |
| SHARE UPDATE EXCLUSIVE | VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE, SHARE, EXCLUSIVE, ACCESS EXCLUSIVE |
| SHARE | CREATE INDEX (非并发) | ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE |
| SHARE ROW EXCLUSIVE | CREATE TRIGGER | 几乎所有写锁 |
| EXCLUSIVE | REFRESH MATERIALIZED VIEW CONCURRENTLY | 几乎所有锁 |
| ACCESS EXCLUSIVE | DROP TABLE, TRUNCATE, VACUUM FULL | 所有锁(排他) |
行级锁:
| 锁类型 | 获取方式 | 冲突 |
|---|---|---|
| FOR UPDATE | SELECT … FOR UPDATE | 其他 FOR UPDATE / FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHARE |
| FOR NO KEY UPDATE | UPDATE/DELETE(默认) | 其他 FOR UPDATE / FOR SHARE / FOR KEY SHARE |
| FOR SHARE | SELECT … FOR SHARE | 其他 FOR UPDATE / FOR NO KEY UPDATE / FOR KEY SHARE |
| FOR KEY SHARE | 外键检查 | 其他 FOR UPDATE / FOR SHARE |
死锁检测机制:
graph TD
T1["Transaction 1
Holds: Row R1 Lock
Waits: Row R2 Lock"]
T2["Transaction 2
Holds: Row R2 Lock
Waits: Row R1 Lock"]
T1 -->|"等待"| R2["Row R2 Lock"]
T2 -->|"等待"| R1["Row R1 Lock"]
subgraph Deadlock["死锁条件"]
Cycle["T1 → R2 → T2 → R1 → T1
形成循环等待"]
end
Deadlock -> DDetect["Deadlock Timeout
(默认 1s) 触发检测"]
DDetect -> Abort["Abort T1(代价较小者)"]
死锁检测参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
deadlock_timeout | 1s | 死锁检测扫描间隔(设置越小检测越快,但 CPU 开销越大) |
log_lock_waits | off | 记录超过 deadlock_timeout 的锁等待日志 |
max_locks_per_transaction | 64 | 每事务可持有的最大锁数 |
锁监控查询:
-- 查看当前所有等待锁的关系
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.query AS blocked_query,
blocking_locks.pid AS blocking_pid,
blocking_activity.query AS blocking_query,
blocked_activity.wait_event_type || ': ' || blocked_activity.wait_event AS wait_event
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_locks.pid = blocked_activity.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_blocks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_locks.pid = blocking_activity.pid
WHERE NOT blocked_locks.granted;
15 PostgreSQL 的 FDW(Foreign Data Wrapper)
答案:
FDW(Foreign Data Wrapper)是 PostgreSQL 的 SQL/MED(SQL Management of External Data)实现,允许 PostgreSQL 查询外部数据源(其他 PG 实例、MySQL、CSV 文件、Kafka、MongoDB 等)。
FDW 架构:
graph TD
subgraph Local["本地 PostgreSQL"]
FDW["Foreign Data Wrapper
(extension)"]
FST["Foreign Server
(连接配置)"]
FM["Foreign Table
(外部表映射)"]
end
subgraph Remote["远程数据源"]
D1["PostgreSQL
mysql_fdw"]
D2["MySQL
mysql_fdw"]
D3["S3/CSV
file_fdw"]
D4["MongoDB
mongo_fdw"]
end
SQL["SELECT * FROM remote_table"] --> FM
FM --> FST
FST --> FDW
FDW --> D1
FDW --> D2
FDW --> D3
FDW --> D4
常用 FDW 扩展:
| FDW | 扩展名 | 适用数据源 | 是否支持写入 |
|---|---|---|---|
| postgres_fdw | postgres_fdw | 其他 PostgreSQL 实例 | 是(INSERT/UPDATE/DELETE) |
| mysql_fdw | mysql_fdw | MySQL/MariaDB | 是 |
| file_fdw | file_fdw | CSV/文本文件 | 否 |
| s3_fdw | s3_fdw | AWS S3 / 兼容对象存储 | 否 |
| mongo_fdw | mongo_fdw | MongoDB | 是 |
| odbc_fdw | odbc_fdw | 支持 ODBC 的数据源 | 是 |
| tds_fdw | tds_fdw | SQL Server / Sybase | 是 |
postgres_fdw 配置示例:
-- 1. 安装扩展
CREATE EXTENSION postgres_fdw;
-- 2. 创建外部服务器定义
CREATE SERVER pg_remote
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.1.100', port '5432', dbname 'appdb');
-- 3. 创建用户映射
CREATE USER MAPPING FOR local_user
SERVER pg_remote
OPTIONS (user 'remote_user', password 'secret');
-- 4. 创建外部表
CREATE FOREIGN TABLE remote_orders (
id BIGSERIAL,
customer_id BIGINT,
amount NUMERIC(10,2),
created_at TIMESTAMPTZ
)
SERVER pg_remote
OPTIONS (schema_name 'public', table_name 'orders');
-- 5. 查询(如同本地表)
SELECT * FROM remote_orders WHERE created_at > '2025-06-01';
FDW 性能优化实践:
| 优化手段 | 说明 | 配置 |
|---|---|---|
| 查询下推(Pushdown) | 将 WHERE 过滤下推到远端 | fetch_size 控制每次 fetch 行数 |
| 批量读取 | 增大每次网络传输的行数 | fdw_startup_cost(影响 Planner 选择) |
| 异步 Fetch | 并行预取下一批数据 | 需使用 async_capable(PG14+) |
| 连接复用 | 同一会话复用远端连接 | 默认复用 |
| 物化中间结果 | 对于频繁查询的外部数据,使用 CREATE MATERIALIZED VIEW | 定期 REFRESH MATERIALIZED VIEW |
16 PostgreSQL 的 JSONB 与全文搜索
答案:
PostgreSQL 提供 JSONB(二进制 JSON)和全文搜索(Full-Text Search, FTS)两种非结构化数据处理能力,均通过 GIN 索引加速。
JSONB 操作符:
| 操作符 | 说明 | 示例 |
|---|---|---|
-> | JSON 对象字段访问(返回 JSON) | data->'name' |
->> | JSON 对象字段访问(返回文本) | data->>'name' |
#> | 路径访问(返回 JSON) | data#>'{address,city}' |
#>> | 路径访问(返回文本) | data#>>'{address,city}' |
@> | 包含(JSON 左侧是否包含右侧) | data @> '{"status": "active"}' |
<@ | 被包含 | '{"status": "active"}' <@ data |
? | 是否存在键 | data ? 'email' |
| `? | ` | 是否存在任一键 |
?& | 是否存在所有键 | data ?& ARRAY['name', 'age'] |
| ` | ` | |
- | 删除键 | data - 'temporary_field' |
JSONB 索引:
-- 默认 GIN 索引(支持 @>, ?, ?|, ?&)
CREATE INDEX idx_users_data ON users USING GIN (data);
-- 带路径的 GIN 索引(JSONPATH 查询优化,PG13+)
CREATE INDEX idx_users_data_path ON users USING GIN (data jsonb_path_ops);
-- B-Tree 索引用于 `->>` 操作(仅特定列)
CREATE INDEX idx_users_email ON users ((data->>'email'));
全文搜索(FTS)核心概念:
graph TD
Raw["原始文档
'The quick brown fox jumps...'"]
Parser["Parser
分词器"]
Tokens["Token
'The', 'quick', 'brown', ..."]
Dict["Dictionary
词典"]
Raw --> Parser
Parser --> Tokens
Tokens --> Dict
subgraph Dictionary["词典处理"]
Stop["Stop Words 移除
(the, a, an, of, ...)"]
Stem["Stemming 词干化
(jumps → jump)"]
end
Tokens --> Stop --> Stem
Stem --> Tsvector["tsvector
'brown':3 'fox':4 'jump':6 'quick':2"]
FTS 配置示例:
-- 创建索引(to_tsvector + GIN)
CREATE INDEX idx_docs_content ON documents
USING GIN (to_tsvector('english', content));
-- 搜索
SELECT title, ts_headline('english', content, query) AS highlight
FROM documents,
plainto_tsquery('english', 'quick brown fox') AS query
WHERE to_tsvector('english', content) @@ query
ORDER BY ts_rank(to_tsvector('english', content), query) DESC
LIMIT 10;
-- 中文分词(需 zhparser 或 jieba 扩展)
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese
ADD MAPPING FOR n,v,a,i,e,l WITH simple;
CREATE INDEX idx_docs_cn_content ON documents
USING GIN (to_tsvector('chinese', content));
JSONB + FTS 组合示例:
-- 文档表(关系型 + JSONB + FTS)
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
metadata JSONB NOT NULL, -- 结构化元数据
content TEXT NOT NULL, -- 全文内容
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_docs_metadata ON documents USING GIN (metadata);
CREATE INDEX idx_docs_fts ON documents
USING GIN (to_tsvector('english', content));
-- 查询:结构化 + 全文混合检索
SELECT id, metadata->>'title' AS title
FROM documents
WHERE metadata @> '{"status": "published", "category": "tech"}'
AND to_tsvector('english', content) @@ plainto_tsquery('english', 'distributed database')
AND created_at > '2025-01-01';
17 PostgreSQL 的 Connection Pooling 原理
答案:
PostgreSQL 采用进程模型,每个连接对应一个独立的 Backend Process。在高并发场景下,连接数过多会导致内存耗尽和上下文切换开销剧增,必须引入连接池(Connection Pooling)机制。
无连接池的问题:
无连接池时:
5000 个应用连接 → 5000 个 Backend Process
每进程内存 = shared_buffers 中无此连接额外开销
但:每进程 ~10MB 栈 + 私有内存
5000 × 10MB ≈ 50GB 内存 → OOM
解决方案:
sidecar 连接池 → 5000 个应用连接 → 连接池中的 50 个 Server 连接 → 50 个 Backend Process
50 × 10MB = 500MB → 可接受
连接池架构对比:
| 连接池 | 语言 | 多线程 | 协议 | 特点 |
|---|---|---|---|---|
| PgBouncer | C | 单进程多线程 | PostgreSQL Wire Protocol | 轻量、稳定、事务池模式 |
| Pgpool-II | C | 多进程 | PostgreSQL Wire Protocol | 读写分离 + 连接池 + 负载均衡 |
| Odyssey | C | 多线程 | PostgreSQL Wire Protocol | 高性能、查询路由、SCRAM |
| pgagroal | C | 多线程 | PostgreSQL Wire Protocol | 高性能、内存安全 |
| Application-Level | 应用语言 | — | JDBC/HikariCP | HikariCP 适用于 Java |
PgBouncer 池化模式:
| 模式 | 行为 | 适用场景 | 是否支持 SET/LISTEN |
|---|---|---|---|
| session | 一个客户端连接占用一个服务器连接,持续到断开 | 需要保持会话状态 | 是 |
| transaction | 仅在事务期间占用服务器连接,事务结束即归还 | 短事务、无状态服务 | 否 |
| statement | 仅在单条语句期间占用(不跨语句) | 极端短查询 | 否 |
18 PostgreSQL 的配置参数调优框架
答案:
PostgreSQL 的配置参数根据生效范围分为三组:影响全局的 postgresql.conf 级别参数、影响当前会话的 Session 级参数、影响特定查询的 Query 级参数。
参数生效层级:
graph TD
PG_CONF["postgresql.conf
(全局默认)"]
CMD_LINE["命令行启动参数
-c shared_buffers=4GB"]
DB_ALTER["ALTER DATABASE
(数据库级)"]
USER_ALTER["ALTER ROLE
(用户级)"]
SESSION_SET["SET statement
(会话级)"]
PG_CONF --> CMD_LINE
CMD_LINE --> DB_ALTER
DB_ALTER --> USER_ALTER
USER_ALTER --> SESSION_SET
参数优先顺序:SET > ALTER ROLE > ALTER DATABASE > postgresql.conf > 编译默认值。
分类调优参数:
内存参数:
| 参数 | 推荐计算公式 | 说明 |
|---|---|---|
shared_buffers | 物理内存 25%,上限 8GB | 数据页缓存 |
effective_cache_size | 物理内存 75% | OS Page Cache 估计值,仅影响规划器 |
work_mem | (内存 - shared_buffers) / (max_connections × 2) | 排序 / 哈希操作内存 |
maintenance_work_mem | 物理内存 10%,上限 2GB | VACUUM / CREATE INDEX |
wal_buffers | shared_buffers 的 1/32,最小 64MB | WAL 缓冲区 |
I/O 参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
random_page_cost | SSD: 1.1, HDD: 4.0 | 随机页读取成本 |
effective_io_concurrency | SSD: 200, HDD: 2 | 并发 I/O 请求 |
wal_compression | on | WAL 页压缩 |
wal_sync_method | fdatasync | WAL 刷盘方法 |
checkpoint_completion_target | 0.9 | Checkpoint 刷盘摊平 |
min_wal_size | 2GB | WAL 空间下限 |
max_wal_size | 8GB | WAL 空间上限 |
并发参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
max_connections | 配合连接池 ≤ 200 | 最大客户端连接数 |
max_worker_processes | CPU 核数 × 2 | 后台 Worker 上限 |
max_parallel_workers | CPU 核数 | 并行 Worker 上限 |
max_parallel_workers_per_gather | CPU 核数 / 2 | 单查询并行度 |
parallel_tuple_cost | 0.01 | 并行元组传输成本 |
parallel_setup_cost | 1000 | 并行启动成本 |
Autovacuum 参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
autovacuum_max_workers | 4~8 | 最大 Worker 数 |
autovacuum_naptime | 30s~60s | 调度间隔 |
autovacuum_vacuum_cost_limit | 2000 | I/O 节流上限(提升可减少 VACUUM 延迟) |
autovacuum_vacuum_cost_delay | 2ms | I/O 节流间隔 |
autovacuum_freeze_max_age | 2 亿 | 防回卷 VACUUM 触发阈值 |
19 PostgreSQL 的 EXPLAIN 高级技巧与性能诊断
答案:
EXPLAIN 提供 PostgreSQL 查询性能诊断的第一手信息。结合 pg_stat_* 系统视图和 pg_stat_statements 扩展进行持续性能监控。
EXPLAIN (ANALYZE, BUFFERS, SETTINGS):
-- 完整诊断级 EXPLAIN
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, TIMING, WAL)
SELECT o.*, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2025-01-01'
ORDER BY o.created_at DESC;
输出关键检查点:
| 检查项 | 正常 | 异常 |
|---|---|---|
rows vs actual rows | 偏差 < 2x | 偏差 > 10x → 统计信息过旧,执行 ANALYZE |
Buffers: shared hit=100 read=1 | hit » read(95%+ 命中) | read » hit → 缓存不足或全表扫描 |
Planning Time | < 10ms | > 100ms → 复杂视图/子查询膨胀 |
Execution Time | 符合预期 | 异常高 → 检查 I/O 或锁等待 |
Workers Launched | CPU 密集型查询启动并行 | 并行未启动 → 检查 max_parallel_workers_per_gather |
pg_stat_statements 查询分析:
-- 查看最耗时的前 10 条查询
SELECT
queryid,
LEFT(query, 80) AS query_short,
total_exec_time / calls AS avg_time_ms,
rows / calls AS avg_rows,
shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0) AS hit_ratio,
calls
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat%'
ORDER BY total_exec_time DESC
LIMIT 10;
等待事件分析(PG 等待事件体系):
-- 查看当前会话的等待事件
SELECT
pid,
wait_event_type,
wait_event,
state,
query,
AGE(clock_timestamp(), query_start) AS query_duration
FROM pg_stat_activity
WHERE state != 'idle'
AND wait_event IS NOT NULL;
| 等待事件类型 | 常见事件 | 可能原因 | 解决方向 |
|---|---|---|---|
| Lock | relation、transactionid | 锁冲突、死锁等待 | 检查 pg_locks,优化事务顺序 |
| IO | DataFileRead、WALWrite | 磁盘 I/O 瓶颈、shared_buffers 不足 | 检查 I/O 延迟,增加 shared_buffers |
| Client | ClientRead、ClientWrite | 应用端慢,网络延迟 | 检查应用和网络 |
| Activity | ArchiverMain、BgWriterMain | 后台进程正常活动 | 无需处理 |
| Timeout | PgSleep | pg_sleep() 调用 | 检查应用逻辑 |
索引使用诊断:
-- 检查未使用的索引
SELECT
schemaname,
tablename,
indexname,
idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelid NOT IN (
SELECT indexrelid FROM pg_constraint WHERE conindid = indexrelid
);
-- 检查冗余索引
SELECT
pg_size_pretty(SUM(pg_relation_size(indexrelid))) AS total_size
FROM pg_index
WHERE indrelid = 'orders'::regclass;
20 PostgreSQL 的扩展生态(Contrib Extensions)
答案:
PostgreSQL 拥有丰富的扩展生态,覆盖功能增强、性能诊断、数据集成、监控发布等场景。
必装扩展:
| 扩展 | 功能 | 启用方式 |
|---|---|---|
pg_stat_statements | 查询性能统计 | shared_preload_libraries |
pg_buffercache | Shared Buffers 内容分析 | CREATE EXTENSION |
pgstattuple | 表/索引膨胀分析 | CREATE EXTENSION |
pg_visibility | 可见性映射(VM)分析 | CREATE EXTENSION |
pageinspect | 数据页内容查看 | CREATE EXTENSION |
pg_walinspect | WAL 内容分析(PG13+) | CREATE EXTENSION |
auto_explain | 自动记录慢查询执行计划 | shared_preload_libraries |
pg_prewarm | 预热缓存 | CREATE EXTENSION |
bloom | Bloom Filter 索引 | CREATE EXTENSION |
postgres_fdw | 外部表访问 | CREATE EXTENSION |
功能扩展:
| 扩展 | 功能 | 适用场景 |
|---|---|---|
uuid-ossp / pgcrypto | UUID 生成 | 主键、分布式 ID |
pg_trgm | 相似度搜索(模糊匹配) | LIKE '%something%' 的 GIN 索引加速 |
fuzzystrmatch | 字符串近似匹配 | soundex()、levenshtein()、metaphone() |
hstore | 键值对存储(JSONB 前身) | 简单键值数据 |
ltree | 树状路径数据类型 | 分类/目录层级 |
citext | 大小写不敏感字符串 | 用户搜索、标签 |
pg_partman | 分区表自动管理 | 时间序列分区维护 |
pgaudit | 审计日志 | 合规审计需求 |
pgfincore | 分析 Page Cache 命中 | 性能诊断 |
postgis | 地理空间数据 | GIS 应用 |
auto_explain 配置:
-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements, auto_explain'
-- auto_explain 参数
auto_explain.log_min_duration = '1s' -- 记录超过 1s 的查询
auto_explain.log_analyze = on -- 输出 ANALYZE 信息
auto_explain.log_buffers = on -- 输出 Buffer 信息
auto_explain.log_nested_statements = on -- 记录嵌套语句
auto_explain.log_timing = on -- 记录时间
auto_explain.log_verbose = on -- 输出完整信息
auto_explain.sample_rate = 1.0 -- 采样率(生产可设 0.1)
PostGIS:
CREATE EXTENSION postgis;
-- 创建空间表
CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY,
name TEXT,
geom GEOMETRY(Point, 4326) -- WGS84 经纬度坐标系
);
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);
-- 距离查询
SELECT name FROM locations
WHERE ST_DWithin(geom, ST_MakePoint(116.4074, 39.9042), 0.05);
-- 经纬度 (116.4, 39.9) 附近约 5km 的位置