1

On SQL Server 2017(RTM-CU17) with query optimizer hot fixes on I have an indexed view that is taking a considerable amount of time to update. I am at a loss and cannot figure out why a full table scan is occurring for the update.

The indexed view has a sum and is joining two tables on primary key to foreign key with high selectivity (avg 5 foreign rows per primary key). If the primary table row is updated a seek typically occurs to the foreign key table for the aggregate data. If the row has a key that is out of bounds of the histogram (higher than the max value of the RANGE_HI_KEY) it decides to table scan the secondary table with the foreign key, even if the secondary table has the key value in the statistics. What I get in production is an estimated 110 million rows but only 6 actual rows...pretty far off. Since it is hot data in an ascending key it actually reads all 110 million rows before it finds the 6 it needs.

I found a Microsoft issue that was patched that is very similar to what is happening but does not correct it in this situation: https://support.microsoft.com/en-us/help/3192154/a-non-optimal-query-plan-choice-causes-poor-performance-when-values

As I cant share production code I was able to simply recreate it and the plans can be found here, one with an in bound seek, one with an out of bounds scan: https://www.brentozar.com/pastetheplan/?id=SknQqFzy8

Here is the SQL I used to create the plans above...very simple. I am not sure why a scan is happening, any help would be very appreciated!

--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
Ben H
  • 26
  • 4
  • I propose to shift this post to https://dba.stackexchange.com/questions/tagged/sql-server?tab=Newest – KumarHarsh Dec 27 '19 at 08:57
  • As per the post below, if you include a FK in your repro does it still exhibit the issue? – Nick.Mc Dec 27 '19 at 09:01
  • Tried your script on SQL Server 2016 (SP2-CU11) (Developer) and both plans had a nested loops join with index seek on tblTrans_Detail. The second (out of bounds) query had the same plan (merge join&table scan), as you have posted, when executed with OPTION (QUERYTRACEON 4199 or USE HINT ('ENABLE_QUERY_OPTIMIZER_HOTFIXES')). – lptr Dec 27 '19 at 11:11

2 Answers2

1

When column like id INT IDENTITY(1,1) which is ever increasing is your Clustered Index then you do no need to disturb FillFacor.Let it be default Fillfactor 0 or 100.

This will reduce no of pages,so optimizer will have to read less number of pages.

Rebuild Statistics from time to time,to incude outside bound value 721

Update Statistics tbltrans  with FullScan
GO

Foreign key constraint is not define.

 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

Make sure that it is trusted,

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

Trusted FK help Optimizer make better execution plan.

Beside there are several reason for table scan. Optimizer more often make good enough plan quickly which is cost effective.

So all table scan are not really bad,If you force InDex seek using hint then cost of query may increase which is not desirable.

You must have notice that whenever there is insert in tblTrans and its detail then Index view is also updated in same query plan of Insert.That means Insert cost increases because of Updation of view index.

So if tblTrans and its detail tables are highly transactional then you should avoid Index View.

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

Statistics of both table should updated.

**Edit 1**

According to your current situation

Filfactor point is ok even if did not shoe any improvement.

Trusted FK didn't help in this situation because current problem is related with View.

I was able to reproduce the problem.I cannot put execution plan here.

I populated tbltran with 700000 records,and tblTrans_Detail having each FK 7 recors i.e. 700000x7 records.

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.

Currently there is no define relation between these 2 tables or there is no index in tbltrans_detail and tbltrans.

View is not disturb, View is as it is.

So when I run this query, I too get tblTransdetail Table/Index (IX_tblTrans_Detail_TransID) Scan (same problem)

--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

It was reading all rows of tblTrans_Detail to fetch 7 rows.

When I create index like this,

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

I get Index seek on IX_tblTrans_Detail_TransID and

Estimated number of rows=Actial number of Rows=7

Real Trick : To arrange index key Transid in both table in particular manner.Earlier Transid in table tblTrans_Detail were scattered here and there so it was scanning complete table.

Now After creating Non Clustered index index key Transid in tblTrans_Detail in both table are arrange in partuclar manner.So Optimizer quickly find require number of rows.

So may be 110 millions row u still get Index Scan.So may be you should drop the idea of view because View Index is getting updated every time when insert fire.

Update Statistics and Rebuild Index.

Alternatively,

  1. Disable view
  2. Create Filtered Index on tbl trans

    Create NonClustered Index nci_custid_tblTrans on TblTrans (Custid) where Flag=2 Go

Important Notes :

Optimizer most often make good enough plan qucikly in cost effective manner.

You can view both XML Plan of yours, in both plan you can find

StatementOptmEarlyAbortReason="GoodEnoughPlanFound"

So according to Execution Plan there is not really any problem.

KumarHarsh
  • 5,046
  • 1
  • 18
  • 22
  • I'm curious as to whether adding the FK to the sample code in the question stops the table scan. – Nick.Mc Dec 27 '19 at 09:01
  • Thanks for the info on fill factor - I hadnt even noticed it. Changing this to 0 and adding the trusted FK still does not change the scan vs seek behavior. The only thing that changes it updating statistics with full scan so all of the hot data is in bounds. I did find an interesting "bug" reported that is very similar. https://feedback.azure.com/forums/908035-sql-server/suggestions/32896771-poor-cardinality-estimation-on-value-outside-max-h – Ben H Dec 30 '19 at 14:36
  • @BenH , can you say how many records in `dbo.tblTrans.Flag = 2` out of 110 millions ? – KumarHarsh Dec 31 '19 at 08:08
  • @KumarHarsh , there are just under 3 million with flag 2 – Ben H Dec 31 '19 at 13:55
  • @BenH, Is the query taking too much time/reasonable time or same time.Read my edited. – KumarHarsh Jan 03 '20 at 11:14
  • @KumarHarsh - thanks for all your help looking into this. I am going to attempt to disable view and create indexes. I did some research using selectivity trace flags...it appears its a bug that also affects cascade deletes. I submitted it to MS as a bug: https://feedback.azure.com/forums/908035-sql-server/suggestions/39359128-cascade-deletes-and-indexed-view-updates-causing-f – Ben H Jan 03 '20 at 18:31
  • I had something similar where I was getting a scan of half a million rows instead of a seek of one row. The actual duration was 400ms vs. 200ms so it wasn't a huge issue. I did notice the abort reason for me was `StatementOptmEarlyAbortReason="TimeOut"`. Also it was able to use the seek *before* I deleted a bunch of redundant indexes so it had to re-do the plan and looks like it gave up! So I'm hoping it will find it again soon! – Simon_Weaver Apr 30 '21 at 08:45
0

After additional research I have discovered this is also occurring on cascade deletes as well. Enabling trace flag 2363 shows "Calculator failed. Replanning." and then coming up with "Selectivity: 1" (everything in child table). In-bounds data will seek, any key value higher than highest histogram key range will scan.

I have submitted this to MS as a bug through the following link: https://feedback.azure.com/forums/908035-sql-server/suggestions/39359128-cascade-deletes-and-indexed-view-updates-causing-f

Update: this was patched in SQL Server 2017 CU22: indexed view update is table scanning 110 million rows for 6 actual rows found out of histogram key bounds

Ben H
  • 26
  • 4