patternsqlMajor
Eliminate Key Lookup (Clustered) operator that slows down performance
Viewed 0 times
clusteredoperatorslowseliminatethatdownperformancelookupkey
Problem
How can I eliminate a Key Lookup (Clustered) operator in my execution plan?
Table
I put the clustered index column
Execution plan here.
Or view it:
This is what the Key Lookup operator says:
Query:
```
declare
@EffDateFrom datetime ='2017-02-01',
@EffDateTo datetime ='2017-08-28'
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
IF OBJECT_ID('tempdb..#Data') IS NOT NULL
DROP TABLE #Data
CREATE TABLE #Data
(
QuoteID int NOT NULL, --clustered index
[EffectiveDate] [datetime] NULL, --not indexed
[Submitted] [int] NULL,
[Quoted] [int] NULL,
[Bound] [int] NULL,
[Exonerated] [int] NULL,
[ProducerLocationId] [int] NULL,
[ProducerName] varchar NULL,
[BusinessType] varchar NULL,
[DisplayStatus] varchar NULL,
[Agent] [varchar] (50) NULL,
[ProducerContactGuid] uniqueidentifier NULL
)
INSERT INTO #Data
SELECT
tblQuotes.QuoteID,
tblQuotes.EffectiveDate,
CASE WHEN lstQuoteStatus.QuoteStatusID >= 1 THEN 1 ELSE 0 END AS Submitted,
CASE WHEN lstQuoteStatus.QuoteStatusID = 2 or lstQuoteStatus.QuoteStatusID = 3 or lstQuoteStatus.QuoteStatusID = 202 THEN 1 ELSE 0 END AS Quoted,
CASE WHEN lstQuoteStatus.Bound = 1 THEN 1 ELSE 0 END AS Bound,
CASE WHEN lstQuoteStatus.QuoteStatusID = 3 THEN 1 ELSE 0 END AS Exonareted,
tblQuotes.ProducerLocationID,
P.Name + ' / '+ P.City as [ProducerName],
CASE WHEN tblQuotes.PolicyTypeID = 1 THEN 'New Business'
WHEN tblQuotes.PolicyTypeID = 3 THEN 'Rewrite'
END AS BusinessType,
tblQuotes.DisplayStatus,
tblProducerContacts.FName +' '+ tblProducerContacts.LName as Agent,
tblProducerContacts.Producer
Table
tblQuotes already has a clustered index (on QuoteID) and 27 nonclustered indexes, so I am trying not to create any more.I put the clustered index column
QuoteID in my query, hoping it will help - but unfortunately still the same.Execution plan here.
Or view it:
This is what the Key Lookup operator says:
Query:
```
declare
@EffDateFrom datetime ='2017-02-01',
@EffDateTo datetime ='2017-08-28'
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
IF OBJECT_ID('tempdb..#Data') IS NOT NULL
DROP TABLE #Data
CREATE TABLE #Data
(
QuoteID int NOT NULL, --clustered index
[EffectiveDate] [datetime] NULL, --not indexed
[Submitted] [int] NULL,
[Quoted] [int] NULL,
[Bound] [int] NULL,
[Exonerated] [int] NULL,
[ProducerLocationId] [int] NULL,
[ProducerName] varchar NULL,
[BusinessType] varchar NULL,
[DisplayStatus] varchar NULL,
[Agent] [varchar] (50) NULL,
[ProducerContactGuid] uniqueidentifier NULL
)
INSERT INTO #Data
SELECT
tblQuotes.QuoteID,
tblQuotes.EffectiveDate,
CASE WHEN lstQuoteStatus.QuoteStatusID >= 1 THEN 1 ELSE 0 END AS Submitted,
CASE WHEN lstQuoteStatus.QuoteStatusID = 2 or lstQuoteStatus.QuoteStatusID = 3 or lstQuoteStatus.QuoteStatusID = 202 THEN 1 ELSE 0 END AS Quoted,
CASE WHEN lstQuoteStatus.Bound = 1 THEN 1 ELSE 0 END AS Bound,
CASE WHEN lstQuoteStatus.QuoteStatusID = 3 THEN 1 ELSE 0 END AS Exonareted,
tblQuotes.ProducerLocationID,
P.Name + ' / '+ P.City as [ProducerName],
CASE WHEN tblQuotes.PolicyTypeID = 1 THEN 'New Business'
WHEN tblQuotes.PolicyTypeID = 3 THEN 'Rewrite'
END AS BusinessType,
tblQuotes.DisplayStatus,
tblProducerContacts.FName +' '+ tblProducerContacts.LName as Agent,
tblProducerContacts.Producer
Solution
Key lookups of various flavors occur when the query processor needs to obtain values from columns that are not stored in the index used to locate the rows required for the query to return results.
Take for example the following code, where we're creating a table with a single index:
We'll insert 1,000,000 rows into the table so we have some data to work with:
Now, we'll query the data with the option to display the "actual" execution plan:
The query plan shows:
The query looks at the
The "Output List" contains the columns returned by the RID Lookup.
A table with a clustered index and a non-clustered index makes an interesting example. The table below has three columns; ID which is the clustering key,
Take this example query:
We're asking SQL Server to return every column from the table where the
If we modify the non-clustered index so that it includes the
Then re-run the query:
We now see a single non-clustered index seek since SQL Server simply needs to locate the row where
Take for example the following code, where we're creating a table with a single index:
USE tempdb;
IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO
CREATE TABLE dbo.Table1
(
Table1ID int NOT NULL IDENTITY(1,1)
, Table1Data nvarchar(30) NOT NULL
);
CREATE INDEX IX_Table1
ON dbo.Table1 (Table1ID);
GOWe'll insert 1,000,000 rows into the table so we have some data to work with:
INSERT INTO dbo.Table1 (Table1Data)
SELECT TOP(1000000) LEFT(c.name, 30)
FROM sys.columns c
CROSS JOIN sys.columns c1
CROSS JOIN sys.columns c2;
GONow, we'll query the data with the option to display the "actual" execution plan:
SELECT *
FROM dbo.Table1
WHERE Table1ID = 500000;The query plan shows:
The query looks at the
IX_Table1 index to find the row with Table1ID = 5000000 since looking at that index is much faster than scanning the entire table looking for that value. However, to satisfy the query results, the query processor must also find the value for the other columns in the table; this is where the "RID Lookup" comes in. It looks in the table for the row ID (the RID in RID Lookup) associated with the row containing the Table1ID value of 500000, obtaining the values from the Table1Data column. If you hover the mouse over the "RID Lookup" node in the plan, you see this:The "Output List" contains the columns returned by the RID Lookup.
A table with a clustered index and a non-clustered index makes an interesting example. The table below has three columns; ID which is the clustering key,
Dat which is indexed by a non-clustered index IX_Table, and a third column, Oth.USE tempdb;
IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO
CREATE TABLE dbo.Table1
(
ID int NOT NULL IDENTITY(1,1)
PRIMARY KEY CLUSTERED
, Dat nvarchar(30) NOT NULL
, Oth nvarchar(3) NOT NULL
);
CREATE INDEX IX_Table1
ON dbo.Table1 (Dat);
GO
INSERT INTO dbo.Table1 (Dat, Oth)
SELECT TOP(1000000) CRYPT_GEN_RANDOM(30), CRYPT_GEN_RANDOM(3)
FROM sys.columns c
CROSS JOIN sys.columns c1
CROSS JOIN sys.columns c2;
GOTake this example query:
SELECT *
FROM dbo.Table1
WHERE Dat = 'Test';We're asking SQL Server to return every column from the table where the
Dat column contains the word Test. We have a couple of choices here; we can look at the table (i.e. the clustered index) - but that would entail scanning the entire thing since the table is ordered by the ID column, which tells us nothing about which row(s) contain Test in the Dat column. The other option (and the one chosen by SQL Server) consists of seeking into the IX_Table1 non-clustered index to find the row where Dat = 'Test', however since we need the Oth column as well, SQL Server must perform a lookup into the clustered index using a "Key Lookup" operation. This is the plan for that:If we modify the non-clustered index so that it includes the
Oth column:DROP INDEX IX_Table1
ON dbo.Table1;
GO
CREATE INDEX IX_Table1
ON dbo.Table1 (Dat)
INCLUDE (Oth); <---- This is the only change
GOThen re-run the query:
SELECT *
FROM dbo.Table1
WHERE Dat = 'Test';We now see a single non-clustered index seek since SQL Server simply needs to locate the row where
Dat = 'Test' in the IX_Table1 index, which includes the value for Oth, and the value for the ID column (the primary key), which is automatically present in every non-clustered index. The plan:Code Snippets
USE tempdb;
IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO
CREATE TABLE dbo.Table1
(
Table1ID int NOT NULL IDENTITY(1,1)
, Table1Data nvarchar(30) NOT NULL
);
CREATE INDEX IX_Table1
ON dbo.Table1 (Table1ID);
GOINSERT INTO dbo.Table1 (Table1Data)
SELECT TOP(1000000) LEFT(c.name, 30)
FROM sys.columns c
CROSS JOIN sys.columns c1
CROSS JOIN sys.columns c2;
GOSELECT *
FROM dbo.Table1
WHERE Table1ID = 500000;USE tempdb;
IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO
CREATE TABLE dbo.Table1
(
ID int NOT NULL IDENTITY(1,1)
PRIMARY KEY CLUSTERED
, Dat nvarchar(30) NOT NULL
, Oth nvarchar(3) NOT NULL
);
CREATE INDEX IX_Table1
ON dbo.Table1 (Dat);
GO
INSERT INTO dbo.Table1 (Dat, Oth)
SELECT TOP(1000000) CRYPT_GEN_RANDOM(30), CRYPT_GEN_RANDOM(3)
FROM sys.columns c
CROSS JOIN sys.columns c1
CROSS JOIN sys.columns c2;
GOSELECT *
FROM dbo.Table1
WHERE Dat = 'Test';Context
StackExchange Database Administrators Q#183006, answer score: 47
Revisions (0)
No revisions yet.