知识全景图/ 软件工程与系统/ 如何设计业务数据库/ 如何设计数据库查询视图、读模型与报表宽表

如何设计数据库查询视图、读模型与报表宽表

不要先背 CQRS 和宽表;先解释“明细是对的,首页和月报为什么是错的”

这一页只解决一个问题: 当账目事实已经写对,用户在列表、首页、搜索、月报和导出里为什么还会看到错数。读模型不是概念装饰,它就是“用户读到的那一层数据库设计”。
一、事故:明细有 88 元,首页和月报没有

用户新增一笔 88 元晚餐。明细页能看到,说明事实表写入成功;但首页“本月支出”没变,分类列表没有,搜索搜不到,月报导出也少 88 元。此时不要再问“查哪张表”,先问“这些页面各自读的是不是同一个世界”。

错在哪里说明什么设计动作
明细对,首页错首页不是事实表,是聚合读模型定义来源、刷新时机、重算范围
明细对,搜索错搜索索引不是事实源索引可延迟,但必须可补建和对账
页面对,导出错两个读路径口径分叉报表宽表要有口径版本
昨天对,今天错旧批次或旧事件覆盖了新结果读模型要有构建批次和水位
二、建读模型前只问 6 个问题
问题例子
这个读模型服务哪个页面或报表账目列表、首页摘要、月报导出不能混成一张万能表
它的事实来源是什么账目、状态事件、分类快照、导入来源、汇率版本
它允许延迟多久列表要接近实时,月报可以批量生成,搜索可以标注延迟
它错了按什么范围重算按用户、账本、月份、分类、entry_id 或 batch_id
它怎样证明和事实源一致条数、金额、hash、抽样样本、差异明细
旧结果能不能覆盖新结果用 version、watermark、build_batch_id 拦住旧批次回写
三、最小可信模型

不要一上来做“万能宽表”。先为一个读问题建一个最小模型。下面是月报读模型,不是事实表。

monthly_report_rows - id - ledger_id - report_month - metric_version - build_batch_id - source_entry_id - amount_minor - currency - category_snapshot_id - included_status_rule - source_updated_at - row_hash - published_at
吸收点: 事实表回答“发生了什么”;读模型回答“某个场景要怎样读”。读模型可以冗余,但必须能从事实源重建。
四、列表、首页、月报各守一条边界
读场景不要怎么做应该怎么做
列表SELECT * 加 offset 深分页用稳定游标:occurred_at + id,并统一状态、权限、来源字段
首页摘要把聚合结果当随手缓存写清来源事实、刷新触发、失效范围、重算办法
月报宽表只存一个汇总金额保存口径版本、构建批次、来源 entry、分类快照和发布时间
搜索索引把搜索结果当事实裁决搜索只负责找,打开详情仍回事实表和权限校验
关键边界: 读模型错了,不要直接手改宽表金额。优先回到事实源,重放生成逻辑,或按明确范围重建。
五、用 6 个样本证明它可信
样本通过标准
新增一笔账明细、列表、首页、搜索、月报最终一致,能解释延迟
作废一笔账月报和首页撤掉金额,搜索不再展示有效结果
发生时间跨月修改旧月和新月都被重算,不只更新新月
分类改名列表可显示新名,历史月报不静默漂移
旧批次晚到旧 build_batch_id 不能覆盖已发布的新版本
搜索漏文档对账能发现 entry_id 差异,并按 id 补建
六、下一步只按事故下钻
如果月报金额对不上
时间、金额、币种与统计口径,先把口径固定。
如果读模型靠异步更新
CDC、数据同步与派生数据一致性,处理位点、重放和对账。
如果想做发布前验收
测试、校验与数据正确性护栏,把差异做成门禁。
总结: 读模型设计不是“多建几张方便查的表”。它的核心是:为一个读场景固定来源、口径、刷新、版本、重算和对账。只要这 6 件事说不清,宽表越多,用户看到的世界越容易和事实源分叉。