使用COALESCE(…)优化子查询的选择


8

我在应用程序中使用的视图很大。我认为我已经缩小了性能问题,但是不确定如何解决。视图的简化版本如下所示:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

这可能并不能说明查询结构的全部原因,但也许可以给您一个想法-该视图将两个我设计不佳的非常糟糕的表连接在一起,并尝试从中综合一些信息。

因此,由于这是应用程序中使用的视图,因此在尝试优化时将其包装在另一个SELECT中,如下所示:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

因为该应用程序正在搜索结果中的特定人员。

问题似乎是该COALESCE(pe.StaffName, se.StaffName) AS StaffName部分,而我是从的视图中选择的StaffName。如果将其更改为pe.StaffName AS StaffNameor se.StaffName AS StaffName,性能问题将消失(但请参见下面的更新2)。但这不会这样做,因为其中一侧或另一侧FULL OUTER JOIN可能会丢失,因此一个或另一个字段可能为NULL。

我可以重构这个替换为COALESCE(…)其他东西,将其重写为子查询吗?

其他说明:

  • 我已经添加了一些索引来解决其余查询的性能问题-如果没有的COALESCE话,它很快。
  • 令我惊讶的是,即使WHERE包括包装子查询和语句,查看执行计划也不会引发任何标志。我在分析器中的子查询总费用为0.0065736。mph 执行需要四秒钟。
  • 将应用程序更改为不同的查询(例如,返回pe.StaffName AS PEStaffName, se.StaffName AS SEStaffName和执行WHERE PEStaffName = 'X' OR SEStaffName = 'X'可能会奏效,但作为最后的手段-我真的希望我可以优化视图而不必诉诸应用程序。
  • 为此,存储过程可能更有意义,但是应用程序是使用Entity Framework构建的,我无法弄清楚如何使它与返回表类型的SP完美配合(完全是另一个主题)。

指标

到目前为止,我添加的索引如下所示:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

更新资料

嗯……我尝试模拟上面的严重更改,但没有帮助。即,在) Z上面之前,我添加了AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'),但是性能是相同的。现在我真的不知道从哪里开始。

更新2

@ypercube关于需要完全连接的评论使我意识到我的综合查询遗漏了一个可能重要的组件。虽然是的,但我需要完全连接,我在上面做的测试通过删除COALESCE和测试连接的一侧是否为非null值,将使完全连接的另一侧不相关,优化器可能正在使用此连接事实加快了查询速度。另外,我已经更新了示例,以表明它StaffName实际上是联接键之一,这可能与问题有很大关系。我现在也倾向于他的建议,那就是将其分为三路并举而不是完全加入,这可能是答案,并且将简化COALESCE我正在做的s的工作量。现在尝试。


您添加了哪些索引?您是否在索引中包含StaffName?
马克·辛金森

@MarkSinkinson我在每个表上都有一个非聚集索引KeyField,都INCLUDE对该StaffName字段和其他几个字段进行索引。我可以在问题中发布索引定义。我正在测试服务器上进行这项工作,因此我可以添加您认为可能有助于尝试的任何索引!
S'pht'Kr 2014年

1
您具有WHERE pe.ThisThing = 1 AND se.OtherThing = 0取消联接FULL OUTER并使查询等同于内部联接的条件。您确定需要完全加入吗?
ypercubeᵀᴹ

@ypercube我很抱歉,这对我来说是不好的空中编码,更多的是我在两个表上都有条件,但是是的,我在真实查询中的任何一侧都考虑了null。我正在合并两个表并查找匹配项,但是当左右没有匹配记录时,我需要来自任何一个表的可用数据,所以是的,我需要完全联接。
S'pht'Kr 2014年

1
一个想法:这是一个漫长的尝试,但是您可以尝试将内部查询分为三个部分(INNER JOINLEFT JOIN带有WHERE IS NULLcheck,带有IS NULL的RIGHT JOIN),然后UNION ALL分为三个部分。这样就不需要使用COALESCE()它,并且可能(可能)帮助优化程序找出重写。
ypercubeᵀᴹ

Answers:


4

这是相当长的时间,但是由于OP表示有效,因此我将其添加为答案(如果发现任何错误,请随时进行纠正)。

尝试将内部查询分为三个部分(INNER JOINLEFT JOINWHERE IS NULLcheck,RIGHT JOINIS NULLcheck),然后UNION ALL将其分为三个部分。这具有以下优点:

  • FULL与(比较常见的)INNERLEFT联接相比,优化器可用于联接的转换选项更少。

  • Z派生表可以被删除从视图定义(你可以做到这一点无论如何)。

  • NOT(pe.ThisThing = 1 AND se.OtherThing = 0)仅在将需要INNER连接的一部分。

  • 轻微的改善,使用COALESCE()将是最小如果有的话(我认为se.SEIdpe.PEId不可为空。如果有更多的字段不可为空,你就可以去除更多的COALESCE()调用)。
    更重要的是,优化程序可能在按下任何条件您的查询涉及这些列(现在COALESCE()不阻止推送)。

  • 以上所有内容将为优化器提供更多选项,以转换/重写使用该视图的任何查询,以便它可以找到一个可以使用基础表上的索引的执行计划。

总之,该视图可以写成:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;

0

我的直觉是,这应该不成问题,因为到时候COALESCE(pe.StaffName, se.StaffName) AS StaffName两个源中所有行的所有内容都应该已经被提取并匹配了,因此该函数调用是一个简单的内存中比较为空和-挑。显然不是这种情况,因此也许源之一(如果它们是视图或内联派生表)或基表(即缺乏索引)中的某些内容使查询计划者认为需要单独扫描这些列。

如果没有您正在运行的确切查询的更多细节,支持的结构以及所生成的查询计划,我们建议的只是推测。

要尝试强制进行所有其他操作之间的比较,您可以尝试仅在带表的表(pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName)中选择两个值,然后在外部查询(COALESCE(peStaffName, seStaffName) AS StaffName)中进行选择,或者甚至可以将内部查询的数据推入临时表然后通过从中进行选择来进行外部查询(但这将需要一个存储过程,并且取决于转储到tempdb的行数可能很昂贵,因此本身就存在问题)。


谢谢David,我一直都在妄想症,即使在结构方面我应该披露多少(pe => PatientEvent,所以……),但我知道这使事情变得更加困难。我认为实际上是基于索引进行联接,然后进行“简单的内存比较”来进行过滤…但是未过滤的派生表Z目前返回约150万行。我想要做的是将谓词重写为查询,Z以便使用索引…但是现在我也很困惑,因为当我手动将谓词放在那里时,它仍然不使用索引…所以现在我不确定。
S'pht'Kr 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.