SQL 性能:SELECT DISTINCT 与 GROUP BY

Posted

技术标签:

【中文标题】SQL 性能:SELECT DISTINCT 与 GROUP BY【英文标题】:SQL Performance: SELECT DISTINCT versus GROUP BY 【发布时间】:2012-12-07 01:32:05 【问题描述】:

我一直在努力改善现有 Oracle 数据库驱动应用程序的查询时间,该应用程序运行缓慢。该应用程序执行几个大型查询,例如下面的查询,可能需要一个多小时才能运行。在下面的查询中将DISTINCT 替换为GROUP BY 子句将执行时间从100 分钟缩短到10 秒。我的理解是SELECT DISTINCTGROUP BY 的运作方式几乎相同。为什么执行时间之间存在如此巨大的差异?查询在后端执行的方式有什么区别?有没有SELECT DISTINCT跑得更快的情况?

注意:在以下查询中,WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A' 仅代表可以过滤结果的多种方式之一。提供此示例是为了说明连接所有表的原因,这些表没有包含在 SELECT 中的列,并且会产生大约十分之一的可用数据

使用DISTINCT的SQL:

SELECT DISTINCT 
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS,
    (SELECT COUNT(PKID) 
        FROM ITEM_PARENTS 
        WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
        ) AS CHILD_COUNT
FROM
    ITEMS
    INNER JOIN ITEM_TRANSACTIONS 
        ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
        AND ITEM_TRANSACTIONS.FLAG = 1
    LEFT OUTER JOIN ITEM_METADATA 
        ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
    LEFT OUTER JOIN JOB_INVENTORY 
        ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID     
    LEFT OUTER JOIN JOB_TASK_INVENTORY 
        ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
    LEFT OUTER JOIN JOB_TASKS 
        ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID                              
    LEFT OUTER JOIN JOBS 
        ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
    LEFT OUTER JOIN TASK_INVENTORY_STEP 
        ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID 
    LEFT OUTER JOIN TASK_STEP_INFORMATION 
        ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE 
    TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
ORDER BY 
    ITEMS.ITEM_CODE

使用GROUP BY的SQL:

SELECT
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS,
    (SELECT COUNT(PKID) 
        FROM ITEM_PARENTS 
        WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
        ) AS CHILD_COUNT
FROM
    ITEMS
    INNER JOIN ITEM_TRANSACTIONS 
        ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
        AND ITEM_TRANSACTIONS.FLAG = 1
    LEFT OUTER JOIN ITEM_METADATA 
        ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
    LEFT OUTER JOIN JOB_INVENTORY 
        ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID     
    LEFT OUTER JOIN JOB_TASK_INVENTORY 
        ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
    LEFT OUTER JOIN JOB_TASKS 
        ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID                              
    LEFT OUTER JOIN JOBS 
        ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
    LEFT OUTER JOIN TASK_INVENTORY_STEP 
        ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID 
    LEFT OUTER JOIN TASK_STEP_INFORMATION 
        ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE 
    TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
GROUP BY
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS
ORDER BY 
    ITEMS.ITEM_CODE

这里是使用DISTINCT的查询的Oracle查询计划:

这里是使用GROUP BY的查询的Oracle查询计划:

【问题讨论】:

group by显示查询。 我没有你的问题的答案,但我希望看到这两个查询、它们的解释计划和逻辑 GET 的数量可能有助于理解(FWIW 我本来希望 DISTINCT 有一个性能优势,如果有的话)。 在 SQL Server 中,您可以获得查询执行计划。您可以在 Oracle 中获得类似的东西吗?这会告诉你区别在哪里。 顺便说一句:当您只想要末尾带有“A 型”的记录时,为什么要加入大长链 LEFT? 两件事; 1)将您的 GROUP BY 查询放入您的问题中,2)对每个查询运行 EXPLAIN PLAN,并将输出添加到问题中。 【参考方案1】:

性能差异可能是由于在SELECT 子句中执行了子查询。我猜它正在为不同的 before 的每一行重新执行此查询。对于group by,它将在group by 之后执行一次

尝试用连接替换它,而不是:

select . . .,
       parentcnt
from . . . left outer join
      (SELECT PARENT_ITEM_ID, COUNT(PKID) as parentcnt
       FROM ITEM_PARENTS 
      ) p
      on items.item_id = p.parent_item_id

【讨论】:

+1 - 这正是我所想的(包括潜在的解决方案),但我对 Oracle 的了解还不够,无法确定。 这似乎是瓶颈。我尝试删除子查询,并且查询的执行速度与 GROUP BY 版本一样快(100 分钟 vs 20 秒)。谢谢!【参考方案2】:

我很确定GROUP BYDISTINCT 的执行计划大致相同。

由于我们不得不猜测(因为我们没有解释计划),这里的区别是 IMO 内联子查询在 AFTER GROUP BYBEFORE 之后执行> DISTINCT.

因此,如果您的查询返回 1M 行并聚合为 1k 行:

GROUP BY 查询将运行子查询 1000 次, 而 DISTINCT 查询将运行子查询 1000000 次。

tkprof 解释计划将有助于证明这一假设。


在我们讨论这个问题时,我认为重要的是要注意查询的编写方式对读者和优化器都有误导性:您显然希望从 item/item_transactions 中找到所有具有 @987654327 的行@ 值为“TYPE A”。

IMO 你的查询会有更好的计划,如果这样写会更容易阅读:

SELECT ITEMS.ITEM_ID,
       ITEMS.ITEM_CODE,
       ITEMS.ITEMTYPE,
       ITEM_TRANSACTIONS.STATUS,
       (SELECT COUNT(PKID) 
          FROM ITEM_PARENTS 
         WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID) AS CHILD_COUNT
  FROM ITEMS
  JOIN ITEM_TRANSACTIONS 
    ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
   AND ITEM_TRANSACTIONS.FLAG = 1
 WHERE EXISTS (SELECT NULL
                 FROM JOB_INVENTORY   
                 JOIN TASK_INVENTORY_STEP 
                   ON JOB_INVENTORY.JOB_ITEM_ID=TASK_INVENTORY_STEP.JOB_ITEM_ID
                WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
                  AND ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID)

在许多情况下,DISTINCT 可能表示查询编写不正确(因为好的查询不应返回重复项)。

另请注意,您的原始选择中没有使用 4 个表。

【讨论】:

感谢您的回复。给出的查询只是一个示例,它显示了一个用于过滤结果的远连接表。几乎所有加入此查询的表中的列都可能用于 WHERE 子句。 您仍应在适当的时候使用 SEMI-JOIN(EXISTS 或 IN)而不是 DISTINCT,这对未来的读者来说更清楚,也许对优化器更重要。【参考方案3】:

首先要注意的是Distinct 的使用表示代码异味,也就是反模式。这通常意味着缺少连接或生成重复数据的额外连接。查看上面的查询,我猜group by 更快(没有看到查询)的原因是group by 的位置减少了最终返回的记录数。而distinct 正在爆出结果集并进行逐行比较。

更新方法

对不起,我应该更清楚。记录何时生成 用户在系统中执行某些任务,因此没有时间表。一种 用户可以在一天内或每小时生成数百条记录。这 重要的是每次用户运行搜索时,最新的 记录必须归还,这让我怀疑一个具体化的 视图可以在这里工作,特别是如果填充它的查询需要 运行时间长。

我确实相信这是使用物化视图的确切原因。因此,该过程将以这种方式进行。您将长时间运行的查询作为构建您的物化视图的部分,因为我们知道用户只在他们在系统中执行一些任意任务之后才关心“新”数据。所以你要做的是查询这个基础物化视图,它可以在后端不断刷新,所涉及的持久化策略不应该扼杀物化视图(一次保留几百条记录不会粉碎任何东西)。这将允许 Oracle 获取读锁(请注意,我们不关心有多少源读取我们的数据,我们只关心写入者)。在最坏的情况下,用户将拥有几微秒的“陈旧”数据,因此除非这是华尔街的金融交易系统或核反应堆系统,否则即使是最敏锐的用户也应该不会注意到这些“光点”。

如何做到这一点的代码示例:

create materialized view dept_mv FOR UPDATE as select * from dept; 

现在的关键是,只要您不调用刷新,您就不会丢失任何持久数据。由您决定何时再次“基线化”您的物化视图(也许是午夜?)

【讨论】:

+1 表示代码异味。通过 PK 加入表的查询不应返回重复;如果他们这样做,也许有什么不对劲:) 您在这一点上绝对正确。由于多年来在没有对模式大修的情况下添加了带有新表的模块,因此该模式设计得很糟糕,有很多冗余。不幸的是,我不得不忍受我所拥有的。【参考方案4】:

如果您只需要删除重复项,则应使用 GROUP BY 将聚合运算符应用于每个组和 DISTINCT。

我认为性能是一样的。

在你的情况下,我认为你应该使用 GROUP BY。

【讨论】:

以上是关于SQL 性能:SELECT DISTINCT 与 GROUP BY的主要内容,如果未能解决你的问题,请参考以下文章

sql group by 与 distinct

SQL中Distinct和group by性能区别

SQL中Distinct和group by性能区别

SQL SELECT DISTINCT 语句

sql中select distinct id和select distinct *之间的区别

MYSQL 查询慢 SELECT DISTINCT