电商订单库和报表库如何拆分:从 OLTP 到 OLAP
订单库负责交易正确性,报表库负责分析查询。两者拆分的关键是数据同步、状态建模、宽表设计和指标口径。
电商订单库和报表库如何拆分:从 OLTP 到 OLAP
电商系统早期经常直接在订单库上做报表。订单量不大时,这样简单直接:查订单表,按日期聚合,算 GMV、订单量、支付人数。
但业务增长后,问题会很快出现。报表查询要扫大量历史数据,订单库还要支撑下单、支付、退款、发货等核心链路。复杂聚合一旦压垮主库,影响的不是报表,而是交易。
所以订单库和报表库迟早要拆。
订单库和报表库的职责
订单库是 OLTP 系统。它负责核心交易链路,强调事务、状态流转、低延迟和强一致。
报表库是 OLAP 系统。它负责统计、聚合、多维分析和历史查询,强调扫描效率、查询吞吐和指标服务。
订单库回答的问题是:这笔订单现在是什么状态,能不能支付,能不能退款。
报表库回答的问题是:今天 GMV 多少,不同渠道转化如何,哪些商品卖得最好,退款率有没有异常。
订单库的典型表
订单库通常会有:
orders
order_items
payments
refunds
shipments
order_status_logs
orders 保存订单主信息。order_items 保存商品明细。payments 保存支付流水。refunds 保存退款和售后。shipments 保存物流信息。order_status_logs 保存状态变化过程。
订单库的索引要围绕业务操作设计,比如按订单号查询、按用户查询订单列表、按支付单号查支付状态、按状态扫描待处理订单。
它不应该为了报表随意增加大量分析索引,更不应该让复杂报表直接扫主库历史数据。
订单库的表更关注事务和状态:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE order_status_logs (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32) NOT NULL,
changed_at TIMESTAMP NOT NULL
);
报表需求是什么
电商报表常见需求包括:
- 每日 GMV。
- 支付订单数。
- 下单用户数。
- 支付转化率。
- 商品销售排行。
- 类目销售分析。
- 渠道销售分析。
- 地域销售分析。
- 退款率和售后率。
- 新老用户贡献。
这些查询通常按时间范围扫描大量订单,再按商品、用户、渠道、地区、店铺、类目等维度聚合。
这正是 OLAP 数据库擅长的访问模式。
数据同步方案
最简单的是定时任务同步。比如每五分钟从订单库拉取增量数据,写入报表库。优点是简单,缺点是延迟和一致性控制较弱。
更常见的是 CDC 同步。通过 MySQL binlog 或 PostgreSQL WAL 捕获订单库变化,把 insert、update、delete 转成事件。
如果业务系统本身已经发送订单事件,也可以通过消息队列同步。比如订单创建、支付成功、退款成功都发送到 Kafka。
复杂实时场景会使用 Kafka + Flink。Kafka 承接变更流,Flink 做清洗、关联和聚合,然后写入 ClickHouse、Doris 或 StarRocks。
实践中经常采用全量 + 增量。先全量同步历史订单,再用 CDC 或消息队列追增量。
报表库如何建模
报表库通常不会照搬订单库的范式模型。
订单库为了事务和数据一致性,会把订单、商品、支付、退款拆成多张表。报表库为了查询效率,经常需要宽表。
订单明细宽表可以包含:
order_id
order_item_id
user_id
sku_id
product_id
category_id
shop_id
channel
province
city
order_time
pay_time
order_status
pay_status
quantity
origin_amount
pay_amount
discount_amount
refund_amount
宽表的价值是减少查询时 Join。报表查询可以直接按日期、渠道、商品、类目聚合。
报表库的宽表更关注分析维度:
CREATE TABLE order_report_wide (
order_date Date,
order_id UInt64,
order_item_id UInt64,
user_id UInt64,
sku_id UInt64,
product_name String,
category_name String,
channel LowCardinality(String),
province LowCardinality(String),
pay_amount Decimal(12, 2),
refund_amount Decimal(12, 2),
order_status LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(order_date)
ORDER BY (order_date, channel, category_name, sku_id);
快照字段很重要
电商报表里有一个容易被忽略的问题:维度会变化。
商品名称会改,类目会调整,用户等级会变化,店铺归属也可能变化。如果报表查询总是关联最新维度表,历史报表就可能被改写。
因此宽表里通常要保存快照字段。订单发生时的商品名称、类目、渠道、价格、用户等级,都应该在事实表里保留当时版本。
报表统计的是历史事实,不应该被后来的维度变更随意影响。
状态变化如何处理
订单不是一次写完就结束。它会经历创建、支付、发货、完成、取消、退款等状态。
如果报表库只同步订单最终状态,就无法分析过程。比如无法知道支付耗时、取消发生在哪个阶段、退款前订单经历了什么。
所以订单状态日志很重要。它能帮助还原状态流转,也能支撑漏斗和异常分析。
对于报表库,可以同时保留订单当前快照表和订单状态事件表。当前快照表方便查最新状态,状态事件表方便分析过程。
一致性与对账
订单报表最怕数字对不上。
实时链路可能因为重复消息、乱序更新、迟到事件、任务重启而产生偏差。因此关键指标最好有离线校准。
常见做法是实时链路给出近实时趋势,离线任务在 T+1 重新计算权威指标,并和实时结果对账。
对账不仅要看总数,还要能定位差异。比如按日期、渠道、状态分组比较,找到是哪一类订单产生偏差。
查询优化
报表库通常按订单时间或支付时间分区。大多数经营报表都有时间范围,时间分区能显著减少扫描。
排序键或分桶字段要围绕高频过滤条件设计,比如商家、商品、渠道、用户、状态。
高频指标可以做预聚合。比如日级 GMV、小时级订单量、商品销量排行,不一定每次都从明细表实时计算。
冷热数据也可以分层。最近 30 天数据保留高性能存储,历史数据压缩或放到成本更低的存储。
常见坑
第一个坑是报表直接查订单主库。早期省事,后期风险极高。
第二个坑是没有订单状态日志。只有最终状态,很多业务问题无法追溯。
第三个坑是宽表没有快照字段。维度一变,历史报表跟着变。
第四个坑是指标口径不统一。GMV 是否包含退款,支付金额是否扣优惠券,取消订单是否计入下单量,都必须明确。
第五个坑是只做实时不做校准。实时链路适合看趋势,但财务和经营核心指标需要可对账。
小结
订单库和报表库拆分,本质是 OLTP 和 OLAP 的职责拆分。
订单库保护交易链路,报表库服务分析查询。中间通过 CDC、消息队列、Flink 或定时任务同步数据。
真正难的不是把数据搬过去,而是设计好状态事件、宽表快照、指标口径和对账机制。只有这些做好,报表库才不是另一份混乱数据,而是可信的分析系统。