LazyProjection 延迟物化

更新时间:
复制 MD 格式

在包含宽列的 TopN 等查询中,宽列会随 Scan、Sort、Limit、Shuffle 等算子全程搬运,即使最终只返回少量行,也需要读取、解压和传输大量宽列数据。LazyProjection(延迟物化)让下层算子只携带小列和行定位信息,在得到最终结果行后再回表读取宽列,从而显著减少 I/O 和网络开销。该功能自 Hologres V5.0.0 起支持,默认开启。

背景信息

应用场景

LazyProjection 适用于以下典型场景:

  • 宽表分析:表中包含大量列或长文本、JSON 等宽列,查询只需返回排序后的少量明细。

  • 日志检索:按时间排序取最近若干条日志,并返回完整的日志内容。

  • TopN 查询:ORDER BY ... LIMIT N 类查询,N 远小于参与排序的行数。

  • 过滤后回表:先按小列过滤、排序,再取出少量命中行的宽列。

  • 向量检索二阶段取数:先基于向量距离等小列得到 TopN 候选,再取回候选行的原文、属性等宽列。

这些场景的共同特点是:参与计算(过滤、排序、截断)的行数多,最终输出的行数少,且输出结果中包含宽列。

未开启 LazyProjection 时的问题

未开启 LazyProjection 时,执行计划通常为 Scan → Sort → Limit → Shuffle。宽列从 Scan 阶段起就被读取、解压,并随每一行经过 Sort、Limit 和 Shuffle 全程搬运。即使最终只返回 N 行,读取、解压和传输的数据量仍与「参与计算的行数 × 列宽」成正比,大量资源消耗在最终会被丢弃的宽列上。

工作原理

LazyProjection(下文简称 LP)在 Sort、Limit、Shuffle 等算子之后插入延迟物化算子:

  1. 下层算子只携带 (小列, shard_id, row_locator),不再携带宽列,行宽显著变小。

  2. 顶层的 LP 算子拿到最终 N 行后,按这些行的 row_locator 回到存储读取宽列,再拼接成完整的结果行。

由此,宽列的读取、解压和传输量从「参与计算的行数 × 列宽」降低到「N × 列宽」。

以下以日志表 logs 为例,查询按 ts 排序取前 100 条日志,其中 msg 为宽列:

SELECT ts, msg FROM logs WHERE level >= 4 ORDER BY ts LIMIT 100;

关闭 LP 时的执行计划(简化示意):

Limit
  ->  Sort                     -- 含宽列 msg,行宽大
        Sort Key: ts
        ->  Seq Scan on logs   -- 读取并解压 msg
              Filter: (level >= 4)

开启 LP 时的执行计划(简化示意):

Lazy Project
  Delayed Columns: msg         -- 按最终 100 行的 row_locator 回表读取 msg
  ->  Limit
        ->  Sort               -- 只含 ts、shard_id、row_locator,行宽小
              Sort Key: ts
              ->  Seq Scan on logs   -- 不读取 msg
                    Filter: (level >= 4)

其中,row_locator 是引擎内部使用的行定位信息,对上层算子不透明。LP 仅改变宽列的读取时机,不改变 SQL 语义,查询结果、行序和行数均与关闭 LP 时一致。

使用限制

  • 仅 Hologres V5.0.0 及以上版本支持 LazyProjection。

  • 仅面向输出行数少、且需要回传宽列的查询。窄列不会被延迟:同一数据源的可延迟列合计估算宽度不超过 hg_experimental_lazy_projection_min_delayed_column_width(默认 512 字节)时,不使用 LP。

  • 仅在 Sort、Limit、Shuffle 等白名单算子组成的链路上生效。Join 方面,支持 Inner Hash Join、Cross Hash Join 以及 LEFT/RIGHT OUTER HASH JOIN,暂不支持 FULL OUTER JOIN 及其他 Outer Join 实现。

  • LP 仅为执行层的内部优化,不改变查询结果。

生效条件

LP 由优化器基于成本自动决定是否插入,无需改写 SQL。是否插入取决于以下几方面:

  • 计划形态与位置:LP 插入在 Sort、Limit、Shuffle 等算子之后,且从 Scan 到 LP 插入点之间的算子须满足使用限制中的算子链路要求。

  • 硬门槛:需同时满足以下条件。

    • LP 处(root)的输出行数不高于 hg_experimental_lazy_projection_max_output_rows(默认 2000,取值范围 0~10000),高于此值时不插入 LP。

    • 同一数据源的可延迟列合计估算宽度严格大于 hg_experimental_lazy_projection_min_delayed_column_width(默认 512 字节)。

  • 成本判断:满足硬门槛后,优化器汇总上游算子(主要是 Sort,其次是 Motion、Join)因行变窄而节省的成本,乘以 hg_experimental_lazy_projection_benefit_discount 后,与 LP 自身的回表成本比较。收益与成本的比值须大于 hg_experimental_lazy_projection_min_benefit_cost_ratio 才会插入 LP。

一般规律:下层参与计算的行数越多、LIMIT 越小、延迟列越宽,越容易命中 LP。下层行数很少时,即使列很宽,节省的成本也可能不足以抵消回表成本,此时不会插入 LP。

开启与关闭

LP 默认开启。如需调整,可通过以下方式设置:

  • 会话级开启或关闭:

    -- 开启
    SET hg_experimental_enable_lazy_projection = on;
    -- 关闭
    SET hg_experimental_enable_lazy_projection = off;
  • 数据库级开启,对该数据库后续新建的连接生效,需要具备管理权限:

    ALTER DATABASE <DATABASE_NAME> SET hg_experimental_enable_lazy_projection = on;
  • 恢复数据库级默认值:

    ALTER DATABASE <DATABASE_NAME> RESET hg_experimental_enable_lazy_projection;

关闭后,查询完全退化到非 LP 的执行路径。已在执行中的查询不受影响,从下一条新查询起不再使用 LP。

参数说明

说明

除总开关外,其余为成本模型参数,用于调优,一般保持默认值即可。

参数名

默认值

说明

hg_experimental_enable_lazy_projection

on

总开关。设置为 off 时,优化器不生成 LP 计划节点。

hg_experimental_lazy_projection_max_output_rows

2000

LP 处(root)输出行数高于此阈值时不插入 LP。取值范围:0~10000。

hg_experimental_lazy_projection_min_delayed_column_width

512

同一数据源的可延迟列合计估算宽度须严格大于此值,才进入成本判断。单位:字节。

hg_experimental_lazy_projection_benefit_discount

0.028

上游算子收益汇总后的缩放系数,缩放后再与 LP 自身成本比较。调小更保守,调大更激进。

hg_experimental_lazy_projection_fetch_cost_unit

0.00025

回表成本系数,即每个估算 source block 中每字节延迟列的成本。调大则回表成本更高,更难命中 LP。

hg_experimental_lazy_projection_output_cost_unit

28.6

输出成本系数,即每个估算 output block 中每字节输出行的成本。调大则更难命中 LP。

hg_experimental_lazy_projection_min_benefit_cost_ratio

1.0

收益与成本的比值须大于此值才插入 LP。调小(如 0)更易命中,调大更保守。

确认功能是否生效

可通过以下方式确认查询是否使用了 LP。执行计划的查看方法请参见EXPLAIN和EXPLAIN ANALYZE。

  1. 查看 EXPLAIN 执行计划:计划顶层出现 Lazy Project 节点,并带有 Delayed Columns 字段,列出被延迟读取的列名。涉及多个数据源时,可能嵌套多层 Lazy Project 节点。同时,下层 Sort、Limit 节点的 width 会显著变小。

  2. 查看 EXPLAIN ANALYZE 统计信息:Lazy Project 节点会输出以下累计统计字段,可据此推算每轮攒批数、每轮 RPC 数以及每行回表字节数。

    字段

    说明

    lp_rpc_count

    累计发出的回表 RPC 数。

    lp_rows_seeked

    累计从存储取回的行数。

    lp_input_batches

    累计进入主流程的子节点 batch 数。

    lp_distinct_shards

    每个 batch 内涉及的 shard 数之和(累计值)。

  3. 对比 Read bytes:开启 LP 后,存储端 Scan 不再读取宽列,EXPLAIN ANALYZE 结果中的 Read bytes 会显著下降。这是衡量 LP 收益最直接的指标。

调优建议

  • 希望更多查询命中 LP:在评估影响的前提下,可提高 hg_experimental_lazy_projection_max_output_rows(不超过 10000),降低 hg_experimental_lazy_projection_min_benefit_cost_ratio(如设置为 0),或降低 hg_experimental_lazy_projection_output_cost_unit。

    SET hg_experimental_lazy_projection_min_benefit_cost_ratio = 0;
  • 希望更保守地使用 LP:可调低 hg_experimental_lazy_projection_benefit_discount,或调高 hg_experimental_lazy_projection_min_benefit_cost_ratio、hg_experimental_lazy_projection_fetch_cost_unit。

  • 临时关闭 LP:

    SET hg_experimental_enable_lazy_projection = off;

使用示例

  1. 创建日志表,其中 msg 为宽列。

    CREATE TABLE logs (
        id int NOT NULL,
        ts int NOT NULL,
        level int NOT NULL,
        msg text NOT NULL
    ) WITH (
        orientation = 'column',
        clustering_key = 'ts'
    );
  2. 执行 TopN 查询。

    -- 默认已开启,可省略
    SET hg_experimental_enable_lazy_projection = on;
    
    SELECT * FROM logs WHERE level >= 4 ORDER BY ts LIMIT 100;
  3. 查看执行计划,确认是否命中 LP。

    EXPLAIN SELECT * FROM logs WHERE level >= 4 ORDER BY ts LIMIT 100;

    执行计划中出现 Lazy Project(Delayed Columns: msg),且下层 Sort、Scan 节点的 width 显著变小,即表示命中 LP。

    说明

    是否命中 LP 取决于数据量与列宽。表中数据量较小,或 msg 列的估算宽度不足时,可能不会触发 LP。