Apache Doris 高性能 Open Lake Variant 读写技术解析(含对比数据)

数字逻辑设计大程——以撒的结合(Verilog语言) 前言 在大二学习数字逻辑设计课程(即计算机组成课程的前引课)时,期末大程题目我和同组的github成员lwaekfjlk 决定写一款基于Verilog语言的游戏。原型是我们两人都非常喜爱的一款游戏——The Binding of Isaac: Rebirth。在三天三夜的奋战后,基本完成了预期目标。由于亲自编写了其中种种,我们深知新手初期使用Verilog语言有多困难。而可悲的是,这又是之后计算机组成课程和体系结构课程所需的!因此,我们将top部分源码放在这里,供同袍们参考。若想引用还请注明出处。 代码部分 阅读详情

导读:Apache Doris 5.0 多模湖仓预告(5)|技术解读篇:面对日志、事件、AI 调用等不断变化的半结构化数据,如何既保留灵活结构,又实现高效查询?从 Doris 4.2 开始,Iceberg、Paimon 中的 Variant 数据可以直接高效读写,并通过按需读取、拆列优化和向量化计算提升查询效率。本轮测试中,相比 Spark,Doris 冷跑综合加速 15.10×、热跑 19.14×;拆列场景热跑综合加速达 58.64×,部分 Paimon 测试超过 106×。本文将介绍这些性能提升背后的关键技术。

作者:李文强,SelectDB 多模湖仓负责人

一、Variant:让变化的数据保留类型

一张事件表,今天多了设备属性,明天增加模型调用信息;同一个 amount,有的记录是数字,有的却是字符串。这类数据既要保留不断变化的结构,又要接受 SQL 过滤、聚合和关联。Variant 正是为这样的半结构化数据提供类型化表达。

从 Doris 4.2 开始,Iceberg 与 Paimon 的 Variant 高效读写成为 Doris 湖仓能力的一部分。用户可以直接查询湖表中的半结构化数据,按嵌套字段进行过滤、聚合和关联,并将 Variant 值写回湖表。下文将介绍这些能力背后的类型表示、读取优化与计算机制。

理解 Variant,需要沿着三个层次往下看:它能表达什么值,文件怎样保存这些值,Doris 怎样把这些值交给计算算子。

Variant 适合保存日志上下文、业务事件扩展属性、设备参数,以及 AI 应用中的调用信息、评测属性等半结构化内容。不同记录可以具有不同字段,同一路径也可以保存不同类型的值。

先把表结构与值的结构分开看。假设有一张 events 事件表,包含 id BIGINT 和 payload VARIANT 两列:id 保存事件编号,payload 保存事件的扩展内容。payload 只是本文示例中用户自定义的列名,VARIANT 才是这一列的数据类型;这个列名也可以换成 attributes 或其他名称。

一行的 payload 对应一个 Variant 值。这个值可以是数字、字符串等标量,也可以是由“字段名 → 子值”组成的对象,或者由多个元素组成的数组;对象和数组还可以继续嵌套。为便于阅读,下方示例用 JSON 写法展示逻辑内容,每行代码对应一条记录中的 payload 值;实际存储使用二进制编码,第二章会进一步展开。

例如,下列两个逻辑值可以出现在同一列中:

{"region":"west","amount":120,"device":{"os":"linux"}}
{"region":"east","amount":"unknown","tags":["new","app"]}

第一行的根值是一个对象:region 对应字符串 "west",amount 对应数字 120,device 对应一个嵌套对象,其中的 os 对应字符串 "linux"。第二行则没有 device,增加了数组字段 tags,并把 amount 保存为字符串。这些差异都发生在同一列的不同值内部。

variant-logical-structure.png

图 1|从 events 表中的 payload 列,展开到一个 Variant 值的嵌套字段。图示表达逻辑结构;第二章再介绍实际的二进制编码。

后文的 payload['region'] 表示从这一列的值中取出 region 字段,payload['device']['os'] 则沿 device → os 逐层取值。例如,对第一行执行 CAST(payload['device']['os'] AS STRING),结果是字符串 linux。列名、字段路径和目标类型分别回答“从哪一列取、取哪个值、按什么类型计算”。

表结构不必为每次新增属性执行一次变更。但灵活性并不意味着类型消失:数字 120 和字符串 "120" 仍然是不同的值,查询时需要明确预期的类型和异常值处理方式。

实践中,可以把业务主键、事件时间、分区字段等稳定属性放在普通列中,将持续变化的扩展属性放入 Variant。这样,稳定字段仍便于分区和关联,扩展字段则保留演进空间。

二、Variant 的存储与编码

2.1 metadata 与 value:把结构和类型保存在二进制里

未拆列的 Variant,通常由 metadata 和 value 两部分共同表达:

组成保存什么为什么有用
metadata编码头、字段名字典及相关偏移对象可以引用字段名编号,读取时可复用字典解析结果
value类型标记、标量内容、对象或数组结构及偏移保留类型,并定位嵌套值的二进制片段

这里的 metadata 是值编码的一部分,与 Catalog 中的表元数据不是同一个概念。对象通过字段名编号、偏移和子值组织内容;数组通过元素偏移定位内容;标量则按自身类型编码。Doris 可以在二进制表示上取字段,无须先还原为 JSON 字符串。

不过,二进制里能够快速定位一个字段,不等于对象存储只需读取这个字段的字节。 如果多个字段仍位于同一个二进制载荷中,实际 I/O 还受 Parquet 页与列块布局影响。要进一步利用列式读取,需要看拆列。

2.2 Shredding:把选定路径变成有类型的物理列

Shredding 可以理解为按路径拆列:例如把 region 拆成字符串列,把 amount 拆成整数列。它们随后可以使用 Parquet 的列式编码、压缩和统计信息。

未拆出的字段,以及与选定物理类型不匹配的值,可以保留在 residual,即残余二进制值中。例如某条记录的 amount 是字符串 "unknown",不能仅因为其他记录的该字段拆成整数列,就丢弃这个值。

沿用前面的 amount 字段,假设文件 writer 选择将它按整数拆列。下面只展示这个对象字段的两类物理槽位,不是完整 Parquet schema:

对象中的 amount类型子列 typed_value二进制 fallback:value
数字 120120无
字符串 “unknown”无字符串的 Variant 编码
显式 null无Variant null 的编码
字段不存在无无

表中的“无”表示该物理槽位未存值;Variant null 则由实际存在的二进制编码表达。这个例子假设 payload 本身是对象,讨论的是对象内部的字段。拆列不等于强制类型转换,类型子列为空也不等于逻辑值就是 null。reader 必须结合 fallback 和字段存在性恢复语义。

residual 是残余二进制内容的统称,fallback 强调某条路径不能由类型子列承载时的取值来源。它们可以出现在不同嵌套层级,并非都汇总到根级的一个大对象中。写入时由 writer 按拆列 schema 组织这些物理列,表中对用户暴露的逻辑列仍然是 payload VARIANT。

Variant 物理布局.png

图 2|Variant 的两种物理布局:未拆列时保留二进制文档;拆列后,类型子列与残余值共同表达逻辑内容。图中字节仅作编码示意。

同一张表的不同文件,可以拆出不同路径,也可以同时存在拆列与未拆列文件。因此,reader 必须识别每个文件的实际布局,再决定读取哪些列、是否需要残余值,以及何时重建对象。

2.3 与 JSON 的区别:重点在类型、布局与执行方式

Doris 的 JSON 类型采用二进制 JSONB 表示。下面比较的是这种二进制 JSON 表示与湖上 Variant,而不是把 JSON 查询都视为从文本重新解析。

维度Doris JSONB本文讨论的湖上 Variant
主要表示二进制 JSON 文档带类型的二进制值,可结合拆列布局
类型表达面向 JSON 语义的二进制表示可表达 decimal、日期时间、二进制等更多类型
字段访问在二进制文档上访问路径二进制路径定位,或直接访问类型子列
子列优化使用 JSONB 不会自然产生 Parquet Variant 子列已拆出的路径可以参与列裁剪与统计裁剪
互操作重点JSONB 的理解或转换Variant 编码、表格式与各引擎的共同支持

因此,Variant 的价值不只是“把 JSON 换一种编码”。它把类型信息、可选的列式布局和执行引擎的计算能力连接起来。原样交换、低频取值可以使用 JSON;频繁进行路径过滤、聚合和湖上跨引擎读写时,Variant 提供了更多优化空间。

日期时间、二进制等值转成 JSON 文本后,还可能丢失原来的类型身份。文本中转适合受控的 JSON 数据转换,不应默认视为所有 Variant 值的无损交换方式。

三、一条 Variant 查询在 Doris 中怎样执行

3.1 FE 规划:识别类型、路径与扫描任务

用户提交 SQL 后,FE 负责解析、分析与生成执行计划。Catalog 适配层将 Iceberg、Paimon 中的 Variant 映射为 Doris 能识别的逻辑类型;优化器分析查询需要的列、可识别的嵌套路径和过滤条件,形成扫描任务,再将执行计划分发给 BE。

这里有两类相关但不同的优化:普通列裁剪决定是否需要 payload;嵌套路径裁剪进一步表达只需要 payload['region']、payload['amount']。FE 从表达式中收集可识别的常量路径,并区分过滤与投影需求。这些需求传到 reader 后,才能结合实际文件布局转化为物理列读取。路径请求本身并不保证每个文件都能只读叶子列。

02-doris-execution.png

图 3|Doris 的总体执行流程。FE 规划扫描与执行任务,BE 以批次执行算子;查询返回结果,写入则进入目标表 sink 和提交链路。图中的算子为示意,具体次序与分布由执行计划决定。

3.2 BE 执行:扫描、表达式与分布式计算

BE 从湖上扫描数据,组织为包含多列数据的 Block 批次。路径提取、类型转换、过滤、聚合和关联等工作在执行计划安排的算子中完成;需要跨节点交换数据时,由 Exchange 衔接不同执行阶段。过滤可能下推,聚合也可能分布在 Exchange 两侧,不能把概念流程理解为每条查询固定的串行次序。

对 Variant 而言,关键不是让所有算子都理解完整嵌套对象,而是让需要的路径尽早变成可计算的列,同时尽量延后无用的整对象重建。若最终只需要 SUM(amount),大量未参与计算的扩展字段就没有必要反复转换成对象或文本。

四、高效读取:少读无关列,少构造中间对象

4.1 按文件和 Row Group 决定投影范围

以按地区统计金额为例,查询主要需要 region 和 amount 两条路径。Parquet scanner 会根据当前 Row Group 的物理布局与 residual 情况决定:

  1. 可以证明叶子列足够:读取所需类型子列。

  2. 叶子列还不足以表达结果:保留必要的残余值并参与恢复。

  3. 无法证明局部投影完整:回退到完整 Variant 投影。

这使优化能够适应不同文件的布局,同时保证字段不因裁剪而丢失。动态路径、整对象输出和混合类型都会影响实际读取范围。

variant-three-read-paths.png

图 4|同一条 amount 路径的三种读取方式:直接复用类型叶子、按行组合类型值与 fallback、沿未拆列二进制定位。图中展示可走快速路径的情况;整对象或复杂结构按实际需求恢复。

4.2 拆列路径:复用叶子、补齐残余值,再安全裁剪

情况一:目标路径完全由兼容的类型叶子提供。reader 会沿文件中的物理结构找到目标路径,检查相关层级的有效性与残余值需求。若该路径无需 residual 补充,并且物理类型适合直接使用,就保留已经解码的叶子列,交给后续 CAST 和计算,省去“重新编码为完整对象,再提取同一个字段”的往返。部分需要保留特殊物理语义的类型会转换为精确的 Variant 值,同样可以避免完整对象重建。

情况二:同一路径的不同记录分别由类型列和 fallback 提供值。例如,第一条记录的 amount=120 位于整数子列,第二条记录的 amount="unknown" 位于该字段的二进制 fallback。Doris 会逐行沿路径检查类型值是否存在:类型路径完整时取叶子;在某层需要残余值时,则进入该层的二进制内容,继续定位剩余路径。最终组合的是 amount 这一条路径的结果,保留各行原来的值与类型。

如果目标字段根本没有被拆出,Doris 也可以在对应对象层级的 residual 中查找。字段名到字典编号的解析可以按不同 metadata 复用,只有确实需要解释残余编码时才构建相关索引。因此,“存在 residual”并不意味着“必须重建整个 Variant”。这条优化主要针对能够直接解析的对象键路径;数组或复杂子树不满足快速路径条件时,仍需按实际需求恢复相应值。

这也解释了列裁剪和统计裁剪的区别:前者减少不需要的物理列,后者跳过不匹配的数据范围。当 fallback 中仍有需要参与判断的值时,只看类型子列的统计信息并不足以证明可以跳过。

例如过滤 CAST(payload['amount'] AS BIGINT) > 1000。如果对应类型列在某个 Row Group 的最大值为 900,且转换与残余值条件允许,该 Row Group 就可能被跳过。实现会检查对象键路径、常量比较、类型与排序语义、转换安全性,以及该路径末端是否还存在 fallback 值。Page Index 裁剪也需要相应校验。

4.3 未拆列路径:直接定位与复用解析结果

情况三:文件没有拆出类型子列,目标值保存在 metadata/value 编码中。Doris 会解析 metadata 的字段名字典,建立可复用的索引;访问对象路径时,先把字段名映射为编号,再结合对象中的字段编号与偏移定位子值,沿嵌套层级继续查找,无须先生成 JSON 文本。

同一份字典可以被多行共享使用,路径解析也可以复用已经定位的前缀。例如访问 device.os 与 device.version 时,可以减少重复定位 device 的工作。对于已定位的标量,reader 可以组织为适合后续计算的列;对于尚需继续访问的子树,则可以保留路径状态,延后构造完整内容。这些优化减少了重复字典解析、字段查找和中间值构造。

这些优化主要减少字段访问的 CPU 和中间对象开销,与“读取独立 Parquet 子列减少 I/O”属于不同机制。即使文件没有拆列,也无须先把每一行变成 JSON 文本。

五、高效计算:让提取结果尽快进入类型列计算

5.1 同一个 Variant 列,可以保留三种执行形态

文件中的物理布局与内存中的计算表示不是同一件事。为了衔接不同输入和操作,Doris 的 Variant 列支持三种执行形态:

形态保留的内容适合的处理
Encoded编码字节与 metadata 引用保留二进制值,进行路径定位与序列化
Typed可空的强类型标量列对提取出的标量执行类型转换和后续计算
Shreddedreader 提供的物理树、路径与子列直接利用叶子列,按需恢复逻辑值

这些形态是执行层的表示方式,用户看到的逻辑类型仍是 Variant。筛选、截取和行选择可以继续保留拆列形态;reader 能直接提供合适的类型叶子时,就不必先重建完整文档。

04-variant-compute.png

图 5|Variant 计算流程:路径提取连接不同执行形态,类型转换输出标量列,供过滤和聚合使用;整对象请求触发必要物化。图中省略了跨节点传输等序列化边界。

5.2 从路径提取到 CAST,再到向量化算子

看下面这条查询:

SELECT
    CAST(payload['region'] AS STRING) AS region,
    SUM(CAST(payload['amount'] AS BIGINT)) AS total_amount
FROM iceberg_catalog.demo.events
WHERE CAST(payload['amount'] AS BIGINT) >= 100
GROUP BY CAST(payload['region'] AS STRING);

它可以拆成三个容易理解的步骤:

  1. 取路径:从 Variant 中提取 region 与 amount。如果已有类型叶子,尽量复用;否则沿二进制结构定位相应值。

  2. 定类型:CAST 将结果转换为 SQL 所需的 STRING、BIGINT 列。标量转换中存在直接处理 typed 表示的分支,避免绕回完整文档编码。

  3. 按列计算:比较、筛选、分组和求和处理转换后的批次列,接入 Doris 的向量化执行链路。

对于编码形态的标量,转换实现还会按实际类型分组,批量完成转换后恢复原来的行顺序。这让混合类型数据也能衔接批量计算。

5.3 延迟物化:在真正需要时构造值

这里需要区分三个概念:shredding 是写文件时按路径拆列的过程;unshredded 描述未拆列的物理布局;重建或物化 则是在读取与执行时,将需要的子列和残余编码恢复为具体的逻辑值。读取未拆列文件,并不等于先执行一次“反向拆列”。

重建范围取决于请求内容和实际执行路径。只取 amount 且命中路径优化时,可以仅组织该路径的结果;请求 device 子对象,需要得到该子树的完整逻辑内容;请求完整 payload,则必须保留并恢复整个值。复杂子树、数组或未命中快速路径的情况,也可能回退为先重建完整值,再提取所需部分。对于拆列对象,恢复过程按嵌套结构组合类型子字段与未拆出的残余字段,同时保留字段缺失、显式 null 和原始类型的区别,而不是把几段 JSON 文本简单拼接起来。

这种按需恢复要求规划与执行配合:如果下游需要完整对象,扫描就必须保留恢复它所需的信息;只读取了投影叶子的状态不能凭空补回已省略字段。Doris 因此把“直接使用子列”和“生成规范值”的边界显式保留到执行过程中。

跨节点 Exchange 尤其需要注意:reader 持有的内存状态不能原封不动传到另一台 BE。若传输时仍保留拆列表示,序列化会将需要的内容转为可独立传输的表示;这个表示可以只包含规划确认仍需使用的投影,不必总是恢复原始完整对象。如果路径已经转成普通标量列,传输的就是这些列。因此,延迟物化不等于整条分布式查询全程都不物化,也不等于没有数据复制。

对用户来说,最直接的做法是:只选择需要的路径,并明确用于计算的目标类型。若只需地区汇总,就避免把整个 payload 一起放进下游结果;整对象需求可能增加读取、编码和传输工作。

六、高效写回:沿二进制与批次传递数据

本文所分析的实现中,Doris 向 Iceberg 写出未拆列 Variant,目标使用支持 Variant 的表格式版本与 Parquet 数据文件;Paimon 则通过原生 writer 衔接拆列能力。能够读取拆列文件,与能够写出拆列文件,是两项独立能力。因此,Iceberg 读取可以利用已有文件的拆列布局,而当前 Doris 写回链路不会自动生成同样的类型子列。

05-variant-write.png

图 6|Variant 写入流程:Iceberg 通过 Arrow Variant 写出 Parquet;Paimon 通过 Arrow C Data 与 JNI 交给原生 writer。两条路径都在生成文件后进入各自的表提交机制。

6.1 Iceberg:Variant → Arrow → Parquet → 表提交

Iceberg 的 Variant 映射为 Arrow Variant extension,底层结构是 struct<metadata: binary, value: binary>。Doris 的序列化层将二进制值交给相应 builder,再由 Parquet writer 生成文件。

这避免了先输出 JSON 文本、再由另一侧解析并猜测类型的中间过程。

6.2 Paimon:Variant → Arrow 批次 → JNI → 原生 writer

Paimon 写入将 Doris Block 转为 Arrow RecordBatch,经 Arrow C Data 接口送入 Java。Variant 按 Paimon 的 struct<value: binary, metadata: binary> 组织,Java 侧以列式行视图适配 Arrow 数据,并复用行游标交给 writer。

这里利用批次传递减少逐字段跨语言交互,省去为整个批次构造逐单元格 Java 对象矩阵的中间步骤。

Paimon 原生 writer 可以采用显式拆列 schema,也可以推断拆列 schema。Doris 通过该 writer 衔接这些能力;仓库用例覆盖拆列写入和混合文件布局。不同文件拆出不同路径是正常情况,reader 需要按文件解释实际结构。

七、如何使用并验证这些优化

7.1 从正确的值开始

以下示例假设 Catalog 和支持 Variant 的目标表已经准备好,表包含 id BIGINT 与 payload VARIANT。示例依据分支实现整理,本次未连接集群执行。

INSERT INTO iceberg_catalog.demo.events VALUES
    (1, PARSE_TO_VARIANT('{"region":"west","amount":120}')),
    (2, PARSE_TO_VARIANT('{"region":"east","amount":80,"channel":"app"}'));

INSERT INTO paimon_catalog.demo.events
SELECT id, payload
FROM iceberg_catalog.demo.events;

PARSE_TO_VARIANT 表达的是将 JSON 文本解析为 Variant 内容。CAST('text' AS VARIANT) 表达的是将字符串值转换为 Variant;即使字符串长得像 JSON 对象,也不应把这两种写法混为一谈。

如果来源已经具有 decimal、日期时间等明确类型,应尽量保留类型,不要先把所有值转成字符串再入湖。跨引擎验证时,重点检查数值精度、时间语义、缺失值与 null,以及嵌套数组等边界。

7.2 用 Profile 判断优化发生在哪里

判断 Variant 是否读得高效,可以把整对象查询与单路径查询作为观察入口,但二者输出量不同,不能仅凭端到端耗时宣称加速倍数。应同时看读取字节、算子耗时和物化工作量。

Profile 指标观察重点
VariantLeafProjectionRowGroupColumnsRow Group 中采用叶子投影的情况
VariantResidualProjectionRowGroupColumns局部投影仍需残余值的情况
VariantFullProjectionRowGroupColumns完整投影的情况
VariantDirectLeafRows直接取得叶子值的行数
VariantReconstructedRows / VariantReconstructionTime完整 Variant 重建的工作量与耗时
VariantUnshreddedDirectSeekRows未拆列数据按路径直接定位的情况
VariantUnshreddedPrefixReuseRows嵌套路径前缀复用的情况

7.3 评估性能时,把数据与查询一起看

  • 数据:字段数、嵌套深度、混合类型和缺失值比例。

  • 文件:拆列覆盖了哪些查询路径、残余值比例,以及文件和 Row Group 大小。

  • 查询:单路径还是整对象、过滤选择性、转换目标类型,以及聚合和关联方式。

  • 执行:冷暖缓存、并发度、实际读取字节、跨节点传输,以及 Paimon 的读取路径分布。

Doris 内表的子列与索引属于另一条存储链路,直接查询 Iceberg、Paimon 文件不会自动获得内表索引。对湖上 Variant,应围绕文件布局、读取路径和实际执行计划解释收益。

八、性能测试

前文介绍了 Variant 的读取与计算机制,下面看它们在湖上查询中的实际表现。测试先由 Spark 将 Variant 数据写入湖表,再分别使用 Doris 和 Spark 执行查询,覆盖拆列(shredded)与未拆列(unshredded)两种布局。

在本轮 Spark 写入数据的 6 组测试中,按相同查询耗时求和,Doris 的冷跑综合加速比为 15.10×,热跑为 19.14×。这里比较的是导入完成后的查询耗时,不包含数据导入耗时。

8.1 测试环境

测试采用某客户场景复刻的 10 个查询 query01–query10,数据存储在 OSS 上,文件格式为 Parquet;湖表包括 Iceberg、Paimon Append 与 Paimon PK。每种表类型分别测试拆列、未拆列两种布局,共 6 个组合;每个引擎在每种冷热状态下均覆盖 60 条次查询。测试环境为 3 台机器,每台 16 核、64 GB 内存;查询引擎为 Doris 4.2 与 Spark 4.0.1。

8.2 整体表现:拆列与未拆列布局均有收益

查询状态查询条次/引擎Doris 合计(秒)Spark 合计(秒)综合加速比
冷跑60640.6109,671.72215.10×
热跑60481.3969,215.46719.14×

进一步按布局汇总,拆列数据的冷跑、热跑综合加速比分别为 55.24×、58.64×;未拆列数据分别为 7.06×、9.08×。这些结果说明,在本轮查询与部署配置下,两种布局均表现出 Doris 的查询优势,拆列组合的差距更大。

query-totals.png

图 7|Spark 写入 Variant 数据后的查询总耗时。每根柱汇总一种布局下三种表类型的 30 条查询;冷跑与热跑使用从零开始的相同线性刻度,耗时越低越好。

8.3 分组结果:Paimon 拆列数据的差距最明显

下表每行均为同一组合 query01–query10 的耗时合计,单位为秒。

冷跑

数据布局表类型Doris 耗时(秒)Spark 耗时(秒)加速比
拆列Iceberg65.6081,903.72029.02×
拆列Paimon DUP20.6922,030.47398.13×
拆列Paimon PK20.6171,972.22595.66×
未拆列Iceberg186.8751,299.0666.95×
未拆列Paimon DUP172.9811,255.8087.26×
未拆列Paimon PK173.8371,210.4306.96×

热跑

数据布局表类型Doris 耗时(秒)Spark 耗时(秒)加速比
拆列Iceberg61.7601,893.54230.66×
拆列Paimon DUP17.9531,920.502106.97×
拆列Paimon PK18.0481,918.227106.28×
未拆列Iceberg126.8341,141.6999.00×
未拆列Paimon DUP131.7401,103.8858.38×
未拆列Paimon PK125.0611,237.6129.90×

拆列数据中,Iceberg 的热跑加速比为 30.66×,Paimon DUP 与 Paimon PK 分别为 106.97×、106.28×。未拆列数据的热跑加速比为 8.38×–9.90×。这里的百倍差距来自对应组合的 10 条查询总耗时之比。

query-speedup.png

图 8|六个 Spark 写入组合的查询加速比。每根柱为 Spark 与 Doris 的 10 条查询总耗时之比;两种布局使用相同线性刻度,1× 虚线表示耗时相同。

结语:让灵活结构进入高效的列式计算

Variant 把不断变化的数据保留为带类型的值;二进制编码减少文本转换,拆列布局为列裁剪和统计裁剪创造条件。Doris 再通过路径投影、叶子列复用、标量转换、批次计算与延迟物化,将这些条件转化为实际执行收益。

对社区湖仓用户,最值得抓住的是这条主线:保存时保留类型,读取时按需取路径,计算时尽量利用类型列,写回时保留二进制语义。 Iceberg 与 Paimon 的适配细节有所不同,但都可以沿着这条主线理解和验证。

芒果YOLOv8改进52:主干Backbone篇之GhostNetV2:提出原创GhostNetV2 架构升级版:引入长距离注意力机制增强廉价操作,构建更强端侧轻量型骨干,打造高效轻量级检测器 长距离注意力机制增强廉价操作,打造轻量级检测器 阅读详情

相关推荐

告别卡顿!用LVGL+DMA刷新STM32F407的SPI屏幕,性能提升实战

本文详细介绍了如何通过LVGL+DMA技术优化STM32F407驱动SPI屏幕的性能,实现接近60FPS的流畅刷新。从硬件瓶颈分析到SPI与DMA的精细化配置,再到LVGL的双缓冲机制与局部刷新优化,提供了一套完整的性能提升方案。特别适合嵌入式GUI开发中面临卡顿问题的工程师参考。

weixin_42531588的博客 378

【数据结构】基于比较的排序算法 - 交换排序

本文介绍交换排序中的冒泡排序与快速排序。冒泡排序通过相邻元素比较交换,实现升序,时间复杂度最坏为O(N²),空间复杂度O(1),稳定且适合小规模有序数据。快速排序采用分治思想,以基准值划分区间,平均时间复杂度O(N log N),空间复杂度O(log N),但不稳定,实际应用中性能优异,可通过随机基准、小区间优化等提升效率。

careathers的博客 320

保姆级教程:将YOLOv8模型封装成Docker镜像,集成到CVAT实现自动标注

本文提供了一份详细的保姆级教程,指导如何将YOLOv8模型封装成Docker镜像并集成到CVAT平台,实现自动化标注。通过环境配置、模型服务化封装、Docker镜像构建与优化、CVAT平台集成等步骤,帮助开发者高效完成计算机视觉项目的自动标注工作流,显著提升标注效率。

weixin_28692817的博客 412

pandas 深度入门:从数据结构到一份真实销售报表

本文系统讲解Pandas核心应用,从三分钟安装到完整月度报表生成,涵盖数据读取、清洗、筛选、分组聚合、表连接、透视表与时间序列等13个关键环节。强调“先看数据再动手”,掌握Series/DataFrame/Index结构本质,熟练运用loc/iloc、merge校验、agg/transform区别,通过真实案例实现高效报表自动化,告别手动Excel操作。最后提供性能优化与高频报错解决方案,助力读者快速上手,真正实现“写二十行代码,跑三秒出结果”。

信息安全领域优质创作者|操作系统领域专家 257

信息隐藏 数字图像空域隐写与分析技术的实现

【代码】信息隐藏 数字图像空域隐写与分析技术的实现。

Yungoal的博客 468

《矩阵论》学习笔记(四):4.2 矩阵的QR分解

《矩阵论》学习笔记(四):4.2 矩阵的QR分解 文章目录《矩阵论》学习笔记(四):4.2 矩阵的QR分解一. Givens矩阵与Givens变换1.1. 概念与理解1.2. Givens矩阵求解方法二. Householder矩阵与Householder变换2.1. 概念与理解2.2. Householder矩阵求解方法2.3. Givens变换与Householder变换的关系三. 矩阵的QR...

shine的博客 3675

工单系统的数据结构化:故障分类体系设计与报表可用性

很多团队上完工单系统半年,会发现一个尴尬的事实:单子照常在走,报表却始终出不来。导出的 Excel 几千行,看不出趋势、算不出口径、答不了"哪类故障占大头"。问题几乎都不在系统,而在数据模型的第一层——。这篇文章把这件事拆开讲:自由文本为什么会失效、分类体系怎么分层、表怎么建、老数据怎么迁、上线前怎么验收。

ITyunwei0987的博客 275

改善电流不匹配的级联电流镜布局设计 2002

模拟金属氧化物半导体 (MOS) 集成电路的性能在很大程度上取决于匹配精度的因素。 电流镜的匹配精度在模拟 IC 性能中尤为重要。 布局技术和器件尺寸被认为是可以在很大程度上改善匹配行为的主要因素。 尽管这些信息很重要,但对这两个因素的研究或研究有限。 在本文中,我们基于 Lan 等人 [1999] 提出的简单电流镜提出了一种新的级联电流镜布局技术。 Cascode电流镜的新布局在参数梯度存在的情况下改善了匹配特性。 讨论了跨晶体管有源区域的阈值梯度对电流镜匹配特性的影响。 使用 SPICE 仿真比较了新

Srfun的博客 4227

【Linux进程(二)】深入 Linux 内核:进程状态切换与底层数据结构的秘密

本文从操作系统理论与Linux内核源码双重视角,深入解析进程状态的底层机制。通过“侵入式链表”与“偏移量计算”揭示task_struct如何同时存在于多类数据结构中,实现高效调度与管理。结合ps命令与实战代码,详解R、S、T、Z、X等状态的成因与行为,涵盖前后台切换、暂停/恢复、僵尸进程、孤儿进程等典型场景,揭示进程生命周期的完整图景,实现“知其然更知其所以然”。

wacsii的博客 291

PP触发EWM产生PMR捡配

PP触发EWM产生PMR捡配 前言 PMR: Production Material Request 在流程订单/生产订单下达后,可以在EWM中产生PMR,在EWM中, PMR其实代表了ERP中的流程订单/生产订单。 You can use SAP EWM in conjunction with the following PP application components: • Production Orders • Process Orders • Repetitive Manufacturing •

ZevYang的博客 1918

Windows密码忘了怎么办?一分钟看懂Windows密码底层机制:无需重装系统也能无痕找回登录密码

忘记Windows开机密码,不一定非得重装系统!本文手把手教你通过提取NTLM Hash值实现“无痕找回密码”,无需PE、无需清除密码,对系统零修改 🛡️ 搭配mimikatz与在线解密平台,小白也能快速上手,了解Windows密码底层机制 🔐

XiaoqiangClub的博客 121

数据结构:9.Map和Set

有的讲解会说这里key是标识作用,这种说法其实没有太大意义,就算知道了他是标识value的作用又如何,在实际的使用过程中,我们只会根据具体的效果选择使用的方法等,不会执着于“标识”这种说法。2.由于哈希函数的设计原则太过理论,它仅仅是在认为:这些通过关键码算出来的index不同 的情况下设计的,然而这种理论化的设计原则在遇到冲突时,恰恰就出现了问题,而我们能做的,就是降低冲突率。虽然哈希表的使用过程中存在冲突,但是在实际的使用过程中,哈希表的冲突率是不高的,冲突的个数是可控的。

2602_95646088的博客 189

YOLOv8实战教程:从模型训练到改进的全流程指南

回顾这段YOLOv8的实战历程,从最初的模型训练到后来的各种改进尝试,我深刻体会到目标检测技术的魅力与挑战。YOLOv8作为一款优秀的框架,不仅易用性强,而且具备很强的可扩展性,让我能在其基础上不断创新。未来,我计划在以下几个方向继续探索:一是将YOLOv8与多模态技术结合,尝试实现图像与文本的联合目标检测;二是深入研究自监督学习在YOLOv8中的应用,减少对标注数据的依赖;三是探索YOLOv8在更广泛场景的部署,如机器人、自动驾驶等。

LXY_LOVE36的博客 279

[C++高阶数据结构·图论下] 最短路径:为什么“挑最小的”一定正确?

本文系统对比最小生成树与最短路径的核心差异,聚焦三类最短路算法:Dijkstra(非负权、贪心确认)、Bellman-Ford(可负权、反复松弛)、Floyd(全源、分层中转)。核心区别在于:MST 关注“连通所有点”的最小代价(边数固定),而最短路关注“两点间路径最短”(边数可变)。关键反直觉点是:负权对 MST 无害,却使 Dijkstra 失效——因“绕远更近”破坏贪心前提。算法本质反映三种处理不确定性的策略:确认式(Dijkstra)、迭代修正(Bellman-Ford)、分层逼近(Floyd)。强

Tairitsu_青云 505

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

[C++高阶数据结构·图论上] 最小生成树:为什么“选边”和“选点”是两条路?

本文系统讲解最小生成树(MST)的核心思想与两大算法:Kruskal与Prim。强调MST目标是用最小权值总和连接所有顶点形成一棵树,而非单边最小。通过三条构造准则(用图中边、恰n-1条边、无环)统一框架。Kruskal按边贪心,依赖并查集判环,适合稀疏图;Prim按点扩张,利用割集特性天然避免成环,需起点,适合稠密图。两算法本质差异在于“判环”机制与数据结构选择,且与最短路径问题在连通性、负权处理上截然相反,需分篇厘清。

Tairitsu_青云 214

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

Vue3 低代码移动表单设计器实战(二):缩放画布下的拖拽系统与树状级联数据结构

本文深入剖析Vue3低代码表单设计器在缩放画布下的拖拽失效问题,揭示SortableJS在CSS Transform下坐标崩溃的数学根源。通过自研独立视觉克隆壳层与逆向几何补偿算法,实现精准拖拽;采用扁平数据存储+运行时树状计算结构,解决级联依赖与深克隆难题,确保逻辑一致性与性能优化,为复杂低代码系统提供可复用的工程范式。

NanChen的博客 333

排序算法深度笔记(上):比较类排序的思想演进与代码实现

与。本文聚焦前者 —— 也就是通过元素间两两比较来确定相对顺序的排序算法,从最朴素的 O (n²) 基础排序,到工业级主流的 O (nlogn) 进阶排序,逐层拆解设计思想、带注释的代码实现与边界细节。当我们理解了比较排序的性能边界,也就自然引出了非比较排序的存在意义。所有代码均遵循统一的实现规范:入口函数前置空数组与单元素边界校验,基于数组原地修改,主函数附带可直接运行的测试用例,与练习代码风格完全对齐。

2401_87069538的博客 598

【数据结构】静态 & 动态顺序表

顺序存储来实现线性表:链式存储实现线性表:存放在一段,数据元素之间的逻辑关系由物理存储关系体现:存储在,数据元素之间的逻辑关系用来表示:且大小固定()的数组:可以自动调节大小()的数组。

2301_80694580的博客 184

【数据结构-3】算法美学之降复杂度

在while循环的循环体内,首先用临时变量保存最后一个位置的数据,再通过for循环把所有元素往后移动一位,最后把tmp中保存的值放到下标为0的位置上去。方法二的思路是:创建新数组,让原数组中后k个数据移动到新数组中前面的位置,让原数组中前numsSize-k个数据移动到新数组中后面的位置,这。这段代码的思路是:三次逆置,第一次逆置前numsSize-k个数据,第二次逆置后k个数据,最后整体逆置。这篇博文分享一道解法不一的算法题,求解过程的目的在于降低算法的时间复杂度,题源力扣。,属于空间换时间的操作。

weixin_44218929的博客 78

数据结构:双向链表实现,链表与顺序表的对比

本节讲解数据结构双向链表,链表和顺序表的特点对比,顺便解释了一下什么是CPU缓存和内存碎片

lei_jiale的博客 368

【数据结构-2】顺序表的实现

线性表是一种数据结构,它分为逻辑结构和物理结构,在逻辑结构上线性表是呈线性的,数据是是连续存储的;在物理结构(物理空间)上,线性表不一定是连续存储的。线性表的实现,主要分为数组和链式结构。顺序表是线性表的一种实现方式,顺序表在逻辑结构和物理结构上都是连续的。顺序表的底层实现是数组。

weixin_44218929的博客 205

数据结构(2)

不同点顺序表链表(单链表)存储空间上物理上一定连续逻辑上连续,但物理上不一定连续随机访问支持:O(1)不支持:O(N)任意位置插入或删除元素可能需要搬移元素,效率低 O(N)只需修改指针指向插入动态顺序表,空间不够时需要扩容和空间浪费没有容量的概念,按需申请释放,不存在空间浪费应用场景元素高效存储 + 频繁访问任意位置高效插入和删除。

likexuec的博客 142

数据结构—B树

一棵m(m>2)阶的B树,是一棵平衡的M路平衡树,可以说空树或满足以下6点。1.根节点至少有两个孩子2.每个分支节点都包含k-1个关键字和k个孩子,ceil(m/2)<=k<=m;孩子比关键字多一个ceil向上取整3.每个叶子节点都包含k-1个关键字,ceil(m/2)<=k<=m;4.所有的叶子节点在同一层。5.每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含元素的值域划分。6.节点的结构是{n,A0,K1,A1,K2,........Kn,An};

fish_xk的博客 127

C++深入浅出探索数据结构的原理

数据结构」这四个字常被简化成一张张示意图:数组是一排格子,链表是箭头串起来的节点,树是倒过来的分叉。图看得懂,但一到写代码就出问题——不知道为什么中间插入慢,不知道为什么std::list明明插入是常数时间却常常更慢,不知道为什么哈希表「平均 O(1)」却可能退化成 O(n)。原因在于,示意图只画了逻辑结构,没有画物理存储。数据结构的性能几乎全部由「数据在内存里怎么放、访问时要跳几次、扩容时要搬多少」决定。

eggwyw的博客 141

【Lua 入门系列】Lua数据结构:从 Table 出发,一文串联数组、链表、栈、队列、集合、映射与树的实现原理

本文从Lua的Table机制出发,系统解析了数组、链表、栈、队列、集合、映射和树等数据结构的实现原理。强调在Lua中,所有数据结构均基于Table的灵活键值存储,通过约定组织方式与操作规则来模拟不同语义。重点剖析了整数索引的连续性、#运算符的边界特性、ipairs遍历机制及nil空洞的影响,揭示“表即基础,语义来自用法”的核心思想,帮助读者建立统一的数据结构心智模型。

努力努力再努力wz的博客 434

数据结构:顺序表

顺序表是用一段地址连续的存储单元依次存储线性表的数据元素的线性结构。它是线性表的顺序存储实现,在C语言中通常用数组来实现。

2502_92937908的博客 330

数据结构:单链表

单链表是一种链式存储的线性表。它由一系列节点数据域:存放数据元素指针域:存放指向下一个节点的指针内存不连续,节点分散存储插入删除效率高(O(1),前提是已定位到位置)查找效率低(O(n)),不支持随机访问相比数组,多消耗指针域空间// 定义节点结构体int data;// 数据域// 指针域,指向下一个节点} Node;// 或者使用 typedef 简化int data;// LinkList 是指向 Node 的指针类型中必须写,因为此时 typedef 尚未生效,不能写Node。

2502_92937908的博客 472
上一篇: Doris vs ClickHouse:企业级分析场景下,OLAP 的能力边界正在如何变化?
下一篇: 半结构化数据在 Apache Doris 里该怎么存:VARIANT 建表、调参与效果验证全记录
SelectDB技术团队
博客等级 码龄4年 2119粉丝 483原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SelectDB技术团队

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值