【问题标题】:General rules for simplifying SQL statements简化 SQL 语句的一般规则
【发布时间】:2010-11-07 08:21:12
【问题描述】:

我正在寻找一些“推理规则”(类似于设置操作规则或逻辑规则),可用于降低 SQL 查询的复杂性或大小。 有没有这样的东西?任何文件,任何工具?您自己找到的任何等效项?它在某种程度上类似于查询优化,但在性能方面不同。

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

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

【问题讨论】:

  • 我的方法是学习一般的关系理论,特别是关系代数。然后学习发现 SQL 中使用的构造,以实现关系代数(例如,通用量化,也就是除法)和微积分(例如存在量化)中的运算符。问题是 SQL 具有关系模型中没有的特性,例如空值,无论如何最好将其重构掉。推荐阅读:SQL and Relational Theory: How to Write Accurate SQL Code By C. J. Date.

标签: sql logic complexity-theory reduction


【解决方案1】:

换一种说法:使用 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 而不是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 倍。

这里的主要思想是将&lt;=替换为=,这需要构建一个内存日历表。到JOIN

【讨论】:

  • 第一个示例中的错误:UNION 执行 OR,而不是 AND。
  • +1 这些是查询转换的一些很好的例子。它还表明,一些优化的查询实际上并不是看起来很简单的查询,例如第一个查询与第三个查询,这很遗憾,因为人们可以假设优化器更容易分析“简单”查询。换句话说,优化似乎并不等于简化
  • Patriot ;),我不同意这一点,因为 UNION 消除了重复,论文不等价: 像这个查询: SELECT * FROM mytable WHERE col1 > @value1 OR col2 @value1 UNION SELECT * FROM mytable WHERE col2
  • @Alex:只要表定义了 PRIMARY KEY,它们是等价的。满足两个 OR'ed 条件的行将仅选择一次,无论是使用 OR 还是使用 UNION。如果表有完全相同的重复项(这意味着没有 PRIMARY KEY),那么是的,它们将被 UNION 而不是 OR。
  • 我喜欢你指出的在 SQl 中,丑陋的代码通常是最好的性能。当人们想要使用性能良好的代码并使其更加“优雅”并扼杀性能时,这让我发疯。
【解决方案2】:

以下是使用 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里,以前没见过常用的,但确实很有意义。
【解决方案3】:
  • 我想显而易见的是寻找任何可以用基于 SQL 'Set' 的操作替换的游标。
  • 在我的列表中,接下来是查找任何可以重写为不相关查询的相关子查询
  • 在长存储过程中,将单独的 SQL 语句分解为它们自己的存储过程。这样他们就可以获得自己的缓存查询计划。
  • 寻找可以缩短其范围的事务。我经常在事务中发现可以安全地在外部的语句。
  • 子选择通常可以重写为直接连接(现代优化器擅长发现简单的连接)

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

【讨论】:

  • 关于将存储过程分解为更多:使用临时表时不要这样做:然后SqlServer(不知道其他)将在每次执行时重新计算查询计划,从而损害性能!
  • @Hans Kesting:如果所有临时表的所有 DDL 创建语句都是存储过程中的第一个语句,我认为这是不正确的。
【解决方案4】:

我喜欢用连接查询替换所有类型的子选择。

这个很明显:

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 在大请求中选择好的执行计划。

【讨论】:

  • 这些不一定总是给出完全相同的结果:如果在“右”表中对于“左”中加入的任何特定值有多个匹配项,则在表上加入将导致重复“ 桌子。 EXISTSNOT EXISTS 没有这个问题。 (可以使用DISTINCT 解决,但这会降低效率。)
【解决方案5】:

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

  • 每个人都使用相同的别名
  • 没有游标。没有循环
  • 既然可以存在,为什么还要考虑 IN
  • 缩进
  • 编码风格的一致性

这里还有一些东西What are some of your most useful database standards?

【讨论】:

  • 同意。在团队中拥有标准可以提高可读性、可维护性和性能。至少为了可读性,有几个可用的工具,例如SQLinForm 格式化程序/美化器
【解决方案6】:

鉴于 SQL 的性质,您绝对必须了解任何重构对性能的影响。 Refactoring SQL Applications 是一个很好的重构资源,非常强调性能(参见第 5 章)。

【讨论】:

    【解决方案7】:

    虽然简化可能不等于优化,但简化对于编写可读的 SQL 代码很重要,而这对于检查 SQL 代码的概念正确性(而不是语法正确性,您的开发环境应该为您检查)至关重要.在我看来,在理想情况下,我们会编写最简单、最易读的 SQL 代码,然后优化器会将该 SQL 代码重写为任何形式(可能更冗长)将运行得最快。

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

    简化 where 子句的最重要的可能是德摩根定律(注意“·”是“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 查询转换为使用联接的查询,但布尔逻辑可以帮助您了解联接类型以及查询应返回的内容。例如,对于表ABINNER JOIN 类似于A AND BLEFT OUTER JOIN 类似于(A AND NOT B) OR (A AND B),简化为A OR (A AND B)FULL OUTER JOIN 简化为A OR (A AND B) OR BA OR B

    【讨论】:

    • 我还发现我经常使用暗示重写规则,即( P =&gt; Q ) &lt;=&gt; ( NOT ( P ) OR Q )
    【解决方案8】:

    我的方法是学习一般的关系理论,特别是关系代数。然后学习发现 SQL 中使用的构造,以实现关系代数(例如全称量化,也称为除法)和微积分(例如存在量化)中的运算符。问题是 SQL 具有关系模型中没有的特性,例如空值,无论如何最好将其重构掉。推荐阅读:SQL and Relational Theory: How to Write Accurate SQL Code By C. J. Date

    在这方面,我不相信“大多数 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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-07
      • 2012-03-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-23
      相关资源
      最近更新 更多