0% found this document useful (0 votes)
33 views59 pages

INDEXING BASCIS - Unknown

The document provides an overview of indexing basics in SQL Server including pages, extents, heaps, clustered indexes, non-clustered indexes, and building B-Tree indexes. It demonstrates how to create clustered and non-clustered indexes on a sample table and view the index statistics. Maintaining proper fill factor and using indexes appropriately can improve performance by reducing page splits and unnecessary reads.

Uploaded by

Kevin Anderson
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views59 pages

INDEXING BASCIS - Unknown

The document provides an overview of indexing basics in SQL Server including pages, extents, heaps, clustered indexes, non-clustered indexes, and building B-Tree indexes. It demonstrates how to create clustered and non-clustered indexes on a sample table and view the index statistics. Maintaining proper fill factor and using indexes appropriately can improve performance by reducing page splits and unnecessary reads.

Uploaded by

Kevin Anderson
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 59

Page 1 of 59

INDEXING BASCIS
Pages, extents:
The fundamental unit of data storage in sql server is a page (8 KB).

This means SQL server database have 128 pages per MB.

Each page begins with 96 bytes header.

Max amount of data contained in a single row on a page is 8,060 bytes.

Extents are a collection of eight physically contiguous pages.

Heap:
The heap is a table without clustered index.

The data rows are not sorted in any particular order.

Data pages are not linked in a linked list (i.e; data pages are just marked for the
fact that they belong to particular table).

Heap can have multiple extents.

Clustered index:
The data rows are stored in order based on clustered index key.
Page 2 of 59

The clustered index is implemented as a B-tree index structure.

Data pages in the leaf level or leaf node are linked in a doubly-linked list.

Clustered index has one row in sys.partitions with index_id = 1 and if a values of 0
means have a heap and if greater than 1 means they do have a non-clustered
indexes.

Clustered index B-Tree:

Root node is nothing but that’s the entry to your table this is got enough
information to take you to actual row in the leaf.

Intermediate nodes can be multiple. Here in diagram it is showing only one level
of Intermediate node. Intermediate node will take you to the leaf node in
clustered index and this is the place where data also stored.

They are doubly linked list so you do know what is the next and previous page.

Non-Clustered index:
They have a B-tree index structure similar to the one in clustered indexes.

The difference is that they will not affect the order of the data rows.
Page 3 of 59

Each index row contains the non-clustered key value, a row locator and any
included, or nonkey, columns.

Building B-Tree
Assumption: Each page contains 10 rows.

This page is denoted by 0:1

Now adding 10 more rows to this table.

From above the new page is denoted by 0:2


Page 4 of 59

The above will be root or intermediate node for below leaf nods which contains
leaf node information.

Clustered Index B-Tree

If we want to search for row no 3 then sql server goes to B-tree and identifies root
node and it is not searching for a value of 3 .To get a value of 3 sql server have to
travels through multiple other intermediate nodes. since the value is 3 it will
Page 5 of 59

come to 1-30 in intermediate and no need to check for >31 and comes to 1-10 in
leaf node.

Understand non-clustered index we were storing only key in leaf node where as in
clustered index stores data rows in leaf node.

Fill Factor
It is a method to pre-allocate some space for future expansion for a particular
row. If we are using varchar data types there is a probability that the length may
change which means row length which is smaller now can become bigger in
future.to accommodate this fill factor will helps you.

To avoid PAGESPLITS and degrade performance.

Demo
USE tempdb
GO

CREATETABLE INDEXING
(ID INTIDENTITY(1,1),
NAME CHAR(4000),
COMPANY CHAR(4000),
PAY INT)

/* THIS TABLE HAVE 4 COLUMNS OF WHICH 2 CHAR COLUMNS OF 4000 THIS MEANS I
HAVE A ROW OF CLOSE TO 8000 BYTES PLEASE MAKE SURE
THAT EACH ROW GETS A SINGLE PAGE AND A PAGE CANNOT STORE MORE THAN ONE ROW
*/

SELECTOBJECT_NAME(OBJECT_ID) TABLENAME,
ISNULL(name,OBJECT_NAME(OBJECT_ID)) INDEXNAME,
index_id,
type_desc
FROMSYS.indexes
WHEREOBJECT_NAME(OBJECT_ID)='INDEXING'

This shows that the table is Heap.

U can see index_id = 0 as discussed earlier it is Heap.


Page 6 of 59

SETNOCOUNTON-- USED TO SUPRESS THE N ROWS EFFECTED MESSAGE GET FROM SSMS

INSERTINTO INDEXING VALUES('VINOD','EXTREMEEXPERTS',10000)

--STATUS CHECK
SELECTOBJECT_NAME(OBJECT_ID) NAME,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_LEVEL,
record_count AS RECORDCOUNT,
page_count ASPAGECOUNT,
fragment_count AS FRAGMENTATION
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('INDEXING'),NULL,NULL,'D
ETAILED')
GO

IN_ROW_DATA which means the rows data is been able to fit inside a page hence
it’s inside the row.
INSERTINTO Indexing VALUES
('Steve','Central',15000),('Pinal','SQLAuthority', 13000)
GO
-- Status Check
SELECT
OBJECT_NAME(object_id) Name,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_Level,
record_count AS RecordCount,
page_count ASPageCount,
fragment_count AS Fragmentation
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('Indexing'),NULL,NULL,'D
ETAILED');
GO

As see from above two pic’s record count, page count, fragmentation is changed.
INSERTINTO Indexing VALUES
('Dummy','JunkCompany', 1000)
GO 100
Page 7 of 59

Inserting 100 rows.


-- Status Check
SELECT
OBJECT_NAME(object_id) Name,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_Level,
record_count AS RecordCount,
page_count ASPageCount,
fragment_count AS Fragmentation
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('Indexing'),NULL,NULL,'D
ETAILED');
GO

-- Clustered Index
CREATECLUSTEREDINDEX CI_IndexingID ON Indexing(ID)
GO

-- Status Check
SELECT
OBJECT_NAME(object_id) Name,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_Level,
record_count AS RecordCount,
page_count ASPageCount,
fragment_count AS Fragmentation
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('Indexing'),NULL,NULL,'D
ETAILED');
GO

Now index_type is CLUSTERED INDEX and INDEX_ID = 1 (previously it is 0 for


heap)

It has 2 depth level at depth level of 0 record count and page count is 103.
Page 8 of 59

To manage these 103 records you have another page which has 103 records but
page count is 1
INSERTINTO Indexing VALUES
('MoreJunk','MoreJunkCompany', 100)
GO 700

-- Status Check
SELECT
OBJECT_NAME(object_id) Name,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_Level,
record_count AS RecordCount,
page_count ASPageCount,
fragment_count AS Fragmentation
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('Indexing'),NULL,NULL,'D
ETAILED');
GO

Now we can see 803 rows which have a depth level of 0 and these 803 rows are
managed by 2 pages in the intermediate node(see row 2 page count) and further
2 page is managed by 1 page in Root node (see row 3 page count).
-- Statistics speaks a lot !!!
DBCCSHOW_STATISTICS ('Indexing', CI_IndexingID)
GO
Page 9 of 59

If we don’t have uniqueness with respect to clusted index sql server will go a head
and create another 4 byte to make it unique internally.
-- Non-Clustered Index
CREATENONCLUSTEREDINDEX NCI_Pay on Indexing(Pay)
GO

-- Status Check
SELECT
OBJECT_NAME(object_id) Name,
index_type_desc AS INDEX_TYPE,
alloc_unit_type_desc AS DATA_TYPE,
index_id AS INDEX_ID,
index_depth AS DEPTH,
index_level AS IND_Level,
record_count AS RecordCount,
page_count ASPageCount,
fragment_count AS Fragmentation
FROMsys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('Indexing'),NULL,NULL,'D
ETAILED');
GO

The Non-clustered index stores only the key and not the data so in 5th row page
count = 1 (root page) have 2 records and page count = 2 have 803 records.

-- Show the Statistics for the Non-Clustered Index


DBCCSHOW_STATISTICS ('Indexing', NCI_Pay)
Page 10 of 59

Here we can see average key length or two integers of 4 bytes which is 8 bytes.

In 2nd table the 1st column (pay) has four bytes and when we add second column
(pay, id) has eight bytes.

ID which is the clustered index currently as part of the pointer hence this
conforms the fact that the Non-clustered index goes about using the key or the
non-clustered index but the leaf node has got the pointer to the row data which is
a clustered index key or would be row id.

Additional information
A view with a clustered index is Indexed view.

Indexed views have the same storage structure as clustered tables

Performance Trivia: Fill factor value of 50 percent can cause database read
performance to degrade by two times.

i.e; you will have to go ahead and read 2 pages for the same amount of data
which could you have read in one single read.

Trivia facts:

Column keys per index – 16 (we cannot have more than 16 columns as part of
composite index we can use included columns which is covering index)

Non-clustered indexes per table – 999

Statistics on non-indexed columns – 30,000

XML indexes – 249

Max bytes in any index key- 900 bytes

An index is not the solution for every problem. The system was running fine a few
moments ago and now it is slow index is May or may not solve the problem.

Index solves the problem if they used properly and reasonably.


Page 11 of 59

Primary key
It automatically creates clustered Index

Except when explicitly specifyNon-Clustered index is specified.

Except when clustered index already exists.

It automatically creates unique index on column. If the key specified is not unique
internally sql server uses a non-unique cluster index of 4-bytes that it adds as part
of unique identifier and makes it unique.

NOTE: Non Unique clustered index adds 4-byte unique identifier column.
SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[PKSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

INSERTINTO [Sales].[PKSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-----------------------------------------------------------------------
-- CREATE PK
-- Clustered Index not specified

ALTERTABLE [Sales].[PKSalesOrderDetail]
ADDCONSTRAINT [PK_PKSalesOrderDetail_SalesOrderDetailID]
PRIMARYKEY ([SalesOrderDetailID] ASC
)
GO

-- Check Database >> Tables >> Indexes - Primary Key automatically creates CI
-- Drop PK
ALTERTABLE [Sales].[PKSalesOrderDetail]
DROPCONSTRAINT [PK_PKSalesOrderDetail_SalesOrderDetailID]
GO

-----------------------------------------------------------------------
Page 12 of 59

-- CREATE PK
-- Non-Clustered Index specified

ALTERTABLE [Sales].[PKSalesOrderDetail]
ADDCONSTRAINT [PK_PKSalesOrderDetail_SalesOrderDetailID]
PRIMARYKEY NONCLUSTERED ([SalesOrderDetailID] ASC
)
GO

-- Check Database >> Tables >> Indexes - Primary Key automatically creates
NCI
-- Drop PK
ALTERTABLE [Sales].[PKSalesOrderDetail]
DROPCONSTRAINT [PK_PKSalesOrderDetail_SalesOrderDetailID]
GO

-----------------------------------------------------------------------
-- Create Clustered Index (no primary key)

CREATECLUSTEREDINDEX
[CL_PKSalesOrderDetail_Index] ON [Sales].[PKSalesOrderDetail]
( [SalesOrderID] ASC,
[CarrierTrackingNumber] ASC
)
GO

-- CREATE PK and do not specify kind of index


ALTERTABLE [Sales].[PKSalesOrderDetail]
ADDCONSTRAINT [PK_PKSalesOrderDetail_SalesOrderDetailID]
PRIMARYKEY ([SalesOrderDetailID] ASC
)
GO

-- Check Database >> Tables >> Indexes - Primary Key automatically creates
NCI
-- Clean up
DROPTABLE [Sales].[PKSalesOrderDetail]
GO

Over Indexing
Consumes unnecessary disk space

Queries may use less efficient index

Less efficient execution plan

Reduction in overall server performance

Confusion among developer when troubleshooting

Best practice: Drop unused Indexes


Page 13 of 59

USE [MyAdventureWorks]
GO

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[NewSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure Time
SETSTATISTICSTIMEON
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[NewSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

Without an index the table takes 1.256 sec to insert.

-- Truncate Table
TRUNCATETABLE [Sales].[NewSalesOrderDetail]
GO

-- Create New Indexes on New Table and Measure the insert time.
-- Create Clustered Index
ALTERTABLE [Sales].[NewSalesOrderDetail]
ADDCONSTRAINT
[PK_NewSalesOrderDetail_SalesOrderID_NewSalesOrderDetailID]
PRIMARYKEYCLUSTERED
([SalesOrderID] ASC,
Page 14 of 59

[SalesOrderDetailID] ASC)ON [PRIMARY]


GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber]
ON [Sales].[NewSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_OrderQty]
ON [Sales].[NewSalesOrderDetail]
([OrderQty] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_ProductID]
ON [Sales].[NewSalesOrderDetail]
([ProductID] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_SpecialOfferID]
ON [Sales].[NewSalesOrderDetail]
([SpecialOfferID] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_UnitPrice]
ON [Sales].[NewSalesOrderDetail]
([UnitPrice] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_UnitPriceDiscount]
ON [Sales].[NewSalesOrderDetail]
([UnitPriceDiscount] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_LineTotal]
ON [Sales].[NewSalesOrderDetail]
([LineTotal] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_rowguid]
ON [Sales].[NewSalesOrderDetail]
([rowguid] ASC)ON [PRIMARY]
GO
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_ModifiedDate]
ON [Sales].[NewSalesOrderDetail]
([ModifiedDate] ASC)ON [PRIMARY]
GO

We unwantedly created many indexes.

-- Insert Test Again


-- Measure the Insert time in New Table
INSERTINTO [Sales].[NewSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO
Page 15 of 59

To insert into same table after creating many unwanted indexes time taken in
12.332 sec which is 10 times more than time taken in heap table.

Duplicate Indexing
Reduce the performance of Insert, update, delete query

Wasteful of space

No performance advantages to SELECT statements

Best practice: drop duplicate indexes


SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[DupSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure Time
SETSTATISTICSTIMEON
SETSTATISTICSIOON
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[DupSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO 5
Page 16 of 59

Took 4 seconds
-- Truncate Table
TRUNCATETABLE [Sales].[DupSalesOrderDetail]
GO

-- Create Non-Clustered Index


CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber1]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber2]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber3]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber4]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber5]
ON [Sales].[DupSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
Page 17 of 59

-- Measure the Insert time in New Table


INSERTINTO [Sales].[DupSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO 5

Create number of duplicate indexes based on CarrierTrackingNumber

Took 46 seconds

Clustered Index
Page 18 of 59

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[MySalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure Time
SETSTATISTICSTIMEON
SETSTATISTICSIOON
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[MySalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO 10

-- Select Data from Table


SELECT*
FROM [Sales].[MySalesOrderDetail] sod
WHERE sod.[SalesOrderDetailID] > 40000
AND sod.[SalesOrderDetailID] < 160000
GO

-- Create New Indexes on New Table and Measure the Logical Read
-- Create Clustered Index
ALTERTABLE [Sales].[MySalesOrderDetail]
ADDCONSTRAINT [PK_MySalesOrderDetail_SalesOrderDetailID]
PRIMARYKEYCLUSTERED
([SalesOrderDetailID] ASC
)ON [PRIMARY]
GO

-- Select Data from Table


SELECT*
FROM [Sales].[MySalesOrderDetail] sod
WHERE sod.[SalesOrderDetailID] > 40000
AND sod.[SalesOrderDetailID] < 160000
GO
Page 19 of 59

See logical reads

Unique Index

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[UniSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[UniSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-- Select statement and distinct (CTRL+M)


SELECT [SalesOrderDetailID]
FROM [Sales].[UniSalesOrderDetail]
GO
SELECTDISTINCT [SalesOrderDetailID]
FROM [Sales].[UniSalesOrderDetail]
GO

Both queries returns same amount of rows.


Page 20 of 59

-- Add Unique Constraint


ALTERTABLE [Sales].[UniSalesOrderDetail]
ADDCONSTRAINT [UX_UniSalesOrderDetail_SalesOrderDetailID]
UNIQUENONCLUSTERED
(
[SalesOrderDetailID]
)ON [PRIMARY]
GO

-- Select statement and distinct (CTRL+M)


SELECT [SalesOrderDetailID]
FROM [Sales].[UniSalesOrderDetail]
GO
SELECTDISTINCT [SalesOrderDetailID]
FROM [Sales].[UniSalesOrderDetail]
GO

-- Add Unique Index


CREATEUNIQUENONCLUSTEREDINDEX
[IX_UniSalesOrderDetail_SalesOrderDetailID]
ON [Sales].[UniSalesOrderDetail]
(
[SalesOrderDetailID]
Page 21 of 59

)ON [PRIMARY]
GO

When we create unique constraint it will create unique non-clustered index


automatically.

The index created by unique constraint cannot be edited.


Page 22 of 59

The unique index which is created explicitly can be edited now we can uncheck
unique to remove uniqueness.

Included Columns

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[InclSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO
Page 23 of 59

-- Measure the Insert time in New Table


INSERTINTO [Sales].[InclSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO
-- Add a column nvarchar
ALTERTABLE [Sales].[InclSalesOrderDetail]
ADD Col1 NVARCHAR(1000)DEFAULT(REPLICATE('a',1000))NOTNULL
GO

-- Select statement and distinct (CTRL+M)


SELECT [SalesOrderID],[ProductID],[col1]
FROM [Sales].[InclSalesOrderDetail] sod
WHERE sod.[SalesOrderID] > 40000
AND sod.[SalesOrderID] < 50000
GO

-- Create Clustered Index


ALTERTABLE [Sales].[InclSalesOrderDetail]
ADDCONSTRAINT [PK_InclSalesOrderDetail_SalesOrderDetailID]
PRIMARYKEYCLUSTERED
([SalesOrderDetailID] ASC
)ON [PRIMARY]
GO

-- Select statement and distinct (CTRL+M)


SELECT [SalesOrderID],[ProductID],[col1]
FROM [Sales].[InclSalesOrderDetail] sod
WHERE sod.[SalesOrderID] > 40000
AND sod.[SalesOrderID] < 50000
GO
Page 24 of 59

-- Create Non-Clustered Index


-- This will throw error
CREATENONCLUSTEREDINDEX [IX_InclSalesOrderDetail_Cover]
ON [Sales].[InclSalesOrderDetail]
([SalesOrderID],[ProductID],[col1])ON [PRIMARY]
GO

Because of col1 its failed.


-- Create Included-Clustered Index
CREATENONCLUSTEREDINDEX [IX_InclSalesOrderDetail_Included]
ON [Sales].[InclSalesOrderDetail]
([SalesOrderID])
INCLUDE ([ProductID],[col1])
ON [PRIMARY]
GO

This is covering index.sql server add these two columns in leaf node while
keeping the salesorderid as part of the key.
-- Select statement (CTRL+M)
SELECT [SalesOrderID],[ProductID],[col1]
FROM [Sales].[InclSalesOrderDetail] sod
WHERE sod.[SalesOrderID] > 40000
AND sod.[SalesOrderID] < 50000
GO

-- Compare performance between Primary Key and Covered Index


-- Select statement (CTRL+M)
SELECT [SalesOrderID],[ProductID],[col1]
FROM [Sales].[InclSalesOrderDetail] sod
WITH(INDEX([PK_InclSalesOrderDetail_SalesOrderDetailID]))
WHERE sod.[SalesOrderID] > 40000
AND sod.[SalesOrderID] < 50000
GO
Page 25 of 59

-- Select statement (CTRL+M)


SELECT [SalesOrderID],[ProductID],[col1]
FROM [Sales].[InclSalesOrderDetail] sod
WITH(INDEX([IX_InclSalesOrderDetail_Included]))
WHERE sod.[SalesOrderID] > 40000
AND sod.[SalesOrderID] < 50000
GO

Filtered Indexes

USE [MyAdventureWorks]
GO

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[FilteredSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO
-- Measure the Insert time in New Table
INSERTINTO [Sales].[FilteredSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
Page 26 of 59

,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-- Create New Indexes on New Table


-- Create Clustered Index
ALTERTABLE [Sales].[FilteredSalesOrderDetail]
ADDCONSTRAINT [PK_FilteredSalesOrderDetail]
PRIMARYKEYCLUSTERED
([SalesOrderID] ASC,
[SalesOrderDetailID] ASC)
GO
-- Selecting Data for SpecialOfferID 2
SELECT [SalesOrderID], [SalesOrderDetailID], [SpecialOfferID]
FROM [Sales].[FilteredSalesOrderDetail] sod
WHERE [SpecialOfferID] = 2
GO

-- Create Non-Clustered Index


CREATENONCLUSTEREDINDEX [IX_FilteredSalesOrderDetail_SpecialOfferID]
ON [Sales].[FilteredSalesOrderDetail]
([SpecialOfferID] ASC)
GO
-- Selecting Data for SpecialOfferID 2
SELECT [SalesOrderID], [SalesOrderDetailID], [SpecialOfferID]
FROM [Sales].[FilteredSalesOrderDetail] sod
WHERE [SpecialOfferID] = 2
GO

-- Create Non-Clustered Index


CREATENONCLUSTEREDINDEX [FIX_FilteredSalesOrderDetail_SpecialOfferID]
ON [Sales].[FilteredSalesOrderDetail]
([SpecialOfferID] ASC)
WHERE [SpecialOfferID] = 2
GO
Page 27 of 59

-- Selecting Data for SpecialOfferID 2


SELECT [SalesOrderID], [SalesOrderDetailID], [SpecialOfferID]
FROM [Sales].[FilteredSalesOrderDetail] sod
WHERE [SpecialOfferID] = 2
GO

After using the filtered index the plan is same but it uses the filtered index
where as preiously is uses non-clustered
inex([IX_FilteredSalesOrderDetail_SpecialOfferID]) and the subtree cost also
now it is reduced which is shown in below.

-- Comparing the performance of two SELECT (CTRL + M)


-- Selecting Data for SpecialOfferID 2
SELECT [SalesOrderID], [SalesOrderDetailID], [SpecialOfferID]
FROM [Sales].[FilteredSalesOrderDetail] sod
WITH(INDEX([IX_FilteredSalesOrderDetail_SpecialOfferID]))
WHERE [SpecialOfferID] = 2
GO
-- Selecting Data for SpecialOfferID 2
SELECT [SalesOrderID], [SalesOrderDetailID], [SpecialOfferID]
FROM [Sales].[FilteredSalesOrderDetail] sod
Page 28 of 59

WITH(INDEX([FIX_FilteredSalesOrderDetail_SpecialOfferID]))
WHERE [SpecialOfferID] = 2
GO

------------------------------------------------------------------------
-- Clean up
DROPTABLE [Sales].[FilteredSalesOrderDetail]
GO

Disabled Index

USE [MyAdventureWorks]
GO

SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[DeleteSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[DeleteSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
Page 29 of 59

,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-- Create New Indexes on New Table and Measure the insert time.
-- Create Clustered Index
ALTERTABLE [Sales].[DeleteSalesOrderDetail]
ADDCONSTRAINT
[PK_DeleteSalesOrderDetail_SalesOrderID_DeleteSalesOrderDetailID]
PRIMARYKEYCLUSTERED
([SalesOrderID] ASC,
[SalesOrderDetailID] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_DeleteSalesOrderDetail_CarrierTrackingNumber]
ON [Sales].[DeleteSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO

------------------------------------------------------------------------
-- Select Data (CTRL + M)
SELECT [CarrierTrackingNumber]
FROM [Sales].[DeleteSalesOrderDetail]
WHERE [CarrierTrackingNumber] ISNOTNULL
GO

It is doing non-clustered index seek shown in above red box.


-- Disable non-clustered index
ALTERINDEX [IX_DeleteSalesOrderDetail_CarrierTrackingNumber]
ON [Sales].[DeleteSalesOrderDetail] DISABLE
Page 30 of 59

GO

-- Select Data (CTRL + M)


SELECT [CarrierTrackingNumber]
FROM [Sales].[DeleteSalesOrderDetail]
WHERE [CarrierTrackingNumber] ISNOTNULL
GO

As the non-clustered index is disabled it uses clustered index sacn.


-- Insert Test Again
INSERTINTO [Sales].[DeleteSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

------------------------------------------------------------------------
-- Select Data (CTRL + M)
SELECT [CarrierTrackingNumber]
FROM [Sales].[DeleteSalesOrderDetail]
WHERE [CarrierTrackingNumber] ISNOTNULL
GO
Page 31 of 59

After insert again executing the select query remains no change in plan.
-- Disable clustered index
ALTERINDEX [PK_DeleteSalesOrderDetail_SalesOrderID_DeleteSalesOrderDetailID]
ON [Sales].[DeleteSalesOrderDetail] DISABLE
GO

-- Insert Test Again


INSERTINTO [Sales].[DeleteSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-- Select Data (CTRL + M)


SELECT [CarrierTrackingNumber]
FROM [Sales].[DeleteSalesOrderDetail]
WHERE [CarrierTrackingNumber] ISNOTNULL
GO

This concludes that disabling the non-clustered index will not effect the insert
or select but disabling the clustered index will effect the insert and select
query.
Page 32 of 59

Summary

Index Performance

Remove un-used index

Create missing index

Remove duplicate index

Index Defragmentation

DEMO
USE [MyAdventureWorks]
GO

-- Create New Table Empty Table


SELECT*
INTO [Sales].[MainSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-- Measure the Insert time in New Table


INSERTINTO [Sales].[MainSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO

-----------------------------------------------------------------------------
-----
Page 33 of 59

-- Duplicate Index Creation


-- Create Non-Clustered Index

CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber]
ON [Sales].[MainSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber1]
ON [Sales].[MainSalesOrderDetail]
([CarrierTrackingNumber] ASC)ON [PRIMARY]
GO

-- script that tells how many duplicate indexes are created --

WITH MyDuplicate AS (SELECT


Sch.[name] AS SchemaName,
Obj.[name] AS TableName,
Idx.[name] AS IndexName,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 1)AS Col1,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 2)AS Col2,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 3)AS Col3,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 4)AS Col4,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 5)AS Col5,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 6)AS Col6,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 7)AS Col7,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 8)AS Col8,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 9)AS Col9,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 10)AS Col10,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 11)AS Col11,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 12)AS Col12,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 13)AS Col13,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 14)AS Col14,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 15)AS Col15,
INDEX_Col(Sch.[name] +'.'+ Obj.[name], Idx.index_id, 16)AS Col16
FROMsys.indexes Idx
INNERJOINsys.objects Obj ON Idx.[object_id] = Obj.[object_id]
INNERJOINsys.schemas Sch ON Sch.[schema_id] = Obj.[schema_id]
WHERE index_id > 0)
SELECT MD1.SchemaName, MD1.TableName, MD1.IndexName,
MD2.IndexName AS OverLappingIndex,
MD1.Col1, MD1.Col2, MD1.Col3, MD1.Col4,
MD1.Col5, MD1.Col6, MD1.Col7, MD1.Col8,
MD1.Col9, MD1.Col10, MD1.Col11, MD1.Col12,
MD1.Col13, MD1.Col14, MD1.Col15, MD1.Col16
FROM MyDuplicate MD1
INNERJOIN MyDuplicate MD2 ON MD1.tablename = MD2.tablename
AND MD1.indexname <> MD2.indexname
AND MD1.Col1 = MD2.Col1
AND(MD1.Col2 ISNULLOR MD2.Col2 ISNULLOR MD1.Col2 = MD2.Col2)
AND(MD1.Col3 ISNULLOR MD2.Col3 ISNULLOR MD1.Col3 = MD2.Col3)
AND(MD1.Col4 ISNULLOR MD2.Col4 ISNULLOR MD1.Col4 = MD2.Col4)
AND(MD1.Col5 ISNULLOR MD2.Col5 ISNULLOR MD1.Col5 = MD2.Col5)
AND(MD1.Col6 ISNULLOR MD2.Col6 ISNULLOR MD1.Col6 = MD2.Col6)
AND(MD1.Col7 ISNULLOR MD2.Col7 ISNULLOR MD1.Col7 = MD2.Col7)
AND(MD1.Col8 ISNULLOR MD2.Col8 ISNULLOR MD1.Col8 = MD2.Col8)
Page 34 of 59

AND(MD1.Col9 ISNULLOR MD2.Col9 ISNULLOR MD1.Col9 = MD2.Col9)


AND(MD1.Col10 ISNULLOR MD2.Col10 ISNULLOR MD1.Col10 = MD2.Col10)
AND(MD1.Col11 ISNULLOR MD2.Col11 ISNULLOR MD1.Col11 = MD2.Col11)
AND(MD1.Col12 ISNULLOR MD2.Col12 ISNULLOR MD1.Col12 = MD2.Col12)
AND(MD1.Col13 ISNULLOR MD2.Col13 ISNULLOR MD1.Col13 = MD2.Col13)
AND(MD1.Col14 ISNULLOR MD2.Col14 ISNULLOR MD1.Col14 = MD2.Col14)
AND(MD1.Col15 ISNULLOR MD2.Col15 ISNULLOR MD1.Col15 = MD2.Col15)
AND(MD1.Col16 ISNULLOR MD2.Col16 ISNULLOR MD1.Col16 = MD2.Col16)
ORDERBY
MD1.SchemaName,MD1.TableName,MD1.IndexName

-- Clean up the Duplicate


DROPINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber1]
ON [Sales].[MainSalesOrderDetail]
GO

-----------------------------------------------------------------------------
-----
-- CTRL + M
-- Run following query

SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
FROM [Sales].[MainSalesOrderDetail]
WHERE [ProductID] = 717
GO

From above it is shown that some Index is missing.

To know what is miising right click as shown above pointer missing index
detail which gives script of missing index.
Page 35 of 59

Another method to know the missing indexes are shown below.


--Missing index script--
SELECTTOP 25
dm_mid.database_id AS DatabaseID,
dm_migs.avg_user_impact*(dm_migs.user_seeks+dm_migs.user_scans)
Avg_Estimated_Impact,
dm_migs.last_user_seek AS Last_User_Seek,
object_name(dm_mid.object_id,dm_mid.database_id)AS [TableName],
'CREATE INDEX [IX_'+object_name(dm_mid.object_id,dm_mid.database_id)+'_'
+REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.equality_columns,''),',
','_'),'[',''),']','')+
CASE
WHEN dm_mid.equality_columns ISNOTNULLAND dm_mid.inequality_columns
ISNOTNULLTHEN'_'
ELSE''
END
+REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.inequality_columns,''),',
','_'),'[',''),']','')
+']'
+' ON '+ dm_mid.statement
+' ('+ISNULL(dm_mid.equality_columns,'')
+CASEWHEN dm_mid.equality_columns ISNOTNULLAND dm_mid.inequality_columns
ISNOTNULLTHEN','ELSE
''END
+ISNULL(dm_mid.inequality_columns,'')
+')'
+ISNULL(' INCLUDE ('+ dm_mid.included_columns +')','')AS Create_Statement
FROMsys.dm_db_missing_index_groups dm_mig
INNERJOINsys.dm_db_missing_index_group_stats dm_migs
ON dm_migs.group_handle = dm_mig.index_group_handle
INNERJOINsys.dm_db_missing_index_details dm_mid
ON dm_mig.index_handle = dm_mid.index_handle
WHERE dm_mid.database_ID =DB_ID()
ORDERBY Avg_Estimated_Impact DESC
GO

-- Create Missing Index


CREATEINDEX [IX_MainSalesOrderDetail_ProductID]
ON [MyAdventureWorks].[Sales].[MainSalesOrderDetail]([ProductID])
INCLUDE ([SalesOrderID], [CarrierTrackingNumber], [OrderQty])
GO

-- script for unused index--


SELECTTOP 25
o.name AS ObjectName
, i.name AS IndexName
, i.index_id AS IndexID
, dm_ius.user_seeks AS UserSeek
, dm_ius.user_scans AS UserScans
Page 36 of 59

, dm_ius.user_lookups AS UserLookups
, dm_ius.user_updates AS UserUpdates
, p.TableRows
,'DROP INDEX '+QUOTENAME(i.name)
+' ON '+QUOTENAME(s.name)+'.'+QUOTENAME(OBJECT_NAME(dm_ius.object_id))as'drop
statement'
FROMsys.dm_db_index_usage_stats dm_ius
INNERJOINsys.indexes i ON i.index_id = dm_ius.index_id AND dm_ius.object_id=
i.object_id
INNERJOINsys.objects o on dm_ius.object_id= o.object_id
INNERJOINsys.schemas s on o.schema_id= s.schema_id
INNERJOIN(SELECTSUM(p.rows) TableRows, p.index_id, p.object_id
FROMsys.partitions p GROUPBY p.index_id, p.object_id)
p
ON p.index_id = dm_ius.index_id AND dm_ius.object_id= p.object_id
WHEREOBJECTPROPERTY(dm_ius.object_id,'IsUserTable')= 1
AND dm_ius.database_id =DB_ID()
AND i.type_desc ='nonclustered'
AND i.is_primary_key = 0
AND i.is_unique_constraint = 0
ORDERBY (dm_ius.user_seeks + dm_ius.user_scans + dm_ius.user_lookups)ASC
GO

-- Drop Unused Index


DROPINDEX [IX_NewSalesOrderDetail_CarrierTrackingNumber] ON
[Sales].[MainSalesOrderDetail]
GO

-- Running Unused Index will not bring Unused index again in query
-----------------------------------------------------------------------------
-----
-- Clean up
DROPTABLE [Sales].[MainSalesOrderDetail]
GO

Index Defragmentation
Page 37 of 59

Rebuild the indexes if the fragmentaion is more than 30%.

DEMO
-- Create New Table Empty Table
SELECT*
INTO [Sales].[DefragSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO

-----------------------------------------------------------------------------
-----
-- Create Clustered Index
CREATECLUSTEREDINDEX [IX_DefragSalesOrderDetail_SalesOrderDetailID]
ON [Sales].[DefragSalesOrderDetail]
([SalesOrderDetailID] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_DefragSalesOrderDetail_OrderQty]
ON [Sales].[DefragSalesOrderDetail]
([OrderQty] ASC)ON [PRIMARY]
GO
-- Create Non-Clustered Index
CREATENONCLUSTEREDINDEX [IX_DefragSalesOrderDetail_ProductID]
ON [Sales].[DefragSalesOrderDetail]
([ProductID] ASC)ON [PRIMARY]
GO

-- Inserting the data


INSERTINTO [Sales].[DefragSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO 5

-----------------------------------------------------------------------------
-----
-- Identify Index Defragmentation
SELECT ps.database_id,OBJECT_NAME( ps.OBJECT_ID),
ps.index_id, b.name,
ps.avg_fragmentation_in_percent
FROMsys.dm_db_index_physical_stats(DB_ID(),NULL,NULL,NULL,NULL)AS ps
INNERJOINsys.indexesAS b ON ps.OBJECT_ID= b.OBJECT_ID
AND ps.index_id = b.index_id
WHERE ps.database_id =DB_ID()
AND b.OBJECT_ID=OBJECT_ID('Sales.DefragSalesOrderDetail')
ORDERBY ps.OBJECT_ID
GO
Page 38 of 59

-----------------------------------------------------------------------------
-----
-- Rebuild and Reorganize Script
-----------------------------------------------------------------------------
-----
-- Reorganization
ALTERINDEX IX_DefragSalesOrderDetail_OrderQty
ON [Sales].[DefragSalesOrderDetail] REORGANIZE
GO

-- Rebuild
ALTERINDEX IX_DefragSalesOrderDetail_ProductID
ON [Sales].[DefragSalesOrderDetail] REBUILD
GO

-- Rebuild
ALTERINDEXALL
ON [Sales].[DefragSalesOrderDetail] REBUILD
GO

Columnstore Index
Accelerate common data warehouse queries

Stores data of single column in multiple pages.

Data is stored in compressed format.


Page 39 of 59

DEMO
SETNOCOUNTON

-- Create New Table Empty Table


SELECT*
INTO [Sales].[ColSalesOrderDetail]
FROM [Sales].[SalesOrderDetail]
WHERE 1 = 2
GO
Page 40 of 59

-- Create clustered index


CREATECLUSTEREDINDEX [CL_ColSalesOrderDetail]
ON [Sales].[ColSalesOrderDetail]
( [SalesOrderDetailID])
GO

-- Create Sample Data Table


-- WARNING: This Query may run upto 2-10 minutes based on your systems
resources
-- Measure the Insert time in New Table
INSERTINTO [Sales].[ColSalesOrderDetail]
([SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate])
SELECT [SalesOrderID],[CarrierTrackingNumber],[OrderQty]
,[ProductID],[SpecialOfferID],[UnitPrice]
,[UnitPriceDiscount],[LineTotal],[rowguid],[ModifiedDate]
FROM [Sales].[SalesOrderDetail]
GO 100

-- Performance Test
-- Comparing Regular Index with ColumnStore Index
SETSTATISTICSIOON
GO

-- Select Table with regular Index


SELECT ProductID,SUM(UnitPrice) SumUnitPrice,AVG(UnitPrice) AvgUnitPrice,
SUM(OrderQty) SumOrderQty,AVG(OrderQty) AvgOrderQty
FROM [Sales].[ColSalesOrderDetail]
GROUPBY ProductID
ORDERBY ProductID
GO

-- Create ColumnStore Index


CREATENONCLUSTERED
COLUMNSTORE INDEX [IX_ColSalesOrderDetail_ColumnStore]
ON [Sales].[ColSalesOrderDetail]
(UnitPrice, OrderQty, ProductID)
GO

-- Select Table with Columnstore Index


SELECT ProductID,SUM(UnitPrice) SumUnitPrice,AVG(UnitPrice) AvgUnitPrice,
SUM(OrderQty) SumOrderQty,AVG(OrderQty) AvgOrderQty
FROM [Sales].[ColSalesOrderDetail]
GROUPBY ProductID
ORDERBY ProductID
GO

-- Cleanup
DROPTABLE [Sales].[ColSalesOrderDetail]
Page 41 of 59

Index Practical performance tips and tricks

1. Index and Page types

--Description: Shows the 3 different PAGE Types inside SQL Server.

USE tempdb
GO

CREATETABLE my_Big_table
( a1 VARCHAR(2000),
b2 VARCHAR(8000),
d4 TEXT);

INSERTINTO my_Big_table VALUES


(REPLICATE('x', 2000),REPLICATE('y', 8000),
REPLICATE('a', 10000))

We know that sql have 8KB page limit. Now a1 , b2 going to have 10,000
characters this is bigger than the normal page size of 8KB
-- Shows all 3 types of pages available inside SQL Server
SELECTOBJECT_NAME(object_id)AS TABLE_NAME,rows,
type_desc AS PAGE_TYPE
FROMsys.partitions p
INNERJOINsys.allocation_units a
ON p.partition_id = a.container_id
WHEREobject_id=object_id('my_big_table');
Page 42 of 59

Sql have 3 different pages to store the data.Sql server used 3 different pages
though the number of rows is 1 you the one in ROW_DATA and 1 in
LOB_DATA (here it is text datatype) and 1 in ROW_OVERFLOW_DATA (this is
a data which could in get accombidated in a single page for a single row and
hence it currently using the overflow page)

2. Index and Non-Deterministic columns


--Description: Shows effect of indexing on non-deterministic columns

CREATETABLE calc_table
(
id INT,
DOB DATETIME,
Calc ASDATENAME(YYYY, DOB)
)
GO

CREATEINDEX ind1_calc_table
ON calc_table(Calc);
GO

-- Check the column property


SELECTCOLUMNPROPERTY(OBJECT_ID('calc_table'),'Calc','IsDeterministic');

o/p : 0

SELECTCOLUMNPROPERTY(OBJECT_ID('calc_table'),'Calc','IsIndexable');

o/p : 0

SELECTCOLUMNPROPERTY(OBJECT_ID('calc_table'),'id','IsIndexable');

o/p : 1 –- which means id column can be indexed

SELECTCOLUMNPROPERTY(OBJECT_ID('calc_table'),'Calc','IsPrecise');

o/p : 1
Page 43 of 59

3. Index and SET values.


--Description: Shows the effect of SET values on Index Creation.

CREATETABLE set_table
(
ID INT,
Calc AS 2*ID
)
GO

SETQUOTED_IDENTIFIEROFF;
SETANSI_NULLsOFF;
GO

CREATEINDEX ind1
ON set_table(calc);
GO

SETQUOTED_IDENTIFIERON;
SETANSI_NULLsON;
GO

CREATEINDEX ind1
ON set_table(calc);
GO

Now index will be created.

4. Importance of Clustered Index


--Description: Shows the Clustered Index Order is not always the same. For
specific order requirements, use the ORDER BY Clause

CREATETABLE Clustered_Order(
id INTIDENTITY(1,1)PRIMARYKEY,
name VARCHAR (15))

INSERT Clustered_Order
VALUES ('Vinod'),('Kumar'),('Pinal'),('Dave')
GO

CREATEUNIQUEINDEX UNQ_Name_TestData ON Clustered_Order(name)


GO

Select*from Clustered_Order
Page 44 of 59

Some times select statement does not gives the order as shown in above
for id column then we have to use order by clause.

5. Effect of compression and fill factor.


--Description: Shows if FILLFACTOR is being honored by the COMPRESSION
techniques inside SQL Server

CREATETABLE tbl_compress(id int, name char (1000))


GO

SETNOCOUNTON

DECLARE @i int= 0
WHILE (@i < 5000)
BEGIN
INSERTINTO tbl_compress VALUES (@i,REPLICATE('a', 500))
SET @i = @i + 1
END
GO

CREATECLUSTEREDINDEX CI_Compress on tbl_compress(id)


WITH (FILLFACTOR= 30)

-- Status Check
SELECT max_record_size_in_bytes, page_count, avg_page_space_used_in_percent,
index_level, index_id, index_depth
FROMsys.dm_db_index_physical_stats(db_id('MyAdventureWorks'),object_id('dbo.t
bl_compress'),null,null,'DETAILED')
WHEREobject_name(object_id)like'tbl_compress'

The index_leaf node(which is 0 in 4th column ) have all the data have
37.5151% of fill factor which is close to 30 but not 30.
Page 45 of 59

-- Start Compression work - ROW compression


ALTERINDEX CI_Compress ON tbl_compress REBUILD
WITH (data_compression=ROW)

-- Status Check
SELECT max_record_size_in_bytes, page_count, avg_page_space_used_in_percent,
index_level, index_id, index_depth
FROMsys.dm_db_index_physical_stats(db_id('MyAdventureWorks'),
object_id('dbo.tbl_compress'),null,null,'DETAILED')
WHEREobject_name(object_id)like'tbl_compress'

Now it comes to 31.66% after doing compression.It has taken the 34.5% for
intermediate node too.
-- Start PAGE Compression now
ALTERINDEX CI_Compress ON tbl_compress REBUILD
WITH (data_compression= PAGE)

-- Status Check
SELECT max_record_size_in_bytes, page_count,
avg_page_space_used_in_percent,
index_level, index_id, index_depth
FROMsys.dm_db_index_physical_stats(db_id('MyAdventureWorks'),object_id('dbo.t
bl_compress'),null,null,'DETAILED')
WHEREobject_name(object_id)like'tbl_compress'

Instead of 3 level node now we have only 2 level node and compression
properly taken the 30% value.

Therefore, row and page compression honours the fill factor that we defined.

6. Index and Functions


--Description: Shows how Indexing and usage of functions can negate the use
of Indexes on that column.

-- Create some indexes


CREATEUNIQUECLUSTEREDINDEX [Clustered_SalesOrderDetails]
Page 46 of 59

ON [Sales].[SalesOrderDetail]
([SalesOrderDetailID] ASC)
GO
CREATENONCLUSTEREDINDEX [NCI_TrackingNumber]
ON [Sales].[SalesOrderDetail]
([CarrierTrackingNumber] ASC)
GO
CREATENONCLUSTEREDINDEX [NCI_ModDate]
ON [Sales].[SalesOrderDetail]
([ModifiedDate] ASC)
GO

Select*from Sales.SalesOrderDetail

-- CTRL + M
Select CarrierTrackingNumber, [SalesOrderDetailID] from
Sales.SalesOrderDetail
Where CarrierTrackingNumber likeN'00EC-47DF-BB'
GO
Select CarrierTrackingNumber, [SalesOrderDetailID] from
Sales.SalesOrderDetail
WhereCONVERT(VARCHAR(50),CarrierTrackingNumber)likeN'00EC-47DF-BB'
GO

-- CTRL + M
Select ModifiedDate, [SalesOrderDetailID] from Sales.SalesOrderDetail
Where ModifiedDate ='2007-11-11'
GO
Select ModifiedDate, [SalesOrderDetailID] from Sales.SalesOrderDetail
WhereCONVERT(DATE,ModifiedDate)='2007-11-11'
GO

We have an index on modified date and we are using convert statement on


modified date.
Page 47 of 59

Conclusion is Using the functions can differ in performance .

7. Dynamic Management Views(DMV)


--Description: DMV's and the INDEXPROPERTY options exposed.

SELECT obj.name,
ind.name,
ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IndexFillFactor'),0)
[fill_factor],
create_date, modify_date, ind.type_desc,
fill_factor, has_filter,

ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IndexDepth'),0)
[IndexDepth],

ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IsAutoStatistics'),0
) [IsAutoStatistics],

ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IsStatistics'),0)
[IsStatistics],
ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IsUnique'),0)
[IsUnique],

ISNULL(INDEXPROPERTY(OBJECT_ID(obj.name),ind.name,'IsClustered'),0)
[IsClustered]
FROMsys.objects obj
INNERJOINsys.indexes ind
ON obj.object_id= ind.object_id
WHERE obj.type='U'

Modify_date represents the last modified date of the table.


Page 48 of 59

8. Table Scan , Index Scan , Index Seek


--Description: Shows differences in Table Scan, Index Scan, Index Seek

USE tempdb
GO
CREATETABLE ScanSeek(ID INT,ID1 INT,ID2 VARCHAR(8))
GO
-- Inserting Data
INSERTINTO ScanSeek(ID,ID1,ID2)
SELECT 1, 1,'One'
UNIONALL
SELECT 2, 2,'Two'
UNIONALL
SELECT 3, 3,'Three'
UNIONALL
SELECT 4, 4,'Four'
GO
-- Selecting data - Table Scan - CTRL + M
SELECT*
FROM ScanSeek
WHERE ID1 = 1
GO

-- Creating Clustered Index


CREATECLUSTEREDINDEX [IX_ScanSeek_ID] ON ScanSeek
(
[ID] ASC
)ON [PRIMARY]
GO
-- Selecting data - Index Scan - CTRL + M
SELECT*
FROM ScanSeek
WHERE ID1 = 1
GO
Page 49 of 59

-- Selecting data - Index Seek - CTRL + M


-- Changing the where condition
SELECT*
FROM ScanSeek
WHERE ID = 1
GO

-- Creating nonclustered Index


CREATENONCLUSTEREDINDEX [IX_ScanSeek_ID1] ON ScanSeek
(
[ID1] ASC
)ON [PRIMARY]
GO
-- Selecting data - Index Seek - CTRL + M
-- Original where condition
SELECT ID, ID1
FROM ScanSeek
WHERE ID1 = 1
GO
Page 50 of 59

9. Index and Order of columns


--Description: Column Used in Index matters

USE tempdb
GO
IFEXISTS(SELECT*FROMsys.objects
WHEREobject_id=OBJECT_ID(N'[dbo].[OrderTable]')ANDtypein(N'U'))
DROPTABLE [dbo].[OrderTable]
GO
CREATETABLE OrderTable(ID INT,
ID1 VARCHAR(4),
ID2 VARCHAR(4),
FirstName VARCHAR(100),
LastName VARCHAR(100),
City VARCHAR(100))
GO
-- Inserting Data
INSERTINTO OrderTable(ID, ID1, ID2, FirstName,LastName,City)
SELECTTOP 100000 ROW_NUMBER()OVER (ORDERBY a.name) RowID,
LEFT(NEWID(),4),LEFT(NEWID(),3),
'Bob',
CASEWHENROW_NUMBER()OVER (ORDERBY a.name)%2 = 1
THEN'Smith'
ELSE'Brown'END,
CASEWHENROW_NUMBER()OVER (ORDERBY a.name)%10 =
1 THEN'New York'
WHENROW_NUMBER()OVER (ORDERBY a.name)%10
= 5 THEN'San Marino'
WHENROW_NUMBER()OVER (ORDERBY a.name)%10
= 3 THEN'Los Angeles'
WHENROW_NUMBER()OVER (ORDERBY a.name)%427
= 1 THEN'Salt Lake City'
ELSE'Houston'END
FROMsys.all_objects a
CROSSJOINsys.all_objects b
GO
INSERTINTO OrderTable(ID, ID1, ID2, FirstName,LastName,City)
SELECT 100001,'A2C4','E67','FirstNameTest','SecondNameTest','CityNameTest'
GO

SELECTCOUNT(DISTINCT ID) ID,


COUNT(DISTINCT ID1) ID1,COUNT(DISTINCT ID2) ID2,
COUNT(DISTINCT FirstName) FirstName,
COUNT(DISTINCT LastName) LastName,COUNT(DISTINCT City) City
FROM OrderTable
GO
Page 51 of 59

If we see here ID1 have got a more distinct values compared to ID2 which
means ID1 has got a better cardinality when sql server can use it
-- Selecting data - Table Scan - CTRL + M
SELECT ID, ID1, ID2
FROM OrderTable
WHERE ID = 100001
GO

-- Creating Clustered
CREATECLUSTEREDINDEX [IX_OrderTable_ID] ON [dbo].[OrderTable]
(
[ID] ASC
)ON [PRIMARY]
GO

-- Selecting data - Index Seek - CTRL + M


SELECT ID, ID1, ID2
FROM OrderTable
WHERE ID = 100001
GO

-- Selecting data - Index Scan - CTRL + M


SELECT ID, ID1, ID2
FROM OrderTable
WHERE ID1 like'A2C4'
GO
Page 52 of 59

-- Execute Selects CTRL+M


SELECT ID, ID1, ID2
FROM OrderTable
WHERE ID1 ='A2C4'AND ID2 ='E67'
GO
SELECT ID, ID1, ID2
FROM OrderTable
WHERE ID2 ='E67'AND ID1 ='A2C4'
GO

-- Create non clustered index


CREATENONCLUSTEREDINDEX [IX_OrderTable_ID2] ON [dbo].[OrderTable]
(
[ID2] ASC
)ON [PRIMARY]
Page 53 of 59

-- Execute Selects
SELECT ID, ID2,ID1
FROM OrderTable
WHERE ID1 ='A2C4'AND ID2 ='E67'
GO
SELECT ID, ID2,ID1
FROM OrderTable
WHERE ID2 ='E67'AND ID1 ='A2C4'
GO

Here you can see both has used the ID2 Index so it is Index seek.

-- Create non clustered index


CREATENONCLUSTEREDINDEX [IX_OrderTable_ID1] ON [dbo].[OrderTable]
(
[ID1] ASC
)ON [PRIMARY]
GO
-- Execute Selects CTRL+M
SELECT ID, ID2, ID1
Page 54 of 59

FROM OrderTable
WHERE ID1 ='A2C4'AND ID2 ='E67'
GO
SELECT ID, ID2, ID1
FROM OrderTable
WHERE ID2 ='E67'AND ID1 ='A2C4'
GO

Here I has switched to index seek on ID1 rather than on ID2 remember the
intial query of distinct values in that we have a probability that ID1 have more
distinct values when compared to ID2 .
Hence because of higher cardinality sql server has inteligently
switched from Index scan to Index seek and to better index inside a same non-
clustered indexes that it is got.

Sql server uses a cost based optemiser that is the reason why we get this.

/*
Due to higher cardinatlity of ID1 that index is [IX_OrderTable_ID1] is used
*/

-- Execute Selects CTRL+M


SELECT ID1, LastName, City, FirstName, ID2
FROM OrderTable
WHERE City ='Salt Lake City'
GO
Page 55 of 59

-- Create non clustered index


CREATENONCLUSTEREDINDEX [IX_OrderTable_ID1_City] ON [dbo].[OrderTable]
(
ID1, City
)ON [PRIMARY]
GO
-- Execute Selects CTRL+M
SELECT ID1, LastName, City, FirstName, ID2
FROM OrderTable
WHERE City ='Salt Lake City'
GO

Again it shows clustered index scan and it is not used non-clusterd just before
created. It still shows the missing indexes.

-- Create non clustered index


CREATENONCLUSTEREDINDEX [IX_OrderTable_City_ID1] ON [dbo].[OrderTable]
(
City, ID1
)ON [PRIMARY]
Page 56 of 59

GO
-- Execute Selects CTRL+M
SELECT ID1, LastName, City, FirstName, ID2
FROM OrderTable
WHERE City ='Salt Lake City'
GO

It started to use Index seek which is the non-clustered index and it is using the
order table city ID1 index which is just now created.

Therefore, the order in the where clauses do not matter and order in which
the columns are represented inside the indexes in their keys do really matter.
Page 57 of 59

Check any new index is required and missing index would tell the particular
information.
Check maintenance jobs on rebuild and reorganising the indexes.
Page 58 of 59
Page 59 of 59

You might also like