2.2 数据库选型:JSON 文档还是关系型

环境跑通后,第一场真正的技术讨论来了:文章数据存哪。云开发默认提供的是文档型数据库(类似 MongoDB 的 JSON 文档模型),但腾讯云生态里关系型数据库也很成熟。我们俩一个写了多年 MySQL,一个只会 JSON,吵了半个晚上。这一节把当时的对比过程和最终决定完整记录下来,比教科书上的概念对照更贴近真实决策。

场景:一篇文章长什么样

先看数据本身的形态。一篇图文文章,剥到最薄是这样:

{
  "title": "深秋的巷口",
  "cover": "posts/2026/10/cover-abc.jpg",
  "body": "这里是正文的 Markdown 源码,长短随意",
  "tags": ["摄影", "随笔"],
  "authorId": "u_1001",
  "createdAt": 1761292800000,
  "likes": 12
}

关键词是不规则:正文长短不一,标签数量从零到十几个,以后还可能加封面图列表、扩展字段、审核状态。这种读到再写、随时长 schema 的形态,是文档型数据库的主场。

再看查询形态。图文应用的访问模式高度收敛:列表分页查、按 ID 查详情、按标签筛、按时间排序。没有复杂的跨表 JOIN,没有事务一致性要求,最多就是点赞数和文章详情的最终一致。这个判断很关键,选型不是选最强的,是选最贴访问模式的。

把访问模式和存储特性对号入座,结论其实已经写在里面了:

访问模式 频次 文档型支持方式
首页列表分页 最高 where 加 orderBy 走索引
文章详情 高 按 _id 直查
标签筛选页 中 数组包含查询
个人主页 中 authorId 条件查询
作者信息展示 高 列表里冗余字段,免关联
点赞计数更新 中 原子自增操作

六种访问模式里没有一种需要多表关联,全部是单集合条件查询。如果哪天报表需求多到要跑复杂统计,文档型数据库的聚合管道也能做,这个在列表获取一节里展示了我们写的聚合查询。访问模式决定存储选型,而不是反过来,这个顺序别搞反。

方案对比:文档型 vs 关系型

把两种方案在我们这个场景下的差异摊开:

对比项 JSON 文档型 关系型(MySQL 等)
数据形态 灵活 schema,字段随加随有 结构固定,改表要迁移
存储单位 集合(Collection)存文档(Document) 表(Table)存行(Row)
嵌套数据 天然支持,标签数组直接存 需要拆子表再 JOIN
查询能力 灵活过滤、聚合,无跨表 JOIN 完整 SQL,事务和外键
与云函数协作 同一环境内 SDK 直连 另开实例,网络和费用都独立
成本 跟环境套餐一起,免费额度内 单独实例,最低配置也是固定开销
学习成本 会 JSON 就会写 要懂建模和 SQL 优化

最后两行是决策的落点。独立开发者最稀缺的是 注意力和预算:文档型数据库在云开发环境里是自带的,和云函数、存储同一个生态,SDK 直接读写,没有连接池、没有实例续费、没有慢查询告警需要兜底。

关系型当然有它不可替代的场景:订单系统要事务,财务对账要强一致,复杂报表要多表关联。可我们的图文项目一条都不沾。选型讨论里最容易犯的错,就是为永远不会出现的需求过度设计,这个反面例子在腾讯云实践里屡见不鲜。

顺带记录一个小插曲:讨论到一半我们差点因为 用户表加关联订单 的假想需求推翻文档型方案,冷静下来一想,图文项目连付费功能都不在规划里。把假想需求写在白板上逐条划掉之后,文档型方案反而更清晰了。远了想不如近了做,MVP 阶段存储选型的决策窗口就该花在真实访问模式上。

另外还有一层现实考虑:团队就两人,前端只熟 JSON,让他去学 MySQL 建模至少要一周,而文档型他看一眼就会写。上线速度面前,这一周比任何技术指标都值钱。

决定:文档型,但加上纪律

最终我们选了文档型,但加了几条自定的纪律,防止灵活变成混乱。这几条纪律后来在数据表设计一节里落到了字段层面:

  • 每个集合先写字段约定再写代码,字段名、类型、是否必填形成文档,放项目 README 里
  • 时间一律存毫秒时间戳(int 类型),杜绝字符串日期格式不统一
  • 需要查询的字段提前建索引,写代码前先想查询条件,而不是上线后再补
  • 一对多关系用引用 ID,不用大数组无限嵌套

文档型数据库里嵌套自由不等于嵌套合理。 tags 这种短数组可以直接存,但评论如果也塞进文章文档里,文档体积会随时间膨胀,列表查询时会把无用数据一起拉出来。我们的评论是独立集合,用 postId 做引用。这个分界线是:会被列表整体读取的数据放在主文档,独立增长的数据拆出去。

集合规划最终落地成这样一张图,项目里就按它来建:

集合 存什么 读写特点
posts 文章主数据 读多写少,列表走索引
users 用户信息 按登录态查,读为主
comments 评论记录 随文章增长,独立分页
likes 点赞流水 写频繁,原子计数配合
counters 计数器汇总 聚合结果缓存,降低扫描

前两个是 MVP 就有的,后三个是业务跑起来后逐步拆出来的。一开始只有 posts 和 users 完全没问题,集合随业务长,比一上来铺五个空集合务实得多。

实际操作上,云开发控制台的数据库面板可以直接建集合、贴 JSON 写记录,也能加索引,全程不用 SQL:

# 或者用 CLI 完成同样的事
tcb db create collection posts
# 写一条记录验证
tcb db add posts --data '{"title":"测试","authorId":"u_1001"}'

数据库的官方能力细节可以对照本书目录里数据库相关章节,那里有完整的增删改查实操。存储选型部分如果好奇对象存储的用法,可以看后面的图片直传一节,和数据库是同一套环境内的配合。

踩坑:选了文档型才踩的坑

坑一:把关系型思维带进来。 写第一版列表查询时,我习惯性地先查文章 ID 列表再循环逐条查详情,一页十篇文章就是十一次查询。后来用聚合(aggregate)一次查完,接口耗时从八百毫秒降到两位数。文档型不等于不需要优化,只是优化姿势不同。同样的错误还出现在分页上:传统 OFFSET 分页在文档型里要改用游标分页(拿上一页最后一条的 createdAt 作为下一页起点),深度翻页才不会越翻越慢。

坑二:字段名笔误造成沉默错。 有的地方写 createdAt,有的写成 created_at,文档型数据库不报错,查询时默默少半边数据。纪律文档加代码审查能挡住,ESLint 也可以加规则扫字段写法。上线后才发现时已经积了几千条脏数据,清洗花了一整晚。

坑三:索引在数据量上来后补,很疼。 列表按 createdAt 倒序排,早期几百条没感觉,破万之后分页明显变慢。临时加索引会触发全量扫描构建,线上正在写入时操作有风险。正确做法是上线前按查询条件预建索引,宁可先建后用,哪怕个别索引白建了,成本也可忽略。

坑四:免费额度按读写次数算。 有次写脚本批量修数据,一个循环里逐条 update,几万次调用把当天额度刷爆。批量操作用批量更新接口或聚合管道,几条命令解决,别按关系型逐行更新的思路来。

坑五:文档大小有上限。 有篇长文连图带字贴成一篇巨文,文档体积接近单条上限,写入偶发失败。长正文考虑拆分章节字段,或者正文里只存 Markdown 而把渲染产物单独缓存。图片路径本来就只占几十字节,真正占体积的是塞错位置的 base64。

坑六:集合命名漂移。 今天叫 posts 明天叫 articles,代码里两边都引用过,结果一半查询打空集合。集合命名同样写进约定文档,统一复数、统一小写、不用缩写,比事后改名一地鸡毛强。

小结

选型的核心就一句话:让数据形态贴合访问模式。图文应用是读多写少、形态不规则、弱一致需求的典型场景,与文档型数据库天然匹配,而且与云函数同环境免联调、费用并入套餐,综合得分远超关系型。纪律与索引是文档型方案能走远的两条腿。

你的场景如果反过来——强事务、多表关联、报表驱动——那就别勉强,云开发里混用关系型实例也是常见做法,只是辛苦和预算都要自己担。选型决策之外,更完整的项目级权衡可以回看第一章的传统 vs Serverless。

延伸阅读