SQL指南:如何更好地编写查询(第1部分)

了解有关反模式,执行计划,时间复杂度,查询调整和SQL优化的信息


结构化查询语言(SQL)是计算机科学行业中必不可少的技能,通常来说,学习此技能相对简单。 但是,大多数人忘记了SQL不仅与编写查询有关,它还只是下一步的第一步。 确保查询性能或匹配工作环境是完全不同的事情。

这就是为什么此SQL指南将为您提供一些评估查询的步骤的简要概述:

  • 首先,您将简要概述SQL学习在数据科学领域中的重要性。
  • 接下来,您将首先学习如何处理和执行SQL查询,以了解创建高质量查询的重要性。 更具体地说,您将看到对请求进行了分析,重写,优化和最终评估。
  • 考虑到这一点,您不仅将了解初学者在编写查询时进行的查询反模式,而且还将了解有关这些可能的错误的替代方法和解决方案的更多信息; 此外,您将了解有关基于集合的查询方法的更多信息。
  • 您还将看到这些反模式源自性能问题,并且除了“手动”方法可以改进SQL查询之外,您还可以使用其他一些有助于您查看查询计划的工具,以更结构化,更深入的方式分析查询; 和
  • 您将简要了解时间复杂度和大的O表示法,以在执行请求之前及时了解执行计划的复杂度。
  • 您将简要了解如何优化查询。

为什么要学习SQL处理数据?


SQL尚未灭亡:这是您在数据处理和分析行业的职位描述中发现的最抢手的技能之一,无论您是申请数据分析,数据工程师,数据专家还是任何其他角色。 70%的2016年O'Reilly数据科学薪资调查受访者证实了这一点,他们表示他们在专业环境中使用SQL。 此外,在本次调查中,SQL在编程语言R(57%)和Python(54%)之上脱颖而出。

您会明白:在IT行业中工作时,SQL是必不可少的技能。

对于1970年代初开发的语言来说还不错吧?

但是为什么这么经常使用呢? 尽管他已经存在了这么久,他为什么不死呢?

原因有多种:第一个原因可能是公司主要将数据存储在关系数据库管理系统(RDBMS)或关系数据流管理系统(RDSMS)中,并且需要SQL才能访问此数据。 SQL是通用的数据:它使与几乎所有数据库进行交互甚至在本地构建自己的数据库成为可能!

如果这还不够,请记住,有许多SQL实现在供应商之间不兼容并且不一定符合标准。 因此,对标准SQL的了解是您在行业(计算机科学)中找到自己的出路的必要条件。

此外,可以肯定地说,较新的技术也已加入SQL,例如Hive(一种用于查询和管理大型数据集的类似SQL的查询语言界面)或Spark SQL(可用于执行SQL查询)。 同样,您发现那里的SQL与您可以学习的标准有所不同,但是学习曲线会简单得多。

如果您想进行比较,可以将其视为学习线性代数:将所有这些努力都投入到这一主题中,您知道您也可以使用它来掌握机器学习!

简而言之,这就是为什么您应该学习以下查询语言的原因:

  • 即使对于初学者,它也很容易学习。 学习曲线非常简单且渐进,因此您将尽快编写查询。
  • 它遵循“一次学习,随处使用”的原则,因此这是您宝贵的时间!
  • 这是对编程语言的重要补充; 在某些情况下,编写查询甚至比编写代码更可取,因为它效率更高!
  • ...

您还在等什么呢? :)

SQL处理和查询执行


为了提高SQL查询的性能,您首先需要知道单击快捷方式以执行查询时内部会发生什么。

首先,将请求解析为解析树;然后, 分析该请求是否符合语法和语义要求。 解析器创建输入请求的内部表示。 然后,此输出将传输到重写机制。

然后,优化器必须找到给定查询的最佳执行或查询计划。 执行计划准确地确定每个操作使用哪种算法,以及如何协调操作。

为了找到最佳的执行计划,优化器列出所有可能的实施计划,确定每个计划的质量或成本,接收有关数据库当前状态的信息,然后从中选择最佳的作为最终实施计划。 由于查询优化器可能不完善,因此用户和数据库管理员有时必须手动检查和调整由优化器创建的计划,以提高性能。

现在您可能想知道什么是“良好的查询计划”。

正如您已经读过的,计划成本的质量起着重要作用。 更具体地说,诸如评估计划所需的磁盘I / O数量,计划的CPU成本,数据库客户端可以观察到的总响应时间以及总执行时间之类的事情很重要。 这就是时间复杂性的概念出现的地方。 稍后您将详细了解。

然后,执行选定的查询计划,并由系统执行机制对其进行评估,并返回查询结果。


编写SQL查询


从上一节可能还不清楚,垃圾进场,垃圾进场(GIGO)的原理自然会在处理和执行查询的过程中体现出来:制定查询的人也具有SQL查询性能的关键。 如果优化器收到一个格式不正确的请求,则他可以做很多...

这意味着您可以在编写请求时做一些事情。 正如您在引言中已经看到的那样,这里的责任是双重的:它不仅涉及编写符合特定标准的查询,而且还涉及收集有关性能问题可能隐藏在查询中的想法。

理想的出发点是在查询中考虑可能出现问题的“位置”。 而且,一般而言,新手可以使用四个关键字来预期会出现性能问题:

  • 条件所在;
  • 任何关键字INNER JOINLEFT JOIN ; 还有
  • 状况;

当然,这种方法既简单又幼稚,但是对于初学者来说,这些要点是很好的指针,可以肯定地说,当您第一次开始时,这些地方就会发生错误,而且奇怪的是在这些地方也很难注意到它们。

但是,您还应该了解性能应该变得有意义。 但是,仅考虑这些语句和关键字是不好的,这在考虑SQL性能时并不需要。 在请求中具有WHEREHAVING并不一定意味着它是一个错误的请求...

请查看下一部分,以了解有关反模式和构建查询的其他方法的更多信息。 这些提示和技巧仅供参考。 除其他事项外,如何以及是否真的需要重写请求取决于数据量,数据库以及完成请求所需的次数。 这完全取决于您的请求目的,并且具有与数据库一起使用的一些先验知识至关重要!

1.仅检索必要的数据


编写SQL时不必遵循“数据越多越好”的结论:您不仅冒着因获取比实际需要更多的数据而感到困惑的风险,而且由于查询接收到的数据过多,因此性能可能会受到影响。

这就是为什么通常应注意SELECTDISTINCT SELECTLIKE语句的原因。

SELECT


在编写查询时已经可以检查的第一件事是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; 

DISTINCT行动


SELECT DISTINCT用于返回不同的值。 DISTINCT是应尽可能避免的一点。 与其他示例一样,仅当将此语句添加到请求中时,执行时间才会增加。 因此,考虑是否确实需要此DISTINCT操作以获得想要的结果总是有用的。

LIKE声明


在查询中使用LIKE运算符时,如果模式以%_开头,则不使用索引。 这将防止数据库使用索引(如果存在)。 当然,从另一个角度来看,也可以说这种类型的请求潜在地使得有可能获得太多不一定满足请求目的的记录。

同样,了解存储在数据库中的数据可以帮助您制定一个模板,该模板将正确过滤所有数据,以仅查找对于查询真正重要的行。

2.限制您的结果


如果您无法避免过滤SELECT ,则可以用其他方式限制结果。 这就是诸如LIMIT和数据类型转换之类的方法出现的地方。

TOPLIMITROWNUM


您可以将LIMITTOP语句添加到查询中,以指定结果集的最大行数。 以下是一些示例:

  SELECT TOP 3 * FROM Drivers; 

请注意,例如,如果您使用SELECT TOP 50 PERCENT *更改第一条查询行,则可以选择指定PERCENT

 SELECT driverslicensenr, name FROM Drivers LIMIT 2; 

或者,您可以在查询中添加与使用LIMIT等效的ROWNUM

 SELECT * FROM Drivers WHERE driverslicensenr = 123456 AND ROWNUM <= 3; 

数据类型转换


应始终使用最有效的方法,即 最小的数据类型。 当您提供巨大的数据类型时,总会有风险,而较小的数据类型将足够。

但是,当您向查询添加数据类型转换时,仅执行时间增加。

另一种选择是尽可能避免数据类型转换。 另请注意,并非总是可以从查询中删除或跳过数据类型转换,但您应始终努力将其包括在内,并且应在执行查询之前检查添加的效果。

3.不要使查询比应有的复杂


数据类型转换将导致以下几点:您不应过度设计查询。 尝试使它们简单有效。 这似乎太简单或愚蠢,甚至无法暗示,主要是因为请求可能很复杂。

但是,在以下各节中提到的示例中,您将看到,您可以轻松地开始使简单查询变得比其复杂。

OR运算符


当您在查询中使用OR运算符时,很可能您没有使用索引。

请记住,索引是一种数据结构,可以提高在数据库表中搜索数据的速度,但是它很昂贵:将需要额外的记录,并且还需要额外的存储空间来维护索引数据结构。 索引用于快速搜索或搜索数据,而不必每次访问数据库表时都搜索数据库中的每一行。 可以使用数据库表中的一个或多个列来创建索引。

如果不使用数据库中包含的索引,则查询的执行将不可避免地花费更长的时间。 这就是为什么最好在查询中寻找使用OR运算符的替代方法。

考虑以下查询:

 SELECT driverslicensenr, name FROM Drivers WHERE driverslicensenr = 123456 OR driverslicensenr = 678910 OR driverslicensenr = 345678; 

运算符可以替换为:

IN条件; 或

 SELECT driverslicensenr, name FROM Drivers WHERE driverslicensenr IN (123456, 678910, 345678); 

两个带有UNION SELECT

提示:在这里,您必须注意不要使用不必要的UNION操作,因为您多次查看同一张表。 同时,您应该了解,在查询中使用UNION时,执行时间会增加。 UNION操作的替代方法:重新构造查询,以便将所有条件都放在单个SELECT ,或使用OUTER JOIN代替UNION

提示:请记住,尽管OR以下各节将提到的其他运算符)很可能不使用索引,但索引搜索并非总是可取的!

NOT运算符


当查询中包含NOT运算符时,很可能未使用索引,例如OR运算符。 这将不可避免地减慢您的请求。 如果您不知道这是什么意思,请考虑以下查询:

 SELECT driverslicensenr, name FROM Drivers WHERE NOT (year > 1980); 

此请求的运行速度肯定会比您预期的要慢,这主要是因为此请求的制定要比其复杂得多:在这种情况下,最好寻找替代方案。 考虑用比较运算符(例如><>!>代替NOT ; 上面的示例实际上可以被重写,如下所示:

 SELECT driverslicensenr, name FROM Drivers WHERE year <= 1980; 

它看起来已经好了吧?

AND运算符


AND运算符是另一个不使用索引的运算符,并且如果使用的索引过于复杂且效率低下,则可以减慢查询速度,如以下示例所示:

 SELECT driverslicensenr, name FROM Drivers WHERE year >= 1960 AND year <= 1980; 

最好使用BETWEEN语句重写此查询:

 SELECT driverslicensenr, name FROM Drivers WHERE year BETWEEN 1960 AND 1980; 

ANYALL运算符


此外, ANYALL运算符是您应注意的运算符,因为如果将它们包括在查询中,则不会使用索引。 替代聚合功能(例如MINMAX在这里很有用。

提示:在使用建议的替代方法的情况下,应注意,许多聚合函数(例如,许多行中的SUMAVGMINMAX都可能导致较长的查询。 在这种情况下,您可以尝试最小化处理或预先计算这些值的行数。 再次,您知道了解您的环境,请求的目的很重要,...当您决定使用哪个请求时!

隔离条件中的列


同样,在计算或标量函数中使用列的情况下,也不会使用索引。 一种可能的解决方案是简单地选择一个特定的列,使其不再成为计算或函数的一部分。 考虑以下示例:

 SELECT driverslicensenr, name FROM Drivers WHERE year + 10 = 1980; 

看起来好笑吧? 而是尝试修改计算并像下面这样重写查询:

 SELECT driverslicensenr, name FROM Drivers WHERE year = 1970; 

4.缺乏暴力


最后一个提示意味着您不应尝试过多限制请求,因为这可能会影响其性能。 对于联接和HAVING子句尤其如此。

联接中的表顺序


在联接两个表时,考虑联接中表的顺序可能很重要。 如果看到一个表明显大于另一个表,则可能需要重写查询,以便将最大的表放置在联接的最后。

连接条件过大


如果在SQL连接中添加了太多条件,则必须选择特定路径。 但是,可能这条路径并不总是更有效。

有条件


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 ,...)为由多行创建。

您会看到,质量评估,编写和重写请求并不是一件容易的事,因为它们应尽可能高效。 编写需要在专业环境中的数据库上执行的查询时,防止反模式和考虑替代选项也将是责任的一部分。

此列表只是一些反模式和技巧的小概述,希望对初学者有所帮助。 如果您想了解较老的开发人员认为最常见的反模式,请查看此讨论

基于集合与过程的方法来编写查询


前面提到的反模式意味着它们实际上归结为构建查询的基于集合的方法和过程方法的不同。

过程查询方法与编程非常相似:您告诉系统该做什么和如何做。

例如,在连接中存在过多条件,或者在滥用HAVING条件的情况下(如上述示例),您在其中通过执行一个函数然后调用另一个函数来查询数据库,或者使用包含条件,循环,用户定义函数的逻辑( UDF),游标...以获取最终结果。 使用这种方法,您通常会请求数据的一个子集,然后请求数据的另一个子集,依此类推。

毫不奇怪,这种方法通常称为“逐步”或“逐行”查询。

另一种方法是基于集合的方法,您只需指出要做什么。 您的角色是为要从查询中接收的结果集指定条件或要求。 您可以将数据检索的方式留给确定查询实现的内部机制:让数据库引擎确定执行查询的最佳算法或处理逻辑。

由于SQL是基于集合的,因此这种方法比过程的效率更高并不奇怪,它还解释了为什么在某些情况下SQL可以比代码运行得更快。

咨询是一种基于集合的查询方法,也是信息技术行业中大多数领先的雇主都会要求您掌握的一种方法! 通常有必要在这两种类型的方法之间进行切换。

请注意 ,如果您需要程序请求,则应考虑重写或重构它。

下一部分将介绍计划和查询优化。

Source: https://habr.com/ru/post/zh-CN465547/


All Articles