导致扫描的持久计算列

Ale*_*man 10 sql-server optimization execution-plan

将常规列转换为持久计算列会导致此查询无法执行索引查找。为什么?

在多个 SQL Server 版本上进行了测试,包括 2016 SP1 CU1。

再现

问题在于table1, col7

表和查询是原始版本的部分(和简化)版本。我知道查询可以用不同的方式重写,并且出于某种原因避免了这个问题,但我们需要避免接触代码,为什么table1不能被搜索的问题仍然存在。

正如 Paul White 所展示的(谢谢!),如果强制执行,则搜索可用,所以问题是:为什么优化器不选择搜索,以及我们是否可以做一些不同的事情来使搜索按预期进行,而无需更改代码?

为了澄清有问题的部分,这是错误执行计划中的相关扫描:

计划

Pau*_*ite 12

为什么优化器没有选择搜索


TL:DR扩展的计算列定义会干扰优化器最初对连接重新排序的能力。由于起点不同,基于成本的优化通过优化器采用不同的路径,并以不同的最终计划选择结束。


细节

除了最简单的查询之外,优化器不会尝试探索任何可能计划的整个空间。相反,它会选择一个看起来合理的起点,然后在一个或多个搜索阶段中花费预算量的精力来探索逻辑和物理变化,直到找到一个合理的计划。

对于这两种情况,您获得不同的计划(具有不同的最终成本估算)的主要原因是起点不同。从不同的地方开始,优化在不同的地方结束(在有限数量的探索和实现迭代之后)。我希望这是相当直观的。

我提到的起点在某种程度上基于查询的文本表示,但是在通过查询编译的解析、绑定、规范化和简化阶段时,内部树表示会发生变化。

重要的是,确切的起点在很大程度上取决于优化器选择的初始连接顺序。这个选择是在加载统计数据之前以及在推导出任何基数估计之前做出的。然而,每个表中的总基数(行数)是已知的,是从系统元数据中获得的。

因此,初始连接排序基于启发式。例如,优化器尝试重写树,使得较小的表先于较大的表连接,内连接先于外连接(和交叉连接)。

计算列的存在会干扰这个过程,尤其是优化器将外连接向下推到查询树的能力。这是因为计算列在连接重新排序发生之前被扩展为其底层表达式,并且将连接移过复杂表达式比将其移过简单列引用要困难得多。

涉及的树相当大,但为了说明,非计算列初始查询树以:(注意顶部的两个外连接)

日志操作选择
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NaryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NaryJoin
                        LogOp_Get TBL:dbo.table1(别名 TBL:a4)
                        日志操作选择
                            LogOp_Get TBL:dbo.table6(别名 TBL:a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL:[a3].col18
                                ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                        日志操作选择
                            LogOp_Get TBL:dbo.table1(别名 TBL:a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1].col2
                                ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                        日志操作选择
                            LogOp_Get TBL:dbo.table5(别名 TBL:a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2].col2
                                ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4].col2
                            ScaOp_Identifier QCOL:[a3].col19
                    日志操作选择
                        LogOp_Get TBL:dbo.table7(别名 TBL:a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL:[a7].col22
                            ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4].col2
                        ScaOp_Identifier QCOL:[a7].col23
                日志操作选择
                    LogOp_Get TBL:table1(别名 TBL:cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc].col6
                        ScaOp_Const TI(smallint,ML=2) XVAR(smallint,Not Owned,Value=4)
                LogOp_Get TBL:dbo.table5(别名 TBL:a5) 
                LogOp_Get TBL:table2(别名 TBL:cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL:[a5].col2
                        ScaOp_Identifier QCOL:[cdc].col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4].col2
                        ScaOp_Identifier QCOL:[cdc].col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt].col1
                        ScaOp_Identifier QCOL: [cdc].col1
            LogOp_Get TBL:table3(别名 TBL:ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr].col9
                ScaOp_Identifier QCOL: [cdt].col1

计算列查询的相同片段是:(注意低得多的外连接、扩展的计算列定义以及(内)连接排序中的一些其他细微差异)

日志操作选择
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NaryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NaryJoin
                    LogOp_Get TBL:dbo.table1(别名 TBL:a4)
                    日志操作选择
                        LogOp_Get TBL:dbo.table6(别名 TBL:a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL:[a3].col18
                            ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                    日志操作选择
                        LogOp_Get TBL:dbo.table1(别名 TBL:a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1].col2
                            ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                    日志操作选择
                        LogOp_Get TBL:dbo.table5(别名 TBL:a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2].col2
                            ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4].col2
                        ScaOp_Identifier QCOL:[a3].col19
                日志操作选择
                    LogOp_Get TBL:dbo.table7(别名 TBL:a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL:[a7].col22
                        ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4].col2
                    ScaOp_Identifier QCOL:[a7].col23
            日志Op_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        日志操作选择
                            LogOp_Get TBL:table1(别名 TBL:cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc].col6
                                ScaOp_Const TI(smallint,ML=2) XVAR(smallint,Not Owned,Value=4)
                        LogOp_Get TBL:table2(别名 TBL:cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc].col1
                            ScaOp_Identifier QCOL: [cdt].col1
                    LogOp_Get TBL:table3(别名 TBL:ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr].col9
                        ScaOp_Identifier QCOL: [cdt].col1
                AncOp_PrjList 
                    AncOp_PrjEl QCOL:[cdc].col7
                        ScaOp_Convert char collat​​e 53256,Null,Trim,ML=6
                            ScaOp_IIF varchar 整理 53256,Null,Var,Trim,ML=6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_内在权利
                                            ScaOp_Identifier QCOL:[cdc].col4
                                            ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=4)
                                    ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=0)
                                ScaOp_Const TI(varchar collat​​e 53256,Var,Trim,ML=1) XVAR(varchar,Owned,Value=Len,Data = (0,))
                                ScaOp_Intrinsic 子串
                                    ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=6)
                                    ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=1)
                                    ScaOp_Identifier QCOL:[cdc].col4
            LogOp_Get TBL:dbo.table5(别名 TBL:a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL:[a5].col2
                    ScaOp_Identifier QCOL:[cdc].col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4].col2
                    ScaOp_Identifier QCOL:[cdc].col2

加载统计信息并在设置初始连接顺序后立即对树执行初始基数估计。以不同的顺序进行连接也会影响这些估计,因此在以后基于成本的优化过程中会产生连锁反应。

最后,对于本节,将外部连接卡在树的中间可以防止在基于成本的优化过程中进一步匹配连接重新排序规则。


使用计划指南(或等效的USE PLAN提示 -查询示例将搜索策略更改为更加面向目标的方法,提供的模板的一般形状和功能为指导。这解释了为什么当使用计划指南或提示时优化器可以table1针对计算和非计算列模式找到相同的搜索计划。

我们是否可以做一些不同的事情来实现搜索

如果优化器自己没有找到具有可接受的性能特征的计划,那么您只需要担心这一点。

所有正常的调优工具都可能适用。例如,您可以将查询分解为更简单的部分、查看和改进可用的索引、更新或创建新的统计信息……等等。

所有这些事情都会影响基数估计、通过优化器采用的代码路径,并以微妙的方式影响基于成本的决策。

您最终可能会求助于使用提示(或计划指南),但这通常不是理想的解决方案。


评论中的其他问题

我同意最好简化查询等,但是有没有办法(跟踪标志)让优化器继续优化并达到相同的结果?

不,没有跟踪标志来执行详尽的搜索,您也不想要。可能的搜索空间是巨大的,超过宇宙年龄的编译时间不会被接受。此外,优化器并不知道每一种可能的逻辑转换(没有人知道)。

另外,为什么需要复杂的扩展,因为列是持久的?为什么优化器不能避免扩展它,把它当作一个普通的列,并达到相同的起点?

计算列被扩展(就像视图一样)以实现额外的优化机会。扩展可能会在过程后期匹配回例如持久化的列或索引,但这会在初始连接顺序固定后发生。