一个 LOD 模型回答五个电商问题¶
本例用一个语义模型、三张原表回答五个常见的用户、交易与退款问题, 涉及固定字段、按用户等权再聚合、include、exclude、固定指标、组合计算和独立参数窗口。 数据全部合成。概念见理解 LOD,规则见 LOD 编写参考。
运行样例¶
先安装 Dosi 和 DuckDB CLI,使用默认的 datus 模式。 核对脚本需要 Python 3.10 以上版本,无须安装第三方 Python 包。
样例文件随 Dosi 安装归档发布,目录为 examples/lod-commerce。
如果 dosi info 显示的样例目录里没有 lod-commerce,请升级 Dosi。
使用安装脚本时,样例位于 ~/.local/share/dosi/examples;自行解压安装包时,
将 DOSI_EXAMPLES 指向归档里的 examples/ 目录。
样例目录包含完整的 model.yaml、含建表语句和全部合成记录的 seed.sql、
queries.json、expected.json、五份独立原始 SQL、核对脚本和 README。
使用已安装的版本时,先复制样例:
export DOSI_EXAMPLES=~/.local/share/dosi/examples
cp -R "$DOSI_EXAMPLES/lod-commerce" ./lod-commerce
也可以在包含本教程的源码根目录复制:
然后进入工作副本,初始化并核对:
cd lod-commerce
dosi --version
duckdb commerce.duckdb < seed.sql
dosi validate --model model.yaml --strict
python3 verify_lod_commerce.py --case-dir . --variants baseline --report report.json
初始化时使用新数据库文件。最后一条命令执行下文五条语义查询,并用独立原始 SQL
核对完整结果。去掉 --variants baseline 可在五种数据状态下重复运行,共核对 25 次,
期间不修改模型。report.json 保存实际结果、生成 SQL 和校验信息。
下列表格使用业务输出列名,queries.json 明确记录 CLI 指标/字段名到这些名称的映射。
核对不要求行顺序一致。
原始数据与业务口径¶
基础数据有 7 个用户、12 笔已支付订单、9 份退款申请,完整覆盖至 2030 年 5 月 31 日。
金额单位为人民币元,类型为 DECIMAL(12,2)。日期区间左闭右开,4 月包括 4 月 1 日,
不包括 5 月 1 日。
| 原表 | 一行代表什么 | 原始字段 |
|---|---|---|
users |
一个注册用户 | user_id, signup_date, region, acquisition_channel |
orders |
一笔支付成功的订单 | order_id, user_id, paid_date, paid_date_key, channel, paid_amount |
refunds |
一份退款申请的当前状态 | refund_id, order_id, user_id, requested_date, resolved_date, resolved_date_key, status, requested_amount |
三个主键分别为用户编号、订单编号和退款申请编号。退款表的 order_id 可以重复,
因为一笔订单可能有多次部分退款。订单都属于已注册用户,退款用户与订单买家一致。
paid_amount 保留原始实付金额,不因退款扣减。同一用户同一天的多笔订单仍是多次购买。
acquisition_channel 是用户注册时的获客来源,取值为 ads、organic、referral;
订单渠道 channel 则是 app 或 web,两者不能混用。地区是当前属性。
本例没有获客成本,也不涉及多触点归因。
退款状态为 SUCCEEDED、REJECTED 或 PENDING。只有成功申请的金额才是已退款金额。
成功申请的 resolved_date 是退款成功日期;处理中申请没有完成日期。
整数日期码采用 YYYYMMDD 编码,NULL 保持 NULL。成功退款累计不超过订单实付金额。
这些日期和金额都是原始字段,没有提前计算业务指标。
模型声明订单到用户、退款到用户两条普通多对一关系,不直接连接两张明细表。 U07 在基础数据中没有订单,不能进入付费用户均值的分母,也不能进入 4 月买家的召回名单。
模型如何区分计算粒度¶
| 用途 | 模型定义 | 为什么采用这个粒度 |
|---|---|---|
| 买家分类 | orders.buyer_order_count = fixed(order_count, orders.user_id) |
每个买家一个购买次数 |
| 用户累计消费等权平均 | orders.buyer_lifetime_paid = fixed(paid_total, orders.user_id);mean_lifetime_paid 对该字段求平均 |
每个买家只贡献一次 |
| 渠道内购买频次 | AVG(include(order_count, orders.user_id)) 与 MAX(include(order_count, orders.user_id)) |
在展示渠道上增加买家粒度 |
| 当天全站基准 | site_amount = fixed(paid_total, orders.paid_date) |
保留日期,不受 app 展示过滤影响 |
| 所选渠道月总额 | app_month_amount = exclude(paid_total, orders.paid_date) |
去掉日期,保留渠道及其筛选 |
| 召回评估 | 用户表承载固定字段,base 分别筛选订单和退款 | 窗口独立,每个用户输出一行 |
表中是 DATUS 声明含义。完整模型还包含对应的核心 SQL:固定字段保留基础聚合,
include 用 AVG 或 MAX 包住基础聚合,fixed/exclude 指标使用 SUM 包装。
两个 compose 指标分别除以当天全站基准和渠道月总额,使用 NULLIF 在分母为零时返回 NULL。
模型不需要嵌套 LOD 或在 source 中提前汇总数据。
公共数据集、关系与基础指标¶
完整的 model.yaml 和 seed.sql 在运行样例里复制出来的样例目录中。
下面是公共模型,包括原始字段、真实主键、
两条多对一关系和普通基础指标。查询时间范围通过声明为时间维度的 orders.paid_date 路由。
展开公共模型
version: 0.2.0.dev0
semantic_model:
- name: commerce
datasets:
- name: users
source: users
primary_key:
- user_id
fields:
- name: user_id
expression: {dialects: [{dialect: ANSI_SQL, expression: user_id}]}
datatype: String
dimension: {}
- name: region
expression: {dialects: [{dialect: ANSI_SQL, expression: region}]}
datatype: String
dimension: {}
- name: acquisition_channel
expression: {dialects: [{dialect: ANSI_SQL, expression: acquisition_channel}]}
datatype: String
dimension: {}
- name: orders
source: orders
primary_key:
- order_id
fields:
- name: order_id
expression: {dialects: [{dialect: ANSI_SQL, expression: order_id}]}
datatype: String
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: user_id
expression: {dialects: [{dialect: ANSI_SQL, expression: user_id}]}
datatype: String
dimension: {}
- name: paid_date
expression: {dialects: [{dialect: ANSI_SQL, expression: paid_date}]}
datatype: Date
dimension:
is_time: true
- name: paid_date_key
expression: {dialects: [{dialect: ANSI_SQL, expression: paid_date_key}]}
datatype: Integer
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: channel
expression: {dialects: [{dialect: ANSI_SQL, expression: channel}]}
datatype: String
dimension: {}
- name: paid_amount
expression: {dialects: [{dialect: ANSI_SQL, expression: paid_amount}]}
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: refunds
source: refunds
primary_key:
- refund_id
fields:
- name: refund_id
expression: {dialects: [{dialect: ANSI_SQL, expression: refund_id}]}
datatype: String
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: order_id
expression: {dialects: [{dialect: ANSI_SQL, expression: order_id}]}
datatype: String
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: user_id
expression: {dialects: [{dialect: ANSI_SQL, expression: user_id}]}
datatype: String
dimension: {}
- name: resolved_date_key
expression: {dialects: [{dialect: ANSI_SQL, expression: resolved_date_key}]}
datatype: Integer
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
- name: status
expression: {dialects: [{dialect: ANSI_SQL, expression: status}]}
datatype: String
dimension: {}
- name: requested_amount
expression: {dialects: [{dialect: ANSI_SQL, expression: requested_amount}]}
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
relationships:
- name: orders_to_users
from: orders
to: users
from_columns:
- user_id
to_columns:
- user_id
- name: refunds_to_users
from: refunds
to: users
from_columns:
- user_id
to_columns:
- user_id
metrics:
- name: paid_total
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(orders.paid_amount)
- name: order_count
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT orders.order_id)
- name: buyers
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT orders.user_id)
- name: refund_requested_total
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(refunds.requested_amount)
后面五题分别展示需要增加的定义。将 fields 合并到同名数据集的字段列表,
将 metrics 合并到模型已有的指标列表,不要替换整个数据集,也不要重复定义同名数据集。
补齐五题的定义后,就是完整电商模型。使用安装目录里的 model.yaml 时,这些定义已经全部存在。
1. 复购用户贡献了多少销售额?¶
截至 5 月 31 日,把有购买行为的用户分为一次购买和多次购买两组,每个用户计数一次, 金额则汇总该组全部订单。截止日期放在上下文中,同时限定固定分类和金额汇总。 查询使用普通原始行指标,按固定值派生的分类字段分组。
模型定义。 将下面两个字段加入 orders.fields。公共模型的 order_count
按订单去重计数;固定字段把它变成每位买家一个购买次数,buyer_group 再把次数转换成分类维度。
订单表只有支付成功的订单,因此其中每位买家至少购买过一次。这段 CASE 不用于划分未购买用户。
datasets:
- name: orders
fields:
- name: buyer_order_count
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT orders.order_id)
datatype: Integer
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(order_count, orders.user_id)"}}'
- name: buyer_group
expression:
dialects:
- dialect: ANSI_SQL
expression: CASE WHEN buyer_order_count = 1 THEN 'once' ELSE 'repeat' END
datatype: String
dimension: {}
查询。
dosi query --model model.yaml --format json \
--metrics buyers,paid_total \
--group-by orders.buyer_group \
--context-filter 'orders.paid_date < DATE '"'"'2030-06-01'"'"'' \
--execute --db commerce.duckdb
| buyer_group | buyers | paid_amount |
|---|---|---|
| once | 3 | 500 |
| repeat | 3 | 900 |
清单将 paid_total 映射为 paid_amount。U01、U02、U03 属于多次购买组。
退款不减少这里的原始实付金额。
2. 哪个获客渠道带来的付费用户累计消费更高?¶
先计算每个付费用户的累计消费,再按注册来源让每个用户等权参与平均。 这衡量用户消费水平,不是平均订单金额,也不是获客投入回报。 有四笔订单的用户不能获得四倍权重。
模型定义。 将字段加入 orders.fields,指标加入模型的 metrics。
固定键是买家,查询则通过 orders_to_users 按获客渠道分组。
普通指标对固定字段求平均,每个展示渠道内每位买家只贡献一次。
如果写成 AVG(orders.paid_amount),得到的就是订单均值。
字段放在订单表上,统计对象自然是付费买家;改放用户表时,需要另外明确未购买用户是否参与。
datasets:
- name: orders
fields:
- name: buyer_lifetime_paid
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(orders.paid_amount)
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(paid_total, orders.user_id)"}}'
metrics:
- name: mean_lifetime_paid
expression:
dialects:
- dialect: ANSI_SQL
expression: AVG(orders.buyer_lifetime_paid)
查询。
dosi query --model model.yaml --format json \
--metrics mean_lifetime_paid \
--group-by users.acquisition_channel \
--context-filter 'orders.paid_date < DATE '"'"'2030-06-01'"'"'' \
--execute --db commerce.duckdb
| acquisition_channel | mean_lifetime_paid |
|---|---|
| ads | 400 |
| organic | 120 |
| referral | 180 |
广告渠道的两个买家分别消费 500 和 300,均值为 400。 指标从订单总体出发,不把未购买的注册用户计入分母。
3. App 和网页用户的购买频次有什么不同?¶
在 4 月、每个订单渠道内,先计算各买家的订单数,再求平均值和最大值。 跨渠道购买的用户分别进入两边总体,同一天两笔订单仍计为两笔。
模型定义。 将下面两个指标加入 metrics。
本题需要各买家在当前渠道和 4 月范围内的订单数,include 在渠道分组上增加买家键,
然后用 AVG 或 MAX 汇总这些次数。固定买家次数会忽略普通渠道筛选,不能表达这里的渠道内购买频次。
metrics:
- name: mean_orders
expression:
dialects:
- dialect: ANSI_SQL
expression: AVG(COUNT(DISTINCT orders.order_id))
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"lod","expr":"AVG(include(order_count, orders.user_id))"}}'
- name: max_orders
expression:
dialects:
- dialect: ANSI_SQL
expression: MAX(COUNT(DISTINCT orders.order_id))
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"lod","expr":"MAX(include(order_count, orders.user_id))"}}'
查询。
dosi query --model model.yaml --format json \
--metrics mean_orders,max_orders \
--group-by orders.channel \
--start-time 2030-04-01 \
--end-time 2030-05-01 \
--time-dimension orders.paid_date \
--execute --db commerce.duckdb
| channel | mean_orders | max_orders |
|---|---|---|
| app | 1.5 | 2 |
| web | 1 | 1 |
两个 include 指标使用相同的新增维度,可以共查;普通销售额指标需要另查。
4. App 每天卖了多少,占当天全站多少?¶
按 4 月的 app 下单日期,输出当天 app 销售额、全站销售额、app 占比、app 月总额,
以及当天对 app 月总额的贡献。普通 app 条件限定分子和 exclude 小计,
固定的全站日基准仍保留网页销售额。4 月时间范围同时限定这三种计算。
日期分组使用原样的 orders.paid_date,即使存储类型已是日期,也不要添加 :day,
因为带键 fixed 指标要求该键原样出现在展示分组中。
模型定义。 将下面四个指标加入 metrics,共用公共模型的 paid_total。
site_amount 只固定日期,忽略普通 app 筛选;app_month_amount 从展示分组中去掉日期,
保留渠道及其筛选。两者仍受 4 月上下文限制。
两个 compose 指标分别用普通日实付除以这两个基准,零分母返回 NULL。
核心表达式展开指标引用,并保留声明要求的嵌套聚合写法。
metrics:
- name: site_amount
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(SUM(orders.paid_amount))
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"lod","expr":"fixed(paid_total, orders.paid_date)"}}'
- name: app_month_amount
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(SUM(orders.paid_amount))
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"lod","expr":"exclude(paid_total, orders.paid_date)"}}'
- name: daily_site_share
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(orders.paid_amount) / NULLIF(SUM(SUM(orders.paid_amount)), 0)
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"compose","expr":"paid_total / NULLIF(site_amount, 0)"}}'
- name: app_month_share
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(orders.paid_amount) / NULLIF(SUM(SUM(orders.paid_amount)), 0)
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","derive":{"type":"compose","expr":"paid_total / NULLIF(app_month_amount, 0)"}}'
查询。
dosi query --model model.yaml --format json \
--metrics paid_total,site_amount,daily_site_share,app_month_amount,app_month_share \
--group-by orders.channel,orders.paid_date \
--where 'orders.channel = '"'"'app'"'"'' \
--start-time 2030-04-01 \
--end-time 2030-05-01 \
--time-dimension orders.paid_date \
--execute --db commerce.duckdb
| channel | paid_date | app_amount | site_amount | daily_site_share | app_month_amount | app_month_share |
|---|---|---|---|---|---|---|
| app | 2030-04-01 | 260 | 260 | 1 | 570 | 260 / 570 |
| app | 2030-04-02 | 160 | 500 | 0.32 | 570 | 160 / 570 |
| app | 2030-04-03 | 150 | 240 | 0.625 | 570 | 150 / 570 |
paid_total 映射为 app_amount。最后一列为便于核对写成精确分数,CLI 实际返回数值比例。
没有 app 订单的日期不补行。小计指标名称对应本报表,其定义实际跟随查询保留的渠道和筛选。
5. 4 月买过、5 月没再买的用户有哪些?¶
每个符合条件的用户输出一行,包括地区、4 月实付、5 月订单数和按完成日期统计的 4 月成功退款。 退款用于评估召回名单,不是排除用户的条件。
明细根数据集为用户表。跨表固定字段通过已声明的用户键关系分别读取订单和退款。 入选条件是 4 月订单数大于零、5 月订单数为零,因此零金额的已支付订单仍算购买。 缺失退款 SUM 显式转成零,缺失 COUNT 本来就是零。用户主键唯一,无须 DISTINCT。
模型定义。 将下面字段加入 users.fields,四个筛选指标加入 metrics。
公共模型的两条关系按用户键分别对应订单和退款,不把两张事实明细表直接相连。
固定键使用用户编号,确保结果按用户计算,不能换成订单或退款编号。
每个期间都写在筛选基础指标里:参考期订单数和金额共用 reference_start/reference_end,
观察期订单、退款则分别使用独立参数名。核心 SQL 代入参数默认值,derive.where
保留查询传值用的参数标记,固定字段再重复基础指标的 SQL。
缺失 COUNT 为零,缺失退款 SUM 为 NULL,因此只在展示字段中显式使用 COALESCE。
datasets:
- name: users
fields:
- name: reference_orders
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN
orders.order_id END)
datatype: Integer
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(reference_order_count, users.user_id)"}}'
- name: reference_paid
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN orders.paid_amount
END)
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(reference_paid_amount, users.user_id)"}}'
- name: observation_orders
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN
orders.order_id END)
datatype: Integer
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(observation_order_count, users.user_id)"}}'
- name: window_refunded_raw
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(CASE WHEN refunds.resolved_date_key >= 0 AND refunds.resolved_date_key < 99991231 AND refunds.status
= 'SUCCEEDED' THEN refunds.requested_amount END)
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false,"lod":{"expr":"fixed(window_refund_amount, users.user_id)"}}'
- name: window_refunded
expression:
dialects:
- dialect: ANSI_SQL
expression: COALESCE(window_refunded_raw, 0)
datatype: Decimal
custom_extensions:
- vendor_name: DATUS
data: '{"v":"1.11","is_dimension":false}'
metrics:
- name: reference_order_count
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN orders.order_id
END)
custom_extensions:
- vendor_name: DATUS
data: |-
{
"v": "1.11",
"derive": {
"type": "filter",
"base": "order_count",
"where": "orders.paid_date_key >= :reference_start AND orders.paid_date_key < :reference_end"
},
"params": [
{
"name": "reference_start",
"type": "int",
"default": 0,
"min": 0,
"max": 99991231
},
{
"name": "reference_end",
"type": "int",
"default": 99991231,
"min": 0,
"max": 99991231
}
]
}
- name: reference_paid_amount
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN orders.paid_amount
END)
custom_extensions:
- vendor_name: DATUS
data: |-
{
"v": "1.11",
"derive": {
"type": "filter",
"base": "paid_total",
"where": "orders.paid_date_key >= :reference_start AND orders.paid_date_key < :reference_end"
},
"params": [
{
"name": "reference_start",
"type": "int",
"default": 0,
"min": 0,
"max": 99991231
},
{
"name": "reference_end",
"type": "int",
"default": 99991231,
"min": 0,
"max": 99991231
}
]
}
- name: observation_order_count
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(DISTINCT CASE WHEN orders.paid_date_key >= 0 AND orders.paid_date_key < 99991231 THEN orders.order_id
END)
custom_extensions:
- vendor_name: DATUS
data: |-
{
"v": "1.11",
"derive": {
"type": "filter",
"base": "order_count",
"where": "orders.paid_date_key >= :observation_start AND orders.paid_date_key < :observation_end"
},
"params": [
{
"name": "observation_start",
"type": "int",
"default": 0,
"min": 0,
"max": 99991231
},
{
"name": "observation_end",
"type": "int",
"default": 99991231,
"min": 0,
"max": 99991231
}
]
}
- name: window_refund_amount
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(CASE WHEN refunds.resolved_date_key >= 0 AND refunds.resolved_date_key < 99991231 AND refunds.status
= 'SUCCEEDED' THEN refunds.requested_amount END)
custom_extensions:
- vendor_name: DATUS
data: |-
{
"v": "1.11",
"derive": {
"type": "filter",
"base": "refund_requested_total",
"where": "refunds.resolved_date_key >= :refund_start AND refunds.resolved_date_key < :refund_end AND refunds.status = :refund_status"
},
"params": [
{
"name": "refund_start",
"type": "int",
"default": 0,
"min": 0,
"max": 99991231
},
{
"name": "refund_end",
"type": "int",
"default": 99991231,
"min": 0,
"max": 99991231
},
{
"name": "refund_status",
"type": "string",
"default": "SUCCEEDED",
"allowed": [
"SUCCEEDED",
"REJECTED",
"PENDING"
]
}
]
}
查询。
dosi select --model model.yaml --format json \
--from users \
--fields users.user_id,users.region,users.reference_paid,users.observation_orders,users.window_refunded \
--where 'users.reference_orders > 0 AND users.observation_orders = 0' \
--param reference_start=20300401 \
--param reference_end=20300501 \
--param observation_start=20300501 \
--param observation_end=20300601 \
--param refund_start=20300401 \
--param refund_end=20300501 \
--param refund_status=SUCCEEDED \
--limit 10000 \
--execute --db commerce.duckdb
| user_id | region | april_paid | may_orders | april_refunded |
|---|---|---|---|---|
| U02 | south | 200 | 0 | 30 |
| U03 | north | 200 | 0 | 0 |
| U05 | south | 40 | 0 | 0 |
| U06 | east | 160 | 0 | 0 |
清单将 reference_paid、observation_orders、window_refunded 映射为带月份的展示列名,
模型字段本身保持可复用。三组参数分别控制参考期、观察期和退款期,refund_status
控制退款状态。参考期订单数与实付金额使用相同参数名。只改退款窗口,会改变展示退款金额,
不会改变入选用户。不要再加全局查询时间范围,让原本独立的窗口意外交叉截断。
明细查询最多返回 10000 行,本例显式使用这个上限。这是小规模分析样例,不演示批量导出或分页。
样例验证了什么¶
五条查询在一个模型中使用 LOD 的主要表达形式,受组合限制的计算分别查询。 它们还揭示常见口径错误:把退款申请数当成订单数、按订单数给用户加权、 把全站分母也筛成 app,以及把独立观察窗口相互截断。
五条查询不穷举所有键形态、空值、跨事实对齐、时间编码、错误或数据库方言, 这些边界由规则文档说明。核对脚本用金额变化、重复购买、新日期和日期边界记录 重复验证相同五个问题,不增加对外业务场景。