PG is all you need
PostgreSQL 统一数据架构:用一个数据库覆盖十个场景
核心论点
PostgreSQL 的扩展生态已覆盖全文搜索、向量检索、时序数据、缓存、消息队列、Pub/Sub、限流、Session、文档存储、地理空间等场景。在绝大多数业务规模下,PostgreSQL 可以作为唯一数据存储,替代 Elasticsearch、Pinecone、Redis、MongoDB、Kafka、InfluxDB 等专用系统。
衡量维度:
- 功能覆盖:10 个需求场景的 Postgres 原生或扩展实现方案
- 性能对比:各场景下 Postgres vs 专用方案的 benchmark 数据
- 工程成本:基础设施、运维、开发效率的量化对比
- 适用边界:Postgres 方案的限制条件和不适用场景
适用读者:需要做技术选型的架构师、技术负责人、中小团队 CTO。
不适用场景:百万级 QPS 热 key 缓存、PB 级日志检索、跨区域实时复制等需要专用系统的极端规模。
多数据库架构的工程代价
典型的"为每个需求选专用工具"技术栈:
- Elasticsearch 负责搜索
- Pinecone 负责向量检索
- Redis 负责缓存
- MongoDB 负责文档存储
- Kafka 负责消息队列
- InfluxDB 负责时序数据
- PostgreSQL 负责关系型存储
这意味着 7 套备份策略、7 套监控面板、7 套安全审计、7 组凭证轮换。每增加一个系统,工程复杂度不是线性增长,而是乘法增长。
SLA 算术:三个 99.9% 可用性的系统组合后可用性为 99.7%,相当于每年 26 小时停机(单系统为 8.7 小时)。每增加一个系统,故障概率相乘。
数据一致性:跨系统数据同步需要编写同步任务、对账逻辑、补偿机制。这些胶水代码本身会成为故障源。
认知负担:团队需要同时掌握 SQL、Redis 命令、Elasticsearch Query DSL、MongoDB 聚合管道、Kafka 协议、InfluxDB 方言——这不是专业化,而是碎片化。
需求场景对比总览
| 需求场景 | 专用方案 | Postgres 方案 | 底层算法 |
|---|---|---|---|
| 全文搜索 | Elasticsearch | pg_textsearch | BM25 |
| 向量搜索 | Pinecone | pgvector + pgvectorscale | HNSW / DiskANN |
| 时序数据 | InfluxDB | TimescaleDB | 时间分区 + 压缩 |
| 缓存 | Redis | UNLOGGED 表 + JSONB | 内存 KV 存储 |
| Pub/Sub | Redis / Kafka | LISTEN/NOTIFY | 发布订阅 |
| 任务队列 | Redis + Bull | FOR UPDATE SKIP LOCKED | 原子认领 |
| 限流 | Redis | 滑动窗口 + UPSERT | 计数器 / 窗口 |
| Session | Redis | UNLOGGED 表 + JSONB | KV 过期存储 |
| 文档存储 | MongoDB | JSONB | 文档索引 |
| 地理空间 | 专用 GIS | PostGIS | R-Tree(2001 年至今行业标准) |
这些不是简化实现。多数 Postgres 扩展使用与专用方案相同甚至更优的算法,由同一批研究者开发,经过多年生产验证。
性能基准对比
单操作延迟对比(Redis vs PostgreSQL)
测试环境:AWS RDS db.t3.medium(2 vCPU, 4GB RAM),100 万条缓存记录,1 万条 session,工具为 pgbench。
| 操作 | Redis | PostgreSQL | 差异 |
|---|---|---|---|
| Cache SET | 0.05ms | 0.08ms | +60% |
| Cache GET | 0.04ms | 0.06ms | +50% |
| Pub/Sub | 1.2ms | 3.1ms | +158% |
| Queue push | 0.08ms | 0.15ms | +87% |
| Queue pop | 0.12ms | 0.31ms | +158% |
单操作维度 PostgreSQL 均慢于 Redis,但所有操作仍在亚毫秒到个位毫秒级别。
组合操作延迟对比
实际业务中,单次请求往往涉及多个操作。以"写入数据 + 失效缓存 + 通知订阅者"为例:
Redis 架构(跨系统调用):
INSERT INTO posts ... 2.0ms
→ 网络跳转 Redis DEL 缓存 1.0ms
→ 网络跳转 Redis PUBLISH 1.0ms
总计 ≈ 4.0ms(无事务保证)PostgreSQL 架构(同一事务):
BEGIN;
INSERT INTO posts ...; -- 2.0ms
DELETE FROM cache WHERE key = 'posts:latest'; -- 0.1ms
NOTIFY posts_new, '...'; -- 0.1ms
COMMIT;
-- 总计 ≈ 2.2ms(ACID 事务保证)组合操作场景下,PostgreSQL 因省去网络跳转反而更快,且具备事务一致性保证。
扩展 vs 专用方案
| 扩展 | 对比方案 | 性能数据 |
|---|---|---|
| pgvectorscale | Pinecone | 99% 召回率下 p95 延迟低 28 倍,成本降低 75% |
| TimescaleDB | InfluxDB | 性能持平或更优,支持完整 SQL |
| pg_textsearch | Elasticsearch | 相同的 BM25 排序算法 |
现代 PostgreSQL 扩展生态
这些扩展并非实验性项目,均已在生产环境长期运行:
| 扩展 | 起始年份 | 生产验证时间 | 代表用户 |
|---|---|---|---|
| PostGIS | 2001 | 24 年 | OpenStreetMap, Uber |
| 全文搜索 | 2008 | 17 年 | Postgres 核心内置 |
| JSONB | 2014 | 11 年 | 支持 ACID 的文档存储 |
| TimescaleDB | 2017 | 8 年 | GitHub 21K+ star |
| pgvector | 2021 | 4 年 | GitHub 19K+ star |
超过 48,000 家公司使用 PostgreSQL,包括 Netflix、Spotify、Uber、Reddit、Instagram、Discord。
AI 浪潮催生的新一代扩展:
| 扩展 | 替代方案 | 功能 |
|---|---|---|
| pgvectorscale | Pinecone, Qdrant | DiskANN 算法,延迟低 28 倍,成本降低 75% |
| pg_textsearch | Elasticsearch | 原生 BM25 排序 |
| pgai | 外部 AI pipeline | 数据变更时自动同步 embedding |
快速安装
CREATE EXTENSION pg_textsearch; -- BM25 全文搜索
CREATE EXTENSION vector; -- 向量搜索
CREATE EXTENSION vectorscale; -- 高性能向量索引
CREATE EXTENSION ai; -- AI embedding 自动同步
CREATE EXTENSION timescaledb; -- 时序数据
CREATE EXTENSION pgmq; -- 消息队列
CREATE EXTENSION pg_cron; -- 定时任务
CREATE EXTENSION postgis; -- 地理空间实现方案与代码示例
搜索类
全文搜索(替代 Elasticsearch)
扩展:pg_textsearch(BM25 排序)
对比方案:Elasticsearch(独立 JVM 集群、映射配置、同步 pipeline)、Algolia(按搜索次数计费、外部 API 依赖)。
Postgres 方案使用与 Elasticsearch 相同的 BM25 排序算法,无需额外基础设施。
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT
);
-- 创建 BM25 索引
CREATE INDEX idx_articles_bm25 ON articles USING bm25(content)
WITH (text_config = 'english');
-- BM25 评分搜索
SELECT title, -(content <@> 'database optimization') as score
FROM articles
ORDER BY content <@> 'database optimization'
LIMIT 10;向量搜索(替代 Pinecone)
扩展:pgvector + pgvectorscale
对比方案:Pinecone(最低 $70/月、独立基础设施)、Qdrant / Milvus / Weaviate(额外运维负担)。
pgvectorscale 使用微软研究院的 DiskANN 算法,在 99% 召回率下延迟和吞吐量均优于 Pinecone。
CREATE EXTENSION vector;
CREATE EXTENSION vectorscale CASCADE;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
-- DiskANN 高性能索引
CREATE INDEX idx_docs_embedding ON documents USING diskann(embedding);
-- 相似度查询
SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector as distance
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;使用 pgai 自动同步 embedding(数据变更时自动重新生成,无需外部同步任务):
SELECT ai.create_vectorizer(
'documents'::regclass,
loading => ai.loading_column(column_name => 'content'),
embedding => ai.embedding_openai(model => 'text-embedding-3-small', dimensions => '1536')
);混合搜索(BM25 + 向量)
在 Elasticsearch + Pinecone 架构中实现混合搜索需要两次 API 调用、结果合并、错误处理。在 Postgres 中是一条 SQL。
-- 加权混合
SELECT
title,
-(content <@> 'database optimization') as bm25_score,
embedding <=> query_embedding as vector_distance,
0.7 * (-(content <@> 'database optimization')) +
0.3 * (1 - (embedding <=> query_embedding)) as hybrid_score
FROM articles
ORDER BY hybrid_score DESC
LIMIT 10;RAG 倒数排名融合(RRF)
WITH bm25 AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY content <@> $1) as rank
FROM documents LIMIT 20
),
vectors AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2) as rank
FROM documents LIMIT 20
)
SELECT d.*,
1.0 / (60 + COALESCE(b.rank, 1000)) +
1.0 / (60 + COALESCE(v.rank, 1000)) as score
FROM documents d
LEFT JOIN bm25 b ON d.id = b.id
LEFT JOIN vectors v ON d.id = v.id
WHERE b.id IS NOT NULL OR v.id IS NOT NULL
ORDER BY score DESC LIMIT 10;模糊搜索
扩展:pg_trgm(Postgres 内置)
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_name_trgm ON products USING GIN (name gin_trgm_ops);
-- 容错搜索:即使拼错也能匹配
SELECT name FROM products
WHERE name % 'posgresql'
ORDER BY similarity(name, 'posgresql') DESC;数据存储类
时序数据(替代 InfluxDB)
扩展:TimescaleDB
对比方案:InfluxDB(独立数据库、Flux 查询语言、有限 SQL 支持)、Prometheus(适合指标监控,不适合应用数据)。
TimescaleDB 提供自动时间分区、高达 90% 压缩率、连续聚合,同时保持完整 SQL 兼容。
CREATE EXTENSION timescaledb;
CREATE TABLE metrics (
time TIMESTAMPTZ NOT NULL,
device_id TEXT,
temperature DOUBLE PRECISION
);
SELECT create_hypertable('metrics', 'time');
-- 时间桶聚合
SELECT time_bucket('1 hour', time) as hour, AVG(temperature)
FROM metrics
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY hour;
-- 自动过期策略
SELECT add_retention_policy('metrics', INTERVAL '30 days');
-- 压缩策略(节省 90% 存储)
ALTER TABLE metrics SET (timescaledb.compress);
SELECT add_compression_policy('metrics', INTERVAL '7 days');文档存储(替代 MongoDB)
特性:原生 JSONB + GIN 索引
Postgres 的 JSONB 提供与 MongoDB 类似的文档存储能力,同时具备 ACID 事务保证。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO users (data) VALUES ('{
"name": "Alice",
"profile": {"bio": "Developer", "links": ["github.com/alice"]}
}');
-- 嵌套字段查询
SELECT data->>'name', data->'profile'->>'bio'
FROM users
WHERE data->'profile'->>'bio' LIKE '%Developer%';
-- JSON 字段索引
CREATE INDEX idx_users_email ON users ((data->>'email'));地理空间(替代专用 GIS)
扩展:PostGIS(2001 年至今,行业标准)
CREATE EXTENSION postgis;
CREATE TABLE stores (
id SERIAL PRIMARY KEY,
name TEXT,
location GEOGRAPHY(POINT, 4326)
);
-- 查找 5km 内的门店
SELECT name, ST_Distance(location, ST_MakePoint(-122.4, 37.78)::geography) as meters
FROM stores
WHERE ST_DWithin(location, ST_MakePoint(-122.4, 37.78)::geography, 5000);基础设施类(替代 Redis)
缓存(UNLOGGED 表)
原理:UNLOGGED 表跳过 Write-Ahead Log(WAL),写入性能接近 Redis,但数据库崩溃后表数据会被清空。这对缓存场景是可接受的——缓存数据本身就是可重建的。
性能:Redis SET 0.05ms vs Postgres UNLOGGED INSERT 0.08ms。
Redis 实现:
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
const user = JSON.parse(await redis.get(`user:${id}`));Postgres 实现:
CREATE UNLOGGED TABLE cache (
key TEXT PRIMARY KEY,
value JSONB NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_cache_expires ON cache(expires_at);
-- 写入(upsert)
INSERT INTO cache (key, value, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '1 hour')
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value,
expires_at = EXCLUDED.expires_at;
-- 读取
SELECT value FROM cache
WHERE key = $1 AND expires_at > NOW();
-- 过期清理(配合 pg_cron 定时执行)
DELETE FROM cache WHERE expires_at < NOW();完整 Node.js 封装:
class PostgresCache {
constructor(pool) { this.pool = pool; }
async get(key) {
const result = await this.pool.query(
'SELECT value FROM cache WHERE key = $1 AND expires_at > NOW()', [key]
);
return result.rows[0]?.value;
}
async set(key, value, ttlSeconds = 3600) {
await this.pool.query(
`INSERT INTO cache (key, value, expires_at)
VALUES ($1, $2, NOW() + make_interval(secs => $3))
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,
[key, value, ttlSeconds]
);
}
async delete(key) {
await this.pool.query('DELETE FROM cache WHERE key = $1', [key]);
}
async cleanup() {
await this.pool.query('DELETE FROM cache WHERE expires_at < NOW()');
}
}Pub/Sub(LISTEN/NOTIFY)
原理:PostgreSQL 内置发布/订阅机制,支持在事务中发送通知,保证原子性。
性能:Redis Pub/Sub 1–2ms vs Postgres NOTIFY 2–5ms。
限制条件(架构师须知):
- payload 上限 8000 字节,大消息需存表后只传 ID
- 消息不持久化,不支持回放——适用于实时通知,不适用于需要持久化和重消费的消息流
- LISTEN 需要长连接,pgBouncer transaction mode 下不可用(需使用 session mode 或直连)
Redis 实现:
// Publisher
redis.publish('notifications', JSON.stringify({ userId: 123, msg: 'Hello' }));
// Subscriber
redis.subscribe('notifications');
redis.on('message', (channel, message) => {
console.log(JSON.parse(message));
});Postgres 实现:
-- 发布
SELECT pg_notify('notifications', '{"userId": 123, "msg": "Hello"}');// Subscriber (Node.js)
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query('LISTEN notifications');
client.on('notification', (msg) => {
const payload = JSON.parse(msg.payload);
console.log(payload);
});触发器驱动的自动通知:插入与通知在同一事务中,保证原子性。
CREATE FUNCTION notify_new_log() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('logs_new', row_to_json(NEW)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER log_inserted
AFTER INSERT ON logs
FOR EACH ROW EXECUTE FUNCTION notify_new_log();SSE 实时流推送示例:
app.get('/logs/stream', async (req, res) => {
const client = await pool.connect();
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
});
await client.query('LISTEN logs_new');
client.on('notification', (msg) => {
res.write(`data: ${msg.payload}\n\n`);
});
});完整 Node.js 封装:
class PostgresPubSub {
constructor(pool) { this.pool = pool; this.listeners = new Map(); }
async publish(channel, message) {
await this.pool.query('SELECT pg_notify($1, $2)', [channel, JSON.stringify(message)]);
}
async subscribe(channel, callback) {
const client = await this.pool.connect();
await client.query(`LISTEN ${channel}`);
client.on('notification', (msg) => {
if (msg.channel === channel) callback(JSON.parse(msg.payload));
});
this.listeners.set(channel, client);
}
async unsubscribe(channel) {
const client = this.listeners.get(channel);
if (client) {
await client.query(`UNLISTEN ${channel}`);
client.release();
this.listeners.delete(channel);
}
}
}任务队列(FOR UPDATE SKIP LOCKED)
原理:FOR UPDATE SKIP LOCKED 实现无锁队列——多个 Worker 并发拉取任务,不会重复处理,不会互相阻塞。Worker 崩溃后,任务自动变为可用。
性能:Redis BRPOP 0.1ms vs Postgres SKIP LOCKED 0.3ms。
Redis 实现(Bull/BullMQ):
queue.add('send-email', { to, subject, body });
queue.process('send-email', async (job) => {
await sendEmail(job.data);
});Postgres 实现:
CREATE TABLE jobs (
id BIGSERIAL PRIMARY KEY,
queue TEXT NOT NULL,
payload JSONB NOT NULL,
attempts INT DEFAULT 0,
max_attempts INT DEFAULT 3,
scheduled_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_jobs_pending ON jobs(queue, scheduled_at)
WHERE attempts < max_attempts;
-- 入队
INSERT INTO jobs (queue, payload)
VALUES ('send-email', '{"to": "user@example.com", "subject": "Hi"}');
-- Worker 认领(原子操作)
WITH next_job AS (
SELECT id FROM jobs
WHERE queue = $1
AND attempts < max_attempts
AND scheduled_at <= NOW()
ORDER BY scheduled_at
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET attempts = attempts + 1
FROM next_job
WHERE jobs.id = next_job.id
RETURNING jobs.*;替代方案:pgmq 扩展提供了更完整的消息队列抽象,适合需要 exactly-once 语义的场景。原生 SKIP LOCKED 适合简单任务队列。
完整 Node.js 封装:
class PostgresQueue {
constructor(pool) { this.pool = pool; }
async enqueue(queue, payload, scheduledAt = new Date()) {
await this.pool.query(
'INSERT INTO jobs (queue, payload, scheduled_at) VALUES ($1, $2, $3)',
[queue, payload, scheduledAt]
);
}
async dequeue(queue) {
const result = await this.pool.query(
`WITH next_job AS (
SELECT id FROM jobs
WHERE queue = $1 AND attempts < max_attempts AND scheduled_at <= NOW()
ORDER BY scheduled_at LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs SET attempts = attempts + 1
FROM next_job WHERE jobs.id = next_job.id
RETURNING jobs.*`,
[queue]
);
return result.rows[0];
}
async complete(jobId) {
await this.pool.query('DELETE FROM jobs WHERE id = $1', [jobId]);
}
async fail(jobId, error) {
await this.pool.query(
`UPDATE jobs SET attempts = max_attempts,
payload = payload || jsonb_build_object('error', $2) WHERE id = $1`,
[jobId, error.message]
);
}
}限流(滑动窗口 + UPSERT)
Redis 实现:
const key = `ratelimit:${userId}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
if (count > 100) throw new Error('Rate limit exceeded');Postgres 实现:
CREATE TABLE rate_limits (
user_id INT PRIMARY KEY,
request_count INT DEFAULT 0,
window_start TIMESTAMPTZ DEFAULT NOW()
);
-- 原子性检查并递增
WITH current AS (
SELECT request_count,
CASE
WHEN window_start < NOW() - INTERVAL '1 minute' THEN 1
ELSE request_count + 1
END AS new_count
FROM rate_limits WHERE user_id = $1
FOR UPDATE
)
UPDATE rate_limits
SET request_count = (SELECT new_count FROM current),
window_start = CASE
WHEN window_start < NOW() - INTERVAL '1 minute' THEN NOW()
ELSE window_start
END
WHERE user_id = $1
RETURNING request_count;Postgres 方案的优势:限流逻辑可以与业务逻辑在同一事务中执行,保证一致性。Redis 方案在需要复杂限流条件(如按用户等级、接口类型组合限流)时需要多次网络调用。
Redis 方案的优势:亚毫秒延迟、百万级 QPS 吞吐。
Session 存储(UNLOGGED 表 + JSONB)
Redis 实现:
await redis.set(`session:${sessionId}`, JSON.stringify(sessionData), 'EX', 86400);Postgres 实现:
CREATE UNLOGGED TABLE sessions (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
-- 写入 / 更新
INSERT INTO sessions (id, data, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '24 hours')
ON CONFLICT (id) DO UPDATE
SET data = EXCLUDED.data, expires_at = EXCLUDED.expires_at;
-- 读取
SELECT data FROM sessions WHERE id = $1 AND expires_at > NOW();Postgres 的独特优势——支持 Session 内部的结构化查询(Redis 做不到):
-- 查找某用户的所有活跃 session
SELECT * FROM sessions WHERE data->>'userId' = '123' AND expires_at > NOW();
-- 查找所有管理员 session
SELECT * FROM sessions WHERE data->'user'->>'role' = 'admin' AND expires_at > NOW();定时任务(替代 Cron)
扩展:pg_cron
CREATE EXTENSION pg_cron;
SELECT cron.schedule('cleanup', '0 * * * *',
'DELETE FROM cache WHERE expires_at < NOW()');
SELECT cron.schedule('rollup', '0 2 * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats');高级查询
图遍历(递归 CTE)
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 1 as depth
FROM employees WHERE id = 42
UNION ALL
SELECT e.id, e.name, e.manager_id, t.depth + 1
FROM employees e
JOIN org_tree t ON e.manager_id = t.id
WHERE t.depth < 10
)
SELECT * FROM org_tree;PostgreSQL 18 的关键性能改进
PostgreSQL 18(2025 年 9 月发布)引入了两项对缓存架构有直接影响的特性。
Asynchronous I/O(io_uring 集成)
PostgreSQL 18 引入了新的 I/O 子系统,基于 Linux io_uring 实现异步 I/O,提升顺序扫描、bitmap heap scan、vacuum 等操作的性能。
核心变化:此前 PostgreSQL 的 I/O 是同步的——发起一次读请求,等待返回,再发起下一次。AIO 允许同时发起多个 I/O 请求,充分利用 SSD 的并行能力。
实测对比(dashboard 聚合查询,30 天数据,每日百万级调用):
SELECT date, sum(events)
FROM activity
WHERE user_id = $1
AND date > now() - interval '30 days'
GROUP BY date;| 场景 | PostgreSQL 17 | PostgreSQL 18(AIO) | 提升 |
|---|---|---|---|
| 冷缓存 | 12–18ms | 6–8ms | ~2x |
| 热 buffer | 4–6ms | 1.8–2.5ms | ~2x |
启用配置(5 行):
io_method = 'io_uring'
effective_io_concurrency = 200
maintenance_io_concurrency = 50
shared_buffers = 16GB
io_combine_limit = 512kB前提条件:Linux kernel >= 5.10、SSD 存储、云厂商支持 io_uring。HDD 或不支持 AIO 的环境无法获得该提升。
B-tree Skip Scan
此前,组合索引 (a, b, c) 在查询条件不包含前缀列 a 时无法有效利用。PostgreSQL 18 支持 skip scan:自动跳过前缀列,对每个 distinct 值做范围扫描。
-- 索引
CREATE INDEX idx_orders_composite ON orders (user_id, status, created_at);
-- 查询不包含 user_id
SELECT * FROM orders
WHERE status = 'pending' AND created_at > '2025-01-01'
ORDER BY created_at DESC LIMIT 50;| 版本 | 执行方式 | 延迟 |
|---|---|---|
| PostgreSQL 17 | 全表扫描或次优路径 | ~45ms |
| PostgreSQL 18 | Skip scan 命中索引 | ~8ms |
案例:移除 Redis 缓存层
背景
一个典型的 Web 应用架构:Application → Redis(缓存层)→ PostgreSQL(持久层)。Redis 集群月费 $340,缓存逻辑约 800+ 行,分布在 12 个文件中。
问题量化
升级到 PostgreSQL 18 并启用 AIO 后,团队对生产查询做了延迟对比:
Redis 缓存的实际加权延迟计算(cache hit rate 73%):
加权延迟 = hit_rate × (Redis GET + 序列化) + miss_rate × (DB查询 + 写Redis)
= 0.73 × (0.1 + 0.3 + 0.8) + 0.27 × (15 + 0.3 + 0.8)
= 0.73 × 1.2 + 0.27 × 16.1
= 0.876 + 4.347
≈ 5.2ms(其中 0.1ms Redis GET,0.3ms 反序列化,0.8ms 网络 RTT,15ms DB 查询,0.3ms 序列化,0.8ms Redis SET RTT)
PostgreSQL 18 直连:1.8–2.5ms(热 buffer)。
当 cache hit rate 不够高(< 95%)时,Redis 缓存层引入的序列化开销和网络 RTT 会侵蚀其延迟优势。
三年成本复盘
| 成本项 | 计算 | 金额 |
|---|---|---|
| Redis 集群 | $340/月 × 36 个月 | $12,240 |
| 缓存失效 Bug | 23 次事故 × 平均 7 小时 × $150/h | $24,150 |
| 合计(可量化部分) | $36,390 |
未量化但真实存在的成本:每个新功能需要设计缓存策略、每次 schema 变更需要同步缓存 key、双套监控和 on-call 手册的维护。
迁移后架构
Application
├── Redis(Session / 限流 / 排行榜 / 分布式锁)
└── PostgreSQL 18(持久化存储 + 缓存 + 队列 + Pub/Sub)Redis 仅保留其确有不可替代优势的场景,其余全部收敛到 PostgreSQL。
迁移策略
建议分四个阶段执行,每阶段约一周,全程监控关键指标。
Phase 1:双写
同时写入 Redis 和 Postgres,仍从 Redis 读取。监控:两端 hit rate 和延迟差异。
// 双写
await redis.set(key, value);
await pg.query('INSERT INTO cache ...', [key, value]);
// 仍从 Redis 读取
let data = await redis.get(key);Phase 2:读切换
优先从 Postgres 读取,Redis 作为 fallback。监控:错误率、延迟分布。
let data = await pg.query('SELECT value FROM cache WHERE key = $1', [key]);
if (!data.rows[0]) {
data = await redis.get(key); // fallback
}Phase 3:停止写 Redis
只写入 Postgres。监控:业务指标是否正常。
Phase 4:关闭 Redis
下线 Redis 实例。观察 1–2 周。
适用边界与限制条件
PostgreSQL 方案不适用的场景
| 场景 | 原因 | 建议 |
|---|---|---|
| P99 < 2ms 硬性要求 | Postgres 延迟波动大于 Redis | 保留 Redis |
| 百万 QPS 热 key | Redis 单实例 100K+ ops/sec,Postgres 10K–50K | 保留 Redis |
| Sorted Set / HyperLogLog | Redis 原生数据结构,Postgres 无直接等价物 | 保留 Redis |
| 大规模消息流(需持久化 + 回放) | LISTEN/NOTIFY 不持久化、不支持重消费 | 使用 Kafka |
| PB 级日志全文检索 | Elasticsearch 的分布式架构更适合 | 使用 Elasticsearch |
| 跨区域实时复制 | Redis 的 geo-replication 更成熟 | 保留 Redis |
PostgreSQL 扩展方案的已知限制
| 特性 | 限制 | 应对方案 |
|---|---|---|
| UNLOGGED 表 | 崩溃后数据清空 | 仅存储可重建数据(缓存、Session) |
| LISTEN/NOTIFY | payload 上限 8KB | 大消息存表,只传 ID |
| LISTEN/NOTIFY | 不持久化,不支持回放 | 需要持久化用 pgmq 或 Kafka |
| LISTEN/NOTIFY | 需要长连接 | pgBouncer transaction mode 下不可用,需 session mode 或直连 |
| AIO (io_uring) | 需要 Linux >= 5.10 + SSD | HDD 或旧内核无法启用 |
决策矩阵
适合用 Postgres 替代 Redis 的条件:使用 Redis 做简单缓存/Session、cache hit rate < 95%、需要事务一致性、可接受 0.1–1ms 额外延迟、小团队运维资源有限。
应保留 Redis 的条件:需要 100K+ ops/sec、依赖 Redis 特有数据结构、有专职运维团队、亚毫秒延迟为刚性需求、需要 geo-replication。
性能调优参考
postgresql.conf 关键参数
shared_buffers = 4GB # RAM 的 25%
effective_cache_size = 12GB # RAM 的 75%
work_mem = 50MB # 复杂查询内存
maintenance_work_mem = 512MB # VACUUM 内存
io_method = 'io_uring' # PG18 异步 I/O
effective_io_concurrency = 200 # SSD 并发 I/O连接池
const { Pool } = require('pg');
const pool = new Pool({
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});索引优化
CREATE INDEX CONCURRENTLY idx_cache_key ON cache(key) WHERE expires_at > NOW();
CREATE INDEX CONCURRENTLY idx_jobs_pending ON jobs(queue, scheduled_at)
WHERE attempts < max_attempts;自动维护
-- 缓存表高频清理
ALTER TABLE cache SET (autovacuum_vacuum_scale_factor = 0.1);
ALTER TABLE sessions SET (autovacuum_vacuum_scale_factor = 0.1);
-- 手动维护(如未启用 autovacuum)
VACUUM ANALYZE cache;
VACUUM ANALYZE jobs;
VACUUM ANALYZE sessions;总结
PostgreSQL 的扩展生态覆盖了搜索、向量、时序、缓存、队列、Pub/Sub、限流、Session、文档、地理空间等场景,且多数扩展使用与专用方案相同的底层算法。PostgreSQL 18 的 AIO 和 Skip Scan 进一步缩小了与 Redis 在缓存场景下的性能差距。
对于绝大多数业务规模,单一 PostgreSQL 实例可以替代多个专用系统,换取:
- 统一的查询语言和事务模型
- 单一的备份、监控、安全审计流程
- 更低的基础设施成本和运维复杂度
- 更高的组合操作性能(省去跨系统网络开销)
在真正遇到性能瓶颈之前,不建议引入额外的数据系统。引入的判断依据应当是实测 benchmark,而非架构模式的惯性。
引用
- Pinecone alternatives - TigerData
- BM25 is now in Postgres - TigerData
- PostGIS - Wikipedia
- PostgreSQL - Wikipedia
- PostgreSQL Customers List
- pgvectorscale - GitHub
- pg_textsearch - GitHub
- pgai - GitHub
- TimescaleDB - GitHub
- pgvector - GitHub
- PostgreSQL 18 Release Notes
- PostgreSQL 18 AIO
- io_uring and PostgreSQL - Andres Freund
- LISTEN/NOTIFY
- FOR UPDATE SKIP LOCKED
- UNLOGGED Tables
- pgBouncer
- pg_stat_statements
- Graphile Worker
- pg-boss
- pgmq