Apache Druid ANSI SQL 兼容模式迁移指南从遗留 null 处理到 SQL 三值逻辑【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druidApache Druid 自 28.0.0 起默认的 null 处理模式已切换为与 ANSI SQL 标准对齐SQL compliant mode。本指南面向依赖旧版legacynull 处理行为的 Druid 运维与开发人员系统说明新旧两种模式的区别、三组核心运行时配置的作用、以及三条可落地的迁移路径在摄入阶段替换 null、在摄入阶段将空字符串强转为 null、以及重写查询使其符合 ANSI SQL 语义。读完本文你将能依据自身业务对 null 的保留需求选择并实施从遗留模式平滑过渡到 SQL 兼容模式的完整方案。背景为什么默认 null 处理会改变在 Apache Druid 28.0.0 之前Druid 默认使用一种遗留模式legacy mode摄入数据时把 null 替换为默认值。这种模式带来的后果是字符串列无法区分空字符串与 null两者被视为同一值数值列无法表示 null 行null在存储时被写为0。自 28.0.0 起Druid 默认以 ANSI SQL 兼容模式写入段segment字符串维度将 null 与空字符串区分存储数值维度将 null 与 0 区分存储。由于 ANSI SQL 标准规定任何与 null 的比较结果都是 unknown三值逻辑x some value只会返回非 null 的行——这会直接影响依赖旧行为的应用查询结果。遗留模式已被标记为废弃并计划移除因此掌握迁移策略对使用 Druid 的团队来说是必经之路。三组核心配置SQL 兼容模式的开关Druid 28.0.0 及之后版本中开启 ANSI SQL 兼容 null 处理模式的默认配置如下| 配置项 | 默认值 | 作用 | | -- | -- | -- | |druid.generic.useDefaultValueForNull|false| 是否将 null 替换为默认值字符串为空串、数值为 0。false表示保留 null | |druid.expressions.useStrictBooleans|true| 表达式求值是否使用严格布尔逻辑并统一用 1 表示 true、0 表示 false | |druid.generic.useThreeValueLogicForNativeFilters|true| 原生过滤器是否使用三值逻辑TRUE / FALSE / UNKNOWN |遗留已废弃模式对应的配置是| 配置项 | 遗留值 | | -- | -- | |druid.generic.useDefaultValueForNull|true| |druid.expressions.useStrictBooleans|false| |druid.generic.useThreeValueLogicForNativeFilters|true|这些配置已被标记为废弃并排入移除计划。在配置被移除后Druid 会忽略配置文件中残留的这些项直接使用默认的 SQL 兼容模式。从源码看这三项配置的解析与默认值分别位于NullValueHandlingConfig.java定义了druid.generic.useDefaultValueForNull与druid.generic.useThreeValueLogicForNativeFilters两个系统属性未显式设置时分别回退到false与true当useDefaultValueForNulltrue或useThreeValueLogicForNativeFiltersfalse时还会在启动日志中输出建议使用 SQL 兼容行为的警告。ExpressionProcessingConfig.java定义了druid.expressions.useStrictBooleans未显式设置时回退到true设为false时同样输出废弃警告。值得说明的是原生过滤器是否真正启用三值逻辑并不是只看useThreeValueLogicForNativeFilters一项。在 NullHandling.java 中useThreeValueLogic()返回sqlCompatible() useThreeValueLogicForNativeFilters useStrictBooleans()——即三个条件同时满足才启用三值逻辑。NotFilter在反转匹配结果时依赖该开关的includeUnknown参数以保证NOT语义符合 SQL 标准。因此可以推断只要这三项中任何一项被设置为非默认值基于非表达式的过滤器就会退化为两值逻辑表达式类过滤器则单独由druid.expressions.useStrictBooleans控制。关于 null 处理在查询层面的语义细节如COUNT(*)与COUNT(expr)的差异、GROUP BY对 null 与空串的区分可以参考 sql-data-types.md存储层面的实现细节参见 segments.md。两种迁移路线按是否保留 null 来选择如果业务逻辑依赖遗留模式的 null 行为迁移到 SQL 兼容模式有两条路线改造摄入数据避免 null在摄入阶段把字符串列的 null 替换为空字符串、把数值列的 null 替换为 0从而让现有查询在新模式下得到与遗留模式一致的结果。适合不关心是否保留 null 的场景。操作手段是修改摄入 SQL 查询与摄入 spec。保留 null重写查询使其符合 ANSI SQL原样保留含 null 的数据但把所有受影响的客户端查询改写为 ANSI SQL 兼容写法。适合有硬性需求必须保留 null 的场景。下文分别给出这两条路线的完整实操示例。路线一A摄入时用 COALESCE / NVL 替换 null如果不需在 Druid 内保留 null可以在摄入时用 transform 将 null 替换为其他值。考虑如下输入数据{time:2024-01-01T00:00:00.000Z,string_example:my_string,number_example:99} {time:2024-01-02T00:00:00.000Z,string_example:,number_example:0} {time:2024-01-03T00:00:00.000Z,string_example:null,number_example:null}下面分别给出 SQL 批量摄入与 JSON 批量摄入两种写法均使用COALESCE把字符串 null 换成空串、用NVL把数值 null 换成 0。SQL-based batchREPLACE INTO no_nulls_example OVERWRITE ALL WITH ext AS ( SELECT * FROM TABLE( EXTERN( {type:inline,data:{\time\:\2024-01-01T00:00:00.000Z\,\string_example\:\my_string\,\number_example\:99}\n{\time\:\2024-01-02T00:00:00.000Z\,\string_example\:\\,\number_example\:0}\n{\time\:\2024-01-03T00:00:00.000Z\,\string_example\:null,\number_example\:null}}, {type:json} ) ) EXTEND (time VARCHAR, string_example VARCHAR, number_example BIGINT) ) SELECT TIME_PARSE(time) AS __time, -- Replace any null string values with an empty string COALESCE(string_example,) AS string_example, -- Replace any null numeric values with 0 NVL(number_example,0) AS number_example FROM ext PARTITIONED BY MONTHJSON-based batch原生index_parallel任务通过transformSpec.transforms实现同样效果{ type: index_parallel, spec: { ioConfig: { type: index_parallel, inputSource: { type: inline, data: {\time\:\2024-01-01T00:00:00.000Z\,\string_example\:\my_string\,\number_example\:99}\n{\time\:\2024-01-02T00:00:00.000Z\,\string_example\:\\,\number_example\:0}\n{\time\:\2024-01-03T00:00:00.000Z\,\string_example\:null,\number_example\:null} }, inputFormat: { type: json } }, tuningConfig: { type: index_parallel, partitionsSpec: { type: dynamic } }, dataSchema: { dataSource: inline_data_native, timestampSpec: { column: time, format: iso }, dimensionsSpec: { dimensions: [ string_example, { type: long, name: number_example } ] }, granularitySpec: { queryGranularity: none, rollup: false, segmentGranularity: MONTH }, transformSpec: { transforms: [ { type: expression, name: string_example, expression: COALESCE(\string_example\,) }, { type: expression, name: number_example, expression: NVL(\number_example\,0) } ] } } } }Druid 摄入后的数据不含任何 null|__time|string_examle|number_example| | -- | -- | -- | |2024-01-01T00:00:00.000Z|my_string| 99 | |2024-01-02T00:00:00.000Z|empty| 0 | |2024-01-03T00:00:00.000Z|empty| 0 |从实现上看COALESCE、NVL与case_searched等都是 Druid 表达式引擎内置函数在 Function.java 中定义了case_searchedFunction.java 中NvlFunc继承自CoalesceFunc并注册为nvlFunction.java 中注册了coalesce。表达式函数的语义可由 FunctionTest.java 中的用例验证例如case_searched(xbaz,is baz,xfoo,is foo,is other)在xfoo时返回is foo否则返回默认分支is other。这些函数既可用于摄入 transform也可直接用于查询表达式。路线一B摄入时用 NULLIF 把空字符串强转为 null遗留模式下Druid 在相等比较时把空字符串当作 null。如果查询依赖用空串表示 null可以在摄入阶段用NULLIF把空串强转为 null。例如以下输入数据{time:2024-01-01T00:00:00.000Z,string_example:my_string} {time:2024-01-02T00:00:00.000Z,string_example:} {time:2024-01-03T00:00:00.000Z,string_example:null}在遗留模式下第三条记录会被写成空字符串因此下面的查询返回 2SELECT count(*) FROM null_string WHERE string_example IS NULL而在 SQL 兼容模式下空串与 null 被区分存储同样的查询只会返回 1。下面演示如何把空串强转为 null 以恢复IS NULL比较的语义。SQL-based batchREPLACE INTO null_string OVERWRITE ALL WITH ext AS ( SELECT * FROM TABLE( EXTERN( {type:inline,data:{\time\:\2024-01-01T00:00:00.000Z\,\string_example\:\my_string\}\n{\time\:\2024-01-02T00:00:00.000Z\,\string_example\:\\}\n{\time\:\2024-01-03T00:00:00.000Z\,\string_example\:null}}, {type:json} ) ) EXTEND (time VARCHAR, string_example VARCHAR) ) SELECT TIME_PARSE(time) AS __time, NULLIF(string_example,) AS string_example FROM ext PARTITIONED BY MONTHJSON-based batch注意这里 transform 表达式被展开成了case_searched形式即(string_example )为真时返回 null否则返回原值{ type: index_parallel, spec: { ioConfig: { type: index_parallel, inputSource: { type: inline, data: {\time\:\2024-01-01T00:00:00.000Z\,\string_example\:\my_string\}\n{\time\:\2024-01-02T00:00:00.000Z\,\string_example\:\\}\n{\time\:\2024-01-03T00:00:00.000Z\,\string_example\:null} }, inputFormat: { type: json } }, tuningConfig: { type: index_parallel, partitionsSpec: { type: dynamic } }, dataSchema: { dataSource: null_string, timestampSpec: { column: time, format: iso }, transformSpec: { transforms: [ { type: expression, expression: case_searched((\string_example\ ),null,\string_example\), name: string_example } ] }, dimensionsSpec: { dimensions: [ string_example ] }, granularitySpec: { queryGranularity: none, rollup: false, segmentGranularity: month } } } }Druid 摄入后的数据不含任何空字符串|__time|string_examle| | -- | -- | -- | |2024-01-01T00:00:00.000Z|my_string| |2024-01-02T00:00:00.000Z|null| |2024-01-03T00:00:00.000Z|null|此时SELECT count(*) FROM null_string WHERE string_example IS NULL返回 2与遗留模式行为一致。NULLIF的 SQL 层行为也可以在 CalciteQueryTest.java 的testNullEmptyStringEquality用例中找到佐证测试验证了NULLIF(dim2, a) IS NULL这类过滤条件的翻译结果。路线二重写查询使其符合 ANSI SQL 语义如果需要保留数据中的 null可采用如下 ANSI SQL 兼容的查询改写策略让结果与遗留 null 处理一致改写不等值查询显式包含 null例如x some value改为(x some value OR x IS NULL)。用 COALESCE / NVL 把 null 替换为具体值例如x 1改为NVL(numeric_value, 0)1。考虑以下null_example数据源|__time|string_examle|number_example| | -- | -- | -- | |2024-01-01T00:00:00.000Z|my_string| 99 | |2024-01-02T00:00:00.000Z|empty| 0 | |2024-01-03T00:00:00.000Z|null| null |Druid 会把 null 字符串排除在相等比较之外。例如SELECT COUNT(*) AS count_example FROM null_example WHERE string_example my_stringDruid 返回 1因为 null 被视为 unknown既不等于也不等于该值。要统计 null可以加一个 OR 条件SELECT COUNT(*) AS count_example FROM null_example WHERE (string_example my_string) OR string_example IS NULL此时返回 2。更简洁的做法是使用 null 安全比较IS DISTINCT FROMSELECT COUNT(*) as count_example FROM null_example WHERE string_example IS DISTINCT FROM my_string同理对 null 做算术运算会得到 null。例如SELECT number_example 1 AS additon_example FROM null_example按 ANSI SQL 标准null 与任何值相加仍为 nullDruid 返回|addition_example| | -- | | 100 | | 1 | | null |用NVL可以避免算术中的 null 传播SELECT NVL(number_example,0) 1 AS additon_example FROM null_exampleDruid 返回|addition_example| | -- | | 100 | | 1 | | 1 |注意这里第三条记录从 null 变成了 1NVL(null, 0) 1这与遗留模式下数值 null 按 0 参与运算的语义保持一致。摄入与查询遵循同一套 null 规则SQL 兼容模式下的 null 处理规则同时作用于存储与查询两端。与上述迁移技巧配合还可以直接通过 null 处理教程 中的示例数据集含空字符串与 null 字符串混存的 4 行数据来验证默认模式下的行为例如WHERE string_value ! some_value会排除 null 行COUNT(*)计入所有行而COUNT(string_value)不计入 null 值GROUP BY会把 null 与空串作为两个不同的分组需要同时匹配空串与 null 时用string_value IS NULL OR string_value 需要排除空串时用COUNT(string_value) FILTER(WHERE string_value )数值比较不会计入 null 行numeric_value 1对 null 行返回 null而COALESCE(numeric_value, 0) 1返回 1。这些查询模式本身就是对 SQL 兼容三值逻辑的直观验证也是排查迁移后查询结果差异的有力工具。迁移检查清单综合以上内容从遗留模式迁移到 SQL 兼容模式的完整步骤可以总结为评估依赖排查现有 SQL 查询与原生查询中对空串 / null 相等比较、数值 null 按 0 参与运算的依赖点。选择路线不需要保留 null → 走摄入 transform 替换方案COALESCE/NVL或 NULLIF 强转空串必须保留 null → 走查询重写方案OR IS NULL、IS DISTINCT FROM、NVL/COALESCE。改造摄入使用上面给出的 SQL-based 或 JSON-based 示例在transformSpec中落地替换逻辑并检查段内数据是否符合预期。重写查询将不等值过滤改写为显式包含 null 的写法将算术运算包装为NVL(expr, default)。验证结果用 null 处理教程 中的对照查询IS NULL、COUNT(*)vsCOUNT(expr)、GROUP BY、COALESCE算术逐条核对迁移前后的返回行数与数值。清理配置确认配置文件不再包含druid.generic.useDefaultValueForNull、druid.expressions.useStrictBooleans、druid.generic.useThreeValueLogicForNativeFilters这三个遗留开关的非默认值保持默认 SQL 兼容行为注意这些配置被移除后即使残留也会被忽略。延伸阅读null 处理教程默认 null 处理机制的逐步演示。Null valuesDruid 对 null 值行为的完整描述以及遗留模式与布尔逻辑三值逻辑的配置说明。Handling null valuesDruid 在段存储层面如何保存 null 值的实现细节。【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考