跳转至

时间存储格式(D-FORMAT)

位置:时间 Field 的 custom_extensions。 载荷键:time,一个含 format、storage、granularity 的对象。 默认(不存在时):该列被当作原生 DATE / TIMESTAMP。 忽略时的影响:silent —— 请列进 requires。

什么时候需要配置

时间字段用字符串或整数保存日期时,例如 '20260907' 或 202609,需要用 D-FORMAT 声明编码。原生 DATE/TIMESTAMP 字段通常无需配置;省略 time.format 时,引擎默认按原生日期或时间处理。

time.format 描述存储编码,不控制输出显示格式。配置位于字段的 DATUS custom_extensions 中。字段的 is_time 必须实际为 true,可以显式声明, 也可以由核心 datatype 默认规则得出。 声明描述的是字段 expression 的结果,它可以是计算表达式,不一定是源表中的列。

快速上手

下面在数据集的 fields 中声明一个字符串日期和一个整数月份。省略 storage 时, datatype: String 推断为 "string",Integer 推断为 "int";未声明 datatype 时仍默认为 "string"。将 "time" 列入 requires,可以让不支持该扩展的旧版 Datus 模式引擎 拒绝模型,避免忽略配置后继续计算。

fields:
  - name: dt
    expression:
      dialects: [{dialect: ANSI_SQL, expression: dt}]
    datatype: String
    dimension: {is_time: true}
    custom_extensions:
      - vendor_name: DATUS
        data: '{"v": "1.6", "requires": ["time"], "time": {"format": "%Y%m%d"}}'
  - name: etl_month
    expression:
      dialects: [{dialect: ANSI_SQL, expression: etl_month}]
    datatype: Integer
    dimension: {is_time: true}
    custom_extensions:
      - vendor_name: DATUS
        data: '{"v": "1.6", "requires": ["time"], "time": {"format": "%Y%m", "storage": "int"}}'

假设数据集名为 events,查询的时间范围片段如下。无论存储编码是什么,边界都写 YYYY-MM-DD,包含起点、不包含终点:

time_range:
  dimension: events.dt
  start: "2026-01-01"
  end: "2026-04-01"

过滤条件怎么写

D-FORMAT 编码字段使用 time_range 过滤。 边界始终写 YYYY-MM-DD, 引擎会转换为字段声明的存储编码。

where_sql 中只要引用 D-FORMAT 编码字段,就会返回 unsupported_filter, 即使常量格式正确也不允许。这个限制同样适用于 dataset 的计算字段、函数或 CAST 内部的引用、IS NULL、IN、BETWEEN 和字段间比较。请从 where_sql 中移除 该字段,改用 time_range。如果条件无法用 time_range 表达,例如选择离散日期 或 NULL 值,当前就不能通过 where_sql 对这些字段执行该筛选。

原生时间类型声明(date、datetime、timestamp、timestamp_tz)仍可用于 where_sql,未声明 D-FORMAT 编码的字段也不受此限制。

这项限制针对查询的 where_sql。D-DERIVE 的 filter 和指标表达式内的过滤仍需 按存储编码书写常量,不会自动转换 ISO 日期。例如,%Y%m%d 字符串字段应使用 '20260401',不能使用 '2026-04-01'。可通过 dosi list dimensions 查看字段的 time_format 和 time_storage。

配置项与支持的格式

time 对象接受 format、storage 和 granularity。 除了下面的原生类型名称,format 还支持 %Y%m%d%H%M%S 的 连续、有序、定宽组合。%Y 为四位年份;%m、%d、%H、%M、%S 分别为两位月份、日期、小时(00–23)、分钟和秒,不足两位补零。 可以在任一级结束,但不能跳级、重复或调整顺序。

时间部分之间及前后允许任意固定文本。例如 %Y-%m、%Y年%m月%d日、 %Y.%m.%dT%H:%M、%Y%m%d%H%M%S 都有效。字面的百分号写成 %%。 不支持未知指令、可变宽度修饰符、小数秒和时区指令。 例如 %Y%d、%d/%m/%Y、%-m、%Y%m%d%z 会被拒绝。

新增组合沿用扩展版本 1.6,不升级版本号。尚未支持此功能的旧引擎会拒绝不认识的格式:

custom_extensions:
  - vendor_name: DATUS
    data: '{"v":"1.6","requires":["time"],"time":{"format":"%Y年%m月%d日"}}'

每条数据都必须符合声明的布局,包括固定文本和补零规则。直接比较字段要求列的排序规则 能够保证这些定宽值按时间先后排列;必要时使用合适的二进制或字符编码排序。 引擎不会检查或修改列的排序规则。

原有六种编码保持兼容:

format 存储值 推导出的粒度 允许的 storage
%Y%m%d 20260907 day string, int
%Y-%m-%d 2026-09-07 day string
%Y/%m/%d 2026/09/07 day string
%Y%m 202609 month string, int
%Y 2026 year string, int
%Y-%m-%d %H:%M:%S 2026-09-07 12:30:00 不推导 string

原生时间类型声明不会解析或重新编码字段:

format 含义 推导出的粒度
date 原生日期 day
datetime, timestamp, timestamp_tz 原生日期与时间 不推导

新模型的原生日期时间字段建议直接声明 datatype: Date、DateTime 或 DateTimeTz, 无需再声明 time.format。旧写法继续支持:date 必须对应 Date, datetime 和 timestamp 对应 DateTime,timestamp_tz 对应 DateTimeTz。 原生日期时间类型不能搭配 %Y%m%d 等存储编码。Time 暂无对应的 D-FORMAT 编码。

原生类型声明不接受 storage。编码字段显式声明的 storage 必须与 datatype 一致: String 对应 "string",Integer 对应 "int";其他已声明类型不能使用编码模式。 省略 storage 时根据类型推断;未声明类型时仍默认为 "string"。 "int" 只允许没有任何固定文本的连续时间部分,例如 %Y%m%d%H 或 %Y%m%d%H%M%S,固定数字文本也不允许。最长编码为 14 位,紧凑时间戳应使用 BIGINT、DECIMAL(n,0) 等足够宽的整数值列。物理类型为 DECIMAL(n,0) 的整数日期键 可以声明逻辑类型 Integer,但逻辑类型 Decimal 不作为整数编码处理。 不能单独声明 storage 而不声明 format。以上检查依据字段表达式的结果类型, 不检查源列类型。基础模式会忽略 Datus 块并发出警告,核心类型的时间角色默认规则仍然生效。

granularity 支持 day、week、month、quarter 和 year。 格式能够推导粒度时,可以省略。time.granularity 和 time_granularity 两种写法 都受支持,均未弃用;同时声明时必须一致。声明的粒度不能细于编码所能表达的粒度。 time 对象可以只声明粒度,但不能是空对象。 无论分隔文本是什么,年、月、日编码都分别推导出 year、month、day; 包含小时、分钟或秒的编码不推导粒度。存储到时分秒并不代表支持日内分组, time_range 边界也仍然只接受日期。解析输出时,省略的月份和日期补为 1,省略的 时分秒补为 0。例如 %Y-%m 对应当月第一天,%Y%m%d%H 对应该小时的起点。

分组粒度与时间边界

%Y%m 字段可以按月、季度或年分组,不能按日或周分组,因为存储值没有这些细节。 同理,%Y 支持按年分组。请求更细的粒度会返回 grain_too_fine。

时间范围边界必须与存储周期对齐。对 %Y%m 字段设置 end: "2026-03-15" 会返回 grain_too_fine:如果排除三月,应使用 "2026-03-01";如果包含三月,应使用 "2026-04-01"。错误信息会给出这两个边界,引擎不会自行决定是否纳入不完整的月份。 按年编码的字段则要求边界落在年初。这些检查针对查询传入的边界;窗口计算需要扩大 扫描范围时,由引擎处理。

选出的时间维度会被解析为日期或时间值,不指定粒度时也一样。不要依赖结果保留原始 字符串的样式。D-FORMAT 当前不提供输出显示格式配置。

校验范围与已知限制

声明必须符合实际数据。Dosi 会检查支持的格式和配置组合;字段声明了核心 Field.datatype 时,还会检查所声明的 storage 和原生格式与它是否一致。但 Dosi 不会检查仓库中的列类型或数据值,因此 dosi validate 无法确认字段表达式的实际 输出是否符合 %Y%m%d。

格式或存储类型写错,可能导致仓库报错、分组值变成 NULL,或过滤范围错误却不报错。 例如,实际存储 '2026-09-07',却声明 %Y%m%d,生成的过滤常量也会使用错误格式。 配置时应同时检查表达式的输出类型和实际数据样例。

ClickHouse 使用 parseDateTime,其 DateTime 结果范围约为 1970-01-01 至 2106-02-07。范围以外的日期,包括 SCD2 常用的结束日期 '99991231',不能可靠地 用于分组或解析后的输出。ClickHouse 24.8.14 实测:parseDateTime 对 1960、2107 和 9999 年的日期返回 CANNOT_PARSE_DATETIME。 该版本没有 parseDateTime64。BestEffort 系列也不能直接替换: parseDateTimeBestEffort 会将越界值截到边界,parseDateTime64BestEffort 则会把 9999-12-31 改成 2299-12-31。 直接比较编码值的 time_range 过滤不调用此解析函数,但这不代表超出范围的分组也安全。

basic 模式会忽略 DATUS 扩展并给出警告,requires 也不生效。 依赖日期编码处理的字段应使用 Datus 模式。 当前不支持 epoch_seconds、epoch_millis 等时间戳编码。

生成 SQL 的原理

分组时,引擎将编码字段解析为日期或时间值;请求更粗粒度时,再截断到对应周期。 按存储粒度分组或不指定粒度时,GROUP BY 可以直接使用存储列,在 SELECT 表达式中解析。

time_range 则将边界转换为存储编码。上述示例生成的过滤条件形式如下:

WHERE dt >= '20260101' AND dt < '20260401'
WHERE etl_month >= 202601 AND etl_month < 202604

过滤列不被解析函数包裹,有利于仓库进行分区裁剪。实际裁剪效果取决于仓库、字段表达式 和分区定义。