【问题标题】:Impact of ordering of correlated subqueries within a projection投影中相关子查询排序的影响
【发布时间】:2011-02-02 16:19:23
【问题描述】:

我注意到 SQL Server(在本例中为 SQL Server 2008)如何处理 select 语句中的相关子查询有些出人意料。我的假设是,查询计划不应该仅仅受到 select 语句的投影子句中写入子查询(或列)的顺序的影响。但是,情况似乎并非如此。

考虑以下两个查询,除了 CTE 中子查询的顺序之外,它们是相同的:

--query 1: subquery for Color is second
WITH vw AS
(
 SELECT p.[ID],
  (SELECT TOP(1) [FirstName] FROM [Preference] WHERE p.ID = ID AND [FirstName] IS NOT NULL ORDER BY [LastModified] DESC) [FirstName],
  (SELECT TOP(1) [Color] FROM [Preference] WHERE p.ID = ID AND [Color] IS NOT NULL ORDER BY [LastModified] DESC) [Color]
 FROM Person p
)
SELECT ID, Color, FirstName
FROM vw
WHERE Color = 'Gray';


--query 2: subquery for Color is first
WITH vw AS
(
 SELECT p.[ID],
  (SELECT TOP(1) [Color] FROM [Preference] WHERE p.ID = ID AND [Color] IS NOT NULL ORDER BY [LastModified] DESC) [Color],
  (SELECT TOP(1) [FirstName] FROM [Preference] WHERE p.ID = ID AND [FirstName] IS NOT NULL ORDER BY [LastModified] DESC) [FirstName]
 FROM Person p
)
SELECT ID, Color, FirstName
FROM vw
WHERE Color = 'Gray';

如果您查看这两个查询计划,您会发现每个子查询都使用了一个外连接,并且连接的顺序与编写子查询的顺序相同。有一个过滤器应用于颜色的外部连接的结果,以过滤掉颜色不是“灰色”的行。 (我很奇怪 SQL 会为颜色子查询使用外连接,因为我对颜色子查询的结果有一个非空约束,但是可以。)

大部分行都被滤色器删除了。结果是查询 2 比查询 1 便宜得多,因为第二个连接涉及的行更少。除了构建这样一个语句的所有原因之外,这是预期的行为吗? SQL server 不应该选择在查询计划中尽早移动过滤器,而不管子查询的编写顺序如何?

编辑: 澄清一下,我正在探索这种情况是有正当理由的。我可能需要创建一个包含类似构造的子查询的视图,现在很明显,任何基于从视图投影的这些列的过滤都会因为列的顺序而在性能上有所不同!

【问题讨论】:

  • 你为什么要使用相关子查询?为什么不使用连接呢?
  • 如果您对每个子查询使用两个 CTE 表而不是一个,查询计划会是什么样子。
  • @HLGEM 我通常会使用连接,但在此示例中,我只对另一个表中的前 1(或 0)值感兴趣,其中可能有很多。
  • @Thomas 我认为我不能将 CTE 用于子查询,因为它们是相关的子查询。我无法对 CTE 进行参数化(尽管如果可以的话,那将非常简洁),所以我会有效地遇到我在下面 gbn 的查询中指出的同样问题。

标签: sql sql-server performance tsql subquery


【解决方案1】:

随着 TOP 运算符在这里发挥作用,查询优化器对统计数据非常盲目,因此它会寻找其他线索来了解如何最好地工作,例如首先实例化 CTE 的相关部分。

这是一个外连接,因为如果没有返回任何内容,子查询将被用作 NULL,并且系统首先将其实例化。如果您使用聚合而不是 TOP,您可能会得到一个稍微不同但更一致的计划。

【讨论】:

  • 您可能更喜欢使用 row_number 来获取结果,因为它可以有效地替代 TOP。
  • 是的,我理解你关于统计的观点,但优化器不应该假设任何过滤在最坏的情况下会留下所有行,但最多会删除许多行吗?此外,如果您使用外连接构造更典型的查询,然后在连接列上设置非空约束,则优化器足够聪明,可以使用内连接。为什么不在这种情况下?
  • 在这种情况下我也不能轻易使用聚合,因为 SQL 中没有空合并聚合。
  • select 子句中的标量子查询永远无法过滤数据集,因此它被实现为外连接。您在其他地方过滤它的事实意味着它可能首先通过执行内部连接来过滤掉 NULL,但是这样做没有任何优势,并且 QO 选择使用外部连接(这通常更快,因为系统不必从其工作集中删除行)。
  • 我知道在这种特殊情况下您不能使用聚合。如果 T-SQL 实现了 FIRST(Field) OVER (ORDER BY ...) 函数会很好,但它还没有。
【解决方案2】:

这是一个性能更好的替代版本:

With Colors As
    (
    Select Id, [Color]
        , ROW_NUMBER() OVER ( PARTITION BY ID ORDER BY [LastModified] DESC ) As Num
    From Preference
    Where [Color] Is Not Null
    )
    , Names As
    (
    Select Id, [FirstName]
        , ROW_NUMBER() OVER ( PARTITION BY ID ORDER BY [LastModified] DESC ) As Num
    From Preference
    Where [FirstName] Is Not Null
    )
Select
From Person As P
    Join Colors As C
        On C.Id = P.Id
            And C.Num = 1
    Left Join Names As N
        On N.Id = P.Id
            And N.Num = 1
Where C.[Color]= 'Grey'

另一个更简洁但性能可能也可能不理想的解决方案:

With RankedItems
    (
    Select Id, [Color], [FirstName]
        , ROW_NUMBER() OVER ( PARTITION BY ID ORDER BY Case When [Color] Is Not Null 1 Else 0 End DESC, [LastModified] DESC ) As ColorRank
        , ROW_NUMBER() OVER ( PARTITION BY ID ORDER BY Case When [FirstName] Is Not Null 1 Else 0 End DESC, [LastModified] DESC ) As NameRank
    From Preference
    )
Select
From Person As P
    Join RankedItems As RI
        On RI.Id = P.Id
            And RI.ColorRank = 1
    Left Join RankedItems As RI2
        On RI2.Id = P.Id
            And RI2.NameRank = 1
Where RI.[Color]= 'Grey'

【讨论】:

  • 谢谢 Thomas,我现在正在检查您的第一个查询,它看起来不错。到目前为止,生成的计划似乎都独立于顺序,并且过滤的连接总是首先执行并作为内部连接。
  • 您的第一个查询在 IO 性能方面似乎也比我的原始查询扩展得更好,因为 SQL 会扫描后续连接,而不是使用重复的索引搜索。第二个查询更简洁(虽然不是很清楚),但不幸的是,它需要对 case 语句的结果进行昂贵的排序。通过对 ID LastModified DESC 进行聚类,我可以轻松避免第一次查询中所需的排序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-12
  • 2012-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多