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 判断是否发生了全表扫描或分区裁剪失效。
三、排队时间与编译时间的专项分析
排队时间反映的是"查询已提交但尚未开始执行"的等待。它由三个字段构成,含义不同: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 三个字段。若扫描量不变而耗时上升,优先怀疑仓库资源竞争;若扫描量上升,优先怀疑数据分布或分区裁剪。把上述字段、分解等式与退化模式组合起来,就形成了一条可复用的分析链路:先用 TOTAL_ELAPSED_TIME 筛出慢查询,再用分项字段定位耗时归属,最后按查询指纹做时间序列对比,判断是资源问题、数据问题还是计划问题。这条链路不依赖任何外部工具,仅凭 QUERY_HISTORY 即可完成大部分性能归因。