跳转至

Vertica 查询 Spill 到磁盘的原因与优化

作者:JiangChong | 发布时间:2026-04-08

适用场景:查询执行时间异常长但 CPU 并不高,query_events 中出现 GROUP_BY_SPILLEDJOIN_SPILLEDexecution_engine_profilescumulative size of raw temp data 显著大于 0。

关联Vertica 内存压力诊断与调优 | Vertica 资源池最佳实践 | Vertica 性能调优 - 2 使用系统表排除 Vertica 查询性能故障 | Vertica CPU 持续高负载诊断与优化


理解全文脉络

Spill 是 Vertica 查询性能的「隐形杀手」——查询不会报错、不会拒绝,只是悄悄地变慢。本文从 Spill 的底层机制讲起,逐步展开如何检测、如何量化、如何根除。如果你已经确认查询有 GROUP_BY_SPILLED 事件,可以直接跳到第 4 节看修复方案。


1. 理解 Spill:查询为什么会溢出到磁盘

1.1 Spill 的本质

在 Vertica 中,Spill 指的是查询算子在内存中无法完成计算,被迫将数据写入磁盘临时文件,然后通过外部排序或归并算法继续处理。这不是 Bug——它是 Vertica 的一种降级策略:当内存不够时,用磁盘空间换取继续执行的能力,而不是直接报错。

两种最常见的 Spill 类型:

Spill 类型 触发条件 后果
GROUP_BY_SPILLED GroupByHash 算子的哈希表超过了可用内存 哈希表中的数据溢出到磁盘,改为外部排序聚合
JOIN_SPILLED JoinHash 算子的内表哈希表超过了可用内存 退化为外部归并连接,需要先排序再归并

还有较少见的 WOS_SPILL(WOS 写满溢出到 ROS),但 WOS Spill 是数据加载层面的问题,不涉及查询性能,不在本文讨论范围。

1.2 Spill 的发生机制

GROUP BY region, category, SUM(amount) 为例,看 Vertica 的处理流程:

正常情况(无 Spill)

  1. Scan 算子从磁盘读取数据并解压
  2. 数据进入 GroupByHash 算子
  3. 算子根据 GROUP BY 列计算哈希值,将每行数据放入对应的哈希桶
  4. 哈希桶在内存中累积数据,直到所有数据都处理完
  5. 输出每个桶的聚合结果

Spill 情况(内存不足)

1-3 同上。但当哈希桶越来越大,算子发现内存不够用时: 4. 将当前内存中最大的哈希桶「溢出」到磁盘临时文件 5. 清空该桶的内存空间,继续接收新数据 6. 所有数据处理完后,将磁盘上的溢出数据读回,排序后聚合 7. 最终与内存中剩余桶的结果合并输出

关键代价:Spill 不仅增加了磁盘 I/O(慢),还引入了额外的排序操作(CPU 密集)。一次 Spill 可能让查询执行时间增加 3-10 倍

1.3 为什么 Spill 如此常见

Spill 在 Vertica 中非常普遍,核心原因是:

  • 默认 query_budget 偏保守:general 池的默认 query_budget 通常只有几 GB,而真实的分析查询处理上亿行数据时,哈希表可能轻松超过 10GB。
  • 统计信息缺失:优化器依赖统计信息估算内存需求。如果统计信息不准,它可能严重低估了 GroupByHash 需要的哈希表大小。
  • MPP 架构的「加和效应」:每个节点的查询都独立分配 query_budget。在 5 节点集群上,同样的查询需要 5 倍的节点间网络传输后的内存。

2. 检测 Spill:如何证明查询在溢出

2.1 方法一:query_events(最快)

PROFILE 执行查询后,直接查看是否有 Spill 事件:

SELECT event_type,
       operator_name,
       path_id,
       event_description,
       suggested_action
FROM v_monitor.query_events
WHERE transaction_id = :t_id
  AND statement_id = :s_id
  AND event_type IN ('GROUP_BY_SPILLED', 'JOIN_SPILLED');

如果返回 > 0 行,说明发生了 Spillsuggested_action 会给出优化建议,例如:

GROUP_BY_SPILLED → "Consider a sorted projection. Increase memory available to the plan."
JOIN_SPILLED → "Consider a different join order or increase memory."

2.2 方法二:execution_engine_profiles(量化严重度)

query_events 只能告诉你「是否有 Spill」,但无法告诉你「有多严重」——是溢出了 10MB 还是 10GB?需要从计数器获取。

SELECT operator_name,
       path_id,
       SUM(DECODE(counter_name, 'cumulative size of raw temp data (bytes)', counter_value, NULL)) AS spill_bytes,
       SUM(DECODE(counter_name, 'memory reserved (bytes)', counter_value, NULL)) AS mem_reserved_bytes,
       SUM(DECODE(counter_name, 'rows produced', counter_value, NULL)) AS rows_produced
FROM v_monitor.execution_engine_profiles
WHERE transaction_id = :t_id
  AND statement_id = :s_id
  AND counter_name IN ('cumulative size of raw temp data (bytes)', 'memory reserved (bytes)', 'rows produced')
GROUP BY operator_name, path_id
HAVING SUM(DECODE(counter_name, 'cumulative size of raw temp data (bytes)', counter_value, NULL)) > 0
ORDER BY spill_bytes DESC;

严重度判断

spill_bytes 规模 严重度 行动
< 1 GB 轻微 可接受,查询仍能在合理时间完成
1-10 GB 中等 查询会明显变慢(2-5 倍),建议优化
10-100 GB 严重 查询极慢(5-10 倍),连锁拖慢整个资源池
> 100 GB 灾难 需要立即处理,可能耗尽磁盘空间

2.3 方法三:对比 CPU time 与 Clock time

Spill 的典型特征是 CPU 很低但执行时间很长——因为查询在等磁盘 I/O。

SELECT operator_name,
       path_id,
       SUM(DECODE(counter_name, 'execution time (us)', counter_value, NULL)) AS cpu_time_us,
       SUM(DECODE(counter_name, 'clock time (us)', counter_value, NULL)) AS clock_time_us
FROM v_monitor.execution_engine_profiles
WHERE transaction_id = :t_id
  AND statement_id = :s_id
  AND counter_name IN ('execution time (us)', 'clock time (us)')
GROUP BY operator_name, path_id
ORDER BY cpu_time_us DESC
LIMIT 10;

如果 GroupByHashJoinHashcpu_time / clock_time < 0.3,说明算子大部分时间在等待——极大概率是 Spill 导致的磁盘 I/O。

2.4 方法四:通过 resource_acquisitions 看额外申请频率

Spill 往往伴随频繁的 AcquireAdditional 请求——因为原始 query_budget 不够,算子在执行中不断向 Resource Manager 申请更多内存。

SELECT transaction_id, statement_id,
       COUNT(*) AS additional_requests,
       SUM(memory_inuse_kb) AS total_extra_kb
FROM v_monitor.resource_acquisitions
WHERE request_type = 'AcquireAdditional'
  AND transaction_id = :t_id
  AND statement_id = :s_id
GROUP BY transaction_id, statement_id;

如果 additional_requests > 5total_extra_kb > query_budget,说明查询的初始预算严重不足。


3. query_budget 与 Spill 的关系

3.1 query_budget 的本质

query_budget 是优化器为每条查询分配的「初始内存预算」。它决定了优化器生成执行计划时的内存假设:

query_budget = [MAX]MEMORYSIZE / PLANNEDCONCURRENCY

优化器看到预算后,会评估每个算子需要多少内存,并决定:

  • 使用 GroupByHash(需要内存中的哈希表)还是 GroupByPipe(流式,不需要哈希表)
  • 将 JOIN 的内表加载到内存(Hash Join)还是排序后归并(Merge Join)
  • 分配多少内存给排序缓冲区

如果 query_budget 太小,优化器可能在明知内存不够的情况下,仍然被迫选择 Hash 类算子——因为排序投影不存在,Pipe 类算子不可用。

3.2 query_budget vs MAXMEMORYSIZE

这两个参数经常被混淆,区别如下:

参数 作用域 关系
MAXMEMORYSIZE 池级别——池中所有查询总共能用的内存 池的总上限
query_budget 查询级别——每条查询的初始预算 = [MAX]MEMORYSIZE / PLANNEDCONCURRENCY(只读)

典型误区:管理员看到 query_budget_kb 太小,试图直接设置它。但 query_budget 是计算列,无法直接修改。正确的方式是调整 [MAX]MEMORYSIZEPLANNEDCONCURRENCY

3.3 多大的 query_budget 才够?

这取决于查询的数据量和复杂度。一般经验:

查询类型 处理数据量 推荐 query_budget 说明
简单聚合(< 10 列,< 5 个 GROUP BY) < 1 亿行 2-4 GB 哈希表通常能在 2GB 内完成
中等聚合(含多维钻取) 1-10 亿行 4-8 GB 多维度导致哈希表膨胀
大型 JOIN + 聚合 10-100 亿行 8-20 GB JOIN 内表加载需要额外内存
超大 ETL / 全量扫描 > 100 亿行 20-50 GB 考虑分批复用预聚合,而非单次全量

验证方法:用 PROFILE 执行高频查询,查看 cumulative size of raw temp data。如果为 0,说明当前 query_budget 足够;如果有值,用该值反推需要多大预算。

3.4 Spill 的连锁反应模型

Spill 不只是让一条查询变慢,它会触发连锁反应:

单条查询 Spill ──→ 查询执行时间变长
        ──→ 长时间占用资源池内存和 MAXCONCURRENCY 名额
        ──→ 其他查询排队等待
        ──→ 排队查询堆积
        ──→ 总并发内存占用上升
        ──→ 更多查询的 query_budget 不足
        ──→ 更多 Spill + RESOURCE_REJECTED

这就是为什么有时集群看起来「莫名其妙就慢了」——一条 Spill 查询可以在 10 分钟内拖垮整个资源池。


4. 解决 Spill:三种途径

4.1 途径一:增大内存预算(最快,立即生效)

直接增大 query_budget 让 GroupByHash 和 JoinHash 有足够的内存避免溢出。

操作方式

-- 方案 A:降低 PLANNEDCONCURRENCY,增大每条查询的预算
-- 节点 256GB,目标预算 8GB:256 × 95% / 30 ≈ 8.1 GB
ALTER RESOURCE POOL general PLANNEDCONCURRENCY 30;

-- 方案 B:为报表查询创建大预算专用池
-- query_budget = 512GB × 30% / 6 ≈ 25 GB
CREATE RESOURCE POOL heavy_report_pool
    MAXMEMORYSIZE '30%'
    PLANNEDCONCURRENCY 6
    MAXCONCURRENCY 4;

适用场景:查询无法或不值得改写,硬盘空间充足,增加内存预算能立即见效。

局限PLANNEDCONCURRENCY 降低意味着并发降低——更多查询排队。需要在「单查询速度」和「整体吞吐量」之间平衡。

4.2 途径二:创建排序投影,让 GroupByPipe 替代 GroupByHash(根本解决)

这是消除 GROUP_BY_SPILLED 最彻底的方案。GroupByPipe 是一种流式聚合算子:它假设数据已经按照 GROUP BY 列排好序,因此不需要构建哈希表,直接边读边聚合。

前提条件:投影的 ORDER BY 列与查询的 GROUP BY 列一致。

示例:查询按 region, product_category, date 聚合:

-- 经常跑的聚合查询
SELECT region, product_category, date, SUM(amount)
FROM sales_fact GROUP BY region, product_category, date;

-- 创建排序投影使 GroupByPipe 可用
CREATE PROJECTION sales_grouped
AS SELECT * FROM sales_fact
ORDER BY region, product_category, date
SEGMENTED BY HASH(region) ALL NODES;

创建后,优化器会自动识别排序投影并使用 GroupByPipe,该算子几乎不消耗内存——因为数据已经有序,读完一组就输出一组,不需要哈希表。

GroupByHash vs GroupByPipe 对比

特性 GroupByHash GroupByPipe
内存需求 高(需要完整哈希表) 极低(流式处理)
Spill 风险 不会 Spill
CPU 消耗 高(哈希计算 + 冲突解决) 低(仅比较当前行与上一行)
前提条件 需要有按 GROUP BY 列排序的投影
适用场景 临时查询(无匹配投影) 高频固定模式的聚合查询

详见 Vertica 性能调优 - 3 重新设计 PROJECTION

4.3 途径三:优化查询本身(减少需要处理的数据量)

有时候 Spill 不是内存太小,而是查询处理了太多不必要的数据。

子策略 3a:添加 WHERE 谓词 + 分区裁剪

-- 没有分区的表,全表扫描
SELECT region, SUM(amount)
FROM sales_fact GROUP BY region;  -- 扫描所有 100 亿行 → 必然 Spill

-- 加上分区键过滤,只扫描需要的分区
ALTER TABLE sales_fact PARTITION BY date;
SELECT region, SUM(amount)
FROM sales_fact
WHERE date BETWEEN '2026-05-01' AND '2026-05-28'
GROUP BY region;  -- 只扫描 28 天数据 → 可能无需 Spill

子策略 3b:减少 GROUP BY 列数

每多一个 GROUP BY 列,哈希表的键就多一维,内存消耗指数增长。如果业务上不需要某个分组维度,去掉它:

-- 6 个 GROUP BY 列,哈希表极大
SELECT region, province, city, category, subcategory, date, SUM(amount)
FROM sales GROUP BY 1,2,3,4,5,6;

-- 按需减少到 3 个,哈希表缩小 10 倍以上
SELECT region, category, date, SUM(amount)
FROM sales GROUP BY region, category, date;

子策略 3c:子查询预聚合 + 小表 JOIN

对于「先 JOIN 大表再聚合」的场景,改为「先聚合小粒度再 JOIN」:

-- 原始:大表 JOIN 后再聚合 → JoinHash 内表加载全部数据
SELECT d.category, SUM(f.amount)
FROM fact f JOIN dim d ON f.product_id = d.product_id
GROUP BY d.category;

-- 优化:先聚合 fact,缩小后再 JOIN → JoinHash 内表只有几百行
WITH pre_agg AS (
    SELECT product_id, SUM(amount) AS total
    FROM fact GROUP BY product_id
)
SELECT d.category, SUM(p.total)
FROM pre_agg p JOIN dim d ON p.product_id = d.product_id
GROUP BY d.category;

4.4 三种途径选择指南

查询有 Spill 事件
    ├─ 有排序投影匹配 GROUP BY 列?
    │    ├─ 是 → 4.2 创建排序投影(首选,根治)
    │    └─ 否 → 进入下一步
    ├─ 查询能否优化(缩小数据量)?
    │    ├─ 是 → 4.3 优化查询(加 WHERE、减 GROUP BY、子查询预聚合)
    │    └─ 否 → 进入下一步
    └─ → 4.1 增大 query_budget(次选,治标)
        └─ 同时设置 MAXCONCURRENCY 防止并发过高反噬内存

5. 深入案例

5.1 虚构案例一:hash join spill 导致批处理任务超时

📝 虚构案例

场景:ETL 任务每天凌晨 2 点执行,对 5 亿行的 orders 和 2 亿行的 order_details 做 JOIN 后聚合。任务近期从 15 分钟恶化到 2 小时,导致下游报表无法按时刷新。

诊断

Step 1: PROFILE 执行 ETL SQL
→ JOIN_SPILLED 事件 × 8 个节点
→ cumulative size of raw temp data = 85 GB

Step 2: 查看 query_budget
→ etl_pool: query_budget = 3 GB(MAXMEMORYSIZE 40% / PLANNEDCONCURRENCY 24)

Step 3: 查看 execution_engine_profiles
→ JoinHash 算子: memory_reserved = 3 GB, spill = 82 GB
→ 内表 order_details 有 2 亿行,加载到内存构建哈希表需要约 15GB
→ 3 GB 预算只有需求的 20%

根因:优化器根据统计信息估算内表大小,但统计信息已过期(最后更新是 3 个月前),实际数据增长了 3 倍。

修复

  1. 更新统计信息:SELECT ANALYZE_STATISTICS('order_details');
  2. 降低 PLANNEDCONCURRENCY 增大 query_budgetALTER RESOURCE POOL etl_pool PLANNEDCONCURRENCY 8;
  3. order_details 的 JOIN 键创建排序投影,使 JoinMerge 可用

效果query_budget 从 3GB → 9GB,Spill 从 85GB → 0,ETL 从 2 小时恢复至 12 分钟。

5.2 虚构案例二:用 GroupByPipe 根除 Spill

📝 虚构案例

场景:BI 看板每小时刷新一次,核心查询对 sales_fact(30 亿行)做 GROUP BY region, product_category, date 聚合。每次执行都有 GROUP_BY_SPILLED,spill 约 12GB,耗时 4 分钟。

诊断:查看当前投影:

SELECT projection_name, is_super_projection, segment_expression
FROM v_catalog.projections
WHERE anchor_table_name = 'sales_fact' AND is_super_projection = 't';

→ 只有 super projection,ORDER BY 是随机列,无法使用 GroupByPipe。

修复

CREATE PROJECTION sales_bi_proj
AS SELECT * FROM sales_fact
ORDER BY region, product_category, date
SEGMENTED BY HASH(region) ALL NODES;

SELECT start_refresh();  -- 刷新投影数据

效果

  • 优化器自动选择 sales_bi_proj,使用 GroupByPipe 代替 GroupByHash
  • GROUP_BY_SPILLED 消失
  • 执行时间从 4 分钟 → 30 秒(8 倍提升)
  • 内存消耗从 12GB → 800MB

关键:创建投影后一定要跑 start_refresh() 让数据物理写入新投影。没有数据的投影不会被优化器选用。

5.3 真实案例:统计信息过期 + query_budget 过小双重放大

📋 真实案例

背景:某运营商经分系统,138 节点集群。高峰期 app_pool 大量排队,性能恶化超过 7 倍。

关键发现(详见 Vertica 内存压力诊断与调优 案例 5.3):

  • app_poolquery_budget = 1 GB,但 40% 的查询实际内存消耗 > 1GB
  • 某条大查询因 user_dtal 表统计信息过期,14.5 亿行表被错误选为 JOIN 内表,JoinHash 构建消耗 28GB 内存
  • 统计信息过期导致优化器低估内存需求 → query_budget 不足 → AcquireAdditional 频繁 → 高峰期排队加剧 → 连锁 Spill

这个案例完美展示了 Spill 的双重放大效应:统计信息不准 → 预算低估 → 实际内存远超预留 → Spill + 额外申请 → 连锁反应。


6. 完整诊断流程实战

📝 虚构场景 · 完整演练

场景

  • 4 节点集群(每节点 512GB/64 核),general 池默认配置
  • BI 团队反馈:某个看板查询上周还能 10 秒完成,本周突然要 90 秒
  • SQL 没有任何修改,数据量增加约 20%

诊断过程

Step 1:PROFILE 确认是否有 Spill

PROFILE <dashboard_query>;

GROUP_BY_SPILLED × 1,cumulative size of raw temp data = 18 GB

Step 2:量化 Spill 严重度

→ 18GB spill 属于「严重」级别,查询执行时间 90 秒中约 60 秒花在 GroupByHash 的磁盘 I/O 上

Step 3:查看 query_budget

SELECT pool_name, query_budget_kb
FROM v_monitor.resource_pool_status
WHERE node_name ILIKE '%0001%' AND pool_name = 'general';

query_budget = 5.1 GB(512 × 95% / 64 = 7.6GB?不对...)

等等,512GB × 95% / 64 = 7.6 GB。实际只有 5.1 GB,说明管理员之前调整过。

Step 4:查看是否有排序投影可用

SELECT projection_name, sort_position, projection_column_name
FROM v_catalog.projection_columns
WHERE anchor_table_name = 'sales_fact'
  AND sort_position >= 0
ORDER BY projection_name, sort_position;

sales_fact_super 投影的 ORDER BY 是 date_key, product_key, store_key,而查询的 GROUP BY 是 region, category, date——完全不匹配,无法使用 GroupByPipe。

Step 5:分析数据量变化

上周数据量:约 8 亿行 → 本周数据量:约 9.6 亿行(增长 20%)。GroupByHash 的哈希表从约 4 GB 增长到约 6.5 GB,超过了 query_budget = 5.1 GB 的阈值。这就是超过临界点的「最后一根稻草」。

修复

  1. 创建排序投影(根本解决):
CREATE PROJECTION sales_dashboard_proj
AS SELECT region, category, date, amount
FROM sales_fact
ORDER BY region, category, date
SEGMENTED BY HASH(region) ALL NODES;
SELECT start_refresh();
  1. 短期兜底(增大 query_budget 到 10GB):
ALTER RESOURCE POOL general PLANNEDCONCURRENCY 48;
-- 512 × 95% / 48 ≈ 10.1 GB

效果:排序投影刷新后,查询从 90 秒 → 8 秒,Spill = 0。


7. 快速诊断 SQL 工具箱

诊断目标 SQL
检测 Spill 事件 SELECT event_type, operator_name, suggested_action FROM query_events WHERE transaction_id=:t_id AND statement_id=:s_id AND event_type IN ('GROUP_BY_SPILLED','JOIN_SPILLED');
量化 Spill 大小 SELECT operator_name, path_id, SUM(DECODE(counter_name,'cumulative size of raw temp data (bytes)',counter_value,NULL)) AS spill_bytes FROM execution_engine_profiles WHERE transaction_id=:t_id AND statement_id=:s_id GROUP BY 1,2 HAVING SUM(DECODE(counter_name,'cumulative size of raw temp data (bytes)',counter_value,NULL))>0 ORDER BY 3 DESC;
查询预算检查 SELECT pool_name, query_budget_kb, max_memory_size_kb, planned_concurrency FROM resource_pool_status WHERE pool_name NOT IN ('sysquery','sysdata') AND node_name ILIKE '%0001%';
GroupByHash vs Pipe 确认 SELECT operator_name, path_id, SUM(counter_value) FROM execution_engine_profiles WHERE transaction_id=:t_id AND statement_id=:s_id AND operator_name IN ('GroupByHash','GroupByPipe') AND counter_name ILIKE 'execution%' GROUP BY 1,2;
CPU/Clock 比值 SELECT operator_name, SUM(DECODE(counter_name,'execution time (us)',counter_value,NULL))/NULLIF(SUM(DECODE(counter_name,'clock time (us)',counter_value,NULL)),0)::FLOAT FROM execution_engine_profiles WHERE transaction_id=:t_id AND statement_id=:s_id GROUP BY 1 ORDER BY 2;
额外内存申请 SELECT transaction_id, statement_id, COUNT(*), SUM(memory_inuse_kb) FROM resource_acquisitions WHERE request_type='AcquireAdditional' AND transaction_id=:t_id AND statement_id=:s_id GROUP BY 1,2;
排序投影存在性 SELECT projection_name, sort_position, projection_column_name FROM projection_columns WHERE anchor_table_name='<table>' AND sort_position>=0 ORDER BY 1,2;
统计信息状态 SELECT projection_name, projection_column_name, statistics_type, statistics_updated_timestamp FROM projection_columns WHERE table_name='<table>' AND sort_position>=0 ORDER BY 1,2;

8. 最佳实践清单

  1. PROFILE 高频查询检查 Spill:每个重要的 SQL 至少用 PROFILE 跑一次,确认没有 GROUP_BY_SPILLEDJOIN_SPILLED。这是性能基线的必要步骤。
  2. 排序投影是根治 Spill 的第一选择GroupByPipe 不需要哈希表,理论上永远不 Spill。为高频聚合查询创建匹配的排序投影,投入产出比极高。
  3. query_budget 设到「够用」而非「刚好」:数据会增长。如果现在的查询刚好不 Spill(spill_bytes ≈ 0),给 query_budget 留 30-50% 余量。
  4. 统计信息必须准确:过期统计信息是 Spill 的隐形推手。每次大批量加载后运行 ANALYZE_STATISTICS,让优化器能看到真实数据规模。
  5. 分区 + WHERE 是减少数据量的最简单方法:如果表有 3 年数据但每次只查 1 个月,分区裁剪能减少 90% 以上的 SCAN 数据,间接消除 Spill。
  6. Spill > 10GB 必须干预:不要等查询慢到用户投诉才处理。设置监控,cumulative size of raw temp data > 10GB 触发告警。
  7. 大 JOIN 场景优先考虑预连接投影:运行时 JOIN 需要加载整个内表到内存。预连接投影在加载数据时就完成 JOIN,查询时直接读取结果——零 Spill。
  8. 利用子查询预聚合缩小 JOIN 数据量:先用 GROUP BY 在子查询中大幅压缩数据,再与维度表 JOIN。小表 JOIN 几乎不会 Spill。
  9. 不要一个池管所有:将 ETL、BI 报表、ad-hoc 查询分到不同资源池,各自有独立的 query_budget。一条大查询在专用池里 Spill,不影响其他池。
  10. Data growth 是 Spill 的头号敌人:数据增长 20% 就可能让原本不 Spill 的查询越过临界点。容量规划时应周期性 PROFILE 关键查询,确认 Spill 余量。

扩展阅读