性能-选择带有左联接和空检查的查询

时间:2020-02-25 14:28:47

标签: sql sql-server

我有两个不同的表,分别称为Processing (30M records for now)EtlRecord (4.3M records for now)。 就像表名所暗示的那样,这些表将用于通过ETL进行数据标准化。

我们正在尝试处理具有批处理的记录,每个批处理中有1000条记录。

SELECT TOP 1000 P.StreamGuid
FROM [staging].[Processing] P           (NOLOCK)
LEFT JOIN [core].[EtlRecord] E          (NOLOCK)    ON E.StreamGuid = P.StreamGuid
WHERE E.StreamGuid IS NULL
AND P.CompleteDate IS NOT NULL
AND P.StreamGuid IS NOT NULL

执行此查询大约需要20秒钟。而且,我们希望有越来越多的数据,尤其是在EtlRecord表中。为了提高此查询的性能,我检查了下面共享的实际执行计划。

Execution plan

如您所见,最耗时的部分是索引查找以确定EtlRecord表中的空记录。我尝试了一些更改,但无法对其进行改进。

附加说明

  • 按执行计划提出的所有建议索引已应用于表。因此没有进一步的索引建议。
  • Processing表中有8列是布尔标志,EtlRecord表中有4列。
  • EtlRecord表仅由单个过程使用。因此,事务锁定没有问题。

任何改进此查询的建议都会很有帮助。

3 个答案:

答案 0 :(得分:1)

好吧,在您的查询中,您需要从[staging].[Processing]获取记录,而[core].[EtlRecord]中没有相应的记录。

您可以先删除继续的记录。

DELETE [staging].[Processing]
FROM [staging].[Processing] P           
INNER JOIN [core].[EtlRecord] E
    ON E.StreamGuid = P.StreamGuid;

如果需要,可以对批次使用删除。删除这些记录将简化我们的初始查询,并简化uniqueidentifier的联接。您只需要对每个批次执行以下操作:

SELECT TOP 1000 StreamGuid
INTO #buffer
FROM [staging].[Processing]
WHERE CompleteDate IS NOT NULL
    AND StreamGuid IS NOT NULL;

-- do whatevery you need with this records 

DELETE FROM [staging].[Processing]
WHERE StreamGuid IN (SELECT StreamGuid FROM #buffer);

此外,您已经说过已经创建了所有索引,但是执行计划建议的索引并不总是最好的。这部分:

WHERE CompleteDate IS NOT NULL
    AND StreamGuid IS NOT NULL;

似乎是filtered index的很好的候选者,尤其是对于其中的一列,如果大量行具有NULL值的话。

答案 1 :(得分:1)

首先,DDL和易于使用的示例数据(如下所示)将有很大帮助。您可以复制/粘贴我的解决方案,然后在本地运行它们,以查看我在说什么。

IF OBJECT_ID('tempdb..#processing','U') IS NOT NULL DROP TABLE #processing;
IF OBJECT_ID('tempdb..#EtlRecord','U')  IS NOT NULL DROP TABLE #EtlRecord;

SELECT TOP (100) 
  StreamGuid   = NEWID(),
  CompleteDate = CASE WHEN CHECKSUM(NEWID())%3 < 2 THEN GETDATE() END
INTO #processing
FROM sys.all_columns AS a

SELECT TOP (80) p.StreamGuid
INTO   #EtlRecord
FROM   #Processing AS p;

ALTER TABLE #processing ALTER COLUMN StreamGuid UNIQUEIDENTIFIER NOT NULL;
ALTER TABLE #EtlRecord  ALTER COLUMN StreamGuid UNIQUEIDENTIFIER NOT NULL;
GO
ALTER TABLE #processing ADD CONSTRAINT pk_processing PRIMARY KEY CLUSTERED(StreamGuid);
ALTER TABLE #etlRecord  ADD CONSTRAINT pk_etlRecord  PRIMARY KEY CLUSTERED(StreamGuid);
GO

接下来了解到,如果没有ORDER BY子句,则不能保证您的查询每次都返回相同的记录。例如,如果SQL Server选择并行执行计划,则您肯定会获得不同的行。我还看到了包含ORDER BY会真正提高性能的情况。

记住这一点,而不是这个...

SELECT --TOP 1000 
  P.StreamGuid
FROM      #processing AS p
LEFT JOIN #etlRecord  AS e ON e.StreamGuid = p.StreamGuid
WHERE     e.StreamGuid   IS NOT NULL
AND       P.CompleteDate IS NOT NULL

...将返回与此完全相同的东西:

SELECT TOP 1000 
  P.StreamGuid
FROM      #processing AS p
JOIN      #etlRecord  AS e ON e.StreamGuid = p.StreamGuid
WHERE     p.CompleteDate IS NOT NULL;

请注意,WHERE e.StreamGuid = p.StreamGuid已经暗示两个值均为 NOT NULL 。请注意,此查询...

DECLARE @X INT;
SELECT  AreTheyEqual = IIF(@X=@X,'Yep','Nope');

...返回:

AreTheyEqual
------------
Nope

我同意关于过滤索引发布的解决方案@gotqn。使用我的样本数据,您可以添加如下内容:

CREATE NONCLUSTERED INDEX nc_processing ON #processing(CompleteDate,StreamGuid)
    WHERE CompleteDate IS NOT NULL;

然后,您可以在查询中添加ORDER BY CompleteDate,以迫使优化器选择该索引(在我的系统上,除非添加ORDER BY,否则它不会选择该索引)。 ORDER BY将使您查询确定性和更可预测。

答案 2 :(得分:0)

我建议这样写:

SELECT TOP 1000 P.StreamGuid
FROM [staging].[Processing] P
WHERE P.CompleteDate IS NOT NULL AND
      P.StreamGuid IS NOT NULL AND
      NOT EXISTS (SELECT 1
                  FROM [core].[EtlRecord] E 
                  WHERE E.StreamGuid = P.StreamGuid
                 );

我删除了NOLOCK指令。仅当您真正知道自己在做什么时才使用它-并且准备读取无效数据。

那么您肯定要在EtlRecord(StreamGuid)上建立索引。

您可能还希望在Processing(CompleteDate, StreamGuid)上建立索引。这至少是查询的覆盖索引。