选择合适的数据库:关系型与NoSQL在实际项目中的应用
一个实用的决策框架,根据您的实际需求,在PostgreSQL、MongoDB、Firebase、Redis及其他数据库之间做出选择。

“视情况而定”是所有数据库问题的诚实答案,但它也毫无用处。当你启动一个项目需要选择数据库时,你需要比权衡列表更具体的东西。你需要一个决策框架,它能考虑到你的实际需求——数据形态、查询模式、团队专业知识、扩展预期和操作复杂性。
我在不同的生产项目中使用过PostgreSQL、MongoDB、Firebase/Firestore、Redis和SQLite。每个选择都适合其上下文,也有少数选择是错误的,不得不进行迁移。这些决策背后的模式比任何基准比较都更有用。
决策框架
在查看具体数据库之前,请回答关于您项目的这五个问题:
1. 您的数据是什么形态?
这无关“关系型与文档型”之争。它关乎您的数据实体之间如何相互关联。
高度关系型:用户有订单,订单有商品,商品属于类别,类别有层级结构。如果您的数据模型画出来像一个有许多连接的图,那么您需要强大的关系型能力。
文档型:每个实体相对独立。一篇博客文章包含其标题、正文、标签和作者信息。您很少需要跨实体类型进行连接。
键值型:您需要通过键存储和检索数据。会话数据、配置、功能标志。
时间序列型:事件、指标、日志。写入密集、只追加、按时间范围查询。
2. 您的查询模式是什么?
已知查询:您确切知道应用程序将运行哪些查询。电子商务:“获取用户订单”、“按类别查找产品”、“计算本月总收入”。已知查询倾向于关系型数据库,您可以通过索引和查询计划进行优化。
即席查询:用户可以以不可预测的方式进行搜索、过滤和聚合。分析仪表板、搜索功能、报告工具。这些倾向于灵活的查询引擎。
简单查找:大多数读取是“按ID获取文档”。文档数据库和键值存储在此方面表现出色。
3. 您的数据一致性要求是什么?
强一致性:银行、库存,任何读取过时数据会导致实际问题的情况。具有ACID事务的关系型数据库是安全的选择。
最终一致性:社交动态、分析、缓存。您可以容忍读取稍微过时的数据,以换取性能和可用性。
4. 您的扩展路径是什么?
垂直扩展即可:大多数应用程序。如果您的数据库可以在一台机器上运行并有增长空间,那么任何数据库都可以。现代服务器上的PostgreSQL可以轻松处理数百万行数据。
需要水平扩展:您需要将数据分布到多台机器上。这种情况比大多数开发者想象的要少见,但当您需要时,您的数据库选择会受到限制。
5. 您的团队了解什么?
这个因素被低估了。一个PostgreSQL专家团队在使用PostgreSQL时会更高效,即使MongoDB在理论上更适合数据模型。学习新数据库的成本——调试不熟悉的错误、学习操作最佳实践、理解性能特征——是真实且显著的。
PostgreSQL:默认选择
如果您不确定,请选择PostgreSQL。这在后端工程师中并非一个有争议的观点——它是一个共识。PostgreSQL可以处理关系型数据、JSON文档、全文搜索、地理空间查询和时间序列数据。它在这些方面都不是最好的,但它在所有方面都足够好,可以作为大多数应用程序的单一数据库。
优势
ACID事务。 当您需要原子性地更新多个表时——例如扣减库存并创建订单,或在账户之间转账——PostgreSQL保证了正确性。
丰富的查询能力。 窗口函数、CTE(公共表表达式)、递归查询、横向连接。SQL不仅仅是SELECT * FROM——它是一种强大的查询语言,无需将数据移动到应用层即可表达复杂的分析。
-- 查找每个用户最近的订单及累计总额
WITH ranked_orders AS (
SELECT
user_id,
order_id,
total,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn,
SUM(total) OVER (PARTITION BY user_id ORDER BY created_at) as running_total
FROM orders
)
SELECT * FROM ranked_orders WHERE rn = 1;
JSONB列。 当您的部分数据确实是无模式的——用户偏好、动态表单响应、第三方webhook负载——您可以将其存储为JSONB并进行查询,并获得完整的索引支持。
-- 存储和查询半结构化数据
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB DEFAULT '{}'
);
-- 索引特定的JSON路径
CREATE INDEX idx_products_color ON products USING GIN ((attributes->'color'));
-- 查询JSON数据
SELECT name FROM products
WHERE attributes->>'color' = 'blue'
AND (attributes->>'weight')::numeric < 10;
扩展生态系统。 PostGIS用于地理空间数据,pg_trgm用于模糊文本搜索,TimescaleDB用于时间序列数据,pgvector用于AI嵌入。扩展模型意味着PostgreSQL可以适应专业工作负载,而无需替换您的主数据库。
何时PostgreSQL不是正确的选择
- 需要水平扩展的巨大写入吞吐量。 PostgreSQL垂直扩展良好(更大的机器),但水平分片复杂。如果您需要每秒在数十台机器上写入数百万个事件,请考虑专用解决方案。
- 需要快速原型开发,且模式每天都在变化。 在初创公司的早期阶段,当数据模型每周都发生根本性变化时,与无模式选项相比,迁移的开销可能会拖慢您的速度。
- 当您的团队完全没有SQL经验 且项目时间线不允许学习时。这种情况很少见,但确实存在。
MongoDB:当文档模型有意义时
MongoDB受到的批评比它应得的要多。早期的营销(“无模式!网络规模!”)制造了不切实际的期望,许多开发者将其用于PostgreSQL会是更好选择的用例。但确实存在MongoDB是正确选择的真实场景。
MongoDB的优势
内容管理系统。 每条内容的结构都不同——文章的字段与视频不同,视频的字段与播客不同。在MongoDB中,这些都是同一集合中具有不同形态的文档。在PostgreSQL中,您要么创建一个包含许多可空列的宽表,要么使用JSON列(此时您在PostgreSQL中使用了MongoDB的范式),要么创建每个类型一张表并进行复杂的连接。
事件溯源和日志记录。 高写入吞吐量,文档主要用于写入和读取,很少更新,从不连接。
嵌入式文档减少连接。 如果订单总是需要其商品,并且商品从不脱离订单被访问,那么将商品嵌入到订单文档中意味着您最常见的查询是单个文档查找,而不是跨两个表的连接。
// 带有嵌入式子文档的MongoDB文档
{
_id: ObjectId("..."),
customer: {
name: "John Doe",
email: "john@example.com"
},
items: [
{ product: "Widget", quantity: 2, price: 9.99 },
{ product: "Gadget", quantity: 1, price: 24.99 }
],
total: 44.97,
status: "shipped",
createdAt: ISODate("2026-03-01T10:00:00Z")
}
水平扩展是一项一流功能。 MongoDB的分片是内置的且文档完善。如果您确实需要将数据分布到许多节点(数亿个文档,高吞吐量),MongoDB比PostgreSQL处理得更优雅。
何时MongoDB是错误的选择
- 高度关系型数据。 如果您在每个查询中都编写
$lookup聚合(MongoDB版本的连接),那么您选错了数据库。 - 当您频繁需要跨集合事务时。 MongoDB支持多文档事务,但它们会增加延迟和复杂性。如果您的核心操作需要跨集合的原子性,PostgreSQL的事务模型更自然。
- 当您确实需要一个模式时。 “无模式”听起来很自由,直到您意识到每个从数据库读取的代码都隐式地定义了一个模式。如果没有数据库强制执行的模式,您会将验证移到应用层,在那里更容易出错,也更难一致地执行。
Firebase和Firestore:快速开发
Firebase实时数据库和Firestore服务于一个特定的利基市场:那些开发速度比数据模型纯粹性更重要的应用程序,以及后端主要是一个数据持久化和同步层,而不是一个复杂的业务逻辑引擎的应用程序。
Firebase的亮点
移动优先的实时同步应用。 Firebase的SDK开箱即用地处理离线缓存、实时监听器和冲突解决。使用PostgreSQL从头构建这些功能需要一个WebSocket层、一个缓存策略和大量的自定义代码。
没有后端工程师的小团队。 Firebase消除了管理数据库服务器、构建API层、实现身份验证和处理文件存储的需求。对于一个两人团队构建MVP来说,这种操作开销的减少可能是产品能否发布的决定性因素。
原型设计和验证。 当您需要在两周内与真实用户测试一个想法时,Firebase让您能够完全专注于客户端应用程序。如果想法得到验证,您可以稍后迁移到更传统的后端。
Firebase的局限性
Firestore中的查询限制。 您不能查询未索引的字段。您不能对多个字段进行不等于过滤。您不能进行全文搜索。这些限制迫使您积极地进行非规范化,有时甚至在集合之间复制数据。
// Firestore:您不能这样做
db.collection('products')
.where('price', '>', 10)
.where('rating', '>', 4)
.orderBy('name') // 错误:需要price + rating + name上的复合索引
// Firestore:您可以这样做(使用复合索引)
db.collection('products')
.where('price', '>', 10)
.where('rating', '>', 4)
.orderBy('price') // 必须按不等于条件中使用的字段排序
供应商锁定。 您的数据模型、安全规则和查询模式都是Firebase特有的。从Firebase迁移出去是一次重写,而不是一次迁移。
成本不可预测性。 Firebase按文档读/写计费。一个优化不佳的查询或一个大型集合上的实时监听器可能会产生惊人的账单。我见过一些项目,一个配置错误的监听器所花费的成本比一个专用PostgreSQL服务器一年的费用还要高。
有限的服务器端逻辑。 Cloud Functions可以处理一些服务器端处理,但复杂的业务逻辑——多步骤事务、数据聚合、后台处理——无论如何都需要创造性的变通方法或一个单独的后端。
决策:Firebase vs 传统后端
| 因素 | Firebase | PostgreSQL + API |
|---|---|---|
| MVP时间 | 天 | 周 |
| 运维开销 | 接近零 | 中等 |
| 查询灵活性 | 有限 | 完整SQL |
| 规模化成本 | 不可预测 | 可预测 |
| 供应商锁定 | 高 | 低 |
| 离线支持 | 内置 | 需自行构建 |
| 复杂业务逻辑 | 困难 | 自然 |
| 所需团队 | 仅前端 | 前端 + 后端 |
Redis:缓存、队列及更多
Redis对于大多数应用程序来说不是一个主数据库(尽管带有持久化模块的Redis可以扮演这个角色)。它是一个高性能的数据结构存储,擅长解决特定问题。
缓存
最常见的Redis用例。缓存昂贵的数据库查询、API响应或计算结果。
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_user_profile(user_id: str):
# 首先检查缓存
cached = r.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# 缓存未命中 — 查询数据库
profile = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 缓存5分钟
r.setex(f"user:{user_id}", 300, json.dumps(profile))
return profile
会话存储
Redis的键值模型支持TTL(time-to-live),非常适合会话数据。对于有状态应用程序,它比数据库支持的会话更快,也比基于JWT的解决方案更简单。
速率限制
def is_rate_limited(user_id: str, limit: int = 100, window: int = 60) -> bool:
key = f"rate:{user_id}"
current = r.incr(key)
if current == 1:
r.expire(key, window)
return current > limit
任务队列
Redis的列表和流是优秀的任务队列。像Bull (Node.js)、Celery (Python) 和 Sidekiq (Ruby) 这样的库都使用Redis作为它们的消息代理。
何时不使用Redis
- 作为您的唯一数据库。 Redis默认是内存型的。即使有持久化(RDB快照或AOF日志),它也不是为不能丢失的数据设计的。
- 用于复杂查询。 Redis数据结构(字符串、列表、集合、有序集合、哈希)功能强大,但不能像数据库那样进行查询。您通过键访问数据,而不是通过任意条件。
- 当您没有缓存问题时。 将Redis添加到不需要缓存的技术栈中会增加操作复杂性,而没有任何好处。一个耗时5毫秒的PostgreSQL查询不需要被缓存。
使用多个数据库
大多数中等复杂度的生产应用程序都会使用不止一个数据库。关键在于让每个数据库发挥其最佳作用,而不是试图强迫一个数据库处理所有事情。
一个常见的Web应用程序模式:
- PostgreSQL 用于核心业务数据(用户、订单、产品)。
- Redis 用于缓存、会话和速率限制。
- S3(或等效服务) 用于文件存储(图片、文档、备份)。
对于实时应用程序:
- PostgreSQL 用于持久数据和复杂查询。
- Redis 用于发布/订阅和缓存。
- Firebase/Supabase 用于移动客户端的实时同步。
集成模式
当使用多个数据库时,要建立清晰的所有权。每条数据都有一个权威来源,其他系统则是该数据的缓存或视图。
PostgreSQL (数据源)
├── Redis (读取缓存,TTL后过期)
├── Elasticsearch (搜索索引,通过变更数据捕获同步)
└── Firebase (移动同步,通过webhook更新)
绝不要让两个数据库都认为自己是同一数据的所有者。那条路会导致一致性噩梦,几乎无法调试。
迁移考量
如果您意识到选错了数据库,迁移是可能的,但代价高昂。以下是实际考量:
MongoDB到PostgreSQL是我见过最常见的迁移。通常的原因是应用程序超出了文档查询的范围,需要复杂的连接、事务或聚合。迁移涉及设计关系型模式、编写转换脚本,并更新应用程序中的每个数据库查询。对于一个中等复杂度的应用程序,预算两到四周。
PostgreSQL到MongoDB较为罕见,但当应用程序的数据模型主要变为文档型时会发生。这种迁移在机械上更简单(将表扁平化为文档),但需要重新思考每个查询并失去事务保证。
Firebase到PostgreSQL是最困难的迁移,因为它不仅仅是数据库的改变——它是一次架构的改变。您需要构建一个API层,实现身份验证,用WebSockets或轮询替换实时监听器,并处理离线同步。这更像是一次重写而不是迁移。
最好的迁移是您避免的迁移。 提前多花一天时间思考您的数据模型。与构建过类似应用程序的人交流。最初选择正确数据库的成本总是低于后期迁移的成本。
成本分析
数据库在规模化时的成本可能会令人惊讶,尤其是对于托管服务。
| 服务 | 免费层级 | 小型生产环境 | 中型生产环境 |
|---|---|---|---|
| Supabase (PostgreSQL) | 500MB, 2个项目 | 约$25/月 (8GB, 2核) | 约$100/月 (32GB, 4核) |
| Neon (PostgreSQL) | 0.5GB 存储 | 约$19/月 | 约$69/月 |
| MongoDB Atlas | 512MB 共享 | 约$57/月 (M10 专用) | 约$200/月 (M30) |
| Firebase Firestore | 1GB 存储, 5万次读取/天 | 约$25-100/月 (波动大) | $100-1000/月 (取决于查询) |
| Redis Cloud | 30MB | 约$7/月 (250MB) | 约$60/月 (1GB) |
| PlanetScale (MySQL) | 5GB, 10亿行读取/月 | 约$39/月 | 约$99/月 |
Firebase的成本不可预测性值得强调。我见过一些项目,成本数月保持在每月30美元以下,但在推出一项增加文档读取的功能后,飙升至300美元。而PostgreSQL或MongoDB的成本与机器规模相关,这是可预测的。
我的实际决策过程
当我开始一个新项目时,决策通常是这样的:
- 默认选择PostgreSQL,除非有特殊原因不选择。
- 如果应用程序有缓存需求、速率限制或任务队列,则添加Redis。
- 如果应用程序是移动优先、有实时需求且团队规模较小,则考虑Firebase/Supabase。
- 如果数据模型确实是文档型且关系型需求极少,则考虑MongoDB。
- 仅当特定工作负载需要时,才添加专用数据库(Elasticsearch、TimescaleDB等)。
这个框架在我的各种项目中都表现良好,从餐厅管理平台到移动健康应用程序。关键的洞察是,数据库是基础设施——它应该服务于您的应用程序需求,而不是驱动您的架构。选择那个有效且“无聊”的选项,并将您的工程精力投入到使您的应用程序与众不同的部分。