返回
RSS Snowflake Engineering (Medium) AI 逐段翻译 精选 发布 2026-08-19 22:01 收录于 08-22

十亿行亚秒级:Snowflake 哪种仓库真正能跟上?

DataHot 速览

Manychat 数据工程团队在 Snowflake 上对九种计算配置做了服务端基准测试,从托管 Postgres 到 Interactive Analytics,并用近十亿事件的生产级数据集模拟面向数百万用户的实时看板查询。结果表明,真正决定能否在亚秒级返回的不是仓库规格上调,而是通过聚类键等数据布局实现的分区裁剪(pruning);同时还暴露出编译、缓存局部性和超大工作集这三个新的性能边界。

为什么值得关注:为数据从业者提供了一份完整的 Snowflake 亚秒级查询基准方法和调优思路:在扩容之前先优化数据裁剪,成本与性能收益更直接。

本文目录 19 节
  1. 阵容
  2. 基准测试设置
  3. 数据集
  4. 工作负载层级
  5. 查询:加性和非加性类
  6. 日期范围窗口
  7. 并发
  8. 两种状态:热和冷
  9. 我们遵循的其他测量规则
  10. 数据
  11. Q1 — 唯一订阅者数(COUNT DISTINCT,单次扫描)
  12. Q2 — 参与率(基于SUM的比率,单次扫描)
  13. Q3 — 内容表现(GROUP BY + COUNT DISTINCT)
  14. Q4 — 同期对比增量(COUNT DISTINCT,双扫描)
  15. 为什么交互式胜出:c=1下的内幕
  16. 并发与故障
  17. 冷启动:按需与常驻
  18. 那么选择哪个?
  19. 结论:先修剪,按尾部调整大小

译文

AI 逐段翻译

九种 Snowflake 计算选项的服务器端基准测试——从托管 Postgres 到交互式分析。

想象一下,你正在为数百万用户构建一个面向客户的仪表盘,每个用户都在过滤他们 Instagram 帖子和营销自动化的实时统计数据。底层有近十亿个事件,每次查询都必须在一秒内返回,因为任何更慢的速度都会让观看屏幕的人感觉系统出故障了。

这正是我们现在在 Manychat 正在构建的内容。

我们已经有使用 Snowflake 的丰富经验,但我们想知道哪种计算选项能最高效地承载这一工作负载。因此,我们进行了适当的基准测试:一个生产级别的数据集、相同的查询,以及我们在生产中使用的相同客户端,在九种 Snowflake 配置上进行。

我们预期会得到一个计算规模调整的答案——更大的仓库能否在负载下保持性能?基准测试给出了不同的答案。真正的杠杆是修剪——读取更少的行,而不是购买更多的计算资源——而深入探究后,又发现了另外三个边界:编译、缓存局部性和巨大的工作集。最后一个只有在你不看中位数而看尾部时才可见。

我叫 Anton,是 Manychat 的首席数据工程师。以下是我们发现的内容。

阵容

所以,这是演员阵容:

九种配置,以及它们背后的一个问题:留在 Snowflake 内部,哪种计算选项适合按账户的仪表盘?简单说明我们选择了什么以及为什么。

  1. Snowflake 非聚集 是每个 Snowflake 用户的起点。我们特意在三种仓库规模(XS、M 和 X4 XS)下运行它,以显示在未调整的表上的计算扩展曲线。
  2. Snowflake 聚集 是相同的数据,但带有 (account_id, my_date) 上的聚集键。Snowflake 将每个账户的微分区放在一起,因此分区修剪将全表扫描变成选择性读取。
  3. Snowflake SOS 在聚集表上添加了搜索优化服务,针对点查询进行了市场宣传,此时修剪可能不够。我们运行它看它是否能在普通聚集之上增加任何价值。对于聚集和 SOS,我们只固定仓库为 X4 XS,以隔离数据布局决策带来的效果,而不混合仓库规模的变化(并保持账单合理)。
  4. Snowflake 交互式 是一种独特的表类型,通过 CREATE INTERACTIVE TABLE … CLUSTER BY (…) 创建,并由专为低延迟查找优化的交互式仓库提供服务。交互式仓库旨在保持热状态以满足低延迟工作负载:最小自动挂起间隔为 24 小时,并且每次启动或恢复仓库时,最小计费周期为一小时。我们测试了 XS 和 M 两种规模以回答仓库规模问题。
  5. Snowflake PG 是 Snowflake 托管的 PostgreSQL,位于 eu-central-1:具有 PG 语义和连接池,且无需离开 Snowflake。我们以两种规模运行它——2XL(8 vCPU / 32 GB)和 XL(4 vCPU / 16 GB)——XL 的大小与我们的生产数据库(AWS RDS db.m7g.xlarge)匹配,因此其数字反映了我们当前运行的情况。Postgres 没有 QUERY_HISTORY,因此我们改从 EXPLAIN (ANALYZE) 读取其服务器端时间。

基准测试设置

所有九种配置——包括 Snowflake 列式仓库和托管 Postgres——都在 eu-central-1(法兰克福) 中运行。我们报告 服务器端时间:即每个引擎实际花费的时间,排除了客户端网络。从客户端测量时,每个 Snowflake 查询额外携带约 70 毫秒的往返时间——连接器每次查询进行多次跳转(提交、轮询、获取)——这将使引擎排名部分取决于客户端地理和连接器开销,而不是引擎速度。这正是我们想要避免的。

相反,我们读取每个引擎自己的服务器端时钟——引擎本身花费的时间,不包括客户端网络:

  • 仓库的 Snowflake QUERY_HISTORY 中的 TOTAL_ELAPSED_TIME;
  • 托管 Postgres 没有 QUERY_HISTORY,因此我们取其 EXPLAIN (ANALYZE) 中的 Planning + Execution 时间。

被排除的部分——从应用程序到数据库的往返——是部署成本,而不是引擎成本,因此我们在此将其搁置,并在最后再讨论。

数据集

每个引擎都针对相同的约 9.97 亿行的事实表运行。在 Snowflake 内部,它被物化为四种物理变体(非聚集、聚集、SOS、交互式);它也被加载到 Snowflake 托管的 Postgres 中,具有相同的逻辑内容。

工作负载层级

每个 Manychat 客户(创作者或企业)都为订阅者受众运行自动化,查询成本随订阅者数量扩展:拥有 5 万订阅者的客户比拥有 50 个订阅者的客户查询量重数百倍。因此,我们根据 62 天窗口内接触的不同订阅者数量对客户进行分桶,使用从人口自身的 p50 / p90 / p99 / p99.9 / 最大值中得出的界限:

查询:加性和非加性类

仪表盘需要两类查询,它们对引擎的压力不同:

这里有两件事对后续很重要。首先,每个查询都筛选一个账户在某个日期范围内——这足够选择性,使聚集键或 B-tree 索引能够修剪到几行,而不是扫描整个表。其次,汇总使得加性查询对任何引擎都容易——正是非加性查询,即每个客户在窗口内的 COUNT DISTINCT,才真正暴露存储布局的差异。

这四个查询是一次选择性读取,带有四种不同的聚合:

Q1. Unique subscribers: one scan, non-additive

SELECT COUNT(DISTINCT subscriber_id)
FROM creator_metrics WHERE <filter>;
Q2. Automation CTR (clicks ÷ DMs): one scan, additive SUMs

SELECT SUM(CASE WHEN event_type = 'link_click' THEN event_count END)
     / SUM(CASE WHEN event_type = 'dm'         THEN event_count END) AS click_to_dm_ratio
FROM creator_metrics WHERE <filter>;
Q3.Content performance table: GROUP BY + per-group COUNT DISTINCT, top 50

SELECT content_id,
       COUNT(DISTINCT subscriber_id) AS unique_subscribers,
       SUM(event_count)              AS total_events
FROM creator_metrics WHERE <filter>
GROUP BY content_id
ORDER BY unique_subscribers DESC
LIMIT 50;
Q4. Delta vs prior period: TWO scans (current + prior window), then subtract

WITH current AS (SELECT COUNT(DISTINCT subscriber_id) c FROM creator_metrics WHERE <current window>),
     prior   AS (SELECT COUNT(DISTINCT subscriber_id) c FROM creator_metrics WHERE <prior  window>)
SELECT current.c - prior.c AS delta FROM current, prior;

为可读性简化(subscriber_id 在 schema 中是 contact_id;结果格式保护已去除)。一次扫描(Q1/Q2)、分组扫描(Q3)、两次扫描(Q4)——这一进展直接映射到下面的延迟。

日期范围窗口

我们基准测试三个窗口(7、15、30 天),每次请求随机化,并在层级、查询和账户 ID 之间全局打乱,以免任何引擎通过可预测模式预热其结果缓存。下面完全禁用结果缓存,是双保险。

并发

每个引擎在两个级别运行完整矩阵:

  • c=1(单用户,无争用)
  • c=10(生产环境)

在c=10时,始终有十个查询在飞行中——一个返回,下一个立即发出——这符合我们的生产上限(2个pod × pool_size 10 = 20个连接,所以每个引擎大约10个并发)。下表报告c=10;我们使用c=1只是为了隔离一个机制(见标注)。

两种状态:热和冷

这里有一点你不能平均掉:Snowflake的快速路径取决于查询的数据是否已经在仓库的本地SSD缓存中。

  • ——账户的分区在SSD中,是热工作集,例如,一个重度用户刷新自己的仪表板。交互式布局响应在几十毫秒的低位,而集群式布局大约在百毫秒的低位。
  • ——首次接触数据不在SSD中的账户:两者都是约240毫秒,因为远程存储读取占主导,SSD查找优势消失。

热和冷之间的差距归结为缓存命中率,这又取决于活动工作集相对于仓库SSD容量的大小。有150万客户,每个客户约19MB的热数据,一个XS仓库无法将完整数据集保存在缓存中。因此,广泛的多租户访问将包含许多冷读取,而重复使用的仪表板由同一活跃账户使用时,通常会进入热状态。

下面的延迟表显示热状态性能,因为这是活跃用户的实时仪表板接近的稳定状态。我们在结果受影响时单独指出冷读取的惩罚。

我们遵循的其他测量规则

  • 结果缓存禁用(USE_CACHED_RESULT=FALSE),所以每个查询都执行并扫描数据。
  • 本地SSD数据缓存启用:这是生产现实的热层,在热-冷测试中测量(热路径中 percentage_scanned_from_cache = 100%)。
  • 编译计划缓存启用:参数绑定允许计划重用,将交互式查询编译从约300毫秒(冷)减少到约11毫秒(热)。
  • 编译已预热。 Snowflake每个会话的前约20个查询会支付约300毫秒的冷编译成本,任何常驻服务都会摊还掉。我们首先在单独的账户池中预热编译,否则“冷”数字将衡量编译器,而不是存储。
  • 硬上限每个查询10秒 ——任何更慢的都是超时,而不是延迟。
  • 每个引擎使用相同的客户端和驱动程序,所以驱动程序开销在比较中是恒定的。

数据

延迟是c=10时热状态的服务端p90,以毫秒为单位。我们使用p90来捕捉用户可见的尾部延迟,并指出较大的p50–p90差距,尤其是在巨型租户级别。省略非集群设置,因为其主要结果是并发失败,将在下一节讨论。

Q1 — 唯一订阅者数(COUNT DISTINCT,单次扫描)

两个平缓引擎和一个悬崖。交互式在每个层级都保持在低到几十毫秒。集群式和SOS处于低百毫秒,平缓。两种托管Postgres大小是T4之前最快的东西——亚毫秒到约19毫秒,覆盖约99%的账户——因为单账户查询是对少量行的B-tree索引查找。

然后T5发生,两个Postgres大小急剧分化。XL(4 vCPU / 16 GB)——与生产匹配的大小——p90跳到14秒;2XL(8 vCPU / 32 GB)保持在56毫秒。巨型租户的中位数查询在两者上仍然很快(XL的T5中位数约25毫秒)。是尾部出现分歧,这正是我们报告p90的原因。典型巨型租户触及约9万行,但最重的触及90万至130万行,分散在表中,一个30天查询可以读取超过2GB的页面。在XL的3.9GB shared_buffers上,十个这样的巨型租户同时运行会相互驱逐,所以应该热页面从磁盘重新读取——是秒级I/O,而不是秒级CPU。在2XL的7.9GB(加上24GB操作系统缓存)上,相同的工作集保持驻留,尾部崩溃。简而言之:巨型租户上限是缓存是否能在并发下容纳工作集。更多内存提高上限,直到第二次扫描(Q4)甚至超过这个上限。

Q2 — 参与率(基于SUM的比率,单次扫描)

结果接近Q1:在这个规模,SUM和COUNT DISTINCT读取相同的行,所以聚合类型影响不大。相同的XL巨型租户尾部出现(p90 8.1秒),以及相同的2XL修复(52毫秒)。

Q3 — 内容表现(GROUP BY + COUNT DISTINCT)

交互式在每个层级保持在约26–42毫秒;集群式和SOS在约104–128毫秒。注意在Postgres上,这个GROUP BY不是巨型租户杀手——按content_id分组划分了COUNT DISTINCT工作,所以其XL尾部(7.3秒)比Q1轻。下面的双扫描Q4是Postgres最痛的地方。

Q4 — 同期对比增量(COUNT DISTINCT,双扫描)

这个查询扫描两个窗口并计算增量——读取两个工作集而不是一个。在列式引擎上,聚簇键将两个窗口保持在相同的裁剪分区中,所以额外成本适中,交互式保持最快。在Postgres上是最坏情况:XL巨型租户p90达到34秒,甚至2XL——在单扫描查询上消除了尾部——在这里仍然显示2.8秒的p90,因为两个巨型租户工作集同时并发超过7.9GB缓存。Postgres通过扩展在单扫描上解决了巨型租户;双扫描留下了尾部。

为什么交互式胜出:c=1下的内幕

我们单线程(c=1)运行相同的查询,以找出交互式和集群式之间的差距来源,并发现了两件事。

首先,并发不会破坏裁剪扫描。交互式和集群式都从一到十个查询保持在其热带宽内——交互式约20–30毫秒,集群式约60–110毫秒——而非集群的全扫描爆炸到秒级。

其次,我们没想到的部分:大部分下限是编译,而不是执行。交互式查询编译约需10毫秒,执行约需10毫秒;集群列式查询编译约需40毫秒,执行约需20毫秒——所以差距主要在编译步骤,执行时间则接近得多。我们原本以为这是执行速度的问题,结果发现是编译的问题。这些是单查询、c=1的数据——是延迟下限的机制,而不是上述p90。

并发与故障

上述延迟表格已经使用c=10,四种快速的Snowflake布局在并发下变化不大。非集群表现非常不同,因为它无法修剪扫描。其服务器端p90升至秒级:

当十个全扫描同时运行,覆盖全部9.97亿行,查询竞争相同的计算资源,所以每个单独运行只需几百毫秒的查询现在需要几秒。针对1秒目标:

超过1秒的请求:

超过10秒的请求:

Postgres行需要仔细阅读。通过T4–99.9%的账户——两种规模都不会错过1秒目标。在鲸鱼层级,生产规模的XL在46%的请求上错过目标,22%超过10秒;2XL将其降至6%超过1秒和不到1%超过10秒。这是鲸鱼工作集超出缓冲缓存,是整个基准测试中最明显的例子,中位数美化了引擎:XL的鲸鱼中位数是几十毫秒,而近一半的鲸鱼请求超过整整一秒。

对于非集群的Snowflake,模式在各种规模上一致:在生产并发下,更大的仓库不能弥补缺失的集群键——十个并发的全表扫描只是让每个更慢。让查询保持在一秒以下的是修剪扫描(集群,或交互式表格的温SSD),而不是更多计算。

冷启动:按需与常驻

到目前为止的表格是温状态。按需仓库并不总是温的——它在空闲后自动暂停(此处为五分钟),并在下次查询时恢复。恢复会拆除并重建集群,同时驱逐本地SSD缓存和编译计划缓存,因此空闲后的第一个查询会冷编译,并从远程存储读取,而不是温SSD:

恢复仓库是便宜的部分——186毫秒。伤害的是它留下的东西:两个冷缓存,以及主导的远程存储读取(403毫秒,对比温的约35毫秒)。加起来,你在任何空闲间隙后的第一个请求大约在0.8秒——正好在1秒目标的边缘。

这是部署属性,不是引擎属性。AUTO_SUSPEND是任何仓库上的一个旋钮:将集群仓库固定起来,它跳过恢复并保持温暖。所以冷启动是按需服务的代价,而不是集群的代价。

真正不同的是账单。标准仓库在启动或恢复时有60秒的最低计费,之后按秒计费——保持24/7运行,每天仅几小时流量,你为中间所有空闲时间付费。交互式仓库每次启动或恢复有1小时的最低计费周期,自动暂停间隔至少24小时,所以常驻实际上是它的默认。

无论哪种方式保持温暖,交互式返回在几十毫秒的低位,而集群仓库在几百毫秒的低位。所以保持标准仓库运行消除了冷启动惩罚,但仍然不匹配交互式的温延迟。

那么选择哪个?

每个引擎的行为,以及每个带来的权衡。

SF交互式。最快的列式Snowflake选项:服务器端p90约30毫秒,跨层级和并发下平稳,XS与M相当。由于仓库保持开启,避免了冷启动。在p90上比集群列表格快至4-5倍——差距在尾部扩大(中位数约3倍,p90约5倍),并且完全隐藏在远程测量的网络延迟中。

该优势依赖于缓存局部性。冷首次访问仍需要约240毫秒,因此交互式在活跃账户重复且其热数据适合仓库SSD时最有帮助。保持仓库运行是消除恢复惩罚的关键。

SF集群X4 XS。p90在几百毫秒的低位,跨层级稳定,并发下干净——零请求超过一秒。比交互式慢约3-5倍。作为回报,它按需运行,仅在使用时计费。与基线唯一的区别是集群在(account_id, my_date)上。

SF SOS X4 XS。搜索优化在集群表上添加了无可衡量的好处,它已经修剪到深度2。它仍然增加了构建和维护搜索优化索引的成本。

SF非集群。对于c=1的单查询没问题。在c=10时,每个测试的规模都错过1秒目标,因为全扫描无法修剪,并发查询竞争相同的计算资源。

SF PG(托管Postgres)。对于约99.9%的账户(T1–T4),它是基准测试中最快的——服务器端亚毫秒到约30毫秒,两种规模都是,因为每个查询只是通过B树索引查找一个账户。整个故事是鲸鱼尾部,这是一个缓存适配的故事。单个鲸鱼查询可读取2+ GB的散页。同时运行十个,它们是否保持温暖取决于该工作集有多少适合RAM。在生产规模XL(16 GB)上,鲸鱼相互驱逐,p90运行到7-34秒,约45%的鲸鱼请求错过1秒目标。2XL(32 GB)保持单扫描工作集常驻,降至约50-100毫秒,6%超过1秒。但Q4,双扫描查询,即使在2XL上仍有约2.8秒的p90,因为现在两个鲸鱼工作集必须同时适配——这超出32 GB。因此,托管Postgres为大部分账户提供了优秀的每账户仪表板。其鲸鱼上限通过扩展实例来提升。但这只在工作集停止适配之前有效——而双扫描查询首先达到该点。

它与任何单节点 Postgres(包括 RDS)共享两个限制:真实用户延迟增加了应用到数据库的跳数(同地部署的应用大约为 5 毫秒),以及写入路径——在单个写入器上批量刷新十亿行表——受 IOPS 和时间的约束,无论 CPU 或内存如何。无论数据库是自托管还是由 Snowflake 管理,这两个限制都存在,并且都是分布式和列式选项仍在考虑中的原因。

结论:先修剪,按尾部调整大小

我们尚未选择生产系统——基准测试无法衡量迁移时间、运营所有权或写入路径。但它回答了我们着手测试的工程问题。

在这种工作负载下,十亿行本身不是问题。读取错误的行才会让你付出代价。非聚簇的 Snowflake 在并发下失败,无论我们如何调整大小。聚簇使其变得可预测。交互式进一步缩短了路径——大部分来自编译,而不是执行。托管 Postgres 是 99.9% 账户中最快的引擎,并在尾部满足了鲸鱼级工作集,生产规模的 XL 无法缓存,而 2XL 基本可以。

一旦你看 p90 而不是中位数,规则很简单:先修剪,按尾部调整大小,然后加上部署的网络预算。更大的机器可以提高上限——Postgres 在鲸鱼层级证明了这一点。它仍然无法修复未修剪的扫描。

十亿行亚秒级响应:哪个 Snowflake 仓库真正跟得上 最初发表于 Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science 在 Medium 上,人们通过点赞和回应继续对话。

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存