【问题标题】:Why is subquery and join so slow为什么子查询和连接这么慢
【发布时间】:2014-09-30 15:25:00
【问题描述】:

我需要从 BUNDLES 表中选择具有几个 SAP_STATE_ID 值之一的行。这些值取决于是否应该导出相应的 SAP 状态。

这个查询运行得非常快(在 SAP_STATE_ID 字段上有索引) -

SELECT b.* FROM BUNDLES b WHERE b.SAP_STATE_ID IN (2,3,5,6)

但是...我想动态获取 ID 列表,如下所示:

SELECT b.* FROM BUNDLES b 
WHERE b.SAP_STATE_ID IN 
(SELECT s.SAP_STATE_ID FROM SAP_STATES s WHERE s.EXPORT_TO_SAP = 1)

哎呀,这个查询突然花费了太多时间。我希望 SQL 服务器首先运行子查询(它不依赖于主查询中的任何内容),然后像我的第一个示例一样运行整个事情。我试图重写它以使用连接而不是子查询:

SELECT b.* FROM BUNDLES b 
JOIN SAP_STATES s ON (s.SAP_STATE_ID = b.SAP_STATE_ID) 
WHERE s.EXPORT_TO_SAP = 1

但它的性能同样很差。似乎它正在为 BUNDLES 表的每一行或类似的东西运行子查询。我在阅读执行计划方面不是很熟练,但我试过了。它说 81% 的成本是用于扫描 BUNDLES 的主键索引(我不知道为什么要这样做,有 BUNDLE_ID 字段定义为 PRIMARY KEY,但它根本没有出现在查询中...... )

有没有人解释一下为什么 SQL Server 如此“愚蠢”?有没有办法以良好的性能实现我想要的,但无需提供 SAP_STATE_ID 的静态列表?

表和相关索引的脚本 - http://mab.to/xbYiI0wKj

子查询版本的执行计划 - http://mab.to/8Qh6gpdYZ

带有连接的版本的查询计划 - http://mab.to/YCqeGCUbr

(由于某种原因,这两个计划看起来相同,并且都建议创建 BUNDLES.SAP_STATE_ID 索引,该索引已经存在)

【问题讨论】:

  • 你有关于 SAP_STATES.SAP_STATE_ID 的索引吗?
  • 对,其实是主键
  • 子查询自己运行需要多长时间?
  • 发布带有索引定义的架构可能会有所帮助。
  • 能否将查询计划保存为 .sqlplan 文件并与我们共享?

标签: sql sql-server join


【解决方案1】:

我很确定你的统计数据在表格上是关闭的。如果您想让它快速运行,我会将查询编写为:

SELECT b.*
  FROM SAP_STATES s 
 INNER LOOP JOIN BUNDLES b 
    ON s.SAP_STATE_ID = b.SAP_STATE_ID
 WHERE s.EXPORT_TO_SAP = 1

这会强制一个嵌套循环加入SAP_STATES,它会过滤BUNDLES

【讨论】:

  • 是的,这成功了。我很惭愧,我以前从未听说过这样的选择。我必须了解更多关于这个 LOOP 的事情。
  • 我是否理解创建正确的统计信息应该使优化器选择正确的连接类型,因此这里不需要 LOOP 关键字?
  • 确实如此。我很确定,如果您使用 show plan 运行查询,您会看到返回行的估计和有效数量非常不同。通常更新统计数据就足以解决问题。在某些情况下,您需要做更多奇特的事情,例如“针对未知进行优化”,但我认为这里不需要。
【解决方案2】:

当您使用表(临时表或物理表)时,SQL 引擎会针对它构建统计信息,因此对其中的行数有一个非常清晰的想法,以及哪种是它的最佳执行方法。另一方面,计算表(子查询)没有针对它的统计信息。

因此,虽然人类推断其中的行数似乎很简单,但“愚蠢”的 SQL 引擎并没有意识到这一切。现在,来到查询中,WHERE s.EXPORT_TO_SAP = 1 子句在这里产生了巨大的变化。聚集索引是根据 SAP_STATE_ID 排序和构建的,但要额外检查 WHERE 子句,它别无选择,只能扫描整个表(在最终数据集中)!我敢打赌,如果不是聚集索引,而是 SAP_STATE_ID 列上有一个覆盖 EXPORT_TO_SAP 字段的非聚集覆盖索引,它可能已经成功了。由于聚集索引扫描通常不利于性能,我建议您采用以下方法:

SELECT s.SAP_STATE_ID 
into #Sap_State
FROM SAP_STATES s WHERE s.EXPORT_TO_SAP = 1

SELECT b.* FROM BUNDLES b 
join #Sap_State a on a.sap_state_id = b.sap_state_id

【讨论】:

  • 是的,这种方法提高了性能,我赞成。但是在这种情况下,使用临时表对我来说有点奇怪。为了 100% 干净,选择后还应该有 DROP TABLE #Sap_state。于是我们把原来的一行变成了三行,我很不高兴。
  • 顺便说一句,为什么这种方法有效而其他方法无效?我不明白为什么临时表中的值比子查询结果的记录集中更快。是否只是优化器在其他情况下没有构建正确的执行计划?
  • 用解释编辑了我的答案。
【解决方案3】:

由于某些原因导致 mab.to 出现问题,

我建议确保以下内容

table        index
sap_states   (export_to_sap, sap_state_id )
bundles      (sap_state_id)

select
      b.*
   from 
      sap_states ss
         join bundles b
            on ss.sap_state_id = b.sap_state_id
   where 
      ss.export_to_sap = 1

【讨论】:

  • 我尝试使用 JOIN 重写查询(如问题中所述),但没有成功。您的查询仅交换了 bundles 和 sap_states 表的顺序 - 我试过了,它保持不变。
  • 您是否在bundles.sap_state_id 上创建了索引?此外,使用联接发布查询的解释计划。
猜你喜欢
  • 1970-01-01
  • 2011-03-11
  • 2017-01-21
  • 2020-03-19
  • 2014-03-12
  • 2011-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多