ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Apache Druid 升级迁移指南:数组类型、Front-Coded 字典、子查询字节限制与 ANSI SQL Null 处理

Apache Druid 升级迁移指南:数组类型、Front-Coded 字典、子查询字节限制与 ANSI SQL Null 处理 Apache Druid 升级迁移指南数组类型、Front-Coded 字典、子查询字节限制与 ANSI SQL Null 处理【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druid本文是 Apache Druid 25.0.0 及后续版本引入的破坏性变更breaking changes迁移指南。Apache Druid 在引入新特性时始终尽力保持向后兼容但当旧行为的缺陷或性能瓶颈无法以兼容方式修复时就必须为未来的可维护性引入破坏性变更。本文围绕 docs/release-info/migration-guide.md 梳理的四条主线展开从多值维度MVD迁移到 SQL 兼容的数组类型、迁移到 front-coded 字典编码、从行数子查询限制迁移到字节数限制、以及迁移到 ANSI SQL 兼容的 Null 处理模式。读完本文你将掌握每条迁移路径的动机、底层实现原理、可复制的配置与查询示例以及升级、降级时的注意事项。迁移总览25.0.0 之后发生了哪些变化根据 docs/release-info/migration-guide.md 的说明Druid 25.0.0 及后续版本引入了以下需要主动迁移的变更迁移主题引入版本核心变化多值维度 → 数组类型25.0.0 起支持 SQL 兼容数组尽可能用数组类型替代多值维度Front-coded 字典编码25.0.0实验特性对共享前缀的字符串提供增量压缩行数子查询限制 → 字节数限制30.0.0 起推荐用maxSubqueryBytes防止 Broker OOM遗留 Null 处理 → ANSI SQL 兼容28.0.0默认写入 ANSI SQL 兼容的 Null 处理模式每个主题的详细迁移指南分别位于 MVD 到数组迁移、Front-coded 字典迁移、子查询限制迁移 和 SQL 兼容模式迁移下文逐一展开。从多值维度迁移到数组类型Druid 现在支持 SQL 兼容的数组类型官方建议尽可能使用数组而非多值维度Multi-Value DimensionsMVD。对于新项目以及涉及多种数据类型的复杂用例应使用数组MVD 则保留给特定场景例如需要像普通字符串那样直接对单个元素操作时。如果操作涉及整组值包括行内值的顺序请优先使用数组。数组与 MVD 的行为对比对比项数组Array多值维度MVD数据类型支持 VARCHAR、BIGINT、DOUBLE即ARRAYSTRING、ARRAYLONG、ARRAYDOUBLE仅支持字符串数组VARCHARSQL 兼容性行为符合标准 SQL 数组行为类似 SQL VARCHAR 而非标准数组需要特殊 SQL 函数才能实现类数组行为摄入方式JSON 数组直接摄入为 Druid 数组SQL 批量摄入时通过查询上下文参数arrayIngestMode管理可选值为array、mvd和none。若设为none存储任意类型数组都会抛出异常JSON 数组摄入为 MVDSQL 批量摄入时使用 ARRAY_TO_MV 等函数管理过滤与分组过滤和分组匹配整个数组值可作为 GROUP BY 键按整个数组值分组要按单个数组元素分组需使用 UNNEST 操作符过滤匹配数组内的任意值分组时会对每个值生成一组类似隐式 UNNEST转换用 MV_TO_ARRAY 将 MVD 转换为数组用 ARRAY_TO_MV 将数组转换为 MVD查询时数组与 MVD 的本质差异在 SQL 查询中Druid 对数组列与 MVD 列的处理方式不同数组列的值被视为一个整体SQL ARRAY而 MVD 列的值被视为单个字符串SQL VARCHAR。即便同一个 MVD 中的多个字符串值仍作为该 MVD 列中的一个字段存储查询语义依然如此。以同一个值[a, b, c]分别摄入到数组列与 MVD 列为例当你想用某个值与它做相等比较来过滤时数组列只有相等过滤匹配整个数组时才返回该行例如WHERE array_column ARRAY[a, b, c]。MVD 列只要相等过滤匹配 MVD 中的任意值就返回该行例如WHERE mvd_column a、WHERE mvd_column b、WHERE mvd_column c都会返回该行。编写涉及过滤或分组的查询时必须注意这一差异。当查询同时应用过滤和分组时MVD 可能返回看似不匹配过滤条件的行——因为分组发生在过滤之后具体见下文 按数组元素过滤并分组。数组与 MVD 的等价查询示例以下示例展示数组与 MVD 的成对等价查询。更完整的参考见 查询数组 与 查询多值维度。按数组元素过滤过滤出数组中包含某个值的行-- Array SELECT label, tags FROM array_example WHERE ARRAY_CONTAINS(tags, t3)-- MVD SELECT label, tags FROM mvd_example WHERE tags t3按一个或多个元素过滤过滤出数组或 MVD 包含一个或多个元素的行。注意 ARRAY_OVERLAP 检查是否存在重叠元素而上例的 ARRAY_CONTAINS 检查所有元素是否都被包含-- Array SELECT * FROM array_example WHERE ARRAY_OVERLAP(tags, ARRAY[t1, t7])-- MVD SELECT * FROM mvd_example WHERE tags t1 OR tags t7使用数组相等过滤过滤出数组或 MVD 与参考数组等价的行-- Array SELECT * FROM array_example WHERE tags ARRAY[t1, t2, t3]-- MVD SELECT * FROM mvd_example WHERE MV_TO_ARRAY(tags) ARRAY[t1, t2, t3]按数组分组-- Array SELECT label, tags FROM array_example GROUP BY 1, 2-- MVD SELECT label, MV_TO_ARRAY(tags) FROM mvd_example GROUP BY 1, 2按数组元素分组按数组或 MVD 的单个元素分组-- Array使用 UNNEST 展开数组元素 SELECT label, strings FROM array_example CROSS JOIN UNNEST(tags) as u(strings) GROUP BY 1, 2-- MVD隐式展开直接对单个值分组 SELECT label, tags FROM mvd_example GROUP BY 1, 2按数组元素过滤并分组先过滤包含某个值的行再按元素分组。此例说明尽管数组与 MVD 的过滤结果可能一致但由于 MVD 会隐式展开implicit unnest一旦加上 GROUP BY结果就会不同。考虑上文「按数组元素过滤」的两条查询它们都返回以下两行{label:row1,tags:[t1,t2,t3]} {label:row2,tags:[t3,t4,t5]}但给两条查询都加上GROUP BY 1, 2后输出就变了-- Array SELECT label, tags FROM array_example WHERE ARRAY_CONTAINS(tags, t3) GROUP BY 1, 2 -- MVD SELECT label, tags FROM mvd_example WHERE tags t3 GROUP BY 1, 2数组查询返回{label:row1,tags:[t1,t2,t3]} {label:row2,tags:[t3,t4,t5]}MVD 查询返回{label:row1,tags:t1} {label:row1,tags:t2} {label:row1,tags:t3} {label:row2,tags:t3} {label:row2,tags:t4} {label:row2,tags:t5}MVD 结果看起来多出了 4 行tags不等于t3的行但这符合 Druid 对 MVD 相等性的求值方式过滤时整行匹配分组时再逐值展开。若想在 MVD 上获得与数组等价的查询请使用 MV_FILTER_ONLY 函数SELECT label, MV_FILTER_ONLY(tags, ARRAY[t3]) FROM mvd_example WHERE tags t3 GROUP BY 1, 2如何将数据摄入为数组在 Druid 中摄入数组有两种方式原生批量与流式摄入在 dimensionsSpec 中配置维度。在dimensionsSpec内设置useSchemaDiscovery: true并用dimensions以auto类型列出数组输入。完整示例见 摄入数组原生批量与流式摄入。SQL 批量摄入在查询上下文中添加 上下文参数arrayIngestMode: array并在列出列名和数据类型的 EXTEND 子句 中引用对应的数组类型VARCHAR ARRAY、BIGINT ARRAY或DOUBLE ARRAY。示例见 摄入数组SQL 摄入。最佳实践输入 schema 中始终使用 ARRAY 数据类型。如果确实需要摄入 MVD请显式地用 ARRAY_TO_MV 包裹字符串数组示例见 多值维度SQL 摄入。迁移到 Front-Coded 字典编码Apache Druid 会将字符串列编码成字典以获得更好的压缩率。Front coding前置编码是一种增量编码策略它针对开头部分相似的字符串做优化从而减少存储并提升性能。例如如果正在统计网站访问量大多数 URL 都以https://domain.xyz/开头front coding 就能利用这一模式获得更优压缩。Druid 会自动执行该优化——即使字符串列不匹配前置编码模式其性能通常也不会受影响因此你可以放心地全局启用该特性无需事先了解各列的底层数据形态。注意front coding 是 Druid 25.0.0 引入的实验特性。它适用于所有类型的摄入原生批量、流式、SQL 批量可用于 STRING 列和 COMPLEXjson 列。Front-Coded 字典的底层实现从源码看front coding 的实现位于 FrontCodedIndexedWriter。其核心思想是把字典值按桶bucket分组做增量delta编码桶中第一个值完整写入其余值以「一个整数表示与前一个或桶首字节数组共享的前缀长度 剩余字节」的形式存储前缀长度与值长度均使用 VByte 变长整数编码进一步节省空间。FrontCodedIndexedWriter 要求字典值按排序且唯一的顺序写入Values must be sorted and unique且校验bucketSize必须满足「是 2 的幂且介于 1 到 128 之间」。从 FrontCodedIndexed 中可以确认DEFAULT_BUCKET_SIZE 4DEFAULT_VERSION V0同时定义了V0 0、V1 1两个版本。写入时版本 0 的每个桶内「其余值」以桶内第一个值为前缀基准见writeBucketV0而版本 1 则以桶内前一个值为前缀基准见writeBucketV1注释称之为incremental buckets这也是 v1 通常读写更快、存储更小的原因之一。在 Segment 侧StringEncodingStrategies.getStringDictionaryWriter 会根据stringDictionaryEncoding.type选择字典写入器utf8走传统GenericIndexedWriterfrontCoded则构造FrontCodedIndexedWriter并包裹在EncodedStringDictionaryWriter中额外写入一个版本字节与编码策略 ID 字节读取侧getStringDictionarySupplier通过该 ID 字节区分 FRONT_CODED 与 UTF8 字典并兼容旧版无策略头的字典格式。启用 Front Coding要启用 front coding在摄入规范的tuningConfig中设置indexSpec.stringDictionaryEncoding.type为frontCodedtuningConfig: { indexSpec: { stringDictionaryEncoding: { type:frontCoded, bucketSize: 4, formatVersion: 0 } } }可选的属性如下bucketSize每个桶放置多少个值用于增量编码。设置该属性会指示索引任务使用指定桶大小的压缩字典写入 segment。可设为任意小于等于 128 的 2 的幂默认值为 4。对应源码 StringEncodingStrategy.FrontCoded 中FrontCodedIndexed.DEFAULT_BUCKET_SIZE。formatVersion指定使用的 front coding 版本可选 0 和 1版本 1 需要 Druid 26.0.0 及以上。默认值为 0对应源码中的DEFAULT_VERSION。从 Druid 25.0.0 升级Druid 26.0.0 引入了新版本的 front-coded 字典版本 1通常读取速度更快、存储体积更小。升级到 Druid 26.0.0 及以上版本时Druid 仍默认将 front coding 设置保持为版本 0以便能无缝降级回 Druid 25.0.0。要使用新版本将formatVersion设为 1tuningConfig: { indexSpec: { stringDictionaryEncoding: { type:frontCoded, bucketSize: 4, formatVersion: 1 } } }降级注意事项降级到 Druid 25.0.0一旦升级到版本 1就无法再无缝降级回 Druid 25.0.0。要降级必须将stringDictionaryEncoding.formatVersion设回 0 并重新摄入数据。降级到早于 25.0.0 的版本Druid 25.0.0 之前的版本无法读取含 front-coded 字典的 segment。要降级到更老版本必须删除包含 front-coded 字典的 segment或使用stringDictionaryEncoding.type为utf8重新摄入。从 maxSubqueryRows 迁移到 maxSubqueryBytesDruid 现在允许为子查询大小设置字节级限制以防止 Broker 在处理大型子查询时内存耗尽。子查询在 Druid 中既用于 join也用于公共表表达式如WITH。字节级子查询限制会覆盖 Druid 原有的行级限制官方建议从 Druid 30.0.0 开始逐步采用字节级限制。对于会产生大量行500 万行及以上的查询建议一开始不要设置maxSubqueryBytes。可以先增大maxSubqueryRows如果发现 Druid 需要字节级限制才能处理该查询再配置字节级限制。行级子查询限制Druid 使用maxSubqueryRows属性限制子查询返回的行数。由于它是行级限制无法约束返回数据的整体大小。该属性默认值为 100,000。从源码 ServerConfig 可以看到maxSubqueryRows的默认值正是100000而maxSubqueryBytes默认等于SubqueryGuardrailHelper.LIMIT_DISABLED_VALUE即默认不启用。启用字节级子查询限制设置可选属性maxSubqueryBytes即可设定最大返回字节数该属性优先级高于maxSubqueryRows。使用注意事项你可以在集群级别同时设置maxSubqueryRows和maxSubqueryBytes并在单个查询中覆盖它们。关于如何覆盖默认查询上下文值参考 配置文档。请确保启用 Broker 监控器SubqueryCountStatsMonitor这样 Druid 才会发出子查询统计指标。做法是在 Broker 的runtime.properties配置文件的druid.monitoring.monitors属性中加入org.apache.druid.server.metrics.SubqueryCountStatsMonitor参见 指标监控器。深入学习查询上下文设置查询上下文参数的方法Broker 配置参考maxSubqueryRows与maxSubqueryBytes的完整说明。迁移到 ANSI SQL 兼容的 Null 处理模式从 Apache Druid 28.0.0 起默认的 null 处理 模式改为符合 ANSI SQL 标准。本指南为那些在应用中依赖 Druid 遗留 Null 处理行为的运维人员与用户提供了迁移到 SQL 兼容模式的策略。遗留模式legacy mode计划从 Druid 中移除。SQL 兼容的 Null 处理自 Druid 28.0.0 起Druid 默认以 ANSI SQL 兼容的 null 处理模式写入 segment。这意味着对于字符串维度Druid 将 null 与空字符串区别存储对于数值维度将 null 与 0 区别存储。这会影响到应用行为因为 ANSI SQL 标准规定任何与 null 的比较结果都是 unknown未知。依据这种三值逻辑three-valued logicx some value只会返回非 null 的值。28.0.0 及之后版本默认启用 ANSI SQL 兼容 null 处理的配置如下druid.generic.useDefaultValueForNullfalsedruid.expressions.useStrictBooleanstruedruid.generic.useThreeValueLogicForNativeFilterstrue可通过 Null 处理教程 了解 Druid 默认 null 处理的工作方式。遗留 Null 处理与二值逻辑在 Druid 28.0.0 之前Druid 默认使用遗留模式用默认值代替 null 存储。遗留模式在摄入时创建的 segment 具有以下特征字符串列无法区分空字符串与 nullDruid 将二者视为可互换的值数值列无法表示 null 值的行Druid 存储0而非null。遗留模式已废弃的配置如下druid.generic.useDefaultValueForNulltruedruid.expressions.useStrictBooleansfalsedruid.generic.useThreeValueLogicForNativeFilterstrue这些配置已废弃并计划移除。移除之后即使配置文件中仍存在这些配置Druid 也会忽略它们并使用默认的 SQL 兼容模式。迁移到 SQL 兼容模式的两种路径如果业务逻辑依赖遗留模式的行为可以选择以下两种方式让 Druid 运行在 ANSI SQL 兼容的 null 处理模式下方案一修改摄入数据在摄入期避免 null 或避免空字符串从而保持与遗留模式相同的查询行为。这意味着需要修改摄入 SQL 查询与摄入规范来处理 null 或空字符串例如把字符串列的 null 替换为空字符串、把数值列的 null 替换为 0。代价是现有查询仍会像遗留模式那样工作。如果你不关心保留 null 值这是很好的选择。方案二保留 null 值并将所有 SQL 查询重写为 ANSI SQL 兼容。这样可以原样保留带 null 的输入数据但必须重写所有受影响的客户端查询。如果你有保留 null 值的要求请选择此方案。在摄入期用其他值替换 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}下面的示例展示了如何用 COALESCE 和 NVL 在摄入期避免 Druid 中出现 nullSQL 批量摄入REPLACE 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, -- 用空字符串替换任何 null 字符串值 COALESCE(string_example,) AS string_example, -- 用 0 替换任何 null 数值 NVL(number_example,0) AS number_example FROM ext PARTITIONED BY MONTHJSON 批量摄入{ 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_example|number_example| | -- | -- | -- | |2024-01-01T00:00:00.000Z|my_string| 99 | |2024-01-02T00:00:00.000Z| 空字符串 | 0 | |2024-01-03T00:00:00.000Z| 空字符串 | 0 |在摄入期将空字符串转换为 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}在遗留模式下Druid 会把第三条记录写成空字符串因此以下查询返回 2SELECT count(*) FROM null_string WHERE string_example IS NULL在 SQL 兼容模式下Druid 区分空字符串与 null同样的查询返回 1。下面的示例展示如何将空字符串转换为 null 以适配IS NULL比较SQL 批量摄入REPLACE 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 批量摄入{ 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_example| | -- | -- | |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。将查询重写为 SQL 兼容如果希望 Druid 保留数据中的 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。考虑以下 Druid 数据源null_example|__time|string_example|number_example| | -- | -- | -- | |2024-01-01T00:00:00.000Z|my_string| 99 | |2024-01-02T00:00:00.000Z| 空字符串 | 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 NULLDruid 返回 2。也可以使用 IS DISTINCT FROM 做 null 安全比较SELECT COUNT(*) as count_example FROM null_example WHERE string_example IS DISTINCT FROM my_string类似地对 null 的算术运算返回 null。例如SELECT number_example 1 AS addition_example FROM null_example按 ANSI SQL 标准null 加任何值都为 nullDruid 返回|addition_example| | -- | | 100 | | 1 | | null |使用 NVL 避免算术中出现 nullSELECT NVL(number_example,0) 1 AS addition_example FROM null_exampleDruid 返回|addition_example| | -- | | 100 | | 1 | | 1 |更多参考Null 处理教程了解 Druid 默认 null 处理的工作方式Null 值Druid 对 null 值行为的完整描述Segment 中的 null 处理Druid 如何存储 null 值的细节。迁移自检清单完成升级前建议对照以下清单逐项确认数组 vs MVD确认现有查询是依赖「整组值匹配」还是「单个元素匹配」语义如需保留行内值顺序优先迁移到数组SQL 摄入时显式设置arrayIngestMode避免误用none。Front-coded 字典确认 Segment 使用的formatVersion若要保留降级回 25.0.0 的能力保持版本 0若使用版本 1则升级路径不可逆需重新摄入才能降级。子查询限制将maxSubqueryRows逐步过渡为maxSubqueryBytes并确认 Broker 已启用SubqueryCountStatsMonitor以便观测子查询统计。Null 处理核对druid.generic.useDefaultValueForNull、druid.expressions.useStrictBooleans、druid.generic.useThreeValueLogicForNativeFilters三项配置检查应用中依赖「空字符串等价 null」「null 等价 0」的查询与摄入逻辑按上述两种方案之一迁移。以上所有迁移路径都以 docs/release-info/migration-guide.md 及各子指南为权威依据关键实现细节可在 FrontCodedIndexedWriter、FrontCodedIndexed、StringEncodingStrategy 与 ServerConfig 中进一步核对。【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表