【译】SQL 指引:如何写出更好的查询

SQL 指引:如何写出更好的查询

结构化查询语言(SQL)是数据科学行业的一种不可或缺的技能,通常来讲,学习这项技能是至关简单的。然而大多数人都忘记 SQL 不只仅是写查询语句,这只是第一步。确保查询高性能,或者符合上下文语意又彻底是另一回事了。前端

这就是为何本篇 SQL 教程要引导你,能够经过如下步骤来评估你的查询:react

  • 首先,你将以数据科学工做中学习 SQL 的重要性的简要概述为开始。
  • 接着,你将学习更多有关如何 SQL 查询处理和执行,这样你才可以正确地理解编写高性能查询的重要性:更具体地说,你会看到查询被解析,重写,优化和最终被执行;
  • 考虑到这一点,你不只能够复习初学者编写查询时的一些反模式查询,并且还能够学习关于针对那些可能出现的错误的替代和解决方案,你还将学习更多有关基于集合仍是程序方法进行查询的内容。
  • 你还将看到这些出于性能问题考虑的反模式,除了“手动”方法改进 SQL 查询以外,你还能够经过使用一些其余可帮助你查看查询计划的工具,以更加结构化,深刻的方式分析你的查询;并且,
  • 在执行查询以前,你将简要了解时间复杂度和大 O 符号来在你执行查询以前了解执行计划的时间复杂度;最后,
  • 你将简要地了解如何进一步调整你的查询

你对 SQL 课程感兴趣吗?那就来学习 DataCamp 的数据科学的 SQL 简介课程吧!android

为何我应该为数据科学学习 SQL?

SQL 远未消亡:不管你是申请数据分析师,数据工程师,数据科学家仍是任何其余职位,你均可以从数据科学行业的职位描述中发现 SQL 是最须要的技能之一。参加 O'Reilly 数据科学工资调查报告的 70% 的受访者证明了这一点,他们表示他们会在专业场景中使用 SQL。并且,在本次调查中,SQL(70%)远胜于 R(57%)和 Python(54%)编程语言。ios

你得知一个状况:当你正在努力找数据科学行业的工做时,SQL 是一项必须具有的技能。git

对于一个20世纪70年代初开发的语言来讲,还不错,对吧?github

可是为何被使用的如此频繁?为何 SQL 不会消失,即便它已经存在了很长时间了?算法

有几个缘由:第一个缘由是大多数公司将数据存储在关系型数据库管理系统(RDBMS)或关系数据流管理系统(RDSMS)中,你须要 SQL 才能访问这些数据。 SQL 是数据的通用语言:它使你可以与几乎任何数据库进行交互,甚至能够在本地创建本身的数据库!sql

若是这还不够,请记住有不少 SQL 的实如今供应商之间不兼容,并不必定遵照标准。于是,了解标准 SQL 是你在(数据科学)行业中找到一条路的要求之一。数据库

除此以外,能够确定地说,SQL 也被更新的技术所接受,例如 Hive,用于查询和管理大型数据集的类 SQL 查询语言界面,或可用于执行 SQL 查询的 Spark SQL。虽然你发现标准可能与你已知的有所不一样,但学习曲线将会更加容易。编程

若是你想作一个比较,认为它和学线性代数同样:经过把全部的精力放在这个主题上,你甚至可使用它来掌握机器学习!

简而言之,这就是为何你应该学习这门查询语言:

  • 即便对于新手它也是至关容易学习的。学习曲线是至关容易和平滑的,以致于在学习的任何阶段你都能写出查询。
  • 遵循“一旦学习,到处适用”的原则,因此这是一个对你时间的伟大投资!
  • 它是对编程语言的极好补充; 在某些状况下,编写查询甚至比编写代码更为优先,由于它性能更高!

你还在等什么呢?

SQL 处理 & 查询执行

为了提升你 SQL 查询的性能,当你按快捷方式运行查询时,你首先须要知道内部发生了什么。

首先,查询被解析成“解析树”;分析查询,看是否符合语法和语义要求。解析器建立输入查询的内部表示。而后将输出传递给重写引擎。

而后,优化器的任务是找到给定查询的最佳执行或查询的计划。执行计划准确地定义了每一个操做使用什么算法,以及如何协调操做的执行。

为了找到最佳的执行计划,优化器列举全部可能的执行计划,肯定每一个计划的性质或成本,获取有关当前数据库状态的信息,而后选择其中最佳的一个做为最终的执行计划。因为查询优化器可能并不完善,所以数据库用户和管理员有时须要手动检查并调整优化器生成的计划以得到更好的性能。

如今你可能想知道什么是一个“好的查询计划”。

如你所见,一个计划的质量在查询中起着重要的做用。更具体地说,评估计划所需的磁盘 I/O,CPU成本和数据库客户端能够观察到的整体响应时间以及总执行时间等因素相当重要。这就涉及到了时间复杂度的概念,在后面你将会看到更多与此相关的内容。

接下来,执行所选择的查询计划,由系统的执行引擎进行评估并返回查询结果。

在上节中描述的可能不是很清楚的是,Garbage In, Garbage Out(GIGO)原则在查询处理和执行中会天然地显现:制定查询的人掌握着你 SQL 查询性能的关键,若是优化器获得的是一个很差的查询语句,那么那么它也只能作到这么多...

这意味着在编写查询时能够执行一些操做。如你在介绍中所见,责任是双重的:它不只仅是写出符合必定标准的查询,并且还涉及收集查询中性能问题可能潜伏在哪里的意识。

一个理想的出发点是在你的查询中考虑可能会潜入问题的“地方”。新手一般会在如下四个子句和关键字中遇到性能问题。

  • WHERE 子句
  • 任何 INNER JOINLEFT JOIN 关键字; 还有,
  • HAVING 子句;

固然,这种方法简单而原始,但做为初学者,这些子句和声明是很好的指引,并且确切地说,当你刚开始时,这些地方就是容易出错的地方,更讽刺的是这些错误很难被发现。

然而,你也应该意识到,性能只有在实际场景中才有意义:只是单纯的说这些子句和关键字是很差的没有任何意义。固然,查询中有 WHEREHAVING 子句不必定意味着这是一个坏的查询...

查看如下内容,了解更多有关的构建查询的反模式和可替代的方法。这些提示和技巧可做为指导。如何重写以及是否真的须要重写取决于数据量,数据库,以及查询所需的次数等等。它彻底取决于你查询的目标,而且有一些你要查询的数据库的以前的了解也是相当重要的!

1. 仅检索你须要的数据

当编写 SQL 查询时,「数据越多越好」的思惟方式是不该该的:获取比你实际需求更多的数据不只会有看错的风险,并且性能可能会由于查询太多数据而受到影响。

这就是当心处理 SELECT 语句,DISTINCT 子句和 LIKE 运算符是个不错的主意。

当你写好你的查询时,你能检查的第一件事情就是 SELECT 语句是否已是最紧凑了。你的目标应该是从 SELECT 中删除没必要要的列。这样,你强制本身只提取符合查询目的的数据。

若是具备 EXISTS 的相关子查询,则应尝试在该子查询的 SELECT 语句中使用常量,而不是选择实际列的值。当你只检查数据是否存在时,这是特别方便的。

记住相关子查询是使用外部查询中的值的子查询。注意,尽管 NULL 能够在此上下文中看成“常量”使用,可是这会使人很是困惑!

考虑下面这个例子,并理解使用常量的意义在哪:

SELECT driverslicensenr, name
FROM Drivers
WHERE EXISTS (SELECT '1' FROM Fines
              WHERE fines.driverslicensenr = drivers.driverslicensenr);复制代码

提示:能够很方便知道,使用相关子查询一般不是一个好主意。你应该考虑使用 INNER JOIN 重写来避免它们:

SELECT driverslicensenr, name
FROM drivers
INNER JOIN fines ON fines.driverslicensenr = drivers.driverslicensenr;复制代码

SELECT DISTINCT 语句是用来返回不一样的值的。若是能够,你应该你要尽可能避免使用 DISTINCT 这个子句;就像你在其余例子中看到的同样,若是你把这个子句添加到你的查询中,执行时间确定会增长。所以,常常考虑是否真的须要 DISTINCT 操做来获取想要的结果是一个好主意。。

当你在一个查询中使用 LIKE 操做符时,若是匹配模式以 % 或者 _ 开始,那么是不会使用索引的。它将阻止数据库使用索引(若是存在)。固然,在另外一个方面看,这种类型的查询会潜在地返回过多的记录,这不必定知足你的查询目标。

再次,你对存储在数据库中的数据的了解程度能够帮助你制订一个模式,这能够帮助你从全部数据中正确过滤出和你的查询真正相关的行。

2. 不要输出太多结果

当你不能过滤掉 SELECT 语句中的列时,你能够考虑用其余方法限制你的结果。如下是 LIMIT 语句和数据类型的转换方法。

你能够经过为查询添加 LIMIT 或者 TOP 子句来为查询结果设置最大行数。这儿是一些例子:

SELECT TOP 3 * FROM Drivers;复制代码

注意 你能够进一步指定 PERCENT,好比,你能够经过 SELECT TOP 50 PERCENT * 这个查询语句来替换第一行。

SELECT driverslicensenr, name FROM Drivers LIMIT 2;复制代码

此外,你还能够添加 ROWNUM 子句,这至关于在查询中使用 LIMIT

SELECT *
FROM Drivers
WHERE driverslicensenr = 123456 AND ROWNUM <= 3;复制代码

你应该始终使用最有效的,也就是最小的数据类型。当小的数据类型已经足够的时候你提供一个巨大的数据类型老是有风险的。

然而,当你将数据类型转换添加到查询中时,你确定增长了它的执行时间。

一个替代方案是尽可能避免数据类型转换。可是还要注意,数据类型转换不是总能从查询中被删除或者省略的,并且当你在查询语句包含它们的时候必定要注意,你能够在执行查询以前测试添加它们的影响。

3. 不要让查询比需求更复杂

数据类型转换将你带到了下一个关键点:你不该该过分设计你的查询。试着保持简单高效。做为一个提示,这可能看起来太简单或者愚蠢了,特别是在查询可能变得复杂的状况下。

然而,你将会在下一部分提到的示例中看到,你能够很轻松的把本应更复杂的查询变得简单。

当你在你的查询里使用 OR 操做符时,极可能你没有使用索引。

记住索引是一种数据结构,能够提升数据库表中的数据检索速度,但它是有代价的:它须要额外的写入和额外的存储空间来维护索引结构。索引用来快速定位或查找数据而无需在每次访问数据库时查询每一行。索引可使用数据库表中的一列或多列来建立。

若是你不使用数据库包含的索引,你的查询会花费更长的时间来执行。这就是为何最好在查询中找到使用 OR 运算符的替换方案;

考虑如下查询:

SELECT driverslicensenr, name
FROM Drivers
WHERE driverslicensenr = 123456 OR driverslicensenr = 678910 OR driverslicensenr = 345678;复制代码

你能够将运算符替换为:

SELECT driverslicensenr, name
FROM Drivers
WHERE driverslicensenr IN (123456, 678910, 345678);复制代码
  • 包含 UNION 的两个 SELECT 语句。

提示:这儿你须要当心,没有必要就不要使用 UNION 运算符,由于你会屡次查询同一个表屡次,这是没必要要的。同时,你必须意识到当你在查询语句里使用 UNION 时,执行时间会变长。UNION 操做符的替代是:将全部条件都放在一个 SELECT 结构中,或者使用 OUTER JOIN 替代 UNION 来从新构建查询。

提示:在这里也要记住的一点是,尽管 OR 以及下面将要提到的其余运算符可能不使用索引,索引查找不老是更好的。

就像 OR 运算符同样,当你的查询包含 NOT 操做符时,也极可能不使用索引。这将不可避免的减慢你的查询。若是你不明白这是什么意思,考虑下如下查询:

SELECT driverslicensenr, name FROM Drivers WHERE NOT (year > 1980);复制代码

这个查询跑起来确定比你预料还要慢,主要是由于它构建的太过于复杂了:在这样的状况下,最好寻找一个替代方案。考虑使用比较运算符替换 NOT,好比 ><> 或者 !>;上面的例子可能会被重写为这样:

SELECT driverslicensenr, name FROM Drivers WHERE year <= 1980;复制代码

看起来已经更加整洁了,不是吗?

AND 是另外一个不使用索引的操做符,若是以过于复杂和低效的方式使用,它会减慢你的查询,就像下面的例子:

SELECT driverslicensenr, name
FROM Drivers
WHERE year >= 1960 AND year <= 1980;复制代码

最好使用 BETWEEN 运算符重写这个查询:

SELECT driverslicensenr, name
FROM Drivers
WHERE year BETWEEN 1960 AND 1980;复制代码

ALLALL 运算符你也应该当心使用,将他们包含进查询中会致使不使用索引。替代方法使用聚合功能,在这里比较方便的方法是使用像 MIN 或者 MAX 的聚合函数。

提示:在你使用所提出的方案的状况下,你应该意识到,全部的聚合函数好比 SUMAVGMINMAX 在多行的时候会致使很长时间的查询,在这种状况下,你能够尝试减小要处理的行数或预先计算这些值。当你决定使用哪一个查询时,最重要的是清楚你的环境和查询目标。

在使用列进行计算或者列做为标量函数的参数时,也是不会使用索引的。一个特定的解决方案是简单的隔离这个特殊列,使其再也不是计算或者函数的一部分或参数。请考虑一下示例:

SELECT driverslicensenr, name
FROM Drivers
WHERE year + 10 = 1980;复制代码

这看起来颇有趣,是不?相反,试着从新考虑如何计算,而后像这样重写查询:

SELECT driverslicensenr, name
FROM Drivers
WHERE year = 1970;复制代码

4. 不要暴力查询

最后一个提示,你不该该老是太限制查询,由于这也会影响性能。特别是 join 语句和 HAVING 子句。

当你对两个表使用 join 时,考虑你 join 的两张表的顺序是很重要的。若是一张表比另外一张大不少,你最好重写你的查询让最大的表最后作 join 操做。

  • 减小 Joins 的条件

当你加了太多的条件到你的 joins 语句,你有义务选择一个特定的路径,虽然这个路径并不老是最高效的那个。

HAVING 子句添加进 SQL 是由于 WHERE 关键字不能和聚合方法一块儿使用。HAVING 的典型的用法就是和 GROUP BY 子句来约束分组聚合后的结果,使其知足一些精确匹配条件。然而,你知道的,使用这个子句是不会用到索引的,会致使查询不能很好的执行。

若是你在寻找替代的方案,考虑使用 WHERE 子句,请看以下的查询:

SELECT state, COUNT(*) FROM Drivers WHERE state IN ('GA', 'TX') GROUP BY state ORDER BY state

SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state复制代码

第一个查询使用 WHERE 子句限制须要求和的行数,而第二个查询对表中的全部行进行了求和,而后使用 HAVING 子句来舍弃其中的部分。在这种状况下,选择使用 WHERE 子句显然是更好的,由于你不会浪费任查询资源。

你会发现,这并非限制最终结果集,而是限制查询中的中间记录的数量。

注意 这两个子句之间的区别在于,WHERE 子句引入了单行的条件,而 HAVING 子句引入了一个选择集合或结果的条件,好比 MINMAXSUM,… 这些都已经从多行生成了的。

你看,当你想以尽量的提升性能为前提的时候,评估语句质量,构建查询还有改写查询并非一件容易的工做;当你构建运行在专业环境中的查询的时候,避免反模式和考虑替代方案也将成为你责任的一部分。

这个清单只是一些小的反模式的概述和技巧,可能对新手有些帮助;若是你想了解更多高级开发人员常见的反模式,查看 stackoverflow 的这个讨论

基于集合与程序方法的查询

上述反模式隐含的点实际上归结为基于集合与程序方法构建查询的差别。

程序方法的查询是一种很像编程的一种查询方式:你告诉系统作什么,怎么作。

一个例子是你使用冗余的链接操做或者滥用 HAVING 子句的状况下,就像上面的例子,你能够经过执行一个函数调用另外一个函数来查询数据库,或者使用包含循环,用户定义方法,游标等,来获取最终结果。在这个方法中,你会常常发现你本身请求一个数据的子集,而后再请求这个数据的子集等等。

绝不奇怪,这个方法常常被称为「逐步」或者「逐行」查询。

另外一种方法是基于集合的方法,你只须要指定作什么。你的职责包含从查询中指定要得到的结果集的条件或要求。至于你的数据是如何获取到的,这取决于内部决定查询实现的机制:让数据库引擎来肯定查询最好的算法和执行逻辑。

因为 SQL 是基于集合的,这种方法(基于集合)比程序方法更有效几乎不会让人感到惊讶,这也是一个惊喜,也解释了为何在某些状况下,SQL 能够比代码更快的工做。

提示 在查询中基于集合的方法也是数据科学行业最顶级的雇主所要求你掌握的方法!你常常须要在这两种方法之间切换。

注意 若是你发现你本身有程序类型的查询,你应该考虑重写或者重构它。

从查询到执行计划

-------------知道反模式不是静态的,而是随着你作为 SQL 开发者的成长而演进,当你考虑替代方案的时候也意味着你正在避免反模式查询和重写查询的这个事实,这是一个十分困难的任务。任何帮助均可以派上用场,这就是为何使用一些工具经过更结构化的方式来优化你的查询或许是个不错的选择。

注意 还有一些上一节提到的反模式源于性能的问题的考虑,好比 ANDORNOT 操做符缺乏索引的使用。对性能的思考不只须要结构化的方法,还须要更多的深刻的方法。

然而可能的是,这种结构化和深刻的方法更可能是基于查询计划的,即首先被解析为「解析树」,而后在肯定每一个操做具体使用什么算法,还有如何使执行操做更协调。

正如你在介绍中读到的,你可能须要手动检查优化器的生成计划。在这种状况下,你将须要经过查看查询计划来再次分析你的查询。

要掌握这种查询计划,你将须要使用数据库管理系统为你提供工具,你可使用的工具以下:

  • 生成查询计划的图形表示的一些工具包,看如下这个例子:

  • 其余工具将可以为你提供查询计划的文本描述。一个例子是 Oracle 中的 EXPLAIN PLAN 语句,但指令的名称根据你使用的 RDBMS 而有所不一样。在其余数据库,你可能会看到 EXPLAN(MySQL,PostgreSQL)或者 EXPLAIN QUERY PLAN(SQLite)。

注意若是你平时使用 PostgreSQL,你能够在 EXPLAIN 之间作出区分,这里你只获得了一个描述,它是说明还未执行的查询计划会如何执行,而 EXPLAIN ANALYZE 实际上执行了查询而后返回对预期与实际的查询计划的分析。通常来讲,一个实际的执行计划就是一个实际的查询计划,虽然在逻辑上是等价的,一个实际的执行计划更为有用,由于它包含执行查询时实际发生的其余细节和统计信息。

在本节的剩余部分,你将会学习到更多关于 EXPLAINANALYZE 的信息,以及如何使用这两个去了解更多你的查询计划和查询性能的信息。

提示:若是你想了解更多关于 EXPLAIN 或更详细的查看实例,考虑阅读 Guillaume Lelarge 写的这本书 “Understanding Explain”

时间复杂度和大 O

如今你已经简要的检查了查询计划,你能够在复杂度计算的帮助下开始更深刻的研究具体的性能问题。理论计算机科学这一领域着重于根据难度对问题进行分类;这些计算问题能够是算法,也能够是查询。

然而,对于查询,你并不必定是根据他们的困难程度分类,而是根据运行它而后拿到返回结果的时间来分类。这个被叫作时间复杂度,你可使用大 O 符号来表达和衡量这种复杂性。

使用大 O 符号,输入任意大时,你能够根据输入与运行时间的相对增加速度来衡量运行时间。大 O 表示法排除系数和低阶的项,以便于你关注查询运行时间的关键部分:增加率。当以这种方式表示时,丢弃系数与低阶的项,时间复杂度被认为是渐进式描述的。这意味着输入会变为无穷大。

在数据库语言中,复杂度衡量了数据库表数据增长以后,查询该表数据所花时间相对增长了多少的过程。

注意你的数据库大小不只仅由于表里存储的数据增多而变大,索引在其中对大小影响也起了很大的做用。

正如前面所述,执行计划除了前面所说的之外,还定义了每一步操做使用什么算法,这使得每次查询执行的时间能够在逻辑上表示为查询计划中涉及表大小的函数。换句话说,你可使用大 O 符号和执行计划预估查询的复杂性和性能。

在接下来的小节中,你会了解关于四种时间复杂度类型的通常概念,你将会看到一些示例,说明查询的时间复杂度如何根据你运行它们上下文的不一样而有所不一样的。

提示:索引是故事的一部分!

注意,由于不一样的数据库有不一样类型的索引、不一样的执行计划、不一样的实现,因此下面列出的几个时间复杂度是很通用的,会根据你配置的不一样而变化。

更多阅读在这儿

总而言之,你能够查看如下备忘单,以根据时间复杂度以及其执行状况估计查询的性能:

SQL 调优

考虑到查询计划和时间复杂性,你能够考虑进一步调整 SQL 查询,特别注意如下几点:

  • 大表的全表扫描替换为索引的扫描;
  • 确保你正在使用最佳的表链接顺序;
  • 确保的使用索引优化;还有
  • 缓存小表的全表扫描。

祝贺!你已经看到了这篇博文的结尾,这只是帮助你对 SQL 查询性能的一瞥。你但愿对反模式,查询优化器,审查工具,预估和解释查询计划的复杂性有更多的看法,然而,还有更多的东西等你去发现!若是你想知道更多,能够考虑读这本由R. Ramakrishnan 和 J. Gehrke 写的「Database Management Systems」。

最后,我不想错过这个来自 StackOverFlow 用户那里的引用

「我最喜欢的反模式不是测试你的查询。

这适用于:

  • 你的查询涉及了不止一张表。

  • 你认为你的查询有一个优化的设计,但不肯意去验证你的假设。

  • 你会接受第一个成功的查询,它是不是最优的,你并不清楚。」

如过你想开始使用 SQL,能够考虑学习 DataCamp 的 Intro to SQL for Data Science 课程!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索