简化SQL语句的一般规则


67

我正在寻找一些“推理规则”(类似于设置操作规则或逻辑规则),可以用来减少SQL查询的复杂性或大小。是否存在类似的东西?有论文,有工具吗?您自己发现的任何对等物吗?它在某种程度上类似于查询优化,但在性能方面却不一样。

换种说法:使用JOIN,SUBSELECT和UNION进行(复杂)查询是否有可能(或不能)通过使用一些转换规则将其简化为更简单的等效SQL语句,从而产生相同的结果?

因此,我正在寻找SQL语句的等效转换,例如大多数SUBSELECT都可以重写为JOIN的事实。


1
我的方法是学习一般的关系理论,特别是关系代数。然后学习从关系代数(例如,通用量化又称为除法)和微积分(例如,存在性量化)中找出SQL中用于实现运算符的构造。棘手的是,SQL具有在关系模型中找不到的功能,例如null,无论如何最好将其重构。推荐阅读:SQL和关系理论:如何按CJ Date编写准确的SQL代码
某天,2012年

Answers:


62

换种说法:使用JOIN,SUBSELECT和UNION进行(复杂)查询是否有可能(或不能)通过使用一些转换规则将其简化为更简单的等效SQL语句,从而产生相同的结果?

这正是优化程序谋生的手段(不是说他们总是做得很好)。

由于SQL是一种基于集合的语言,通常有多种方法可以将一个查询转换为另一个查询。

喜欢这个查询:

SELECT  *
FROM    mytable
WHERE   col1 > @value1 OR col2 < @value2

可以转化为:

SELECT  *
FROM    mytable
WHERE   col1 > @value1
UNION
SELECT  *
FROM    mytable
WHERE   col2 < @value2

或这个:

SELECT  mo.*
FROM    (
        SELECT  id
        FROM    mytable
        WHERE   col1 > @value1
        UNION
        SELECT  id
        FROM    mytable
        WHERE   col2 < @value2
        ) mi
JOIN    mytable mo
ON      mo.id = mi.id

,看起来较难看,但可以产生更好的执行计划。

最常见的事情之一是替换此查询:

SELECT  *
FROM    mytable
WHERE   col IN
        (
        SELECT  othercol
        FROM    othertable
        )

与此:

SELECT  *
FROM    mytable mo
WHERE   EXISTS
        (
        SELECT  NULL
        FROM    othertable o
        WHERE   o.othercol = mo.col
        )

在某些RDBMS(如PostgreSQL)中,DISTINCTGROUP BY使用不同的执行计划,因此有时最好将一个替换为另一个:

SELECT  mo.grouper,
        (
        SELECT  SUM(col)
        FROM    mytable mi
        WHERE   mi.grouper = mo.grouper
        )
FROM    (
        SELECT  DISTINCT grouper
        FROM    mytable
        ) mo

SELECT  mo.grouper, SUM(col)
FROM    mytable
GROUP BY
        mo.grouper

在中PostgreSQLDISTINCT排序和GROUP BY散列。

MySQL缺少FULL OUTER JOIN,因此可以将其重写为:

SELECT  t1.col1, t2.col2
FROM    table1 t1
LEFT OUTER JOIN
        table2 t2
ON      t1.id = t2.id

SELECT  t1.col1, t2.col2
FROM    table1 t1
LEFT JOIN
        table2 t2
ON      t1.id = t2.id
UNION ALL
SELECT  NULL, t2.col2
FROM    table1 t1
RIGHT JOIN
        table2 t2
ON      t1.id = t2.id
WHERE   t1.id IS NULL

,但请参阅我的博客中有关如何更有效地执行此操作的文章MySQL

此分层查询在Oracle

SELECT  DISTINCT(animal_id) AS animal_id
FROM    animal
START WITH
        animal_id = :id
CONNECT BY
        PRIOR animal_id IN (father, mother)
ORDER BY
        animal_id

可以转换为:

SELECT  DISTINCT(animal_id) AS animal_id
FROM    (
        SELECT  0 AS gender, animal_id, father AS parent
        FROM    animal
        UNION ALL
        SELECT  1, animal_id, mother
        FROM    animal
        )
START WITH
        animal_id = :id
CONNECT BY
        parent = PRIOR animal_id
ORDER BY
        animal_id

,后者表现更好。

有关执行计划的详细信息,请参阅我的博客中的这篇文章:

要查找与给定范围重叠的所有范围,可以使用以下查询:

SELECT  *
FROM    ranges
WHERE   end_date >= @start
        AND start_date <= @end

,但是在SQL Server这种更复杂的查询中,更快地产生相同的结果:

SELECT  *
FROM    ranges
WHERE   (start_date > @start AND start_date <= @end)
        OR (@start BETWEEN start_date AND end_date)

,不管您信不信,我的博客上也有一篇文章:

SQL Server 还缺乏一种执行累积聚合的有效方法,因此此查询:

SELECT  mi.id, SUM(mo.value) AS running_sum
FROM    mytable mi
JOIN    mytable mo
ON      mo.id <= mi.id
GROUP BY
        mi.id

可以使用,主帮助我更有效地重写,游标(你听我的权利:cursorsmore efficientlySQL Server在一个句子)。

请参阅我的博客中有关此操作的文章:

在金融应用程序中通常会遇到一种查询,查询某种货币的有效汇率,例如 Oracle

SELECT  TO_CHAR(SUM(xac_amount * rte_rate), 'FM999G999G999G999G999G999D999999')
FROM    t_transaction x
JOIN    t_rate r
ON      (rte_currency, rte_date) IN
        (
        SELECT  xac_currency, MAX(rte_date)
        FROM    t_rate
        WHERE   rte_currency = xac_currency
                AND rte_date <= xac_date
        )

可以大量重写此查询以使用等于条件,HASH JOIN而该条件允许a而不是NESTED LOOPS

WITH v_rate AS
        (
        SELECT  cur_id AS eff_currency, dte_date AS eff_date, rte_rate AS eff_rate
        FROM    (
                SELECT  cur_id, dte_date,
                        (
                        SELECT  MAX(rte_date)
                        FROM    t_rate ri
                        WHERE   rte_currency = cur_id
                                AND rte_date <= dte_date
                        ) AS rte_effdate
                FROM    (
                        SELECT  (
                                SELECT  MAX(rte_date)
                                FROM    t_rate
                                ) - level + 1 AS dte_date
                        FROM    dual
                        CONNECT BY
                                level <=
                                (
                                SELECT  MAX(rte_date) - MIN(rte_date)
                                FROM    t_rate
                                )
                        ) v_date,
                        (
                        SELECT  1 AS cur_id
                        FROM    dual
                        UNION ALL
                        SELECT  2 AS cur_id
                        FROM    dual
                        ) v_currency
                ) v_eff
        LEFT JOIN
                t_rate
        ON      rte_currency = cur_id
                AND rte_date = rte_effdate
        )
SELECT  TO_CHAR(SUM(xac_amount * eff_rate), 'FM999G999G999G999G999G999D999999')
FROM    (
        SELECT  xac_currency, TRUNC(xac_date) AS xac_date, SUM(xac_amount) AS xac_amount, COUNT(*) AS cnt
        FROM    t_transaction x
        GROUP BY
                xac_currency, TRUNC(xac_date)
        )
JOIN    v_rate
ON      eff_currency = xac_currency
        AND eff_date = xac_date

尽管笨拙,但后面的查询是 6速度要快几倍。

这里的主要思想是将替换<==,这需要构建一个内存日历表。要JOIN与。


第一个示例中的错误:UNION执行“或”运算,而不是“与”运算。
亚历克斯·马丁里

+1这些是查询转换的出色示例。它还表明,某些优化查询实际上不是看起来很简单的查询,例如第一个查询与第三个查询,这很遗憾,因为人们可能会认为“简单”查询将更容易被优化器分析。换句话说,似乎优化并不一定等于简化
kristof

爱国者;),我不同意这一点,因为UNION消除了重复项,所以这些不是等效的:像此查询:SELECT * FROM mytable WHERE col1> @ value1 OR col2 <@ value2可以转换为:SELECT * FROM mytable WHERE col1> @ value1 UNION SELECT *从mytable WHERE col2 <@ value2
AK 2009年

1
@Alex:只要表中定义了PRIMARY KEY,它们都是等效的。满足两个OR条件的行将被选择一次,无论是OR还是UNION。如果表具有完全相同的重复项(这意味着没有PRIMARY KEY),则可以,它们将用UNION删除,但不使用OR删除。
Quassnoi

1
我喜欢您指出的那样,在SQl中,丑陋的代码通常是性能最好的。当人们想要采用性能良好的代码并使它变得更加“优雅”并破坏性能时,它使我发疯。
HLGEM 2011年

9

以下是使用Oracle 8和9的一些信息(当然,有时做相反的事情可能会使查询更简单或更快捷):

如果括号不用于覆盖运算符优先级,则可以将其删除。一个简单的示例是您的where子句中的所有布尔运算符都相同时:where ((a or b) or c)等效于where a or b or c

子查询可以经常(如果不是总是)与主查询合并以简化它。以我的经验,这通常会大大提高性能:

select foo.a,
       bar.a
  from foomatic  foo,
       bartastic bar
 where foo.id = bar.id and
       bar.id = (
         select ban.id
           from bantabulous ban
          where ban.bandana = 42
       )
;

相当于

select foo.a,
       bar.a
  from foomatic    foo,
       bartastic   bar,
       bantabulous ban
 where foo.id = bar.id and
       bar.id = ban.id and
       ban.bandana = 42
;

使用ANSI连接可以将很多“代码猴子”逻辑与where子句中真正有趣的部分分开:上一个查询等效于

select foo.a,
       bar.a
  from foomatic    foo
  join bartastic   bar on bar.id = foo.id
  join bantabulous ban on ban.id = bar.id
 where ban.bandana = 42
;

如果要检查是否存在某行,请不要使用count(*),而要使用其中之一,rownum = 1或者将查询放在where exists子句中以仅获取一行而不是全部。


哇,最后的建议很好。我从没想过将join逻辑从where子句中拉出来,并与表defs放在一起,虽然我以前从未见过它被普遍使用,但确实很有意义。
Al Crowley

6
  • 我认为显而易见的是寻找可以用基于SQL'Set'的操作代替的任何游标。
  • 在我的列表中,下一步是寻找可以重新编写为不相关查询的任何相关子查询
  • 在长存储过程中,将单独的SQL语句分解为它们自己的存储过程。这样,他们将获得自己的缓存查询计划。
  • 寻找可以缩小范围的交易。我经常在可以安全地放在外部的事务中找到语句。
  • 子选择通常可以重写为直接联接(现代优化器擅长发现简单选择)

正如@Quassnoi所提到的,Optimizer通常做得很好。一种帮助它的方法是确保索引和统计信息是最新的,并且存在适合您的查询工作负载的索引。


关于将存储过程分解为更多内容:使用临时表时不要这样做:SqlServer(不知道其他人)将在每次执行时重新计算查询计划,从而损害性能!
汉斯·基辛

@Hans Kesting:如果所有临时表的所有DDL创建语句都是存储过程中的第一条语句,我认为这是不正确的。
米奇·

5

我喜欢用联接查询替换所有子查询。

这是显而易见的:

SELECT  *
FROM    mytable mo
WHERE   EXISTS
        (
          SELECT  *
          FROM    othertable o
          WHERE   o.othercol = mo.col
        )

通过

SELECT  mo.*
FROM    mytable mo inner join othertable o on o.othercol = mo.col

这是估计的:

SELECT  *
FROM    mytable mo
WHERE   NOT EXISTS
        (
          SELECT  *
          FROM    othertable o
          WHERE   o.othercol = mo.col
        )

通过

SELECT  mo.*
FROM    mytable mo left outer join othertable o on o.othercol = mo.col
WHERE   o.othercol is null

它可以帮助DBMS在大需求中选择良好的执行计划。


1
这些不一定总能得到完全相同的结果:如果在“右”表中对“左”表中的任何特定值进行了多个匹配,则在表上进行JOIN会导致重复。EXISTS并且NOT EXISTS没有这个问题。(可以通过使用它来解决,DISTINCT但会降低效率。)
史蒂夫·钱伯斯

5

我希望团队中的每个人都遵循一套标准,以使代码可读,可维护,可理解,可清洗等。:)

  • 每个人都使用相同的别名
  • 没有游标。没有循环
  • 为什么甚至在可以存在时就想到IN
  • INDENT
  • 编码风格的一致性

这里还有更多内容您最有用的数据库标准有哪些?


同意。团队中拥有标准可以提高可读性,可维护性以及通常的性能。至少出于可读性考虑,有一些可用的工具,例如SQLinForm格式化程序/美化工具
Guido,


3

尽管简化可能不等于优化,但是简化在编写可读的SQL代码中很重要,而这对于能够检查SQL代码中的概念正确性(而不是语法正确性,开发环境应该为您检查)至关重要。在我看来,在理想的世界中,我们将编写最简单易读的SQL代码,然后优化程序将重写该SQL代码,使其以任何形式(可能更冗长)运行最快。

我发现将SQL语句视为基于集合逻辑非常有用,尤其是当我需要组合where子句或找出where子句的复杂否定时。我使用布尔代数定律在这种情况下,。

简化where子句的最重要的大概是DeMorgan的定律(请注意,“·”是“ AND”,“ +”是“ OR”):

  • 非(x·y)=非x +非y
  • 非(x + y)=非x·非y

这将SQL转换为:

NOT (expr1 AND expr2) -> NOT expr1 OR NOT expr2
NOT (expr1 OR expr2) -> NOT expr1 AND NOT expr2

这些规则在简化具有大量嵌套ANDOR部分的where子句时非常有用。

记住该语句field1 IN (value1, value2, ...)与等效field1 = value1 OR field1 = value2 OR ...。这使您可以否定以下IN ()两种方法之一:

NOT field1 IN (value1, value2)  -- for longer lists
NOT field1 = value1 AND NOT field1 = value2  -- for shorter lists

子查询也可以考虑这种方式。例如,此否定where子句:

NOT (table1.field1 = value1 AND EXISTS (SELECT * FROM table2 WHERE table1.field1 = table2.field2))

可以重写为:

NOT table1.field1 = value1 OR NOT EXISTS (SELECT * FROM table2 WHERE table1.field1 = table2.field2))

这些法律没有告诉您如何将使用子查询的SQL查询转换为使用联接的SQL查询,但是布尔逻辑可以帮助您了解联接类型以及查询应返回的内容。例如,对于表AB,一个INNER JOIN是像A AND B,一个LEFT OUTER JOIN是像(A AND NOT B) OR (A AND B)这简化到A OR (A AND B),并且FULL OUTER JOINA OR (A AND B) OR B其简化到A OR B


我还发现我( P => Q ) <=> ( NOT ( P ) OR Q )
经常

0

我的方法是学习一般的关系理论,特别是关系代数。然后学习从关系代数(例如,通用量化又称为除法)和微积分(例如,存在性量化)中找出SQL中用于实现运算符的构造。棘手的是,SQL具有在关系模型中找不到的功能,例如null,无论如何最好将其重构。推荐阅读:SQL和关系理论:如何按CJ Date编写准确的SQL代码

因此,我不认为“大多数SUBSELECT都可以重写为JOIN的事实”表示一种简化。

以以下查询为例:

SELECT c 
  FROM T1 
 WHERE c NOT IN ( SELECT c FROM T2 );

用JOIN重写

SELECT DISTINCT T1.c 
  FROM T1 NATURAL LEFT OUTER JOIN T2 
 WHERE T2.c IS NULL;

加入更加冗长!

或者,认识到该构造正在c例如伪代数的投影上实现反连接

T1 { c } antijoin T2 { c }

使用关系运算符的简化:

SELECT c FROM T1 EXCEPT SELECT c FROM T2;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.