PostgreSQL实例进程:从启动到运行的完整故事
最近在深入研究PostgreSQL的内部机制,发现这玩意儿的进程架构真的挺有意思的。今天就和大家聊聊PostgreSQL实例是怎么运作的,从按下启动按钮到处理查询的整个过程。
先说说整体架构
我一开始接触PostgreSQL的时候,总以为它就是一个单体程序在跑。后来用ps命令一看,好家伙,一堆postgres进程,当时就懵了。研究了一阵子才搞明白,PostgreSQL其实更像一个工厂系统。
主进程] end subgraph backend["查询执行"] BE1[Backend 1] BE2[Backend 2] BE3[Backend N] end subgraph memory["共享内存"] SB[Shared Buffers
数据缓存] WB[WAL Buffers
日志缓冲] CL[CLOG
事务状态] end subgraph bgworkers["后台维护"] BG[Background Writer] CK[Checkpointer] WW[WAL Writer] AV[AutoVacuum] end subgraph storage["存储"] DF[(数据文件)] WF[(WAL日志)] end C1 --> PM C2 --> PM C3 --> PM PM --> BE1 PM --> BE2 PM --> BE3 BE1 --> SB BE2 --> SB BE3 --> SB BE1 --> WB BG --> SB CK --> SB WW --> WB BG --> DF CK --> DF WW --> WF AV --> DF style PM fill:#ff6b6b,stroke:#333,stroke-width:3px style SB fill:#4ecdc4,stroke:#333,stroke-width:2px style WB fill:#ffe66d,stroke:#333,stroke-width:2px
这里面有几个关键角色:
Postmaster是老大,负责总调度。它就像厂长一样,监听5432端口等客户来连,每来一个连接就fork出一个Backend进程去服务。我当时好奇为什么不用线程,后来才知道这样做的好处是进程间隔离得好,一个进程崩了不影响其他的。
Backend进程是真正干活的。一个客户端连接对应一个Backend,这个设计挺有意思的。每个Backend有自己独立的内存空间,负责解析SQL、优化查询、执行操作。不过这也意味着如果你的max_connections设置成500,理论上就可能有500个Backend进程,吃内存还是挺猛的。
后台进程这帮家伙是维护团队,各司其职:
- Background Writer定时扫描脏页异步写入磁盘
- Checkpointer创建检查点,把所有脏页强制刷盘
- WAL Writer负责把日志缓冲刷到磁盘
- AutoVacuum清理死元组,这个后面会详细说
共享内存就像工厂的仓库,所有进程都能访问。主要是Shared Buffers存数据页,WAL Buffers存日志,还有一些锁表、事务状态之类的东西。
启动过程是怎么回事
我们来看看执行pg_ctl start之后发生了什么。
共享内存 Note over Admin,BG: 阶段4: 崩溃恢复检查 PM->>WAL: 检查 pg_control 文件 WAL-->>PM: 返回数据库状态 alt 数据库异常关闭 Note right of PM: 需要恢复! PM->>WAL: 读取WAL日志 PM->>SM: 重放WAL记录 Note right of SM: 恢复到一致状态 else 数据库正常关闭 Note right of PM: 无需恢复 end Note over Admin,BG: 阶段5: 启动后台进程 PM->>BG: fork() Background Writer activate BG Note right of BG: 每200ms扫描 PM->>BG: fork() Checkpointer Note right of BG: 每10min检查点 PM->>BG: fork() WAL Writer Note right of BG: 每200ms刷盘 PM->>BG: fork() AutoVacuum Launcher Note right of BG: 每1min检查 PM->>BG: fork() Stats Collector Note right of BG: 收集统计 Note over Admin,BG: 阶段6: 准备就绪 PM->>OS: 监听端口 5432 Note over PM: 开始接受连接 PM-->>Admin: 启动完成 deactivate SM deactivate BG deactivate PM
这个过程其实有几个点值得注意:
启动的时候会读两个重要配置文件,postgresql.conf是主配置,pg_hba.conf管认证规则。我之前改了postgresql.conf忘了reload,查了半天为什么不生效,后来才发现有些参数必须重启才行。
共享内存的分配比较关键。Postmaster会一次性分配好所有共享内存,这就是为什么shared_buffers改了必须重启的原因。我当时在一台16GB的机器上把shared_buffers设成了12GB,结果启动都启动不起来,操作系统直接拒绝分配这么大的共享内存。后来查资料才知道,一般建议设置为物理内存的25-40%就够了。
如果上次是异常关闭,启动时会做崩溃恢复。Postmaster会检查pg_control文件,发现状态不对就会去读WAL日志重放。这个机制挺靠谱的,我经历过几次服务器断电,重启后PostgreSQL都能自动恢复过来。
启动完后台进程的顺序也是有讲究的。Background Writer和Checkpointer是写数据的,WAL Writer是写日志的,AutoVacuum是清理垃圾的。这些进程都是从Postmaster fork出来的,它们会一直在后台运行,直到数据库关闭。
验证启动是否成功,可以看看进程列表:
bash
ps -ef | grep postgres
# 你会看到类似这样的输出
postgres 1234 1 Ss 09:00 postgres
postgres 1235 1234 Ss 09:00 postgres: checkpointer
postgres 1236 1234 Ss 09:00 postgres: background writer
postgres 1237 1234 Ss 09:00 postgres: walwriter
postgres 1238 1234 Ss 09:00 postgres: autovacuum launcher
第一个进程就是Postmaster,后面的都是它的子进程。
内存这块真的很重要
内存配置是PostgreSQL性能的关键,我当时踩了不少坑。先看看内存的整体结构:
数据页缓存
推荐: RAM的25-40%
示例: 4GB (16GB服务器)"] WB["WAL Buffers
事务日志缓冲
推荐: 16MB
或 shared_buffers的1/32"] CL["CLOG
事务提交日志
记录事务提交状态"] LS["Lock Space
锁管理表
管理各种锁"] PA["Proc Array
进程数组
活动进程信息"] end subgraph local1["Backend 1 本地内存"] WM1["work_mem
查询操作
排序/哈希/聚合
每个操作独立!"] MM1["maintenance_work_mem
维护操作
VACUUM/CREATE INDEX"] TB1["temp_buffers
临时表缓冲"] end subgraph local2["Backend 2 本地内存"] WM2["work_mem"] MM2["maintenance_work_mem"] TB2["temp_buffers"] end subgraph os["操作系统层"] OC["OS Page Cache
文件系统缓存
effective_cache_size
推荐: RAM的50-75%"] end SB --> OC WB --> OC WM1 -.使用.-> SB WM2 -.使用.-> SB style SB fill:#4ecdc4,stroke:#333,stroke-width:3px style WB fill:#ffe66d,stroke:#333,stroke-width:3px style WM1 fill:#f38181,stroke:#333,stroke-width:2px style WM2 fill:#f38181,stroke:#333,stroke-width:2px style OC fill:#a8e6cf,stroke:#333,stroke-width:2px
几个容易搞错的地方
Shared Buffers不是越大越好
我最开始以为这个越大性能越好,就把32GB服务器的shared_buffers设成了24GB。结果发现checkpoint的时候IO压力巨大,而且启动时间变得超长。后来研究才明白,PostgreSQL的设计是依赖操作系统缓存的,shared_buffers设置太大反而会和OS cache抢内存。
一般的建议是物理内存的25-40%,比如16GB的机器设4GB就挺合适:
conf
# 16GB服务器的配置
shared_buffers = 4GB # 25% of 16GB
effective_cache_size = 12GB # 75% of 16GB,这个只是告诉优化器的,不实际分配
work_mem是个大坑
这个参数我被坑惨了。work_mem是每个查询操作使用的内存,注意是每个操作!一个复杂查询可能有多个排序、哈希操作,每个都会用work_mem。
假设你设置了work_mem = 256MB,max_connections = 500,某个查询有5个操作同时跑,那就是256MB × 5 = 1.28GB,如果有100个这样的并发查询,理论上就需要128GB内存!服务器直接OOM。
我的建议是保守设置,比如16MB:
conf
work_mem = 16MB # 保守设置
# 计算公式参考:
# (RAM - shared_buffers) / (max_connections × 预期并发操作数)
# = (16GB - 4GB) / (200 × 3) ≈ 20MB
如果某些查询确实需要更大的work_mem,可以会话级别临时调整:
sql
SET work_mem = '256MB'; -- 仅对当前会话生效
SELECT * FROM huge_table ORDER BY ...;
effective_cache_size容易被忽略
这个参数不分配内存,但它告诉查询优化器操作系统大概有多少缓存可用。设置合理的话,优化器会做出更好的查询计划。一般设置为物理内存的50-75%。
怎么知道配置合不合理
监控缓存命中率是个好办法:
sql
SELECT
sum(heap_blks_read) as disk_reads,
sum(heap_blks_hit) as buffer_hits,
round(
sum(heap_blks_hit)::numeric /
nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100,
2
) as hit_ratio_percent
FROM pg_statio_user_tables;
一般来说,hit_ratio_percent应该在99%以上。如果低于90%,说明缓存太小了,考虑增加shared_buffers。
客户端连接进来后发生了什么
现在我们来看看一个SQL查询的完整生命周期。假设有个客户端要查询数据:
这个过程其实挺复杂的:
解析阶段会把SQL文本转成内部结构,检查语法是否正确,表和字段是否存在。这一步比较快。
规划阶段是重点。优化器会从统计信息里获取表的行数、数据分布等,然后评估不同的执行方案。比如是全表扫描还是用索引,是用嵌套循环还是哈希连接。这个过程相当于给查询找一条最优路径。
我们可以用EXPLAIN看看优化器在想什么:
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE age > 18;
-- 输出类似这样:
-- Seq Scan on users
-- (cost=0.00..15.50 rows=100 width=40)
-- (actual time=0.012..0.156 rows=95 loops=1)
-- Filter: (age > 18)
-- Rows Removed by Filter: 5
-- Buffers: shared hit=5
-- Planning Time: 0.123 ms
-- Execution Time: 0.234 ms
这里面有几个关键信息:
- cost是估算的成本,越小越好
- rows是估计返回的行数
- Buffers: shared hit=5说明从缓存读了5个页,这是好事
- 如果是shared read=5,说明从磁盘读的,会慢很多
执行阶段Backend就开始真正读数据了。它会先去Shared Buffers里找,找到了就直接用(Buffer Hit),找不到就得去磁盘读(Buffer Miss)。读进来的数据会放到Shared Buffers里,下次其他查询可能就能直接用了。
写数据的时候更复杂
INSERT、UPDATE、DELETE这些写操作涉及的东西更多。PostgreSQL用的是WAL(Write-Ahead Logging)机制,简单说就是先写日志,再改数据。
这个设计挺巧妙的。WAL日志是顺序写,速度快。数据文件是随机写,速度慢,但可以异步慢慢写。就算数据还没完全写到数据文件,只要WAL写成功了,崩溃后也能恢复。
MVCC机制
PostgreSQL用的多版本并发控制(MVCC)挺有意思。UPDATE操作不是原地修改,而是创建新版本,旧版本保留着。每行数据有个xmin(创建它的事务ID)和xmax(删除它的事务ID)。
不同事务根据自己的快照看到不同的数据版本,这样读不阻塞写,写也不阻塞读。但代价是会产生很多死元组(dead tuples),需要VACUUM来清理。
我们经历过一次表膨胀的问题。有张表频繁UPDATE,结果发现查询越来越慢,用下面的SQL一看,死元组占了30%:
sql
SELECT
tablename,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
round(
n_dead_tup * 100.0 /
NULLIF(n_live_tup + n_dead_tup, 0),
2
) AS bloat_percent
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
后来手动VACUUM了一下,查询速度才恢复正常。
WAL配置
WAL的配置也有几个要注意的:
conf
# WAL基础配置
wal_level = replica # minimal/replica/logical
wal_buffers = 16MB # WAL缓冲区大小
wal_writer_delay = 200ms # WAL Writer间隔
# WAL文件管理
min_wal_size = 1GB # 保留的WAL最小大小
max_wal_size = 2GB # 触发checkpoint的WAL大小
# 同步提交配置
synchronous_commit = on # 持久性级别
# on: 等待WAL写入并fsync(最安全)
# local: 写入但不等待fsync(较快)
# off: 不等待写入(最快,可能丢数据)
synchronous_commit这个参数很关键。设成on最安全但最慢,off最快但崩溃可能丢最近几个事务的数据。我们在生产环境都是用on,但在一些允许丢数据的场景(比如日志收集)会用off来提升性能。
AutoVacuum这个东西必须搞懂
AutoVacuum是PostgreSQL的清洁工,负责清理MVCC产生的死元组。刚开始我没太重视这个,后来吃了大亏。
每1分钟醒来] --> Check[检查所有表统计] Check --> Calc{dead_tuples >
threshold + scale * live} Calc -->|是| Launch[启动Worker进程] Calc -->|否| Sleep[继续休眠 1分钟] Launch --> Worker[Worker扫描表] Worker --> Clean[标记死元组空间] Clean --> FSM[更新Free Space Map] FSM --> VM[更新Visibility Map] VM --> Stats[更新统计信息] Stats --> Done[Worker完成] Done --> Sleep Sleep --> Check style Start fill:#4facfe,stroke:#333,stroke-width:2px style Worker fill:#95e1d3,stroke:#333,stroke-width:2px style Done fill:#ffe66d,stroke:#333
AutoVacuum默认每分钟检查一次,看哪些表需要清理。触发条件是:
dead_tuples > 50 + 0.2 * live_tuples
比如10000行的表,需要积累50 + 2000 = 2050个死元组才触发。对于频繁更新的表,这个阈值可能太高了。
我们有张订单表,每秒几百个UPDATE,结果AutoVacuum总是跟不上,表越来越大。后来给这张表单独调了参数:
sql
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02, -- 从0.2降到0.02
autovacuum_vacuum_threshold = 100
);
这样10000行的表只需要100 + 200 = 300个死元组就触发,膨胀问题基本解决了。
监控AutoVacuum执行情况也很重要:
sql
SELECT
schemaname,
tablename,
last_autovacuum,
last_autoanalyze,
n_live_tup,
n_dead_tup,
round(
n_dead_tup * 100.0 /
NULLIF(n_live_tup, 0),
2
) AS dead_percent
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
如果发现某些表的last_autovacuum是很久以前,或者dead_percent很高,就得关注了。
还有一点,长事务会阻止VACUUM清理。我们遇到过有个分析查询跑了几个小时,期间所有的VACUUM都无法清理这个查询可见的旧版本,导致表膨胀。所以尽量避免长时间的事务。
几个实用的监控
生产环境跑PostgreSQL,监控是必不可少的。
连接数监控
sql
SELECT
count(*) AS current,
current_setting('max_connections')::int AS max,
round(
count(*)::numeric /
current_setting('max_connections')::numeric * 100,
1
) AS usage_percent
FROM pg_stat_activity;
如果usage_percent经常超过80%,要么增大max_connections,要么考虑用连接池(比如PgBouncer)。我们现在都用PgBouncer,几千个应用连接对应几十个数据库连接,省资源。
找出慢查询
sql
SELECT
pid,
now() - query_start AS duration,
state,
substring(query, 1, 50) AS query
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start < now() - interval '5 seconds'
ORDER BY duration DESC;
这个查询能找出运行超过5秒的SQL。我们在生产环境设置了log_min_duration_statement = 1000,自动记录超过1秒的查询。
锁等待检查
sql
SELECT
pid,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock';
如果发现有查询长时间等锁,可能是有长事务或者死锁。找到阻塞的进程后,必要时可以用pg_terminate_backend杀掉。
磁盘空间
sql
-- 查看最大的表
SELECT
schemaname,
tablename,
pg_size_pretty(
pg_total_relation_size(
schemaname||'.'||tablename
)
) AS total_size
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(
schemaname||'.'||tablename
) DESC
LIMIT 10;
-- 查看WAL占用
SELECT
pg_size_pretty(sum(size))
FROM pg_ls_waldir();
我们有次WAL目录把磁盘撑爆了,原因是archive_command配置错误,WAL文件归档失败堆积。后来修复了归档命令,老的WAL才被清理掉。
踩过的一些坑
连接数耗尽
症状是应用报错"FATAL: sorry, too many clients already"。查了一下发现有几百个idle连接不释放,原来是应用的连接池配置有问题,连接泄露了。
临时解决办法是杀掉空闲太久的连接:
sql
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '2 hours';
长期解决还是要修应用代码,确保连接正确释放。
查询突然变慢
有次线上查询突然慢了10倍,排查发现统计信息过期了,优化器选错了执行计划。手动ANALYZE后恢复正常:
sql
ANALYZE table_name;
后来把autovacuum_analyze_scale_factor调小了,确保统计信息及时更新。
表膨胀
前面提到过,MVCC会产生死元组。如果AutoVacuum跟不上,表会越来越大,查询越来越慢。除了调整AutoVacuum参数,严重的时候可能需要VACUUM FULL:
sql
VACUUM FULL table_name;
但要注意VACUUM FULL会锁表重写,生产环境要慎用。我们一般会在低峰期做,或者用pg_repack这种在线重建的工具。
一些建议
用了几年PostgreSQL,总结一些经验:
启用AutoVacuum并定期检查执行情况,这个真的很重要。定期ANALYZE保持统计信息准确,特别是数据变化大的表。
使用连接池管理连接,别让应用直连数据库。我们用PgBouncer,效果挺好。
监控缓存命中率,保持在99%以上。如果太低说明shared_buffers不够或者查询模式有问题。
避免长事务,它会阻止VACUUM清理,导致表膨胀。我们现在规定分析类查询必须在只读副本上跑。
检查表膨胀率,及时处理超过20%的表。设置慢查询日志,log_min_duration_statement = 1000能帮你找到性能瓶颈。
在测试环境验证所有配置变更,有些参数改错了可能导致性能下降甚至启动失败。
建立完善的备份和恢复策略,定期演练恢复流程。我们用的是pg_basebackup做基础备份,加上WAL归档做增量。
Checkpoint机制也得了解下
Checkpoint是PostgreSQL的一个重要机制,它负责把内存中的脏页全部刷到磁盘,创建一个一致性检查点。崩溃恢复的时候,只需要从最近的checkpoint开始重放WAL就行了。
Checkpoint的触发条件有几个:
- 达到checkpoint_timeout时间(默认5分钟)
- WAL日志达到max_wal_size大小(默认1GB)
- 手动执行CHECKPOINT命令
- 数据库关闭时
Checkpoint配置要小心:
conf
checkpoint_timeout = 10min # 检查点最大间隔
checkpoint_completion_target = 0.9 # 在90%时间内完成
# 这个配置的意思是:
# 如果10分钟触发checkpoint,那会在9分钟内慢慢把脏页写完
# 这样可以平滑IO压力,不会一下子涌入大量写请求
我们之前checkpoint_completion_target设成0.1,结果checkpoint时IO瞬间打满,业务查询都变慢了。改成0.9后,IO压力平滑了很多。
可以在日志里看checkpoint的执行情况:
conf
log_checkpoints = on
日志会输出类似这样的信息:
ini
LOG: checkpoint starting: time
LOG: checkpoint complete: wrote 1953 buffers (11.9%);
0 WAL file(s) added, 0 removed, 1 recycled;
write=8.505 s, sync=0.024 s, total=8.556 s;
sync files=7, longest=0.013 s, average=0.003 s
如果看到checkpoint频繁或者时间很长,可能需要调整参数。
后台进程的协作
几个后台进程其实是配合工作的,理解它们的关系能更好地调优。
Background Writer是个好帮手,它会提前把一些脏页写掉,这样checkpoint到来的时候压力就小多了。但它不会太激进,有个成本控制:
conf
bgwriter_delay = 200ms # 扫描间隔
bgwriter_lru_maxpages = 100 # 每次最多写100页
bgwriter_lru_multiplier = 2.0 # 成本倍数
我们根据服务器的IO能力调整了这些参数。SSD的话可以把bgwriter_lru_maxpages调大一些,让它写得更积极。
进程间通信
PostgreSQL的进程间通信主要靠共享内存和信号量。
所有进程都能访问共享内存,读写数据页、WAL缓冲、锁表等。但为了避免冲突,访问共享资源时需要用轻量锁(LWLock)或自旋锁(SpinLock)保护。
信号量用于进程间的同步,比如一个Backend等待另一个Backend释放锁。
查看当前进程信息:
sql
SELECT
pid,
backend_type,
backend_start,
state,
wait_event_type,
wait_event
FROM pg_stat_activity
ORDER BY backend_start;
backend_type会显示进程类型:
- client backend:客户端连接的Backend进程
- autovacuum worker:AutoVacuum工作进程
- background writer:后台写进程
- checkpointer:检查点进程
- walwriter:WAL写进程
如果看到很多进程的wait_event_type是Lock,说明锁竞争比较严重,可能需要优化业务逻辑。
一些调试技巧
遇到问题的时候,有几个调试方法挺好用。
查看进程在干什么
sql
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
state,
state_change,
query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
能看到每个活跃进程正在执行的SQL,执行了多久。
查看锁等待关系
sql
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
这个能找出谁阻塞了谁,对排查锁等待很有用。
查看表和索引的使用情况
sql
-- 表扫描统计
SELECT
schemaname,
tablename,
seq_scan, -- 顺序扫描次数
seq_tup_read, -- 顺序扫描读取的行数
idx_scan, -- 索引扫描次数
idx_tup_fetch -- 索引扫描读取的行数
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
如果某个表的seq_scan很高但没有idx_scan,可能需要加索引。
查看索引大小和使用情况
sql
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan;
idx_scan为0的索引可能是无用索引,占着空间还拖慢写入速度,可以考虑删掉。
分析慢查询
sql
-- 启用慢查询日志
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 记录超过1秒的查询
SELECT pg_reload_conf();
-- 或者临时设置
SET log_min_duration_statement = 1000;
慢查询会记录到日志文件,可以用pgBadger这种工具分析。
复制和高可用
PostgreSQL的流复制也是基于WAL的。主库把WAL日志流式传输给备库,备库不断重放WAL来保持同步。
主库需要配置:
conf
wal_level = replica # 启用复制级别WAL
max_wal_senders = 10 # 最大WAL发送进程数
wal_keep_size = 1GB # 保留的WAL大小
备库通过一个叫WAL Receiver的进程接收WAL,然后由Startup进程重放。备库可以设置成hot standby模式,允许只读查询:
conf
hot_standby = on # 允许备库只读
我们用备库做查询分流,把报表、分析这些查询都扔到备库上,减轻主库压力。不过要注意,备库重放WAL的时候可能和只读查询冲突,需要调整:
conf
max_standby_streaming_delay = 30s # 备库查询最多延迟重放30秒
如果备库查询时间太长,可能导致主备延迟增大。
一些性能优化的思路
最后说说性能优化,这块其实没有银弹,要具体问题具体分析。
索引优化
索引是最直接有效的优化手段。但索引不是越多越好,每个索引都会拖慢写入速度。我们的原则是:
- 经常用于WHERE、JOIN的列建索引
- 经常用于ORDER BY的列建索引
- 高选择性的列建索引(值分布比较均匀的)
- 定期检查无用索引,及时删除
查询优化
用EXPLAIN ANALYZE看执行计划,找出瓶颈:
- 全表扫描能否改成索引扫描
- 嵌套循环能否改成哈希连接
- 是否可以加条件减少扫描的行数
有时候改写SQL比加索引效果更好。比如用EXISTS替代IN,用JOIN替代子查询等。
分区表
数据量特别大的表可以考虑分区。我们有张日志表,按月分区,查询只扫描相关分区,速度快很多:
sql
CREATE TABLE logs (
id bigint,
created_at timestamp,
message text
) PARTITION BY RANGE (created_at);
CREATE TABLE logs_2024_01 PARTITION OF logs
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE logs_2024_02 PARTITION OF logs
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
分区表的维护比较麻烦,需要定期创建新分区、删除旧分区。我们写了个定时任务自动管理。
连接池
前面提过,用PgBouncer这种连接池能大大减少数据库的连接开销。PgBouncer支持三种池化模式:
- session:整个会话使用同一个数据库连接
- transaction:每个事务使用一个连接
- statement:每个语句使用一个连接
我们用的是transaction模式,兼顾了连接复用和事务隔离。
读写分离
把只读查询分流到备库,可以减轻主库压力。但要注意主备延迟,如果业务对数据一致性要求高,可能不适合读写分离。
我们的做法是,实时性要求高的查询走主库,报表、分析这些可以容忍几秒延迟的走备库。
写在最后
PostgreSQL的进程架构和内存管理其实还有很多细节,这篇文章只是覆盖了主要的部分。真正要用好PostgreSQL,还需要在实践中不断摸索和积累经验。
有些东西我也还在研究,比如JIT编译、并行查询、逻辑复制等。这些特性在特定场景下能带来显著的性能提升,但也有各自的适用条件和限制。
建议多看官方文档,多做测试,多关注社区的讨论。遇到问题的时候,查日志、看执行计划、分析统计信息,大部分问题都能找到线索。
如果这篇文章对你理解PostgreSQL有帮助,那就太好了。有问题或者想交流的,欢迎留言。