查询优化器在保持原始预期功能的同时,减少查询的成本。此外,查询优化还旨在减少访问的数据量,从而进一步降低成本。Snowflake查询优化器内部发生了很多事情,并不是每一个细节都为广大用户群体所知。我利用可用的资源拼凑出了可以分享的内容,虽然随着开发和维护的进行,到你读到这篇文章时,优化器的行为可能已经发生了变化。然而,阅读本章仍然有价值,我希望它能帮助你在设计SQL语句时形成思路。
基于第一章中介绍的信息,在本章中,我讨论了查询优化器的各个方面,从查询的生命周期开始。接着,我会讨论在计划器和优化器内部发生的事情,这本身就是一个非常有趣的话题。
本章的关键信息是采用KISS原则,更详细的解释请见:KISS原则。Tony Robbins也很好地阐述了这一原则:“复杂性是执行的敌人” (视频链接)。我们将经常回到这个主题。
我用来判断代码质量的一个指导原则是查看每个SQL语句的书写和布局是否整洁。如果SQL看起来很好,格式良好且可读性强,那么开发人员很可能非常注重确保最佳执行性能。不要忽视重构代码所需的时间;所有优秀的开发人员都讨厌清理别人的烂摊子。
对于刚入门的开发人员:通过提供高质量、易于理解和有良好文档的代码来帮助那些支持你代码的人。
没有一个查询优化器能够猜测预期的数据集结果。正如俗话所说,垃圾进,垃圾出,你有责任确保提交的SQL语句的质量。
最后,我必须感谢Snowflake的首席软件工程师Jiaqi Yan和首席工程师兼技术负责人Minzhen Yang,他们在2023年Snowflake峰会上对Snowflake查询优化器进行了全面的解释。本章中的一些信息来自他们的演讲,并结合了我自己的理解和知识。任何遗漏、误解或错误都是我个人的责任。
查询生命周期
从表面上看,我们提交查询,稍后接收结果。很简单,是吗?你正在阅读的这本书应该会自动回答这个简单性的问题。理想情况下,这本书是不必要的,因为开发人员确实非常努力地从Snowflake中消除复杂性。
缺乏影响系统行为和应用性能的杠杆和开关是开发人员取得成功的关键指标之一。理解查询优化器的实现可以开启提供SQL的途径。
尽可能简化你的SQL;不要编写复杂或难以理解的代码。
图2-1展示了查询生命周期的概述,并在相应的要点中进一步解释。
查询概述
我现在将从高层次解释图2-1中的各个步骤。
-
SQL语句的接收:
- Snowflake接收一个SQL语句;你无需指定来源,因为有许多不同的入站连接路径。
-
结果缓存:
- 如果查询与之前运行的查询完全匹配且数据没有变化,那么结果集将从结果缓存中返回。
-
查询规划和优化:
- 否则,查询规划器和优化器将使用元数据来计算满足SQL查询的精确数据集和最低成本的访问路径。
-
数据检索:
- 仓库识别并从本地或远程磁盘中检索精确数据,然后将数据返回到云服务。
-
结果集返回:
- 结果集返回给客户端,并存储在结果缓存中以供重用。
图2-1只是一个概述,稍后将提供更全面的解释。
当然,查询可能由于各种原因而执行失败,接下来我将简要讨论其中的一些原因。
查询失败
查询可能由于各种原因而处理失败。我无法列出所有可能的查询执行失败原因,但它们大致分为两类:由于未通过查询优化器处理到执行阶段而失败的查询,以及成功开始执行但由于基础设施容量或互连失败而随后失败的查询。
基础设施故障的一个例子可能是偶尔的仓库故障,Snowflake会自动恢复以透明地完成查询执行,注意在检测和恢复期间结果集可能会略有延迟。
我们的SQL语句质量也可能导致执行失败。例如,缺少连接条件通常会导致笛卡尔积(也称为交叉连接),生成意外大的结果集,导致溢出或内存不足的仓库故障。
在我超过30年的各种RDBMS平台经验中,通常是我的代码有问题。
以下是一些查询可能执行失败的原因示例:
- 无效语法
- 无法访问的对象
- 内存不足
- 客户端进程失败
- 网络或互连失败
- 仓库故障
- 其他未指定原因
我不会深入探讨每个潜在失败的根本原因。以下信息使你能够识别某些查询失败发生的位置,并提供足够的背景来解释这些失败可能发生的原因。
查询编译
在本节中,我将解释未缓存查询是如何处理的。
如果图2-1中的第2步(“如果查询与之前运行的查询完全匹配且数据没有变化,那么结果集将从结果缓存中返回”)为真,那么本节将不会执行。
图2-2展示了当图2-1中的第2步为假时所遵循的处理路径,对应于第3步,即“计划/优化”:
在本节中,我将解释未缓存查询是如何处理的。
如果图2-1中的第2步(“如果查询与之前运行的查询完全匹配且数据没有变化,那么结果集将从结果缓存中返回”)为真,那么本节将不会执行。
图2-2展示了当图2-1中的第2步为假时所遵循的处理路径,对应于第3步,即“计划/优化”。
查询优化器
在深入研究Snowflake查询优化器之前,有必要说明所有的RDBMS都具有查询优化器。因此,我们依赖于通用的术语,对于那些刚刚开始了解查询优化器的人,我们会简要解释这些术语。
在深入研究之前,我们的目标是提供足够的信息,让你在提交代码给查询优化器时考虑代码的执行方式。最简单、最便宜、最有效的性能调优是在写任何代码之前,即在设计阶段进行。本书提供的所有信息旨在为你提供在提交给Snowflake查询优化器之前优化代码的工具。
还要注意,并不是每个阶段或每个阶段内的每个步骤都是强制性的。查询优化器可能会在适当的情况下跳过某些阶段或单独的步骤。例如,如果在提交的SQL语句中未检测到CTE,则无需展开CTE。Snowflake将此过程称为自动跳过冗余阶段。在接下来的部分中,我会指出哪些阶段和步骤可以跳过。
令牌化
在查询优化器的上下文中,令牌化是将SQL语句分解成称为令牌的小单元的过程。令牌化将查询分解为关键字、标识符、字面量、操作符和标点符号。这些令牌用于以机器可用的格式识别查询构成部分的结构和含义。
许多RDBMS查询优化器使用令牌化,但在Snowflake查询优化中并未明确指出它是一个独立的组件。它可能是Snowflake查询优化过程中的一个推断组件,并提及它是为了给从传统RDBMS迁移过来的用户提供背景。
令牌化的另一个用途与网络安全和数据内容替换为无法解读的令牌有关。
解析
解析也称为语法分析,是分析SQL语句结构或令牌的动作。它在生成解析树之前验证查询的完整性和正确性,从解析树中可以生成中间代码。
解析树提供了查询的层次结构和内部关系,并将其转换为查询块内部表示(QBIR)。
语义分析
语义分析接收QBIR并验证结构是否匹配可用和可访问的Snowflake对象。我们假设此时进行了FoundationDB的查找。FoundationDB持有我们的Snowflake账户元数据,即关于每个对象、关系和安全功能的信息。这是记录和描述你的账户的目录。
语义分析涉及解析对象和属性名称,执行标签检查,展开引用的视图,展开用户定义函数(UDF),以及展开常用表表达式(CTE)。
我们预计在语义分析期间进行额外的检查,包括可选步骤,这些步骤可能会被跳过。
我们还理解该组件执行权限检查,以确保只引用可访问的对象,并应用行访问策略和数据掩码策略。可能还执行了其他功能,但这个列表提供了该组件已知(或预期)功能的一个大致情况。
参照完整性
Snowflake的一个最初设计决策是允许声明参照完整性但不强制执行。唯一强制执行的约束是NOT NULL。你可以在这里找到更多信息。
然而,即将推出的Unistore和混合表至少对混合表改变了Snowflake的策略。目前尚不清楚以前未强制执行的约束是否将在未来对标准Snowflake表变为可选的强制执行。你可以在这里和这里找到关于Unistore的更多详细信息。
无论未来的约束状态如何,我强烈建议声明约束,即使它们没有被强制执行,因为它们的存在极大地帮助了通过自助工具进行数据发现,帮助目录工具,并且通常被认为是良好的实践。一些第三方工具依赖于约束的存在,以在提交查询到Snowflake之前消除不必要的表。
只要可能,声明约束是良好的实践。
我还相信未强制执行的约束的存在通知查询优化器的处理,但可以肯定的是,它们的存在对于混合表是至关重要的,因此你应尽可能采用最佳实践。你可以在这里找到更多信息。
逻辑重写器
在语义分析之后,QBIR被传递到逻辑重写器,在那里应用规则和算法将QBIR重新陈述为最优的内部表示。合理地假设优化器统计信息(如第一章所列)通知这些规则和算法。此外,你可以假设这是一个多遍过程,在这个过程中生成和比较许多不同的QBIR表示,以得出最优的内部表示。
微分区修剪器
最优的QBIR被微分区修剪器接收,正如名字所示,通过调用修剪器排除微分区以解决最终的查询结果集。可以假设优化器统计信息(如第一章所列)通知微分区修剪策略。
微分区修剪在查询计划生成的几个阶段中进行,并通过修剪器实现,如第三章所述。现在,在这个概述中,理解微分区修剪的概念就足够了。
初始计划生成
在第一次修剪发生后,生成初始执行计划,称为查询计划(QP)内部表示,然后将其传递给计划重写器。
我们预计在初始计划生成期间进行额外的检查,包括可选步骤,这些步骤可能会被跳过。
计划重写器
在计划重写器中,应用于QP的一组规则导致重写,这可能导致通过单独显示的修剪器实施进一步的微分区修剪。
你可以假设在计划重写期间进行额外的检查,包括可选步骤,这些步骤可能会被跳过。
基于成本的连接排序
基于成本的连接排序意味着以规则为基础的方法来最终确定QP。实际上,这一步的具体细节不多;可能线索在于名字,这一步只是对数据访问路径进行排序。
你可以假设在基于成本的连接排序期间进行额外的检查,包括可选步骤,这些步骤可能会被跳过。
物理查询计划
最后,生成用于执行的物理查询计划。这是通过应用所有先前步骤开发的最优或“最佳”计划。
物理查询计划是一个有向无环图(DAG),进一步的信息可以在这里找到。
可以将DAG看作一个带有单向链接的流程图,这些链接连接到分支逻辑,在那里决定是否继续或结束。在DAG中没有循环或环路,不可能通过一系列有向边返回到同一个节点。
DAG提供了一种有用的方式来表示系统或问题中不同元素之间的依赖关系、工作流和层次关系。
在Snowflake物理查询计划中,分支逻辑是以下之一:
- 处理数据或实现特性(如聚合、过滤或汇总)的操作符。
- 连接操作符或实现并行化特性的管道链接。
你可以通过提供最简单的代码来帮助查询优化器。事实上,最简单的代码通常是最容易阅读和布局最好的。记住:最小的表优先。
查询执行
以混合、列式、压缩格式存储的数据适合并行处理。Snowflake使用大规模并行处理(MPP)计算集群来处理查询,其中集群中的每个节点以列式格式本地存储整个数据集的一部分。
换句话说,如果你能将数据分成离散的组,每个“块”可以由单独的处理单元独立处理。
有组织地存储数据也可以改善集群效果;我将在第4章进一步讨论这一点。
仓库
每个Snowflake节点都是一个仓库,我将在本书稍后讨论仓库及其用途。为了不陷入过多的细节,现在只需记住一个XSmall(特小)仓库有8个CPU和一个关联的缓存,大约16到24 GB的RAM,本地SSD存储和远程附加存储。每增加一个T恤尺寸,仓库使用的CPU数量就会翻倍,内存也会增加。随着我们在本书中的进展,这些值将变得非常重要。
我使用术语“执行单元”是为了尊重底层硬件的多线程/多核/多模式操作;我并不总是知道CSP硬件的能力。
在传统的本地数据库世界中,我们对物理硬件有一定的了解,包括可用处理单元的数量,减去一个操作系统。内存限制也适用,并且这些限制由其他人谨慎管理。在云计算的新世界中,我们与物理硬件抽象开来,并且可以按需分配资源。
我不想显得轻率,不要陷入细节之中。相反,你应该接受仓库有一些限制,但弹性的性质允许我们通过按需分配资源来重新考虑我们的方法。
单指令多数据(SIMD)
Snowflake查询执行实现了SIMD指令,这是一种针对数据级并行优化的技术,其中每个处理单元对不同的数据执行相同的指令。
在图2-3中,我们假设四个处理单元在两个执行周期中对不同的数据执行相同的“乘以二”操作。
压缩
Snowflake查询执行也利用了压缩;然而,现有的详细信息仍然稀缺。压缩可能指的是内存中的压缩数据,在“实时”解压缩,或存储在CPU缓存、本地SSD和远程存储中的数据。
我知道Snowflake使用了包括一些针对特定数据类型的压缩方法的组合。你也可以确信,Snowflake提供了他们服务中最好的压缩,并且不断努力改进他们的服务。
向量化
查询执行引擎也是向量化的,一次处理几千行的批次。实际的批次大小未指定,但鉴于程序员偏好2的倍数的倾向,以及我个人的推测,可能在2,048到8,192行之间,这些行将以列式格式存在。
查询执行可能会将结果集溢出到本地存储(SSD)和远程存储,当结果集超出CPU缓存和分配的内存时。
流控制
存在两种流控制模型。
- 基于拉取:消费者不断向发布者轮询消息。
- 基于推送:发布者在消息可用时将消息推送给消费者。
Snowflake查询执行引擎实现了基于推送的流控制。一旦结果可用,它们就被推送给消费者并在流水线方式中进一步处理。
注意,在语句执行后,其摘要信息可在snowflake.account_usage.query_history中查看,最大延迟为45分钟,并可见一年。
性能分析提供了更多的信息细节,但仅保留两周,因此对长时间运行的语句的分析应在此期间完成,以便专注于优化工作,这有助于更快的执行并减少Snowflake的账单。
总结
Snowflake采用基于成本的方法来实现他们的查询优化器,这与其他RDBMS供应商有许多共同点。每个RDBMS供应商都实现了定制的优化,Snowflake也不例外,这些优化专注于满足边缘情况和特定的分析功能。
为了提供一个能交付稳定执行计划的强大优化器,Snowflake刻意去除了影响系统行为的杠杆和开关,而是依赖于内置的核心查询优化器功能来尽可能多地处理问题。
在本章中,我从概述层面解释了从提交查询到获取相应结果集所采取的步骤。然后你了解了在执行查询之前规划和优化查询所需的处理步骤。
接着你了解了查询是如何执行的,并开始看到查询优化器的复杂性。我还揭示了一些场景,如溢出和并行化,这些场景下调优会有所帮助。
在为深入了解性能调优做准备的基础信息建立后,我将在下一章讨论查询配置文件。