Wednesday, April 11, 2012

Filtered Indexes


                There are many performance and space-saving strategies that we can employ as Database Administrators and Developers.  One of the tried-and-true methods is through the utilization of filtered indexes.  Below is how you would create a filtered index:

create index IX_YourIndexName
on YourTable(YourKeyColumn)
where YourColumn < 10
go

                In the above snippet, you can see that creating a filtered index is very much like creating any other index:  The alteration is through the use of the WHERE clause to narrow down the scope of the indexed rows.  Namely two advantages of filtered indexes are the saved space, as well as the maintenance of the index could potentially be less.

                When would you use a filtered index?  Often times we have tables that contain data that won’t ever be queried (or queried often enough for the storage consumption and indexing benefits).  For instance, if you only typically look for products that actually have a cost associated, you are narrowing down your query.  And because it may heed us to have an additional index on this table, the rows that have no cost associated with them will most likely never be retrieved (or not enough to warrant inclusion in the index).

                Here’s an example of real-time space-saving proof.  (The below example uses the AdventureWorks2012 sample database which can be found here)

                I wanted to choose a relatively large table in the AdventureWors2012 database to emphasize the filtered index advantage, so I chose the Sales.SalesOrderDetail table.  Run the below query to view a summary of the indexes on this table:

use AdventureWorks2012
go

select
       object_name(p.object_id) as table_name,
       i.name as index_name,
       i.type_desc as index_type,
       ps.row_count,
       ps.reserved_page_count,
       ps.reserved_page_count * 8 as reserved_space_kb
from sys.partitions p
inner join sys.indexes i
on p.object_id = i.object_id
and p.index_id = i.index_id
inner join sys.objects o
on p.object_id = o.object_id
inner join sys.dm_db_partition_stats ps
on p.partition_id = ps.partition_id
where o.is_ms_shipped = 0
and object_name(p.object_id) = 'salesorderdetail'
order by ps.row_count desc

                Now focus in on the nonclustered index called IX_SalesOrderDetail_ProductID:  It consumes a total of 2624 KB of space.  In order to get what the indexed columns are for this index, we can run the following simple query:

use AdventureWorks2012
go

select
       object_name(i.object_id) as table_name,
       i.name as index_name,
       i.type_desc,
       c.name as column_name,
       ic.is_included_column
from sys.indexes i
inner join sys.index_columns ic
on i.object_id = ic.object_id
and i.index_id = ic.index_id
inner join sys.objects o
on i.object_id = o.object_id
inner join sys.columns c
on c.object_id = i.object_id
and ic.column_id = c.column_id
where o.is_ms_shipped = 0
and object_name(i.object_id) = 'salesorderdetail'
order by table_name, i.type_desc, index_name

So now we know that the indexed column is ProductID (as the index name would have suggested).  Let’s go ahead and drop that index (for testing purposes only).

-- drop the out-of-the-box NCI on ProductID
drop index IX_SalesOrderDetail_ProductID
on Sales.SalesOrderDetail
go

                Why?  Well say that for whatever business domain reason we typically never query records in this table that have a ProductID of greater than 800.  We don’t want the overhead of maintaining this extra index, so we are going to drop it with hopes of a newer, smaller filtered index.  Now create the filtered index based off of our guidelines:

-- create the filtered index
create index IX_SalesOrderDetail_ProductID_Filtered
on Sales.SalesOrderDetail(ProductID)
where ProductID < 800
go

                There you have it.  Now we have a filtered index on the Sales.SalesOrderDetail.  Go ahead and run the index consumption query again to find out our space savings:

use AdventureWorks2012
go

select
       object_name(p.object_id) as table_name,
       i.name as index_name,
       i.type_desc as index_type,
       ps.row_count,
       ps.reserved_page_count,
       ps.reserved_page_count * 8 as reserved_space_kb
from sys.partitions p
inner join sys.indexes i
on p.object_id = i.object_id
and p.index_id = i.index_id
inner join sys.objects o
on p.object_id = o.object_id
inner join sys.dm_db_partition_stats ps
on p.partition_id = ps.partition_id
where o.is_ms_shipped = 0
and object_name(p.object_id) = 'salesorderdetail'
order by ps.row_count desc

                We should get a reserved_space_kb of now roughly 968 KB.  Due to the use of a filtered index (intelligently created, of course), we were able to knock down the size of that index to about 37% of the original size.

                Filtered indexes can be a great way to minimize wasted storage and index maintenance overhead.  With the knowledge of this tool and the appropriate application, it can yield impressive results.

                The applicable code for this blog post can be found on my GitHub SQLSalt repository.  The following links to the source code are:

                If there are any comments, questions, or issues please feel free to leave a comment or email me at sqlsalt@gmail.com.

No comments:

Post a Comment