索引视图更新是 table 扫描 1.1 亿行以查找超出直方图键边界的 6 个实际行

indexed view update is table scanning 110 million rows for 6 actual rows found out of histogram key bounds

在 SQL Server 2017(RTM-CU17) 上启用了查询优化器修补程序我有一个索引视图需要花费大量时间来更新。我不知所措,无法弄清楚为什么要对更新进行完整的 table 扫描。

索引视图有一个总和,并以高选择性将主键上的两个 table 连接到外键(每个主键平均 5 个外行)。如果更新主 table 行,则对聚合数据的外键 table 进行查找 通常 。如果该行有一个超出直方图范围的键(高于 RANGE_HI_KEY 的最大值),它决定 table 使用外键扫描辅助 table,即使如果辅助 table 在统计信息中具有键值。我在生产中得到的估计是 1.1 亿行,但实际只有 6 行……相距甚远。由于它是升序键中的热数据,它实际上在找到所需的 6 行之前读取了所有 1.1 亿行。

我发现了一个已修补的 Microsoft 问题,该问题与正在发生的情况非常相似,但在这种情况下没有更正:https://support.microsoft.com/en-us/help/3192154/a-non-optimal-query-plan-choice-causes-poor-performance-when-values

由于我无法共享生产代码,所以我能够简单地重新创建它,并且可以在此处找到计划,一个是入界搜索,一个是越界扫描: https://www.brentozar.com/pastetheplan/?id=SknQqFzy8

这是我用来制定上述计划的 SQL...非常简单。我不确定为什么要进行扫描,非常感谢任何帮助!

--SQL Server 2017 (TRM-CU17), compatability 140, Query Optimizer Hotfixes on

CREATE TABLE dbo.tblTrans
(id INT IDENTITY(1,1) NOT NULL,
CustID INT NOT NULL,
Flag SMALLINT NOT NULL)
GO

ALTER TABLE [dbo].[tblTrans] ADD CONSTRAINT [PK_tblTrans_ID] PRIMARY KEY CLUSTERED ([ID]) WITH (FILLFACTOR=90) ON [PRIMARY]
GO

--insert random sample of data
INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),9)
GO 10

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),3)
GO 225

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),7)
GO 25

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),4)
GO 185

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),5)
GO 150

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),8)
GO 15

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),2)
GO 110

CREATE TABLE dbo.tblTrans_Detail
(id INT IDENTITY(1,1) NOT NULL,
transid INT NOT NULL,
Amount MONEY NOT NULL)
GO

ALTER TABLE [dbo].[tblTrans_Detail] ADD CONSTRAINT [PK_tblTrans_Detail_ID] PRIMARY KEY CLUSTERED ([ID]) WITH (FILLFACTOR=90) ON [PRIMARY]
GO

--insert random data into detail table
INSERT INTO dbo.tblTrans_Detail (transid, Amount)
SELECT id, CustID+10 AS amount
FROM dbo.tblTrans

INSERT INTO dbo.tblTrans_Detail (transid, Amount)
SELECT id, CustID+12 AS amount
FROM dbo.tblTrans

INSERT INTO dbo.tblTrans_Detail (transid, Amount)
SELECT id, CustID+13 AS amount
FROM dbo.tblTrans

GO

CREATE VIEW [dbo].[ivw_Get_Trans]
WITH SCHEMABINDING
AS
SELECT
dbo.tblTrans.CustID ,
SUM(dbo.tblTrans_Detail.Amount) AS Amount,
COUNT_BIG(*) AS CBCount
FROM dbo.tblTrans
INNER JOIN dbo.tblTrans_Detail
ON tblTrans.id = tblTrans_Detail.TransID
WHERE ( dbo.tblTrans.Flag = 2 )
GROUP BY dbo.tblTrans.CustID
GO

CREATE UNIQUE CLUSTERED INDEX [idx_vw_Trans] ON [dbo].[ivw_Get_Trans] (
[CustID]
) WITH (FILLFACTOR=90, STATISTICS_NORECOMPUTE=OFF) ON [PRIMARY]
GO

--DBCC SHOW_STATISTICS ('dbo.tbltrans',pk_tbltrans_id)
--MAX RANGE_HI_KEY is 720 (also max id from table), key is selective

INSERT INTO dbo.tbltrans (CustID, Flag)
OUTPUT Inserted.id
VALUES (100, 5)
GO

--ID 721 is inserted
INSERT INTO dbo.tbltrans_detail (transid, Amount)
VALUES (721,13.00)
GO

--new ID is in stats of detail table but not stats for trans table
CREATE INDEX IX_tblTrans_Detail_TransID ON dbo.tbltrans_detail (TransID, Amount)
GO

--DBCC SHOW_STATISTICS ('dbo.tbltrans_detail',ix_tbltrans_detail_Transid)

--DBCC FREEPROCCACHE

--set showplan on

BEGIN TRANSACTION

UPDATE dbo.tblTrans
SET Flag = 2
WHERE ID = 720 --seek (highest value in histogram, tblTrans)
--WHERE ID = 721 --scan (out of bounds in histogram, tblTrans)

UPDATE dbo.tblTrans
SET Flag = 2
--WHERE ID = 720 --seek (highest value in histogram, tblTrans)
WHERE ID = 721 --scan (out of bounds in histogram, tblTrans)

ROLLBACK TRANSACTION

--DROP view dbo.ivw_Get_Trans
--drop table dbo.tblTrans
--drop table dbo.tblTrans_Detail

当像 id INT IDENTITY(1,1) 这样不断增加的列是您的 Clustered Index 时,您无需打扰 FillFacor。让它默认为 Fillfactor 0 或 100。

这将减少页数,因此优化器将不得不读取更少的页数。

不时重建统计数据,以包含外部边界值 721

Update Statistics tbltrans  with FullScan
GO

未定义外键约束。

 ALTER TABLE dbo.tblTrans_Detail WITH CHECK
     ADD CONSTRAINT FK_tbltrans_Id FOREIGN KEY (TransId)
     REFERENCES dbo.tbltrans(id) 

 ALTER TABLE dbo.tblTrans_Detail with check check CONSTRAINT FK_tbltrans_Id

确保它是可信的,

SELECT name, is_disabled, is_not_trusted
FROM sys.foreign_keys
WHERE name = 'FK_tbltrans_Id'

受信任的 FK 帮助优化器制定更好的执行计划。

此外 table scan 还有几个原因。 Optimizer 更频繁地快速制定足够好的计划,这是符合成本效益的。

所以所有 table 扫描都不是很糟糕,如果你强制使用提示进行 InDex 搜索,那么查询成本可能会增加,这是不可取的。

你必须注意到,只要在 tblTrans 中有插入及其详细信息,索引视图也会在相同的查询计划中更新 Insert.That 意味着插入成本增加,因为视图索引的更新。

因此,如果 tblTrans 及其详细信息 table 具有高度事务性,那么您应该避免使用索引视图。

new ID is in stats of detail table but not stats for trans table

两个 table 的统计数据应该更新。

**Edit 1**

根据你现在的情况

即使鞋子没有任何改进,Filfactor 点也可以。

Trusted FK 在这种情况下没有帮助,因为当前问题与视图有关。

我能够重现 problem.I 无法在此处放置执行计划。

我用 700000 条记录填充了 tbltran,tblTrans_Detail 每个 FK 7 条记录,即 700000x7 条记录。

CREATE TABLE dbo.tblTrans
(id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
CustID INT NOT NULL,
Flag SMALLINT NOT NULL)
GO

declare @i int=1
declare @Flag int=1
while (@i<70000)
begin

if(@i%2=0)
set @Flag=2
else if(@i%3=0)
set @Flag=3
else if(@i%7=0)
set @Flag=7
else if(@i%5=0)
set @Flag=5

INSERT INTO dbo.tblTrans (CustID, Flag)
VALUES (FLOOR(RAND()*1000),@Flag)

set @i=@i+1
end


CREATE TABLE dbo.tblTrans_Detail
(id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
transid INT NOT NULL,
Amount MONEY NOT NULL)
GO

    INSERT INTO dbo.tblTrans_Detail with (tablock)(transid, Amount)
    SELECT id, CustID+10 AS amount
    FROM dbo.tblTrans,tblnumber
    where number<=7

where `tblnumber` is number table ,you can create `#temp` table containing 1 to 20 rows maximum.

目前这2个table之间没有定义关系或者tbltrans_detailtbltrans中没有索引。

风景不打扰,风景如画

所以当我 运行 这个查询时,我也得到了 tblTransdetail Table/Index (IX_tblTrans_Detail_TransID) Scan(同样的问题)

--dbcc freeproccache
BEGIN TRANSACTION

UPDATE dbo.tblTrans
SET Flag = 2
--WHERE ID = 720 --seek (highest value in histogram, tblTrans)
WHERE ID = 70000  --1--scan (out of bounds in histogram, tblTrans)

ROLLBACK TRANSACTION

正在读取 tblTrans_Detail 的所有行以获取 7 行。

当我这样创建索引时,

CREATE NONCLUSTERED INDEX [IX_tblTrans_Detail] ON [dbo].[tblTrans_Detail]
(
    [transid] ASC
)include(Amount)
GO 

我在 IX_tblTrans_Detail_TransID

上进行了索引查找
Estimated number of rows=Actial number of Rows=7

真正的技巧:在 table 中安排索引键 Transid,特别是 manner.Earlier table tblTrans_Detail 中的 Transid 散落在这里和那里,因此扫描完成 table.

现在在 Non Clustered index 中创建 Non Clustered index 索引键 TransidtblTrans_Detail 中 table 都在 partuclar manner.So 中排列优化器快速找到需要的行数。

所以可能 110 millions row 你仍然得到索引 Scan.So 可能你应该放弃视图的想法,因为每次插入时视图索引都会更新。

Update Statistics and Rebuild Index.

或者,

  1. 禁用视图
  2. 在 tbl trans 上创建过滤索引

    在 TblTrans (Custid) 上创建非聚集索引 nci_custid_tblTrans 其中标志 = 2 去

重要提示:

优化器通常以经济高效的方式快速制定足够好的计划。

您可以查看您的两个XML计划,在两个计划中您都可以找到

StatementOptmEarlyAbortReason="GoodEnoughPlanFound"

所以根据执行计划确实没有任何问题。

经过进一步研究,我发现这也发生在级联删除上。启用跟踪标志 2363 显示“计算器失败。重新规划。”然后提出“选择性:1”(child table 中的所有内容)。 In-bounds 将查找数据,任何高于最高直方图键范围的键值将被扫描。

我已通过以下 link 将其作为错误提交给 MS:https://feedback.azure.com/forums/908035-sql-server/suggestions/39359128-cascade-deletes-and-indexed-view-updates-causing-f

更新:已在 SQL Server 2017 CU22 中修补: