第一章:PostgreSQL 为什么仍然是最好的通用数据库
第一章:PostgreSQL 为什么仍然是最好的通用数据库
在数据库选型的讨论中,PostgreSQL 几乎总是出现在最终候选里。这不是惯性,而是有充分理由的。
一、数据库选型的真实问题
每当团队开始一个新项目,总有人问:用 PostgreSQL 还是 MySQL?用关系型还是 MongoDB?用 Supabase 还是 PlanetScale?
这些问题的答案取决于你的具体需求,但有一个规律:绝大多数 Web 应用(> 90%)的数据层需求,PostgreSQL 都能胜任,且通常比其他选择更好。
理解为什么,需要从 PostgreSQL 的架构设计说起。
二、MVCC——PostgreSQL 并发的基础
MVCC(多版本并发控制,Multi-Version Concurrency Control)是 PostgreSQL 并发模型的核心。
传统锁机制 vs MVCC
传统锁机制:
读操作:加共享锁(其他读可以,写阻塞)
写操作:加排他锁(读和写都阻塞)
问题:高并发时,读写互相阻塞,性能下降
MVCC 的思路:
每次修改不是覆盖旧数据,而是创建新版本
每个事务看到的是事务开始时的快照
读操作不阻塞写,写操作不阻塞读
具体实现
-- PostgreSQL 的每一行都有两个隐藏字段
-- xmin: 创建这个版本的事务 ID
-- xmax: 删除(或覆盖)这个版本的事务 ID(0 表示未删除)
-- 当你执行 UPDATE
UPDATE users SET name = 'Bob' WHERE id = 1;
-- PostgreSQL 实际做的是:
-- 1. 给旧行设置 xmax = 当前事务ID(标记为删除)
-- 2. 创建新行,xmin = 当前事务ID,name = 'Bob'
-- 3. 旧行不立即物理删除,由 VACUUM 异步清理
对开发者的影响
好处:
-- 长时间的只读查询不会被写操作阻塞
-- 这对报表查询极其重要
-- 事务A:正在运行复杂报表(可能需要 30 秒)
SELECT COUNT(*), SUM(amount) FROM orders WHERE created_at > '2024-01-01';
-- 同时,事务B:正常写入订单(不会被报表查询阻塞)
INSERT INTO orders VALUES (...);
需要注意的副作用:
-- 问题:MVCC 导致表膨胀
-- 已删除/已更新的行版本不会立即释放空间
-- 需要 VACUUM 定期清理
-- 查看表膨胀情况
SELECT
schemaname,
tablename,
n_dead_tup, -- 死行数量
n_live_tup, -- 活跃行数量
round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) as dead_ratio
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
-- 手动触发 VACUUM(通常自动 VACUUM 足够)
VACUUM ANALYZE orders;
三、PostgreSQL vs MySQL——关键差异
查询功能
PostgreSQL 支持比 MySQL 更丰富的查询特性:
-- 1. Window Functions(MySQL 8.0 才支持,PG 很早支持)
SELECT
user_id,
order_date,
amount,
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total,
RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS amount_rank
FROM orders;
-- 2. Recursive CTE(MySQL 8.0+ 支持,PG 一直支持)
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY depth, name;
-- 3. LATERAL JOIN(MySQL 不支持)
SELECT u.id, u.name, recent_orders.*
FROM users u
CROSS JOIN LATERAL (
SELECT id, amount, created_at
FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) recent_orders;
数据类型
PostgreSQL 的数据类型比 MySQL 丰富得多:
-- JSONB:真正的 JSON 数据库能力
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
attributes JSONB -- 可以建索引,可以查询内部字段
);
-- 在 JSONB 字段上建索引
CREATE INDEX idx_products_category ON products USING GIN (attributes);
-- 查询 JSONB 内部字段
SELECT name, attributes->>'color' AS color
FROM products
WHERE attributes @> '{"category": "electronics"}';
-- 数组类型
CREATE TABLE tags (
post_id INT,
tags TEXT[] -- 原生数组类型
);
-- 数组操作
SELECT * FROM tags WHERE 'postgresql' = ANY(tags);
SELECT * FROM tags WHERE tags && ARRAY['database', 'sql']; -- 数组交集
-- UUID
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
...
);
-- 范围类型
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
room_id INT,
during TSTZRANGE -- 时间范围类型
);
-- 查询重叠的预约(不需要写复杂的 BETWEEN 逻辑)
SELECT * FROM reservations
WHERE during && '[2024-01-15, 2024-01-20)'::tstzrange;
-- 自动防止冲突(使用 EXCLUDE 约束)
ALTER TABLE reservations ADD CONSTRAINT no_overlap
EXCLUDE USING GIST (room_id WITH =, during WITH &&);
全文搜索
-- PostgreSQL 内置全文搜索,很多情况下不需要 Elasticsearch
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
search_vector TSVECTOR -- 预计算的搜索向量
);
-- 自动更新搜索向量
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
CREATE TRIGGER update_search_vector
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.english', title, content);
-- 全文搜索
SELECT id, title, ts_rank(search_vector, query) AS rank
FROM articles, plainto_tsquery('english', 'postgresql database performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;
四、JSONB——PostgreSQL 作为文档数据库
很多团队使用 MongoDB 是因为需要灵活的 Schema,但 PostgreSQL 的 JSONB 类型提供了同等灵活性,同时保留关系型数据库的优势。
JSONB vs JSON
-- JSON: 存储原始文本,读取时重新解析
-- JSONB: 解析后以二进制存储,支持索引,查询更快
-- 推荐始终用 JSONB,除非需要保留原始 JSON 格式(如保留空格)
实际使用场景
-- 电商产品的动态属性
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
base_price DECIMAL(10,2),
specs JSONB DEFAULT '{}'
);
INSERT INTO products (name, base_price, specs) VALUES (
'MacBook Pro M3',
1999.00,
'{
"cpu": "Apple M3 Pro",
"ram_gb": 18,
"storage_gb": 512,
"display": "16.2 inch",
"colors": ["Space Black", "Silver"],
"ports": {
"usb_c": 3,
"hdmi": 1,
"sd_card": true
}
}'
);
-- 查询特定规格
SELECT name, specs->>'cpu' AS cpu
FROM products
WHERE (specs->>'ram_gb')::int >= 16;
-- 查询嵌套字段
SELECT name, specs->'ports'->>'hdmi' AS hdmi_count
FROM products
WHERE specs->'ports'->>'sd_card' = 'true';
-- 包含查询(使用 GIN 索引,速度快)
SELECT name FROM products
WHERE specs @> '{"colors": ["Silver"]}';
-- 更新 JSONB 中的特定字段
UPDATE products
SET specs = jsonb_set(specs, '{ram_gb}', '24')
WHERE id = 1;
五、PostGIS——地理信息数据库
PostgreSQL 通过 PostGIS 扩展成为世界上最强大的开源地理信息数据库:
-- 安装 PostGIS 扩展
CREATE EXTENSION postgis;
-- 创建包含地理信息的表
CREATE TABLE stores (
id SERIAL PRIMARY KEY,
name TEXT,
location GEOMETRY(POINT, 4326) -- 经纬度坐标
);
-- 插入数据
INSERT INTO stores (name, location) VALUES
('Store A', ST_MakePoint(-122.4194, 37.7749)), -- 旧金山
('Store B', ST_MakePoint(-118.2437, 34.0522)); -- 洛杉矶
-- 查找 10 公里内的门店
SELECT name,
ST_Distance(
location::geography,
ST_MakePoint(-122.4, 37.78)::geography
) / 1000 AS distance_km
FROM stores
WHERE ST_DWithin(
location::geography,
ST_MakePoint(-122.4, 37.78)::geography,
10000 -- 10 公里,单位米
)
ORDER BY distance_km;
六、PostgreSQL 的商业生态
Supabase
Supabase 是 PostgreSQL 的 Backend-as-a-Service,提供:
- PostgreSQL 实例(自动扩缩容)
- 实时订阅(通过 PG 的 Logical Replication)
- 内置认证(Row Level Security)
- Edge Functions(Deno 运行时)
-- Supabase Row Level Security 示例
-- 每个用户只能看到自己的数据
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own orders"
ON orders FOR SELECT
USING (user_id = auth.uid()); -- auth.uid() 由 Supabase 注入
定价对比:
- 自建 PG(AWS RDS):$50-200/月(单实例)
- Supabase Pro:$25/月(含 8GB 存储 + 自动备份)
- Neon(Serverless PG):按使用量计费,最低 $0
Neon
Neon 是"Serverless PostgreSQL",特点是存储与计算分离:
- 冷启动时间约 500ms(适合低频访问的应用)
- 存储按实际使用量计费(不是预分配)
- 分支功能(类似 Git 分支,每个 PR 可以有独立数据库)
# Neon CLI 创建数据库分支(开发工作流)
neon branches create --name feat-new-schema --parent main
# 在分支上测试迁移
neon connection-string feat-new-schema
# 测试通过后,合并到主分支
neon branches merge feat-new-schema
七、PostgreSQL vs 其他数据库的选型矩阵
| 需求 | 推荐方案 | 理由 |
|---|---|---|
| 通用 Web 应用 | PostgreSQL | 功能全面,生态成熟 |
| 简单 CRUD,团队熟悉 MySQL | MySQL 8.0 | 迁移成本低,功能差距已缩小 |
| 文档存储,Schema 极度灵活 | PostgreSQL JSONB | 无需引入 MongoDB |
| 全文搜索(大规模) | PostgreSQL FTS 或 Elasticsearch | PG FTS 满足 80% 场景 |
| 时序数据 | TimescaleDB(PG 扩展)或 InfluxDB | TimescaleDB 无需学习新查询语言 |
| 地理信息 | PostgreSQL + PostGIS | 业界标准 |
| 纯键值缓存 | Redis | 不要用 PG 做缓存 |
| OLAP 分析(TB 级) | ClickHouse 或 Snowflake | PG 不是列存储,大规模分析慢 |
| 向量搜索(AI 嵌入) | PostgreSQL + pgvector | 无需引入专门的向量数据库 |
八、快速上手:生产级 PostgreSQL 配置
默认的 PostgreSQL 配置是为了在各种硬件上安全启动,不是为了性能优化。生产环境必须调整:
-- postgresql.conf 关键配置(以 16GB RAM 的服务器为例)
-- 内存配置
shared_buffers = 4GB -- 约 25% 的 RAM
effective_cache_size = 12GB -- 约 75% 的 RAM(告诉查询优化器可用的缓存)
work_mem = 128MB -- 每个排序操作/Hash 操作的内存(注意:并发时累积)
maintenance_work_mem = 1GB -- VACUUM、CREATE INDEX 的内存
-- WAL 配置(提高写入性能)
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB
-- 连接配置
max_connections = 200 -- 不要设太高,每个连接占 ~5-10MB
-- 使用连接池(PgBouncer)而不是直接增加 max_connections
-- 查询规划
random_page_cost = 1.1 -- SSD 时设为 1.1(HDD 默认 4.0)
effective_io_concurrency = 200 -- SSD 并发 IO 能力
# 使用 pgbench 验证配置效果
pgbench -c 20 -j 4 -T 60 mydb
关键认知
PostgreSQL 的护城河是它的广度,而不是某一个特定功能:关系型查询、JSON 文档、全文搜索、地理信息、时序数据、向量搜索——它全都能做,且每个都做得足够好。
这意味着对于 90% 的应用,你可以用一个数据库满足所有需求,而不是维护一个 MySQL + MongoDB + Elasticsearch + Redis 的复杂栈。
“如果你不确定该用什么数据库,先用 PostgreSQL。需要换的时候,再换。”