我很难理解为什么SQL Server会提出一个很容易证明与统计数据不一致的估计。
一致性
不能保证一致性。可以使用不同的统计方法在不同时间在不同(但逻辑上等效)子树上计算估计值。
说将这两个相同的子树连接起来应该产生叉积的逻辑没有错,但是同样也没有说推理的选择比其他任何选择都合理的逻辑。
初步估计
在您的特定情况下,不对两个相同的子树执行联接的初始基数估计。当时的树形为:
  LogOp_Join
     LogOp_GbAgg
        LogOp_LeftOuterJoin
           LogOp_Get TBL:ar
           LogOp_Select
              LogOp_Get TBL:tcr
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [tcr] .rId
                 ScaOp_Const值= 508
           ScaOp_Logical x_lopAnd
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .fId
                 ScaOp_Identifier [tcr] .fId
              ScaOp_Comp x_cmpEq
                 ScaOp_Identifier [ar] .bId
                 ScaOp_Identifier [tcr] .bId
        AncOp_PrjList 
           AncOp_PrjEl Expr1003 
              ScaOp_AggFunc stopMax
                 ScaOp_Convert int
                    ScaOp_Identifier [tcr] .isS
     LogOp_Select
        LogOp_GbAgg
           LogOp_LeftOuterJoin
              LogOp_Get TBL:ar
              LogOp_Select
                 LogOp_Get TBL:tcr
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [tcr] .rId
                    ScaOp_Const值= 508
              ScaOp_Logical x_lopAnd
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .fId
                    ScaOp_Identifier [tcr] .fId
                 ScaOp_Comp x_cmpEq
                    ScaOp_Identifier [ar] .bId
                    ScaOp_Identifier [tcr] .bId
           AncOp_PrjList 
              AncOp_PrjEl Expr1006 
                 ScaOp_AggFunc stopMin
                    ScaOp_Convert int
                       ScaOp_Identifier [ar] .isT
              AncOp_PrjEl Expr1007 
                 ScaOp_AggFunc stopMax
                    ScaOp_Convert int
                       ScaOp_Identifier [tcr] .isS
        ScaOp_Comp x_cmpEq
           ScaOp_Identifier Expr1006 
           ScaOp_Const值= 1
     ScaOp_Comp x_cmpEq
        ScaOp_Identifier QCOL:[ar] .fId
        ScaOp_Identifier QCOL:[ar] .fId
第一输入连接已经有一个未投影聚集体简化了,并且第二连接输入具有谓词t.isT = 1推它下面,其中t.isT是MIN(CONVERT(INT, ar.isT))。尽管如此,该isT谓词的选择性计算仍可以CSelCalcColumnInInterval在直方图中使用:
  CSelCalcColumnInInterval
      列:COL:Expr1006 
已从ID为3的统计信息中的QCOL列加载的直方图:[ar] .isT
选择性:4.85248e-005
统计收集生成: 
  CStCollFilter(ID = 11,CARD = 1)
      CStCollGroupBy(ID = 10,CARD = 20608)
          CStCollOuterJoin(ID = 9,CARD = 20608 x_jtLeftOuter)
              CStCollBaseTable(ID = 3,CARD = 20608 TBL:ar)
              CStCollFilter(ID = 8,CARD = 1)
                  CStCollBaseTable(ID = 4,CARD = 28 TBL:tcr)
(正确的)期望是该谓词将20,608行减少为1行。
联合估算
现在的问题是,来自另一个联接输入的20,608行将如何与这一行匹配:
  LogOp_Join
      CStCollGroupBy(ID = 7,CARD = 20608)
          CStCollOuterJoin(ID = 6,CARD = 20608 x_jtLeftOuter)
              ...
      CStCollFilter(ID = 11,CARD = 1)
          CStCollGroupBy(ID = 10,CARD = 20608)
              ...
      ScaOp_Comp x_cmpEq
          ScaOp_Identifier QCOL:[ar] .fId
          ScaOp_Identifier QCOL:[ar] .fId
通常,有几种不同的方法可以估算联接。例如,我们可以:
- 在每个子树中的每个计划运算符处派生新的直方图,在连接处对齐它们(根据需要插入步长值),并查看它们如何匹配;要么
 
- 对直方图执行更简单的“粗略”对齐(使用最小值和最大值,而不是逐步进行);要么
 
- 单独为联接列计算单独的选择性(从基表中进行,无需任何过滤),然后添加非联接谓词的选择性效果。
 
- ...
 
根据所使用的基数估计量和一些启发式方法,可以使用任何这些(或变体)。有关更多信息,请参阅《 Microsoft白皮书使用SQL Server 2014基数估计器优化查询计划》。
虫子?
现在,如问题所述,在这种情况下,“简单”单列联接(在上fId)使用CSelCalcExpressionComparedToExpression计算器:
计算计划:
  CSelCalcExpressionComparedToExpression [ar] .fId x_cmpEq [ar] .fId
列QCOL:[ar] .bId(ID为2的统计信息)的已加载直方图
已加载ID为1的QCOL列的直方图:[ar] .fId
选择性:0
此计算估计将20608行与1个过滤的行连接在一起将具有零选择性:没有行将匹配(在最终计划中报告为一行)。错了吗 是的,这里的新CE可能存在错误。有人可能会争辩说1行将匹配所有行或不匹配任何行,因此结果可能是合理的,但是有理由相信其他情况。
细节实际上是相当棘手的,但是期望基于未过滤的fId直方图进行估计,并通过过滤器的选择性进行修改,从而给20608 * 20608 * 4.85248e-005  = 20608出行是非常合理的。
执行此计算将意味着使用计算器CSelCalcSimpleJoinWithDistinctCounts而不是CSelCalcExpressionComparedToExpression。没有记录的方法可以执行此操作,但是如果您好奇,可以启用未记录的跟踪标志9479:

请注意,最后的联接从两个单行输入产生20,608行,但这并不奇怪。它与原始CE根据TF 9481制定的计划相同。
我提到细节是棘手的(调查很费时),但是据我所知,问题的根本原因与谓词有关rId = 508,选择性为零。该零估计值以通常的方式上升到一行,当它考虑输入树中的较低谓词时,这似乎有助于所讨论的连接处的零选择性估计(因此有的加载统计信息bId)。
允许外部联接保持零行内部估算(而不是提高到一行)(以便所有外部行都合格),可以使用任一计算器提供“无错误”的联接估算。如果您对此感兴趣,则未记录的跟踪标志为9473(单独):

连接基数估计的行为CSelCalcExpressionComparedToExpression也可以修改为不考虑带有另一个未记录的变化标志的``bId''(9494)。我提到所有这些是因为我知道您对这些事情感兴趣。不是因为他们提供了解决方案。除非您向Microsoft报告此问题,然后他们解决(或不解决),否则以不同的方式表达查询可能是最好的解决方法。不管行为是否是故意的,他们都应该对回归有兴趣。
最后,整理一下复制脚本中提到的另一件事:问题计划中Filter的最终位置是基于成本的探索结果,GbAggAfterJoinSel将聚合和filter移到了join之上,因为join输出很小行数。如您所料,过滤器最初位于联接下方。