资讯动态

Databend 类型转换安全分类器(Conversion Safety Helper)解析:让优化器安全地推断等价类

发布时间:2026/9/16 17:05:06 来源:尧图企业网站定制
Databend 类型转换安全分类器Conversion Safety Helper解析让优化器安全地推断等价类【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend导读Databend 的类型系统早已能判断一个表达式能否通过类型检查并求值但对于优化器尤其是InferFilter这类做等价类推理的规则而言更关键的问题是这次类型转换能否足够好地保持相等语义equality semantics从而安全地用作等价类equivalence class推导边本文基于仓库根目录下的 RFC 文档 20260509-conversion-safety-helper.md完整解读其提出的转换安全分类方案ConversionClass六分类体系、common_super_type_with_conversion公共类型推断 API以及它们在databend_common_expression中的实际落地实现与测试用例。读完本文你将理解 Databend 如何区分可求值的转换与可推理的转换并能直接复用该 helper 编写更安全的优化器规则。背景能求值 ≠ 能推理问题源头Issue #17933Databend 目前拥有多种类型强制转换机制函数类型检查function type checking、自动转换规则auto-cast rules、公共超类型推断common super type inference以及TRY_CAST规则。这些机制回答的是表达式能否被类型检查并通过求值。但某些优化器规则需要一个更严格的问题这种转换是否足够好地保持相等语义从而可用于等价类推断Issue #17933 暴露了表达式求值与优化器推理之间的错位。考虑如下 SQLSELECT * FROM ( SELECT 01 AS s1, 1 AS s2, 1 AS n ) t WHERE s1 n AND s2 n;谓词s1 n与s2 n在数值比较下都能求值为 true但它们并不能推出s1 s2——因为在字符串比较下01 1为 false。因此这种相等谓词可以合法求值却不安全作为等价类边。浮点转换有类似问题整数或 Decimal 值转换为 Float 后可能发生值塌缩多个源值映射到同一个目标值若把这样的相等关系作为传递推理边会推导出原 SQL 并不蕴含的谓词。现有 Databend API 没有把这一区别显式表达出来。现有能力盘点RFC 指出Databend 已有丰富的构建块见下表现有 API位置作用check_functionsrc/query/expression/src/type_check.rs检查函数调用能否被类型化并在必要时插入 castFunctionRegistry::{get_auto_cast_rules, is_auto_try_cast_rule}src/query/expression/src/function.rs暴露自动转换规则与基于 TRY_CAST 的规则can_auto_cast_to、common_super_typesrc/query/expression/src/type_check.rs提供类型级强转能力与公共类型推断NumberDataType::can_lossless_cast_to、get_decimal_propertiessrc/query/expression/src/types/number.rs提供部分数值加宽widening信息比较函数注册src/query/functions/src/scalars/comparison.rs描述当前存在的同类型比较函数然而这些 API 都没有回答转换是无损还是有损是单射还是多对一是否依赖运行时值如 String → Number是否基于TRY_CAST如 Variant → 具体类型是否对相等推理安全目标与非目标RFC 明确划定了边界目标提供与任何单一优化器规则无关的、可复用的转换属性描述 helper让InferFilter只查询客观转换事实然后自行应用策略避免把check_function(eq)视为该相等关系可安全用于传递推理的证明保留现有有用行为例如整数/整数通过 Decimal 公共类型完成推断含有符号/无符号整数连接将不安全场景显式化String/Number、Variant/具体类型 TRY_CAST、Integer/Decimal → Float。非目标不改变 Databend 的 SQL 求值语义不删除现有 auto-cast 或 TRY_CAST 规则首个实现不需要完整的 Snowflake 兼容转换矩阵不要求所有支持相等的类型立即被InferFilter使用。潜在消费者谁需要可求值与可推理的区分InferFilter是首个实现的主要消费者但 RFC 列出多个未来候选Join 相等推理join 重排、等价类构建、runtime filter 推导、join key 推断都需要知道相等关系能否安全充当等价边谓词下推与剪枝存储下推、segment 剪枝、bloom 剪枝常常在比较前对常量或列做 cast需避免有损或值依赖的 cast 导致错误剪枝EquivalentConstantsVisitor用col constant的常量替换列仅在相等关系保持目标值语义时才安全范围与常量推断从a b AND b 10推导a 10不仅需要相等安全还可能需要保序ordering属性Bloom filter 与 runtime filter对不同类型 key 应用过滤器时应避免把大量源值塌缩到一个目标值的转换统计与直方图推理混合类型谓词的选择性估计需要知道转换是否有损、值依赖或基于 TRY_CAST。这些是未来候选第一步应保持实现小巧并通过InferFilter验证 API。核心 API 设计六类转换语义RFC 提议将 helper 置于databend_common_expression下新模块conversion其核心类型如下RFC 中的原始 API 草案pub enum ConversionClass { /// Same logical type, no conversion needed. Identity, /// Conversion preserves distinct source values in the target type. LosslessInjective, /// Conversion is deterministic but can lose information or merge values. Lossy, /// Whether conversion succeeds or how it compares depends on runtime value /// contents, e.g. String - Number. ValueDependent, /// Conversion is represented by TRY_CAST or can turn failed conversion into NULL. TryOnly, /// No supported conversion is known. Unsupported, } impl ConversionClass { pub fn is_safe_for_equality_inference(self) - bool { matches!(self, Self::Identity | Self::LosslessInjective) } } pub struct CommonTypeConversion { pub common_type: DataType, pub left: ConversionClass, pub right: ConversionClass, } pub fn classify_conversion(src: DataType, dest: DataType) - ConversionClass; pub fn common_super_type_with_conversion( left: DataType, right: DataType, ) - OptionCommonTypeConversion;设计哲学很明确helper 只描述事实调用方决定策略。例如InferFilter要求两侧转换都对相等推理安全let conv common_super_type_with_conversion(left_ty, right_ty)?; conv.left.is_safe_for_equality_inference() conv.right.is_safe_for_equality_inference()其他调用方可以接受更宽泛的转换集合。RFC 特别强调本方案不引入CastContext参数——CastContext用于表达调用方希望 cast 多宽松而本 helper 描述的是客观转换属性由调用方自行映射到策略。六类转换的判定规则Identity同一类型相同逻辑类型返回Identity前提是该类型可在表达式系统中表示。这不意味着每个优化器规则都必须使用所有 identity 转换。示例String - String、Boolean - Boolean、Timestamp - Timestamp、Variant - Variant。LosslessInjective无损单射整数加宽当NumberDataType::can_lossless_cast_to判定安全时整数 → Decimal当 Decimal 能表示所有源值时Decimal 加宽当 scale 与 leading digits 都保留时Date - Timestamp不同日期映射到不同的午夜时间戳Float32 - Float64内层转换安全时的 Nullable 包装Array/Tuple 内元素/字段转换安全时的递归Null - T不会引入值碰撞EmptyArray - ArrayT。整数/整数公共类型在必要时应选用 Decimal 公共类型以保留现有有符号/无符号行为如Int64 UInt64。Lossy有损整数或 Decimal → FloatFloat → 整数Decimal scale 缩减Decimal → 更窄的 Decimal。这些转换对求值有效但不适用于相等推理。ValueDependent值依赖String → NumberString → BooleanString → Date/Timestamp。这些转换依赖运行时字符串内容同一目标值可对应多种字符串表示如01与1。TryOnly仅 TRY_CAST通过已注册auto_try_cast_rules的 Variant → 具体类型。TRY_CAST 语义可将失败转换转为 NULL因此不安全作为等价类边。Unsupported不支持无任何规则适用时返回Unsupported。公共类型推断规则common_super_type_with_conversion尽量复用现有逻辑同时报告两侧转换类别。建议行为RFC 原文九条递归处理 nullable/null/empty-array复用common_super_type的形状整数/整数选择能同时表示两侧的 Decimal 公共类型整数/Decimal、Decimal/Decimal 用 Decimal 合并浮点/浮点选择更宽的 FloatDate/Timestamp 统一为 TimestampArray/Tuple 递归处理String 混合转换标记为ValueDependent而非LosslessInjectiveVariant 自动 try-cast 转换标记为TryOnly无转换路径时返回Unsupported。关键原则不要用比较函数的 auto-cast 规则作为推理安全性的证据——那些规则服务于表达式求值。仓库中的实际落地实现RFC 提出的方案在仓库中已落地为 src/query/expression/src/conversion.rs。实现与 RFC 草案高度一致并补充了若干细节ConversionClass增加is_lossless_injective()辅助方法is_safe_for_equality_inference()直接委托它CommonTypeConversion额外提供聚合判定方法它同时检查公共类型本身通过is_type_safe_for_equality_inference与两侧转换类别classify_conversion覆盖了 Null/Nullable/EmptyArray/EmptyMap/Array/Map/Tuple/Number/Decimal/Boolean/String/Variant/Date/Timestamp 等路径布尔型与 Number/Decimal/String 之间判为LosslessInjective如Boolean - String、Boolean - Int64TimestampTz - Timestamp、String - Variant、Variant - Decimal等被判为Unsupported。数值判定细节classify_number_conversion在 conversion.rs 中整数→整数且can_lossless_cast_to为 true 时是无损单射否则有损浮点→浮点同理整数↔浮点一律有损。NumberDataType::can_lossless_cast_to定义在 src/query/expression/src/types/number.rs其规则包括浮点→浮点bit_width不大于目标浮点→整数恒为 false整数→浮点bit_width严格小于目标如 Int32 → Float64 可以Int64 → Float64 不行因为后者可能丢精度整数→整数同符号时比较位宽无符号→有符号需要next_bit_width足够大如 UInt32 → Int64 可以UInt64 → Int64 不行。get_decimal_propertiesnumber.rs将各整数类型映射为 (precision, scale)如 Int8(3,0)、Int64(19,0)、UInt64(20,0)这是整数→Decimal 判定与有符号/无符号整数公共 Decimal 类型的基础。整数/整数公共类型number_common_typeconversion.rs实现 RFC 的第 2、4、5 条建议相同类型直接返回整数/整数且可无损互转时取较宽者否则用decimal_common_size构造 Decimal 公共类型浮点/浮点取可无损转换的较宽者。decimal_common_size计算scale max(left.scale, right.scale)、precision scale max(leading_digits)并按 i128/i256 精度上限截断MAX_PRECISION分别对应 38 与 76。复合类型合并combine_conversion_classesconversion.rs为 Tuple 逐字段合并类别优先级从Unsupported最高到Identity最低中间顺序为TryOnly、ValueDependent、Lossy、LosslessInjective——即任何一个字段不安全整个 Tuple 就不安全。消费者实测InferFilter及 runtime filter 构建已实际消费该 helper。在 src/query/service/src/physical_plans/runtime_filter/builder.rs 中if !common_super_type_with_conversion(left_type, right_type) .is_some_and(|conversion| conversion.is_safe_for_equality_inference()) { continue; }不满足相等推理安全的 join 关系会被跳过不会进入等价类构建该文件还在第 388、420 行分别用classify_conversion(...).is_lossless_injective()校验 cast 目标类型与下推目标类型是否无损单射。这印证了 RFC 中InferFilter的消费模式check_equal_expr_type的两个前提条件在真实代码中的落地形态。分类示例五组代表性场景RFC 给出五组典型场景及结论String 与 Number01 1 1 1String - Number: ValueDependent Number - Number: IdentityInferFilter拒绝该相等边。Integer 与 DecimalInt64 - Decimal(19, 0): LosslessInjective Decimal(18, 2) - Decimal(21, 2): LosslessInjectiveInferFilter可以使用该边。Integer 与 FloatInt64 - Float64: Lossy Float64 - Float64: IdentityInferFilter拒绝该相等边。Date 与 TimestampDate - Timestamp: LosslessInjective Timestamp - Timestamp: IdentityInferFilter可以使用该边。Variant 与 NumberVariant - Nullable(Number): TryOnlyInferFilter拒绝该相等边。测试计划与已落地测试RFC 的测试计划包含两类classify_conversion与common_super_type_with_conversion的单元测试覆盖数值加宽、有符号/无符号整数→Decimal、Decimal 加宽/收窄、Date→Timestamp、String→Number/Date/Boolean、Variant→具体类型、Float 精度、nullable/array/tuple 递归以及现有InferFilter测试String/Number 传递性拒绝、Number/Decimal 传播、Number/Float 拒绝、有符号/无符号整数保留、Date/Timestamp 传播、嵌套 Array/Tuple 行为。仓库中对应的测试文件为 src/query/expression/tests/it/conversion.rs已实现大量用例部分代表性断言test_identity_for_all_data_type_variants所有类型变体与其自身的转换恒为Identity且公共类型等于自身test_number_conversion_classes_cover_all_number_types覆盖全部数值类型对例如UInt8-UInt16: LosslessInjective、UInt64-Int64: Lossy、Float32-Float64: LosslessInjective、Int32-Float64: Lossytest_number_common_typesInt64 UInt64的公共类型为Decimal(20, 0)且两侧均LosslessInjectiveInt64 Float64公共类型为 Float64左类别为Lossytest_decimal_conversion_classesInt16 - Decimal(5, 0): LosslessInjective、UInt64 - Decimal(20, 0): LosslessInjective、Int64 - Decimal(18, 0): Lossy、Decimal(10,2) - Decimal(12,4): LosslessInjective、Decimal(10,4) - Decimal(10,2): Lossytest_temporal_string_and_variant_conversionsDate - Timestamp: LosslessInjective、Timestamp - Date: LossyString 到 Number/Date/Boolean 等目标均为ValueDependentVariant 到具体类型均为TryOnlytest_array_map_and_tuple_conversion_classesEmptyArray - Array(Int64): LosslessInjective、Array(String) - Array(Date): ValueDependent、Tuple 逐字段合并含TryOnly优先级高于LosslessInjective。测试还显式固定了Unsupported集合Binary-String、Bitmap-String、Geometry-Geography、不同Vector维度、Opaque/Generic/StageLocation相关转换均不支持。落地路线与开放问题RFC 的推广路线Rollout Plan为在databend_common_expression中新增转换分类 helper已落地为标量转换与嵌套 Array/Tuple 转换补充单元测试已落地于 tests/it/conversion.rs迁移InferFilter消费该 helper已见 runtime filter builder 消费保持现有 SQL 行为不变仅在确实需要相同语义时如 join 相等推理、可剪枝谓词推导引入后续消费者。RFC 同时留下若干开放问题供后续设计决策是否所有相同逻辑类型的 identity 转换都应分类为Identity即使目前不存在对应比较函数Map 是否应支持递归还是在出现明确的相等语义需求前保持不支持common_super_type_with_conversion应紧邻common_super_type放置还是放在新的conversion模块中转换类别是否应同时包含implicit与explicit标志类似 Snowflake 的 castable/coercible 区分该 helper 未来是否应替代can_auto_cast_to的部分职责还是作为并行的语义分类器长期共存从当前仓库看实现选择了新的conversion模块 并行语义分类器的路线conversion.rs独立于 type_check.rs后者仍保留can_auto_cast_to与common_super_type供求值路径使用。这一分层让可求值与可推理两类判断各司其职也保证了 RFC 提出的核心原则——不修改 SQL 求值语义——在实现中得以延续。【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价