The Series
  
 
This is the first in a series of  articles on Index Covering which is a performance technique to cut down disk IO  and improve query performance. The first article here will define the basic  elements in a query execution plan and how to find the bad parts. The next  article will show how to fix them. Finally I'll conclude with some  larger examples from PeopleSoft, JD Edwards, Siebel and SAP.
  
 
Introduction
  
 
Databases have two basic  structures for storing information. Index pages and data pages. Both reside on  the disk. To achieve optimal performance, DBA’s create indexes which are really  just organized subsets of data. 
  
 
Since an index is a subset of  data, extra reads are often needed to find all the additional information.  Typically, this requires a bookmark lookup. 
  
 
These extra reads on a disk can  add up quickly and cause poor performance. The extra IOs also cause the database  to use cache less efficiently. 
  
 
What is a bookmark  lookup?
  
 
Databases have a concept called  “book mark lookups”. This means that the database has to find what it needs in  the index page, then find the corresponding data page for the rest of the  information required. 
  
 
Think of it like this: A person  needs to find George Washington’s birthday. They look in the index of an  encyclopedia to find “Washington, George” which tells them to turn to page 98 to  find an entry on him. On page 98, they find the birthday. This is an example of  a “book mark lookup”. The researcher looks in the index, keeps their finger  there, turns to a page in the book, finds the birthday for the rest of the  information. 
     
 A bookmark lookup occurs when the  database cannot find all the needed information for a query in the index  page and so it goes to the data page to complete the request.

  
 For example:
  
 create table A (col_one int, col_two int,  col_three int)
 Go
  
 create index A_P1 on a(col_one,  col_two)
 Go
  
 [Data added to table…]
  
 Select col_three
 From A
 Where col_one = ‘22’
  
 
In this example, the query would used index A_P1 to find the row pointer to the data page in the table, but since A_P1 index does not contain column col_three, it would have to do a book mark lookup from the index to the data page.
Why are bookmark lookups  bad for performance?
  
 
Not all  bookmark lookups are horrible. It just means  that the database had to do some extra IO to find the data it needed. If you are  only hitting a few rows, then it’s not an issue. If you are having to sort  through hundreds or thousands of rows to find the one row you needed, or are in  a massive nested loop, then book mark lookups can be a problem. 
  
 
There are several examples below  which will illustrate the pain in enterprise apps that can be large and small  depending on the query. 
  
  
 
How do you know you have  a bookmark lookup?
  
 
A bookmark lookup shows up in the  execution plan (ex. set statistics  profile on) looking like this:
  
 
|--Bookmark  Lookup(BOOKMARK:([Bmk1001]),OBJECT:([JDE_CRP].[CRPDTA].[F4311] AS  [T1]))
  
  
 
Ok, so I got bookmark  lookups, so what?
  
 
First, figure out if it’s causing  a drag on the system. Look at the number of rows and executions that the  bookmark lookup is using. Is it 5 or 50,000? This can be seen on the left hand  side of the execution plan using set statistics profile on command. 
  
 
In this example, zero rows were  brought back and the loop was executed only once. This bookmark lookup was not a  problem.
  
  
 
Rows   Executes   StmtText 
 ------  ---------  ---------------------------------------------------
 0      1          |--Nested Loops(Inner Join, 
 0      1              |--Sort(ORDER BY:([T0].[ILUKID]  DESC))
 0      1              |     |--Bookmark Lookup(BOOKMARK:([Bmk1000]),   
  0      1              |         |--Index  Seek(OBJECT:([JDE_CRP].[CRPDTA].[F4111].[x] AS
0      0              |--Clustered Index  Seek(OBJECT:([JDE_CRP].[CRPDTA].[F4311]. 
  
  
 
Here is an example where a  bookmark lookup caused severe problems. It is bringing back 85 million rows and  is being executed 4,017 times.
  
  
 Rows        Executes    StmtText            
 -----------  -----------  ------------------------------------------------------------
 1           1                          |--Nested Loops(Inner Join,  WHERE:([T1].[PR_POSTN_ID]=
 364442      1                     |          |--Index  Scan(OBJECT:([Siebel_Prd].[dbo].
 85205956    4017             |    |--Bookmark  Lookup(BOOKMARK:([Bmk1001])
 85205956    4017             |    |--Nested Loops(Inner  Join, OUTER 
 4017        4017        |          |--Nested  Loops(Inner Join, OUTER 
 4017        4017                 |          |     |--Bookmark  Lookup(BOOKMARK:([Bmk1002
 4017        4017        |          |     |     |--Nested  Loops(Inner Join, 
 4017        4017                  |         |    |    |    |--Bookmark  Lookup(BOOKMARK:([Bmk
 4017        4017                 |          |     |     |     |     |--Index  Seek(OBJECT:([Siebel_
 4017        4017                 |          |     |     |     |--Index  Seek(OBJECT:(
 4017        4017                 |          |     |--Clustered  Index Seek(OBJECT:([Siebel
 85205956    4017             |          |--Index  Seek(OBJECT:([Siebel_Prd].[dbo].[S_