Metabase DatetimeSubtract 表达式完全指南:语法、参数、实战案例与底层实现
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
datetimeSubtract是 Metabase 查询构建器(Query Builder)中处理时间序列数据的核心自定义表达式,它从一个日期/时间值中减去指定数量的时间单位,常用于计算会话、订阅等带"开始/结束"标记的数据。读完本文,你将掌握datetimeSubtract的完整语法与参数约束、三种典型实战场景(计算开始时间、判断当前时间是否落在区间内、时区换算),并从源码层面理解它在 SQL 数据库与 MongoDB 中的真实执行原理。
语法与基础行为
datetimeSubtract的语法非常简洁,只接收三个参数:
| 语法 | 示例 |
|---|---|
datetimeSubtract(column, amount, unit) | datetimeSubtract("2021-03-25", 1, "month") |
| 从日期/时间值中减去指定数量的时间单位 | 结果:2021-02-25 |
从实现角度看,这个表达式在 Metabase 的现代 MBQL(Metabase Query Language)层面对应:datetime-subtract子句。在 src/metabase/lib/schema/expression/temporal.cljc 中可以看到,它与:datetime-add一起被定义为一个三元组子句,三个位置参数分别约束为:
- 第一个参数(
expr):必须是::expression/temporal,即时间类型表达式; - 第二个参数(
amount):必须是整数:int; - 第三个参数(
unit):必须是::temporal-bucketing/unit.date-time.interval,即合法的日期时间间隔单位。
同时该子句的返回类型遵循:lib.type-of/type-is-temporal-type-of-first-arg规则——即返回类型与第一个时间参数的类型一致(见 temporal.cljc 中的类型推断实现),所以对日期列做减法返回日期、对带时间的列做减法返回带时间的值。
三个参数详解
column(被减的时间值)
column可以是以下任意一种:
- 时间戳列(timestamp column)的名称;
- 一个返回 datetime 的自定义表达式;
- 一个符合
"YYYY-MM-DD"或"YYYY-MM-DDTHH:MM:SS"格式的字符串(如上方示例)。
值得注意的是,源码中对该参数的类型推断做了特殊处理:由于格式化的日期字符串在类型系统中可能同时被推断为:type/String与:type/DateTime的并集,而这里做的是日期运算,因此代码会在类型并集中取交集、只保留:type/Date与:type/DateTime中的时间类型(见 temporal.cljc 的type-of-method实现),保证字符串日期也能正确参与运算。
unit(时间单位)
unit可以是以下任一单位:
"year"(年)"quarter"(季度)"month"(月)"day"(天)"hour"(小时)"minute"(分钟)"second"(秒)"millisecond"(毫秒)
在 src/metabase/lib/schema/temporal_bucketing.cljc 中,这些单位由日期间隔单位集合(date-interval-units,包含:year、:quarter、:month、:week、:day)与时间间隔单位集合(time-interval-units,包含:millisecond、:second、:minute、:hour)取并集而来,最终形成datetime-interval-units。前端的单位下拉列表正是按照这一套校验枚举来渲染的,传入列表之外的单位会直接触发表达式校验错误。
amount(减去的数量)
amount有两条硬性约束:
- 必须是整数,不能使用小数。例如你不能减去"半年"(0.5)。
- 可以是负数:
datetimeSubtract("2021-03-25", -1, "month")会返回2021-04-25——减去负数等价于加上正数,这一特性使datetimeSubtract与datetimeAdd完全可以互换使用(详见后文 datetimeAdd 一节)。
实战场景一:计算开始时间
假设你计划一个外出之夜,已知每段行程需要 30 分钟,需要反推每个预约点的出发时间:
| 活动 | 到达时间(Arrive By) | 出发时间(Depart At) |
|---|---|---|
| 喝一杯 | 2022 年 11 月 12 日 18:30 | 2022 年 11 月 12 日 18:00 |
| 晚餐 | 2022 年 11 月 12 日 20:00 | 2022 年 11 月 12 日 19:30 |
| 跳舞 | 2022 年 11 月 13 日 00:00 | 2022 年 11 月 12 日 23:30 |
其中Depart At是一个自定义列,表达式为:
datetimeSubtract([Arrive By], 30, "minute")这就是"用结束时间反推开始时间"的典型场景,在会话分析(session)与订阅周期(subscription)等带起止标记的时间序列数据中非常常见:你往往只知道"结束时间"和"持续时间",用datetimeSubtract即可统一算出"开始时间"。
实战场景二:判断当前时间是否落在区间内
承接上面的例子,现在要判断"当前时间"是否处于出发与到达之间的路程区间。假设"当前"时间是 11 月 12 日 19:45:
| 活动 | 到达时间(Arrive By) | 出发时间(Depart At) | 在路上?(On My Way) |
|---|---|---|---|
| 喝一杯 | 2022 年 11 月 12 日 18:30 | 2022 年 11 月 12 日 18:00 | 否 |
| 晚餐 | 2022 年 11 月 12 日 20:00 | 2022 年 11 月 12 日 19:30 | 是 |
| 跳舞 | 2022 年 11 月 13 日 00:00 | 2022 年 11 月 12 日 23:30 | 否 |
Depart At依然是自定义列:
datetimeSubtract([Arrive By], 30, "minute")On My Way则借助 case 表达式,结合 now(当前时间)与 between 区间判断,检查当前时间是否落在Depart At与Arrive By之间:
case(between(now, [Depart At], [Arrive By]), "Yes", "No")这个组合(datetimeSubtract产出区间下界、now提供当前时刻、between完成区间判定)是 Metabase 中做"实时窗口判断"的通用范式,可用于优惠券有效期内判断、告警窗口判断、运营活动进行中标记等需求。
接受的数据类型
| 数据类型 | 是否支持datetimeSubtract |
|---|---|
| 字符串(String) | ❌ |
| 数字(Number) | ❌ |
| 时间戳(Timestamp) | ✅ |
| 布尔值(Boolean) | ❌ |
| JSON | ❌ |
Metabase 使用 "timestamp" 与 "datetime" 来泛指所有受支持的时序数据类型。关于 Metabase 中这些数据类型的更多信息,可参考 时区文档。
如果你的时间戳在数据库中以字符串或数字形式存储,管理员可以在 表元数据页面 中将其转换为时间戳类型(cast),之后再参与datetimeSubtract运算。
限制:MongoDB 版本要求
如果使用 MongoDB,datetimeSubtract仅支持5.0 及以上版本。
这一限制的根源可以在 MongoDB 驱动源码中找到:modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj 中的注释明确指出:interval 在 MongoDB 中并不是一等公民,无法独立翻译,只能用于对日期表达式做加/减;而带 interval 的日期运算($dateAdd/$dateSubtract)直到 MongoDB 5.0 才首次引入。因此驱动在翻译这类表达式时会读取数据库版本号,当主版本小于 5 时直接抛出 "Date arithmetic not supported in versions before 5" 异常,而不是生成错误的查询。
相关函数与跨工具等价实现
本节覆盖与 MetabasedatetimeSubtract行为一致的其他函数与公式,并说明如何为你的场景选择最合适的方案。
datetimeAdd
datetimeSubtract与 datetimeAdd 完全可以互换——因为amount允许为负数。在之前的活动示例中两者可以任选其一,但应当尽量避免"双重否定"(例如减去一个负数)。
下面两段表达式结果完全一致:
datetimeAdd([Arrive By], -30, "minute")datetimeSubtract([Arrive By], 30, "minute")这一等价关系也体现在 SQL 编译层:在 src/metabase/driver/sql/query_processor.clj 中,:datetime-add与:datetime-subtract两个子句的编译逻辑几乎相同,唯一区别是后者把amount取负后再调用同一个add-interval-honeysql-form,即"减去 30 分钟"在底层就是"加上 -30 分钟"。
SQL
当你使用查询构建器运行问题时,Metabase 会把图形化的查询设置(筛选、汇总等)转换为 SQL 查询,再发送给数据库执行。底层转换的核心逻辑就在 src/metabase/driver/sql/query_processor.clj 的->honeysql [:sql :datetime-subtract]方法中——它把datetimeSubtract(arg, amount, unit)编译为向时间值加上(- amount)个单位的 interval 形式。
如果活动示例数据存放在 PostgreSQL 数据库中:
SELECT arrive_by - INTERVAL '30 minutes' AS depart_at FROM events等价于 Metabase 的datetimeSubtract表达式:
datetimeSubtract([Arrive By], 30, "minute")电子表格(Spreadsheets)
假设活动示例数据位于电子表格中,"Arrive By" 位于 A 列且为日期时间格式,那么电子表格函数:
A:A - 30/(60*24)与下面的表达式产生相同结果:
datetimeSubtract([Arrive By], 30, "minute")多数电子表格软件要求针对不同时间单位使用不同公式(例如减"天"与减"小时"的算法完全不同)。datetimeSubtract的价值正在于把所有这些零散算法统一成一种一致、可读的语法。
Python(pandas)
如果活动示例数据保存在名为df的 pandas 数据帧列中,可以导入datetime模块并使用timedelta:
df['Depart At'] = df['Arrive By'] - datetime.timedelta(minutes=30)这与下面的表达式等价:
datetimeSubtract([Arrive By], 30, "minute")从源码看执行流程:MBQL → 数据库查询
综合前文源码证据,一条datetimeSubtract表达式的完整生命周期可以概括为:
- 校验层:temporal.cljc 中的 Malli schema 校验三个参数的类型——时间值、整数
amount、合法的间隔单位; - 类型推断层:同一文件中的
type-of-method确保返回类型与第一个时间参数一致,即使传入的是格式化的字符串日期; - SQL 编译层:对支持 SQL 的数据库,query_processor.clj 将
:datetime-subtract编译为add-interval-honeysql-form(负的 amount + interval 单位),最终生成各数据库方言的- INTERVAL '30 minutes'类语法; - MongoDB 特殊路径:对 MongoDB,query_processor.clj 将 interval 翻译为
$dateSubtract聚合操作符,并在版本低于 5 时抛出明确异常。
对应地,仓库测试 test/metabase/query_processor/expressions_test.clj 中的temporal-arithmetic-test验证了所有支持"表达式 + 日期运算"特性的数据库驱动上,[:- [:interval 31 :day]](即减去 31 天)都能得到与[:+ [:interval -31 :day]]完全一致的预期结果——再次印证"减法即加负数"的统一语义。
延伸阅读
- 自定义表达式总览
- datetimeAdd 表达式
- now 表达式
- case 表达式
- 时区与数据类型
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考