Table SQL 服务器中的更新减慢了子查询中的索引查找

Table UPDATE in SQL Server slows down an Index Seek in a subquery

我在 SQL Server Management Studio 18 中有以下查询,我们称之为查询 1:

SELECT
    stage.IDContratto,
    SUM(stageReg.Costo) AS Costo
FROM STAGING.TabContrattiRedditivita AS stage
INNER JOIN STAGING.TabCommesse AS stageCom ON stage.CodiceContratto = stageCom.CodiceContrattoCommessa
INNER JOIN STAGING.TabRegistrazioneOreRisorse AS stageReg
    ON stageCom.CodiceCommessa = stageReg.CodiceCommessaCalcolato
    AND stageReg.DataRegistrazione BETWEEN stage.StartDate AND stage.EndDate
WHERE stageCom.SeMotivoNonFatturabilePerditaCommessa = 1
GROUP BY stage.IDContratto

TabContrattiRedditivita 有 16K 行,TabCommesse 有 49K 行,TabRegistrazioneOreRisorse 有 680 万行。查询 1 returns 1.200 行。由于我在 TabRegistrazioneOreRisorse 上设置了 IX_CostiCommessa 非聚集索引(详情如下),此查询在大约 3 分钟内完成,总而言之,这对我来说很好。可以看到实际执行计划here.

但是我实际上在 TabContrattiRedditivita 的更新中使用了 Query1,我们称它为 Query2:

UPDATE STAGING.TabContrattiRedditivita
SET
    ActualCostoCommesseNonFatturanti += costi.Costo,
    TotaleCostoCommesseNonFatturanti += costi.Costo
FROM STAGING.TabContrattiRedditivita AS stage
INNER JOIN (Query1) AS costi ON stage.IDContratto = costi.IDContratto

并且查询 2 在 16 分钟或更长时间内完成,这不太好。可以看到实际执行计划here.

你可能认为这是写操作的问题,但在下面我报告一些奇怪的事实让我认为它不是。

首先,Query1 returns 只有 1.200 行,因此写入操作微不足道(在我的 ETL 中,我将 UPDATE 提高了 2 到 3 个数量级,没有任何性能问题)。其次,正如您在上面看到的,Query2 中的子查询 Query1 的实际执行计划看起来与单独执行的 Query1 的实际执行计划相同(当然,百分比除外)。第三,关于 Query2 的实时统计数据似乎表明 TabRegistrazioneOreRisorse 上的索引查找正在减慢 Query2,而不是 UPDATE 操作,它花费的时间不到 1 秒(注意总 运行时间是 17 分 11 秒):

这与在 Query1 中只用了大约 3 分钟的索引查找相同(总 运行 时间:3 分 10 秒):

所以似乎仅仅是更新的存在导致 Query1 甚至在 执行更新之前显着减慢。

转折点来了:如果我将我的数据仓库 tables TabContrattiRedditivita、TabCommesse 和 TabRegistrazioneOreRisorse 分别复制到临时 tables #Tab1、#Tab2 和 #Tab3 中,然后我创建相同的这些 temp tables 上的 PK 和索引,然后一切突然都起作用了。查询 1:

SELECT
    stage.IDContratto,
    SUM(stageReg.Costo) AS Costo
FROM #Tab1 AS stage
INNER JOIN #Tab2 AS stageCom ON stage.CodiceContratto = stageCom.CodiceContrattoCommessa
INNER JOIN #Tab3 AS stageReg
    ON stageCom.CodiceCommessa = stageReg.CodiceCommessaCalcolato
    AND stageReg.DataRegistrazione BETWEEN stage.StartDate AND stage.EndDate
WHERE stageCom.SeMotivoNonFatturabilePerditaCommessa = 1
GROUP BY stage.IDContratto

执行时间约3分钟,与之前的Query1相同;实际执行计划 here。查询 2:

UPDATE #Tab1
SET
    ActualCostoCommesseNonFatturanti += costi.Costo,
    TotaleCostoCommesseNonFatturanti += costi.Costo
FROM #Tab1 AS stage
INNER JOIN (Query1) AS costi ON stage.IDContratto = costi.IDContratto

执行时间约为3分10秒,而不是像之前的Query2那样的16或17分钟;实际执行计划 here.

怎么会这样?关于如何解决这个问题的任何线索?


注意:我也尝试了几个替代方案,但都没有效果。

我尝试使用#temp table:我将 Query1 INTO #temp,然后以这种方式执行 Query2:

UPDATE STAGING.TabContrattiRedditivita
SET
    ActualCostoCommesseNonFatturanti += costi.Costo,
    TotaleCostoCommesseNonFatturanti += costi.Costo
FROM STAGING.TabContrattiRedditivita AS stage
INNER JOIN #temp AS costi ON stage.IDContratto = costi.IDContratto

结果是一样的,但这次 Query1 是慢的部分:Query1 运行了 16 分钟,Tab3 上的 Index Seek 非常慢,然后 Query2 运行了几秒钟。

我也尝试过以两种方式使用 CTE。方式一:

WITH CostoRegistrazioni AS (Query1)

UPDATE STAGING.TabContrattiRedditivita
SET
    ActualCostoCommesseNonFatturanti += costi.Costo,
    TotaleCostoCommesseNonFatturanti += costi.Costo
FROM STAGING.TabContrattiRedditivita AS stage
INNER JOIN CostoRegistrazioni AS costi ON stage.IDContratto = costi.IDContratto

方式二:

WITH updateStage AS (
    SELECT
        ActualCostoCommesseNonFatturanti,
        TotaleCostoCommesseNonFatturanti,
        costi.Costo
    FROM STAGING.TabContrattiRedditivita AS stage
    INNER JOIN (Query1) AS costi ON stage.IDContratto = costi.IDContratto
)
UPDATE updateStage
SET
    ActualCostoCommesseNonFatturanti += Costo,
    TotaleCostoCommesseNonFatturanti += Costo

两种情况下的结果相同:Query1 运行 16 分钟,TabRegistrazioneOreRisorse 上的索引查找非常慢。


技术细节

  1. 在上面的执行计划中,您在 PK_TableName 上看到聚簇索引扫描,PK_TableName 只是 SQL 服务器在 table 上创建的标准聚簇索引' PK。 IX_CostiCommessa 改为定义如下(在 Tab3 上完全相同):
CREATE NONCLUSTERED INDEX [IX_CostiCommessa]
ON [STAGING].[TabRegistrazioneOreRisorse] (
    [DataRegistrazione] ASC,
    [CodiceCommessaCalcolato] ASC
)
INCLUDE (
    [Costo],
    [SeRisorsaInterna],
    [SeRisolutivo]
)
WITH (
    PAD_INDEX = ON,
    STATISTICS_NORECOMPUTE = OFF,
    SORT_IN_TEMPDB = OFF,
    DROP_EXISTING = OFF,
    ONLINE = OFF,
    ALLOW_ROW_LOCKS = ON,
    ALLOW_PAGE_LOCKS = ON,
    FILLFACTOR = 100
)
  1. Temp table定义如下:
SELECT *
INTO #Tab1
FROM STAGING.TabContrattiRedditivita

SELECT *
INTO #Tab2
FROM STAGING.TabCommesse

SELECT *
INTO #Tab3
FROM STAGING.TabRegistrazioneOreRisorse
  1. 您可能已经注意到在上面的实际执行计划中有一些遗漏的索引通知。我已经尝试创建它们,唯一的影响是减慢了查询速度(甚至是 Query1)。

  2. 您可能在 Query2 的实际执行计划中看到的警告是 ExcessiveGrant,我不确定如何解释:

直接更新可更新的 CTE 应该会更快,因为您不需要重新查询 #Tab1:

WITH costi AS (
    SELECT
      stage.ActualCostoCommesseNonFatturanti,
      stage.TotaleCostoCommesseNonFatturanti,
      SUM(stageReg.Costo) OVER (PARTITION BY stage.IDContratto) AS Costo
    FROM #Tab1 AS stage
    INNER JOIN #Tab2 AS stageCom ON stage.CodiceContratto = stageCom.CodiceContrattoCommessa
    INNER JOIN #Tab3 AS stageReg
       ON stageCom.CodiceCommessa = stageReg.CodiceCommessaCalcolato
      AND stageReg.DataRegistrazione BETWEEN stage.StartDate AND stage.EndDate
    WHERE stageCom.SeMotivoNonFatturabilePerditaCommessa = 1
)
UPDATE costi
SET
  ActualCostoCommesseNonFatturanti += costi.Costo,
  TotaleCostoCommesseNonFatturanti += costi.Costo;

我还推荐以下索引:

stage (IDContratto) INCLUDE (CodiceContratto, StartDate, EndDate, ActualCostoCommesseNonFatturanti, TotaleCostoCommesseNonFatturanti)

stageCom (SeMotivoNonFatturabilePerditaCommessa, CodiceContrattoCommessa) INCLUDE (CodiceCommessa)

stageReg (CodiceCommessaCalcolato, DataRegistrazione) INCLUDE (Costo)

您也可以在 stageCom

上创建过滤索引
stageCom (CodiceContrattoCommessa) INCLUDE (CodiceCommessa, SeMotivoNonFatturabilePerditaCommessa) WHERE (SeMotivoNonFatturabilePerditaCommessa = 1)