全栈独立产品数据库选型复盘:PostgreSQL 还是 MongoDB
全栈独立产品数据库选型复盘:PostgreSQL 还是 MongoDB
一、数据库选型的起点:定义你的数据形状
独立产品在技术选型上最大的诱惑是"一步到位"——在 Day 1 就引入 MongoDB 作为主库,因为"Schema 灵活,开发快"。这个决策在产品上线 3 个月后被反噬的案例非常多。问题的根源不是 MongoDB 不好,而是选型的决策依据错了。
数据库选型的首要问题不是"哪款数据库功能更强",而是"你的数据长什么样":
- 数据之间是否有明确且稳定的关系?用户(User)→ 订单(Order)→ 订单明细(OrderItem)→ 商品(Product),这种多对多关系天然适合关系型数据库。
- 数据的 Schema 是否频繁变化?如果每个文档的结构都可能随着业务迭代而不同(如用户行为日志、AI 输出结果),NoSQL 文档模型更灵活。
- 查询模式是什么?90% 的查询是按 ID 查单条记录 → 文档数据库。90% 的查询是多表关联 + 聚合统计 → 关系型数据库。
- 是否需要事务?支付、库存扣减、余额变动 → 必须 ACID 事务 → PostgreSQL。
二、PostgreSQL 的杀手场景:关系型 + JSONB 的混合优势
2.1 为什么 PostgreSQL 是独立产品的默认选择
在独立产品场景下,PostgreSQL 有一个常被低估的优势:它的 JSONB 类型让你可以同时使用关系模型和文档模型。你不需要在"结构化"和"灵活性"之间二选一。
实际应用:核心业务数据(用户、订单、支付)用标准的关系表存储,享受类型检查、外键约束和 JOIN 查询。非核心/易变数据(用户偏好设置、AI 任务配置、日志元数据)用 JSONB 列存储,享受 Schema 灵活性。
-- 用户表:核心字段用关系列,扩展字段用 JSONB CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT NOW(), -- 可变配置用 JSONB,不需要频繁 ALTER TABLE preferences JSONB DEFAULT '{}', ai_config JSONB DEFAULT '{}', metadata JSONB DEFAULT '{}' ); -- JSONB 的索引能力 CREATE INDEX idx_users_preferences ON users USING GIN (preferences); CREATE INDEX idx_users_ai_model ON users ((ai_config->>'model')); -- 查询示例:找到所有启用"夜间模式"且订阅了 Pro 版本的用户 SELECT id, name, email FROM users WHERE preferences @> '{"theme": "dark", "subscription": "pro"}' AND ai_config->>'model' = 'gpt-4o'; -- 更新 JSONB 内部字段(不会锁全行) UPDATE users SET ai_config = ai_config || '{"maxTokens": 4096, "temperature": 0.3}'::jsonb WHERE id = 'xxx';2.2 PostgreSQL 在独立产品中的生产级实践
几个在独立产品中经过验证的 PostgreSQL 实践:
连接池管理
独立产品的 VPS 资源有限(通常 2~4 核 CPU)。PostgreSQL 的连接开销较高(每个连接 ≈ 10MB 内存),推荐使用 PgBouncer 或应用层的连接池:
/** * PostgreSQL 连接池配置(使用 pg-pool) * 适配独立产品的资源限制 */ import { Pool } from 'pg'; const pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, // 关键配置:连接池大小 max: 10, // 最大连接数(2 核 VPS 建议 10~20) idleTimeoutMillis: 30000, // 空闲连接 30s 释放 connectionTimeoutMillis: 5000, // 连接超时 5s // 预准备语句(减少 SQL 解析开销) statement_timeout: 10000, // 单个查询 10s 超时 }); // 健康检查:确认连接池状态 async function healthCheck(): Promise<boolean> { try { const result = await pool.query('SELECT 1'); return result.rows[0] !== undefined; } catch { return false; } }慢查询监控
独立产品不需要 Prometheus + Grafana 的堆栈。一个轻量级的慢查询日志就够了:
// 在 pool.query 上挂载慢查询监控 const originalQuery = pool.query.bind(pool); const SLOW_QUERY_THRESHOLD = 1000; // 超过 1 秒即为慢查询 pool.query = async (...args: Parameters<typeof originalQuery>) => { const start = performance.now(); try { const result = await originalQuery(...args); const duration = performance.now() - start; if (duration > SLOW_QUERY_THRESHOLD) { console.warn(`[SlowQuery] ${duration.toFixed(0)}ms`, { text: typeof args[0] === 'string' ? args[0].slice(0, 200) : args[0]?.text?.slice(0, 200), }); } return result; } catch (error) { const duration = performance.now() - start; console.error(`[QueryError] ${duration.toFixed(0)}ms`, error); throw error; } };三、MongoDB 的适用场景:何处用它真比 PostgreSQL 好
3.1 MongoDB 的三个最佳场景
场景一:Schema 极度多变的日志/事件流
用户的每次 AI 对话交互都是一条事件记录,但不同任务类型(文字生成、图片生成、分析报告)的输出格式完全不同。如果放在关系表中,要么用 EAV(实体-属性-值)反模式,要么用 JSONB(PostgreSQL 也能做),要么用 MongoDB 的文档模型。
// MongoDB 的事件文档:每个文档的结构可以不同 // 文本生成任务 { _id: ObjectId, userId: "xxx", type: "text_generation", input: { prompt: "写一篇关于..." }, output: { text: "...", tokenCount: 450 }, model: "gpt-4o", cost: 0.012, duration: 2300 } // 图片生成任务(完全不同的字段结构) { _id: ObjectId, userId: "xxx", type: "image_generation", input: { prompt: "a cat...", style: "realistic", size: "1024x1024" }, output: { url: "https://...", width: 1024, height: 1024 }, model: "dall-e-3", cost: 0.04, duration: 8500 }场景二:高频写入 + 低复杂度查询
用户行为埋点、API 访问日志这类场景,写入频率极高(每秒数百到数千条),查询模式简单(按时间范围或用户 ID 过滤),不需要 JOIN。MongoDB 的单文档写入性能优于 PostgreSQL 的单行 INSERT(在同等硬件条件下大约快 2~3 倍)。
场景三:地理空间查询
MongoDB 的 GeoJSON 支持和 2dsphere 索引对于"查找附近 X 公里内的商家"这类查询比 PostgreSQL 的 PostGIS 更轻量——不需要安装扩展,语法更简洁。
3.2 MongoDB 的反模式:不要用它做关系型查询
MongoDB 最常见的错误用法是:把原本属于关系型的数据结构强行塞进文档模型,然后用$lookup(类似 SQL JOIN)做跨集合查询。$lookup的性能远不如 PostgreSQL 的 JOIN(在 10 万+ 文档级别,差距可达 10~50 倍),因为 MongoDB 的$lookup在 3.6 之前的版本只支持 left outer join,且不支持索引优化。
一个简单的判断标准:如果 30% 以上的查询需要跨 3 个以上的集合,用 PostgreSQL。
四、技术选型的权衡:JSONB vs. MongoDB 文档的深度对比
4.1 JSONB 的能力边界
PostgreSQL 的 JSONB 非常强大——支持索引(GIN/BTREE)、支持路径查询(->和->>操作符)、支持部分更新。但它不是 MongoDB 的等价替代:
- 嵌套深度限制:JSONB 的嵌套深度受单行最大 1.6GB 限制,但实际上当嵌套超过 10 层时,查询性能急剧下降。
- 写放大:更新 JSONB 中的一个深层字段时,PostgreSQL 需要重写整个行的新版本(MVCC 机制),而 MongoDB 可以原地更新文档的指定字段(WiredTiger 引擎的写时复制粒度更小)。
- 查询语法的学习成本:JSONB 的路径操作符(
#>、@>、?|)需要额外学习,不如 MongoDB 的 JavaScript 风格查询自然。
4.2 独立产品的最终选型建议
| 场景 | 推荐 | 理由 |
|---|---|---|
| 核心业务数据(用户、订单、支付) | PostgreSQL | 关系明确、需要事务、需要 JOIN |
| 用户配置/偏好 | PostgreSQL JSONB | Schema 灵活 + 可在同一个查询中 JOIN 用户表 |
| AI 对话日志/事件流 | MongoDB | Schema 多变、高频写入、按时间范围查询 |
| 文件/媒体元数据 | PostgreSQL(JSONB)或 MongoDB | 两者都可,选已有的数据库减少运维负担 |
| 搜索功能 | PostgreSQL 全文搜索(pg_trgm) | 独立产品初期不需要 Elasticsearch |
| 缓存/会话存储 | Redis(或用 PostgreSQL UNLOGGED 表) | 高频读写的临时数据不适合存入持久化数据库 |
核心原则:优先用 PostgreSQL + JSONB。只有当数据特征明确符合 MongoDB 的文档模型优势(Schema 多变 + 高频写入 + 不需要关系查询)时,才引入 MongoDB 作为第二个数据库。
五、总结
独立产品的数据库选型不是"PostgreSQL vs. MongoDB"的单选题。PostgreSQL 的 JSONB 类型模糊了关系型和非关系型的边界,让单一数据库可以覆盖 80% 的场景。
选型决策的核心是数据形状:数据关系明确且稳定 → PostgreSQL;Schema 频繁变化且查询模式简单 → MongoDB。务实的情况下,PostgreSQL 作为主库 + MongoDB 作为特定场景(日志/事件流)的补充库,是最优架构。
落地建议:从 PostgreSQL + JSONB 开始。当出现明确不适合 PG 的数据场景(如每秒上千条的事件写入导致 PG 写入瓶颈),再评估引入 MongoDB 的增量成本(运维、备份、应用层双数据库管理)是否值得。不要在产品 Day 1 引入多数据库,那会显著增加运维复杂度。
