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 DISTINCT
和GROUP 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 BY
和DISTINCT
的执行计划大致相同。
由于我们不得不猜测(因为我们没有解释计划),这里的区别是 IMO 内联子查询在 AFTER GROUP BY
但 BEFORE 之后执行> 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的主要内容,如果未能解决你的问题,请参考以下文章