天天看点

SQL最佳实践

作者:闻数起舞

在这篇文章中,我将与大家分享我在过去20年中所学习和使用的一些格式化SQL的最佳实践。这些技巧中有许多是我在继承代码、需要进行更新、追寻bug以及为他人进行代码审查的过程中,出于挫折感而一路收集的。另外,当涉及到培训新员工时,我发现在处理写得不好的SQL时,人们要理解领域和代码要困难得多。

在本文的每一节中,我都会集中讨论一个领域的实践。我将为你提供一个坏做法的例子和最佳做法的例子,以及我以特定方式编写代码的原因。当你在阅读时,你可能会想,"这不是一个高性能的代码",有时可能是这样的情况,因为我试图在一个特定的点上磨练,避免多余的潜在混乱。另外,有时我宁愿选择性能稍差的代码(取决于它的使用频率,代码的目的,以及性能冲击),如果它能使代码更容易阅读和维护。

我猜想,有些人读到这里可能会说:"我的代码执行得很好,所以我不关心。我明白它的作用。"对此(因为多年来我已经听过很多次这样的评论),我想对这种说法提出质疑。与使用干净的代码相比,发现和阅读马虎的 "坏做法 "的代码要花费明显更多的时间。这些做法也将有助于减少错误,你的代码将被每个阅读它的人所欣赏。

当我们谈论编程语言时,你几乎可以按照你认为合适的方式编写语句,代码就会执行。显然,这有一些例外(比如Python中的缩进),但这是另一个问题。这意味着,当我在谈论SQL的最佳实践时,人们可以提出这样的论点:这只是一个观点和写作风格的问题。

你如何写它很重要。比如说,想想一本书。当然,你可以写一整本书,没有段落、换行、句子之间的标准间距等等。是的,它仍然是一本书,是的,读者可能会理解它。然而,如果没有段落,你会有多容易找到一个特定的章节?如果你被要求编辑一些东西,你会有多大的信心知道你确实在正确的章节里?由于这些原因,我在这里概述了一些最佳做法。

请注意,虽然所有这些做法都可以被认为是 "意见",但它们是我在20年里犯过的错误,在有效阅读、编辑和理解代码方面遇到的挑战,以及看着我的团队成员面临同样的挑战之后所采用的做法。我会尽力告诉你这些做法和使用它们背后的逻辑。我将让你考虑这些做法,看看什么对你有用,什么对你的团队有用。

格式化

我想谈的第一件事是格式化。代码应该有良好的格式和视觉上的吸引力,这使得它非常容易阅读。在调试、排除故障和修改你的代码时,拥有正确格式化的代码我们会得到回报。

说到格式化,有许多事情需要考虑,如意图、对齐、逗号位置和文本大小写。如果你能读到这里,你可能已经注意到我的代码是如何对齐的,而且大多数项目都在一行中。这对可读性有很大的影响。

下面是一个已经写好的、相当难读的代码例子。在这个例子中,你会注意到有很多东西违背了最佳实践,比如。

  • 每行项目数
  • 对齐问题
  • 尾部逗号
  • 不良或缺乏混叠
  • 缺少评论
  • 按数字而不是名称分组
  • 聚合函数在选择语句中的位置
  • 多个难以检测的错误

在这篇文章中,我将讨论这些做法以及更多的做法,以帮助你写出干净的、没有错误的代码,让你感到自豪。

不良做法--许多问题

SQL最佳实践

最佳实践

请看下面的代码,并与上面的代码进行比较。哪一个更容易阅读?哪一个提供了关于代码的意图和一些条件值的含义的最佳背景?哪一个版本提供了一个更干净的用户界面,可以让你快速发现bug或完全避免bug?我希望你已经同意,下面的代码有助于克服上述代码中发现的所有挑战。

我听到人们的反驳是,"好吧,你不得不写更多的代码行。"这是不相关的。计算机并不关心这个问题,只需按几下键就可以实现回行和空格或制表符。

在以下章节中,我将逐一讨论这些问题,并提供好的和坏的做法的例子。

SQL最佳实践

统一口径

如果你看一下上面的 "最佳实践 "代码,注意到所有东西都是向左对齐的。所有的逗号、空格和缩进使代码非常容易阅读。

每行一个项目

我的一般规则是每行一个项目。这可以是你的选择语句中的一个元素,或连接语句中的一个条件,或一个案例语句。再一次,看看上面关于格式化的代码,看看每行写一个项目时的可读性差异。这里的关键是要保持一致。我见过那些每行写一个项目的代码,但每隔一段时间就会有一个*连接*子句,其中有一个and和一个or语句在同一行。在阅读和调试时,这可能会让人感到非常沮丧,因为很容易就会忽略添加条件,因为它是写在同一行的。

不好的做法--在一行中设置多个案例条件

在这里,我们看到一个全部在一行的case语句。这是一种不好的做法,因为它使代码难以阅读,也难以迅速发现所有正在评估的条件。另外,如果不是不可能的话,正确注释代码也是非常具有挑战性的。我知道在这个例子中,'main_reporting'不是描述性的,而且看起来与其他的值不一致,但是,有时候你被告知要这样输出值,而不能与其他的值逻辑一致。

SQL最佳实践

最佳实践--多条线路上的多个案例条件

在这里,我们看到一个写在多行上的案例声明,并有注释以帮助提供清晰度。

SQL最佳实践

注释代码

请注释你的代码。我将来会再写一篇关于代码注释的文章,但注释很重要。我觉得我每天都会在LinkedIn或其他网站上看到一个帖子,有人说:"你不需要代码注释。代码就是一个注释。这有什么关系?你不知道如何阅读代码吗?" 说真的,这种话我已经听了很多年了。但现实是这样的。虽然代码是一种语言,如果精通这种语言,读者可以理解代码在做什么。但是,代码从来没有告诉读者为什么有人想让代码以这种方式运作。至于为什么有人想用某种方式来编码,可能性是无穷的。有时,你可以围绕后端数据中的一个错误进行编码,或者也许有商业逻辑决定了代码应该如何运作。

虽然你确实可以阅读代码,并有可能查阅某些表格的文档,但这比输入几个字符要费劲得多。下面是一些好的和坏的注释做法的例子。

糟糕的注释 -

请看下面的代码。我们可以看到,该代码只想返回u.id>1000的结果。在这个非常简单的例子中,这是很明显的。但更重要的问题是为什么有人要这样做?

也许他们是u.id=1000之前的测试用户。或者,代码正在过滤掉所有来自密歇根州的用户,因为出于某种原因,有人认为所有小于u.id 1000的用户都来自密歇根州。这听起来可能是一个可怕的想法,实际上有代码会这样写,但它执行起来都是一样的。这里的重点是,作为新用户,我们不知道,有可能六个月后你也不会知道。

SQL最佳实践

更好的注释--行内注释

这里我们有一个行内评论,告诉我们更多关于为什么我们添加了u.id>1000的条件。很明显,我们有一些测试用户应该被从结果集中清除掉。

SQL最佳实践

糟糕的注释--没有块注释

请看下面的代码。我们可以看到,这个查询将返回被认为是非测试用户的用户。行内注释帮助我们理解,我们的愿望是将这些测试用户从结果集中清除掉。但是我们不得不读几行代码。你可能会说,好吧,这不值得在顶部设置注释块。这只是8行代码,发生了什么是显而易见的。无论是否简单,读者都不知道这段代码背后的原因。但如果代码不是那么简单呢?你肯定会欣赏一些评论。由于这些原因,以及为了在开始写代码之前有更好的规划(后面会有更多的介绍)。

SQL最佳实践

更好的注释 - 块注释

下面是我们刚才看到的同样的代码,现在我们要告诉用户为什么我们要

想运行这段代码和需要注意的事情。

SQL最佳实践

通用表表达式(CTE)

通用表表达式或CTE是一种创建查询结果的内存表的方法。这个表可以在你的SQL脚本的其余部分使用。使用CTE的好处是,你可以减少代码的重复,使你的代码更具可读性,并提高你对结果进行QA检查的能力。

还注意到真正的好块头评论。

不使用CTE的代码实例

在下面的代码中,我们可以看到,有两个子查询正在返回结果。这两个子查询然后被连接在一起,产生最终的结果集。虽然这段代码会执行,但有几个问题。

1.要对子查询进行QA并检查结果真的很难。例如,如果我们想对默认屏幕有多条记录的用户数量进行统计呢?我们不能简单地对子查询执行一些sql。我们必须复制/粘贴子查询,然后修改它来执行这个问题。如果我们在QA过程中能避免改变代码,那就更好了。

2.如果我们需要在代码的其他地方利用这个用户子查询,我们就必须重新写或复制/粘贴这块代码到我们脚本的其他地方。这将不是一个DRY(不要重复自己)的做法。

过程,并暴露出更多潜在的错误。怎么说呢?暂时假设你在脚本中的5个地方使用了用户子查询。还假设你正在处理的代码不容易阅读,因为它没有遵循最佳实践。如果你被要求更新代码,添加另一个条件来清除更多的测试用户,那么你很有可能错过添加这个条件的机会

到子查询的5种用途中的至少一种。

3.在数据库上有更多的周期。每次执行子查询时,都要进行表扫描以返回结果。由于我们的用户子查询包含通配符条件,数据库将有相当多的工作要做。执行一次子查询,将其存储在内存中,然后在你的代码中根据需要重新使用结果集,这要便宜得多(CPU周期和美元,如果你使用的是云数据库)。

4.阅读整个代码块并理解正在执行的内容和原因更加复杂。虽然可以滚动阅读代码,但可能很难轻松理解正在发生的事情。一般来说,如果你必须在显示器上垂直滚动你的代码,你的代码就太长了,应该重构为更小的组件。

SQL最佳实践

使用CTE的例子

下面我们看到一个使用CTE的例子。虽然CTE可以很好地帮助克服我们之前指出的一些挑战,但CTE在显示最终结果集后通常不会留在内存中。

例如,如果你要运行这整个代码块,它就会执行。但是,如果你想在几分钟后从用户的CTE中选择所有的结果,这些数据就不能被查询了。为了解决这个问题,你可以使用易失性表或临时内存表,这些表通常在你的会话(数据库连接)保持活动时就一直存在。以后会有更多关于这个主题的内容。

SQL最佳实践

使用 "SELECT *"

你永远不应该用 "select *"来写查询。我认为这条规则的唯一例外是,如果你试图检查一个表,在这种情况下,你应该始终限制返回的结果数量。这样写查询是个坏主意,原因很多。

1.数据库性能。返回不需要的列比只查询你关心的列更昂贵。

2.调试的挑战。假设你使用的是上一节中描述的CTE,那么追踪某些属性的来源是非常具有挑战性的。

3.表会改变。即使你真的需要选择所有的列,也不能保证你的表不会随着时间而改变。随着表的变化,你将会查询到新的数据,而这些数据并不是有意的,这可能会破坏其他地方的代码,造成混乱,或者影响数据库的性能和成本。

坏的做法 - 使用 SELECT *

SQL最佳实践

最佳实践--只选择所需元素

SQL最佳实践

别名

别名是非常重要的,它可以帮助读者了解元素所在的位置和正在使用的表格。当不使用别名或使用不良的命名规则时,复杂性就会增加,而代码的阅读/理解就会减少。

不好的做法 - 字段上没有使用别名

下面你可以看到,这些表有一个'u'和'p'的别名,但所选元素没有利用这个别名。这可能是非常令人沮丧的,如果不止一个表含有相同名称的字段,就会导致运行时错误。例如,user_id在'users'表和'preferences'表中都能找到。

SQL最佳实践

最佳实践--字段上使用的别名

下面你可以看到,表格和选定的元素利用了表格的别名。这使得代码对终端用户来说非常可读。即使你只有一个表,在表和字段名上使用别名也是一个好的做法。好的习惯造就了好的代码。

SQL最佳实践

不好的做法 - CTE上的通用别名<

下面我们有一个已经创建的CTE,但被分配的表名叫做 "cte"。这是一个非常普通的名字,完全没有告诉终端用户关于表中数据的信息。如果你是一个用户,在阅读CTE后面的选择语句时,你不会有任何迹象表明正在使用什么表。

SQL最佳实践

最佳实践--CTE上的特定别名

下面我们有一个已经创建的CTE,有一个更具描述性的名字。这个名字给了用户一些指示,让他们知道其中包含什么数据。

SQL最佳实践

逗号

在选择语句中,我更喜欢使用前导逗号,而不是尾随逗号,这是我个人意见发挥作用的一种情况。我见过很多人在写选择元素时使用尾部逗号,在其他语言中,使用尾部逗号是常见的做法。然而,在其他语言中,在一个函数中加入大量的参数并不常见,而在SQL中,有大量的元素被选择(并在代码中声明)是很常见的。虽然你可能认为我对这种用法的看法过于偏激,但以下是我认为前导逗号有好处的几个原因。

1.看起来很干净的用户界面。当你看最佳做法的例子时,看看逗号是如何很好地排列的。与使用尾部逗号相比,很容易看出缺少一个逗号,并避免运行时的错误。

2.在处理包裹着行的较长的case语句时,不会出现混乱。看下面的例子,很难分辨行的末尾是一个元素或语句的末尾,还是指定一个被传入函数的参数的末尾。

为了增加一些额外的挫折感,BigQuery的查询格式化器实际上是对你的代码进行重新处理,以显示所有的尾部逗号。

错误的做法--尾部逗号

在这个例子中,我们可以看到没有使用前导逗号,这使得我们很难发现缺少的逗号。

SQL最佳实践

最佳实践--开始处的逗号

在这个例子中,我们可以看到,所有的逗号都是对齐的,这就很容易保证没有一个逗号被遗漏。

SQL最佳实践

错误的做法--尾部逗号的混淆

在这个例子中,我们可以看到前导逗号没有被使用。我们有一个以逗号结尾的换行符,所以很难分辨这个 "最大 "行是否真的是一个单独的语句,或者它是一个较长语句的一部分。有人会说,你不应该在你的代码中使用这样的换行符,虽然我支持在正确的地方使用换行符(因为它使代码更容易阅读),但当你在编辑器中或在Git差异比较中查看你的文字包装的代码时,你仍然会遇到同样的问题。

SQL最佳实践

最佳实践--带缩进的前导逗号在包裹的文本上的应用

在下面的例子中,很容易看到缺少一个逗号。你可能会立即怀疑是否有人忘记了逗号,但由于阅读带有前导逗号的代码非常容易,所以更有可能的是,缺少前导逗号是设计好的,实际上在'cast'语句前面不应该有一个逗号。另外,通过在cast语句中加入缩进,代码看起来有一个更明显的意图,即为什么不需要逗号。

SQL最佳实践

如果有人在第3行的 "desc) "之后执行了回车,你现在就会在第4行有一个共同的开头。通常情况下,我们希望所有的逗号都作为前导字符。但这一逻辑只适用于我们谈论的是被返回的属性(列)。在我们的例子中,第3行末尾的逗号是一个case语句的一部分,所以事情会变得非常混乱。

大写

在过去的很多SQL代码中,这种做法非常普遍,这可能与SQL已经存在很长时间的事实有关,可能比大多数带有语法高亮的文本编辑器还要长。今天,大多数人都在他们的编辑器中使用(或应该使用)语法高亮,所以大写字母不应该成为发现保留字的必要条件。

虽然我有个人意见,不使用大写字母,但你可能不同意,有不同的意见。这是我的理由。

1.我不喜欢我的代码对我大喊大叫。在社会环境和书面交流中,使用大写字母是吼叫的同义词,所以我尽量避免这样写。

2.代码读起来不那么流畅。心理学研究表明,小写的单词比大写的单词更容易识别。这是因为与大写字母相比,小写字母有更多的形状变化。如果全部使用大写字母,阅读速度会下降13-20%。此外,当你混合使用大写和小写的单词时

3.在我打字时按住shift键或锁定/解锁大写字母键是额外的击键动作。我知道,这不是一个好的论据。

陋习--大写字母

在这个例子中,我们可以看到,保留字是大写的,所有其他的字都是小写的。

SQL最佳实践

最佳做法--小写

SQL最佳实践

分组 - 数字与显式字段

我几乎总是用明确的字段名进行分组,而不是用选择语句中的位置号。虽然这对结果没有任何影响,但我发现当涉及到打字和调试时,它可以节省我的时间。通常情况下,我更喜欢使用数字,因为这样可以减少页面上的文字,但是这已经引起了太多的问题,并且花了很多时间去追寻运行时的错误。

不良做法 - 按职位编号分组

在这里你可以看到,我们正在根据所选的项目用位置号进行分组。我不喜欢这种做法的地方是。

1.如果有人在选择语句中除了第一个或最后一个项目之外的任何地方放置一个聚合函数,那么你就必须跳过group by中的一个位置号。如果你后来决定重新排列你所选择的元素,这就会产生挫败感。

2.你必须计算出你有多少个元素,减去聚集的项目,然后手动输入位置号。

SQL最佳实践

最佳实践--按字段名称分组

在这里你可以看到,我们正在用明确的字段名来执行一个分组。虽然看起来这比使用位置数字要多打很多字和工作,但实际上它比使用数字打字要快。怎么说呢?因为你所要做的就是复制选择语句中的内容(减去聚合字段),并将这些值粘贴到分组中。当你使用数字时,你实际上必须输入数字。

SQL最佳实践