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 上的索引查找非常慢。
技术细节
- 在上面的执行计划中,您在 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
)
- Temp table定义如下:
SELECT *
INTO #Tab1
FROM STAGING.TabContrattiRedditivita
SELECT *
INTO #Tab2
FROM STAGING.TabCommesse
SELECT *
INTO #Tab3
FROM STAGING.TabRegistrazioneOreRisorse
您可能已经注意到在上面的实际执行计划中有一些遗漏的索引通知。我已经尝试创建它们,唯一的影响是减慢了查询速度(甚至是 Query1)。
您可能在 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)
我在 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 上的索引查找非常慢。
技术细节
- 在上面的执行计划中,您在 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
)
- Temp table定义如下:
SELECT *
INTO #Tab1
FROM STAGING.TabContrattiRedditivita
SELECT *
INTO #Tab2
FROM STAGING.TabCommesse
SELECT *
INTO #Tab3
FROM STAGING.TabRegistrazioneOreRisorse
您可能已经注意到在上面的实际执行计划中有一些遗漏的索引通知。我已经尝试创建它们,唯一的影响是减慢了查询速度(甚至是 Query1)。
您可能在 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)