WHERE 子句给出了糟糕的查询计划

问题描述

我不确定如何最好地调整此查询和/或索引以避免生硬的 FORCE ORDER 提示

此主查询运行良好,当前在 0 秒内返回 0 行:

SELECT  S1.ID,S.LOAD_DATE,s.Deleted,S1.HUB_FORM_ID
FROM #TMP S

INNER JOIN HUB_FORM H1 ON 
H1.Form_ID = S.HUB_FORM_BK
INNER JOIN  HUB_ORG H2 ON 
H2.Organisation_ID = S.HUB_ORG_BK
INNER JOIN  HUB_PERSON H3 ON 
H3.person_id = S.HUB_PERSON_BK
INNER JOIN  HUB_EVENT H4 ON 
H4.job_id = S.HUB_EVENT_BK
INNER JOIN  HUB_WORKFLOW_STEP H5 ON 
H5.step_id = S.HUB_WORKFLOW_STEP_BK

INNER JOIN LNK_FORM_ENTITY S1 ON
H1.HUB_FORM_ID = S1.HUB_FORM_ID AND H2.HUB_ORG_ID = S1.HUB_ORG_ID AND H3.HUB_PERSON_ID = S1.HUB_PERSON_ID AND H4.HUB_EVENT_ID = S1.HUB_EVENT_ID AND H5.HUB_WORKFLOW_STEP_ID = S1.HUB_WORKFLOW_STEP_ID

INNER JOIN DK_SAT_LNK_FORM_ENTITY S2 ON 
 S1.ID = S2.Parent_ID

在 S2.LOAD_DATE_TO 上添加 WHERE 子句使其运行并运行(在一两分钟后终止)。

WHERE S2.LOAD_DATE_TO = '31/12/9999'

我不确定为什么会这样:

  1. 如果没有过滤器,则不会返回任何行,因此没有任何区别。
  2. 用于在良好计划中包含此字段的表的索引(没有日期过滤器),已经包含该字段作为第二个关键字段,因此我认为任何额外费用都可以忽略不计

注意 - 它并不总是返回 0 行,但无论行是否返回,它都需要运行(并在合理的时间内完成)。

CREATE NONCLUSTERED INDEX [JM_TEST_190221_2] ON [dbo].[DK_SAT_LNK_FORM_ENTITY]
(
    [Parent_ID] ASC,[LOAD_DATE_TO] ASC
)
WITH (PAD_INDEX = OFF,STATISTICS_norECOmpuTE = OFF,SORT_IN_TEMPDB = OFF,DROP_EXISTING = OFF,ONLINE = OFF,ALLOW_ROW_LOCKS = ON,ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

实时查询计划显示它运行了 LNK_ 和 DK_ 表以及随后连接的表中的数百万行,而在原始计划中,它显示 LNK_ 表上的实际行数 = 56(56 次执行 - 预期 1 行)和 DK_ 表上的 0 个实际行(56 次执行)。

如果我在 WHERE 子句后添加 OPTION (FORCE ORDER),它会在 0 秒内再次运行,并使用与原始好的查询计划不同的查询计划。

很明显,这可以在短期内解决问题,但我对使用这种生硬的工具持谨慎态度,因为随着时间的推移,随着数据的变化,它可能并不总是最佳选择。

编辑 我曾尝试使用 FULL SCAN 更新统计信息,并重建关键索引,但没有任何影响。

下面的查询计划 - 感谢您收到任何提示或解释!


原来的好计划(actual plan):没有WHERE子句:https://www.brentozar.com/pastetheplan/?id=HyG3SwTZd

糟糕的计划(来自终止点的实时查询计划):https://www.brentozar.com/pastetheplan/?id=rJpBSPpWO

带有 FORCE ORDER 提示的好计划:https://www.brentozar.com/pastetheplan/?id=SJqxUvT-d

解决方法

显然,您的问题是 HUB_FORM 具有足够的选择性,以至于在一开始就将行数限制为 0。但是优化器没有意识到这一点,因此它颠倒了连接的顺序。

要强制执行订单而不通过 FORCE ORDER 敲打查询的其余部分,我们有两个选择:

  1. 预先计算 #TMP,HUB_FORM 与临时表或表变量的连接。这通常会导致相当多的额外 IO。
  2. 更好的选择是说服优化器先计算连接,但不使用显式提示。

这通常最好通过将连接放在带有 SELECT TOP 的子查询中来完成,但您可能需要通过添加一两个进一步的连接来修改它。

SELECT  S1.ID,S.LOAD_DATE,s.Deleted,S1.HUB_FORM_ID
FROM (
    SELECT TOP (9223372036854775807) S.*
    FROM #TMP S
    INNER JOIN HUB_FORM H1 ON 
        H1.Form_ID = S.HUB_FORM_BK
) S
INNER JOIN  HUB_ORG H2 ON 
H2.Organisation_ID = S.HUB_ORG_BK
INNER JOIN  HUB_PERSON H3 ON 
H3.person_id = S.HUB_PERSON_BK
INNER JOIN  HUB_EVENT H4 ON 
H4.job_id = S.HUB_EVENT_BK
INNER JOIN  HUB_WORKFLOW_STEP H5 ON 
H5.step_id = S.HUB_WORKFLOW_STEP_BK

INNER JOIN LNK_FORM_ENTITY S1 ON
H1.HUB_FORM_ID = S1.HUB_FORM_ID AND H2.HUB_ORG_ID = S1.HUB_ORG_ID AND H3.HUB_PERSON_ID = S1.HUB_PERSON_ID AND H4.HUB_EVENT_ID = S1.HUB_EVENT_ID AND H5.HUB_WORKFLOW_STEP_ID = S1.HUB_WORKFLOW_STEP_ID

INNER JOIN DK_SAT_LNK_FORM_ENTITY S2 ON 
 S1.ID = S2.Parent_ID

如果这不起作用,您可以通过将 TOP 更改为变量并在末尾添加 OPTIMIZE FOR 提示来说服它:

DECLARE @topRows bigint = 9223372036854775807;

SELECT  S1.ID,S1.HUB_FORM_ID
FROM (
    SELECT TOP (@topRows) S.*
    FROM #TMP S
    INNER JOIN HUB_FORM H1 ON 
        H1.Form_ID = S.HUB_FORM_BK
) S
INNER JOIN  HUB_ORG H2 ON 
.........
OPTION (OPTIMIZE FOR (@topRows = 1));

这会导致优化器认为它只会从连接中获取 1 行,但如果在运行时是这种情况,实际上允许更多行。

请注意,这些都不会改变查询的基本语义