4. 查询性能可观测性

在 Snowflake 中,查询性能可观测性的第一手数据几乎都来自 ACCOUNT_USAGE.QUERY_HISTORY 视图(近实时场景可用 INFORMATION_SCHEMA.QUERY_HISTORY)。它记录每一次查询的执行轨迹,但真正决定分析效率的,不是把整行数据拉出来看,而是理解哪些字段构成"延迟分解",哪些字段用于"识别退化模式"。本节围绕这两条主线展开。

一、关键字段:把一行记录拆成可解释的维度

QUERY_HISTORY 的字段可以按用途分为四组。第一组是身份与归属:QUERY_ID、QUERY_TEXT、USER_NAME、ROLE_NAME、WAREHOUSE_NAME、DATABASE_NAME、SCHEMA_NAME。它们回答"谁、在哪个仓库、对哪个对象"发起了查询,是后续归因的锚点。

第二组是时间戳:START_TIME、END_TIME、TOTAL_ELAPSED_TIME、QUEUED_OVERLOAD_TIME、QUEUED_PROVISIONING_TIME、QUEUED_REPAIR_TIME、COMPILATION_TIME、EXECUTION_TIME。这组字段是延迟分解的核心,单位均为毫秒。

第三组是执行特征:EXECUTION_STATUS、ERROR_CODE、ERROR_MESSAGE、PARTITIONS_SCANNED、PARTITIONS_TOTAL、BYTES_SCANNED、ROWS_PRODUCED、CREDITS_USED_CLOUD_SERVICES。它们刻画查询"扫了多少、产出多少、花了多少云服务积分"。

第四组是阻塞与等待:TRANSACTION_BLOCKED_TIME、LOCK_WAIT_TIME、TOTAL_ELAPSED_TIME 与各分项之间的差值。当分项之和明显小于总耗时时,差额往往来自等待、重试或元数据操作。

二、延迟分解:从总耗时到可归因的构成

Snowflake 的查询总耗时可以近似表达为:

TOTAL_ELAPSED_TIME ≈
  QUEUED_PROVISIONING_TIME   -- 仓库冷启动/扩容等待
+ QUEUED_REPAIR_TIME         -- 仓库修复等待
+ QUEUED_OVERLOAD_TIME       -- 仓库并发过载排队
+ COMPILATION_TIME           -- 解析、优化、生成执行计划
+ EXECUTION_TIME             -- 实际扫描、计算、返回
+ 其他(事务阻塞、锁等待、网络等)

这个等式是分析慢查询的起点。举例来说,假设某条查询 TOTAL_ELAPSED_TIME = 42000ms,其中 QUEUED_OVERLOAD_TIME = 28000ms、COMPILATION_TIME = 900ms、EXECUTION_TIME = 12000ms。那么真正的问题不在 SQL 本身,而在仓库并发不足——28 秒的排队占了总耗时的 67%。此时优化 SQL 几乎无效,正确动作是调整仓库规模或多集群配置。

反过来,如果 QUEUED_* 全部接近 0,而 EXECUTION_TIME 占绝对多数,则问题落在执行阶段,需要结合 PARTITIONS_SCANNED / PARTITIONS_TOTAL 判断是否发生了全表扫描或分区裁剪失效。

查询延迟分解与归因路径 TOTAL_ELAPSED_TIME(示例 42000ms) 排队 QUEUED_OVERLOAD 28000ms EXECUTION 12000ms 排队类 QUEUED_PROVISIONING QUEUED_REPAIR QUEUED_OVERLOAD 编译类 COMPILATION_TIME 解析 / 优化 / 计划生成 元数据解析开销 执行类 EXECUTION_TIME PARTITIONS_SCANNED BYTES_SCANNED 等待类 TRANSACTION_BLOCKED LOCK_WAIT_TIME 分项与总耗时差额 归因判断规则 ① QUEUED_* 占比 > 30% → 仓库并发/规模不足,优先调仓库,而非改 SQL ② COMPILATION_TIME 占比 > 20% → 检查元数据规模、绑定变量、动态 SQL 与计划缓存 ③ EXECUTION_TIME 主导且 PARTITIONS_SCANNED ≈ PARTITIONS_TOTAL → 分区裁剪失效或全表扫描 ④ 分项之和 < TOTAL_ELAPSED_TIME 且差额大 → 关注事务阻塞、锁等待与重试

三、排队时间与编译时间的专项分析

排队时间反映的是"查询已提交但尚未开始执行"的等待。它由三个字段构成,含义不同:QUEUED_PROVISIONING_TIME 是仓库从挂起到可用的冷启动时间,常见于自动挂起后首次查询;QUEUED_REPAIR_TIME 是仓库节点故障修复等待;QUEUED_OVERLOAD_TIME 是仓库并发槽位已满导致的排队,是生产环境最常见的排队来源。

分析排队时,建议按仓库和小时聚合:

SELECT WAREHOUSE_NAME,
       DATE_TRUNC('hour', START_TIME) AS hr,
       COUNT(*) AS q_cnt,
       AVG(QUEUED_OVERLOAD_TIME) AS avg_overload_ms,
       APPROX_PERCENTILE(QUEUED_OVERLOAD_TIME, 0.95) AS p95_overload_ms
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE START_TIME >= DATEADD('day', -7, CURRENT_TIMESTAMP())
GROUP BY 1, 2
HAVING p95_overload_ms > 5000
ORDER BY p95_overload_ms DESC;

当某仓库的 p95 排队时间持续超过 5 秒,说明并发压力已稳定超过单集群承载能力,应考虑启用多集群仓库或提高 MAX_CONCURRENCY_LEVEL。

编译时间通常较短,但两类场景会显著拉长:一是查询涉及大量对象(数百张表的 JOIN、复杂视图嵌套),元数据解析成本高;二是使用动态 SQL 或未参数化的字面量,导致计划缓存无法复用,每次都要重新编译。若发现同一 SQL 模板反复出现高编译时间,应改用绑定变量(如 ? 占位符)以命中计划缓存。

四、识别慢查询与性能退化模式

慢查询的识别不能只看单次耗时,而要看"同一查询指纹"的时间序列。常用做法是对 QUERY_TEXT 做归一化(去除字面量、统一大小写),生成查询指纹,再按指纹聚合每日 p95 耗时。当某指纹的 p95 连续三天上升超过 30%,即判定为性能退化。

常见的退化模式有三类:

  • 数据量驱动退化:PARTITIONS_SCANNED 随日期线性增长,而 EXECUTION_TIME 同步上升。根因通常是分区键选择不当或缺少聚簇,导致扫描量随数据增长而膨胀。
  • 并发驱动退化:EXECUTION_TIME 稳定,但 QUEUED_OVERLOAD_TIME 上升。根因是仓库并发不足,属于资源问题而非 SQL 问题。
  • 计划漂移退化:COMPILATION_TIME 与 EXECUTION_TIME 同时跳变,且 PARTITIONS_SCANNED 突变。常见于统计信息变化、聚簇键失效或对象结构变更后优化器选择了更差的计划。
排查退化时,先固定查询指纹,再对比"退化前"与"退化后"两次执行的 PARTITIONS_SCANNED、BYTES_SCANNED、COMPILATION_TIME 三个字段。若扫描量不变而耗时上升,优先怀疑仓库资源竞争;若扫描量上升,优先怀疑数据分布或分区裁剪。
QUERY_HISTORY 在 ACCOUNT_USAGE 中存在约 45 分钟到 3 小时的延迟,且默认保留 365 天。做实时告警时应使用 INFORMATION_SCHEMA.QUERY_HISTORY(延迟约 45 秒,但仅保留 7 天),两者字段语义一致但时效与保留期不同,不可混用同一套阈值。

把上述字段、分解等式与退化模式组合起来,就形成了一条可复用的分析链路:先用 TOTAL_ELAPSED_TIME 筛出慢查询,再用分项字段定位耗时归属,最后按查询指纹做时间序列对比,判断是资源问题、数据问题还是计划问题。这条链路不依赖任何外部工具,仅凭 QUERY_HISTORY 即可完成大部分性能归因。