一张订单日报,先算对“昨天”
想做订单日报,很容易先去选数据同步工具。可在搭链路之前,有一个只用三行数据就能遇到的问题:昨天的支付金额,到底按哪个时间筛?
先在 SQLite 里建一份小样本。金额按分保存;这里的时间都用北京时间的文本表示,方便直接执行对比:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
channel TEXT NOT NULL,
created_at TEXT NOT NULL,
paid_at TEXT,
paid_cents INTEGER NOT NULL
);
INSERT INTO orders VALUES
(1, 'web', '2023-06-13 23:50:00', '2023-06-14 00:05:00', 1000),
(2, 'web', '2023-06-14 23:55:00', '2023-06-15 00:01:00', 2000),
(3, 'app', '2023-06-14 10:00:00', '2023-06-14 10:01:00', 3000);
要统计 6 月 14 日的支付金额。如果从“昨天的订单”这个说法出发,先按创建时间查:
SELECT SUM(paid_cents)
FROM orders
WHERE created_at >= '2023-06-14 00:00:00'
AND created_at < '2023-06-15 00:00:00';
-- 5000
查到了订单 2 和 3,共 50 元。SQL 没错,但订单 2 的钱在 15 日才支付;订单 1 是昨天之前创建的,却在 14 日完成支付,反而被漏掉了。
把条件换成支付时间:
SELECT SUM(paid_cents)
FROM orders
WHERE paid_at >= '2023-06-14 00:00:00'
AND paid_at < '2023-06-15 00:00:00';
-- 4000
这次是订单 1 和 3,共 40 元。那 10 元差额不需要靠数据丢失、消费延迟或精度问题解释,筛选口径已经足够造成它。
分渠道以后,差额还在
报表再加一个分组,事情没有变复杂多少:
SELECT channel, COUNT(*) AS paid_orders, SUM(paid_cents) AS paid_cents
FROM orders
WHERE paid_at >= '2023-06-14 00:00:00'
AND paid_at < '2023-06-15 00:00:00'
GROUP BY channel
ORDER BY channel;
-- app | 1 | 3000
-- web | 1 | 1000
这份样本只保存最终成功的订单金额,所以可以直接汇总。真正在支付流水上统计时,一笔订单可能有多次支付尝试,部分支付方式还允许分次支付;此时不能直接拿流水行数当订单数。
我会先把“这里一行代表什么”写在表旁边。订单、支付尝试和成功支付,是三个不同的粒度。后面即使用了明细层、汇总层这些名字,也不会自动替你选对粒度。
时间也一样。如果业务库保存 UTC,需要先把北京时间这一天的边界转换到 UTC 再查。示例里看着像很自然的两条日期字符串,到了真实库里不能原样照抄。
凌晨已经出报表了,晚到的订单放哪儿
假如订单 1 的支付事件在 14 日 00:05 发生,但同步任务直到 15 日才拿到。日报按支付时间分组,它仍属于 14 日,不能为了让今天的任务好写就算进 15 日。
于是日报除了“算哪天”,还有一个问题:允许回改哪天。
对于这张小报表,重算受影响日期通常比在原结果上修修补补更容易核对。输入保留订单主键,先得到每笔订单当前有效的支付记录,再重新汇总。输出时在一个事务中替换当天的整份结果,不是在旧金额上继续加一次。
整份替换还有个容易漏掉的理由:重算后某个渠道可能不再有任何记录。只对新结果做 upsert,会把那个渠道上一次的金额留在表里。删除当天旧分区、再写入新汇总,或采用等价的分区替换方式,才不会留下这种残影。
如果后面要改成实时看板,迟到数据和更正结果的约定仍然在,只是不能每来一条都重算一整天了。窗口、watermark 和状态管理,都是在这个问题继续变大以后才有具体含义。
在那之前,先把这三笔订单算对,再把同一天重跑一遍。40 元应该还是 40 元。