无法执行查询,甚至无法生成估计执行计划

saa*_*aul 12 sql-server execution-plan sql-server-2019 query-performance

我正在研究SQL Server 2019

我有一个表dbo.AllDates其中包含从19902050的所有日期。我有另一个表dbo.ActualExchangeRates,其中我有在给定来源中找到汇率的日期某些货币的实际汇率。

我正在尝试编写一个查询来获取2010 年2020 年之间所有日期的所有货币。如果找到比率,则写入比率,否则写入NULL

鉴于这种情况和下面给出的代码,有人可以帮助我理解为什么SELECT查询没有生成任何结果,甚至无法看到估计的执行计划吗?

CREATE TABLE dbo.AllDates(Date date)
CREATE TABLE dbo.ActualExchangeRates(Date date, Currency char(3), Rate real)

--Query 1: Not generating any results or estimated plan
SELECT      d.Date, m.Currency, c.Rate
FROM        dbo.AllDates d
INNER JOIN  (
    select
      currency,
      '20100101' as mindate,
      '20201231' as maxdate
    from dbo.ActualExchangeRates
    group by currency
) as m on d.date between m.mindate and m.maxdate
LEFT JOIN   dbo.ActualExchangeRates C ON C.Currency = m.Currency and c.Date = d.Date;
Run Code Online (Sandbox Code Playgroud)

针对空表运行查询 9 分钟后出现以下错误:

消息 701,级别 17,状态 123,第 5 行
资源池“默认”中没有足够的系统内存来运行此查询。

似乎这取决于 SQL Server 在出现错误之前有多少可用内存。对我来说,鉴于表中没有数据,这看起来像是 SQL 引擎中的一个错误。

现在,我知道上面的查询可以用多种不同的方式编写,其他方式也可以生成结果,但我的问题是为什么 SQL Server 会永远挂起该查询,即使两个表都是空的

Mar*_*ith 14

这肯定是一个错误。

我无法告诉您该错误的确切性质的具体细节,但一些细节如下。

在编译执行计划时,Visual Studio 性能分析器显示线程的 CPU 时间消耗如下。

在此输入图像描述

所以大量的CPU时间被花费在

... -> sqllang.dll!COptContext::NormalizeQuery -> ... sqllang.dll!COptContext::PexprTransformTopLevel -> sqllang.dll!CSubRuleImpliedPredInnerAndAllLeftJn::BuildSubstitutes -> sqllang.dll!OptimizerUtil::PexprCreateConjOrDisj

并在

... -> sqllang.dll!COptContext::NormalizeQuery -> ... sqllang.dll!COptContext::PexprTransformTopLevel -> sqllang.dll!COptExpr::DeriveGroupProperties

堆栈显示问题是在编译的查询规范化阶段出现的,并且似乎与规则相关RuleImpliedPredInnerAndAllLeftJn

当使用 禁用该规则时,该问题确实会消失option(queryruleoff ImpliedPredInnerAndAllLeftJn),但这不是问题的良好解决方案(未记录并且可能会影响性能,因为它不再将隐含谓词 of 推[C].[Date]>='2010-01-01' AND [C].[Date]<='2020-12-31'送到 的读取dbo.ActualExchangeRates C)。

@charlieface在对该问题的评论中给出了一个更简单的解决方法,因为当mindate/maxdate被转换为date字符串而不是保留为字符串时,该问题也会消失。

添加option(querytraceon 3604, querytraceon 2373)表明编译似乎已经进入了某种派生属性的循环,LogOp_LeftOuterJoin -> ScaOp_Comp -> ScaOp_Comp -> ScaOp_Logical -> ScaOp_Const -> ScaOp_Comp -> ScaOp_Const -> ScaOp_Comp -> ScaOp_Logical -> ScaOp_Logical -> LogOp_Select -> LogOp_LeftOuterJoin -> ...内存使用量稳步增加,直到最终耗尽内存并且编译尝试结束。(显示几个电路的示例输出

在我的开发机器上

select promised
from sys.dm_exec_query_transformation_stats
where name = 'ImpliedPredInnerAndAllLeftJn'
Run Code Online (Sandbox Code Playgroud)

每次运行失败,之前和之后的数据都会增加约 390,000。


Pau*_*ite 8

这是产品缺陷,马丁·史密斯已经回答过

在某些情况下,优化器会陷入一个循环,将与从早期连接派生的相同的隐含谓词添加到外连接中。最终,添加了如此多的重复谓词,导致进程耗尽内存。

要遇到此问题,您需要:

  • 不符合将其视为单个 n 元连接的不同类型的多个连接
  • ON在引用常量文字投影列的子句中指定的连接谓词
  • 不带表达式的列引用的单独简单等值连接
  • 两个连接中的相同列引用允许谓词推断
  • 不同的基类型在比较之前需要显式转换
  • 第一个连接中的两个不等式(BETWEEN扩展到两个不等式)
  • 隐含范围的两端都需要显式转换
  • 转换必须是恒定可折叠的

满足所有这些条件的简化示例:

DECLARE @T1 AS table (i integer NULL);

SELECT NULL
FROM @T1 AS T1
JOIN (VALUES ('1', '9')) AS V (f, t)
    ON T1.i > V.f
    AND T1.i < V.t
LEFT JOIN @T1 AS T2
    ON T2.i = T1.i;
Run Code Online (Sandbox Code Playgroud)

第一个连接获取带有显式转换的树表示 ( ScaOp_Convert):

DECLARE @T1 AS table (i integer NULL);

SELECT NULL
FROM @T1 AS T1
JOIN (VALUES ('1', '9')) AS V (f, t)
    ON T1.i > V.f
    AND T1.i < V.t
LEFT JOIN @T1 AS T2
    ON T2.i = T1.i;
Run Code Online (Sandbox Code Playgroud)

第二个连接获取隐含谓词

LogOp_Select
LogOp_Join
    LogOp_Get TBL: @T1(alias TBL: T1) @T1
    LogOp_ConstTableGet (1) [empty]
    ScaOp_Const TI(bit,ML=1) XVAR(bit,Not Owned,Value=1)
ScaOp_Logical x_lopAnd
    ScaOp_Comp x_cmpGt
    ScaOp_Identifier QCOL: [T1].i
    ScaOp_Convert int,Null,ML=4
        ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=1) XVAR(varchar,Not Owned,Value=Len,Data = (1,1))
    ScaOp_Comp x_cmpLt
    ScaOp_Identifier QCOL: [T1].i
    ScaOp_Convert int,Null,ML=4
        ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=1) XVAR(varchar,Not Owned,Value=Len,Data = (1,9))
Run Code Online (Sandbox Code Playgroud)

推断谓词已常量折叠为T2.i > 1 AND T2.i < 9

这很好,但优化器现在无法判断这些谓词是否与从其他连接推断出的内容完全匹配,这涉及从字符串类型到整数的显式转换。关于优化器推断隐含约束和域范围的方式,还有更多内容,但这就是要点。

结果是隐含谓词规则再次匹配并生成一组重复的推断谓词:

LogOp_Select
LogOp_Get TBL: @T1(alias TBL: T2)
ScaOp_Logical x_lopAnd
    ScaOp_Comp x_cmpGt
    ScaOp_Identifier QCOL: [T2].i
    ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
    ScaOp_Comp x_cmpLt
    ScaOp_Identifier QCOL: [T2].i
    ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [T2].i
ScaOp_Identifier QCOL: [T1].i
Run Code Online (Sandbox Code Playgroud)

下一次迭代后:

LogOp_Select
    LogOp_Select
        LogOp_Get TBL: @T1(alias TBL: T2) @T1
        ScaOp_Logical x_lopAnd
        ScaOp_Comp x_cmpGt
            ScaOp_Identifier QCOL: [T2].i
            ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
        ScaOp_Comp x_cmpLt
            ScaOp_Identifier QCOL: [T2].i
            ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
    ScaOp_Logical x_lopAnd
        ScaOp_Comp x_cmpGt
        ScaOp_Identifier QCOL: [T2].i
        ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
        ScaOp_Comp x_cmpLt
        ScaOp_Identifier QCOL: [T2].i
        ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
    ScaOp_Comp x_cmpEq
    ScaOp_Identifier QCOL: [T2].i
    ScaOp_Identifier QCOL: [T1].i
Run Code Online (Sandbox Code Playgroud)

...等等,直到内存耗尽。

该问题需要一系列非常特殊的异常情况才能显现出来。因此,如问题正文中所述,可以轻松地解决查询重写问题。在这种特殊情况下,还可以使用未记录的跟踪标志 2324 禁用隐含谓词的应用,该标志可以在查询级别使用 指定QUERYTRACEON