Вы находитесь на странице: 1из 64

CREATE TABLE table1

(
column1 varchar(32) NOT NULL,
column2 int NOT NULL,
column10 varchar(255),
CONSTRAINT PK_table1 PRIMARY KEY (column1, column2)
);

CREATE TABLE table2


(
column1 varchar(32),
column2 int NOT NULL,
column20 varchar(255) NOT NULL,
CONSTRAINT FK_table2_table1 FOREIGN KEY
(column1, column2)
REFERENCES table1(column1, column2)
);

ALTER TABLE table2


DROP CONSTRAINT FK_table2_table1;
ALTER TABLE table1
DROP CONSTRAINT PK_table1;

ALTER TABLE table1


ALTER COLUMN column1 nvarchar(32) NOT NULL;
ALTER TABLE table2
ALTER COLUMN column1 nvarchar(32);

ALTER TABLE table1


ADD CONSTRAINT PK_table1
PRIMARY KEY (column1, column2)

ALTER TABLE table2


ADD CONSTRAINT FK_table1_table2
FOREIGN KEY (column1, column2)
REFERENCES table1(column1, column2)

--
Hope this helps.

Dan Guzman
SQL Server MVP

"RamaKrishna Narla" <rk*****@gmail.com> wrote in message


news:11**********************@j33g2000cwa.googlegr oups.com...
In MS SQL Server, I have the following tables with some data in it.

create table table1 (


column1 varchar(32),
column2 int not null,
column10 varchar(255),
.....
primary key (column1, column2),
);

create table table2 (


column1 varchar(32),
column2 int not null,
column20 varchar(255) not null,
....
foreign key (column1, column2) references table1(column1, column2)
);

Now, I need to change the all column types from varchar to nvarchar to
support internationalized character set.
I am able to do so, for columns column10 in table1 and column20 of
table2 without any problems by using command:

"alter table table1 alter column column10 nvarchar(255);"

But, when I try the similar thing for column1 of table1/table2, am


getting one error message saying like: "ALTER TABLE ALTER COLUMN failed
because one or more objects access this column". I guess, this is
coming because of foreign key relationship between the tables.

NOTE: While defining the table2, for foreign key I have not specified
anything like "on update cascase" ...etc.

How can I resolve this issue? Any suggestions/solutions are really


helpful to me. Thanks in advance.

Apr 1 '06 #2

SINGLE KEY

ALTER TABLE Persons ADD PRIMARY KEY (P_Id)


To allow naming of a PRIMARY KEY constraint, and for defining a PRIMARY KEY constraint on multiple columns, use the following SQL syntax:

ALTER TABLE Persons ADD CONSTRAINT pk_PersonID PRIMARY KEY (P_Id,LastName)

ALTER TABLE [dbo].[CL_Ingredient] DROP CONSTRAINT [CL_Ingredient_UK01];


GO
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Dsc_Ingredient] NVARCHAR (140) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Txt_ExplFormCode] NVARCHAR (100) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Txt_IngrCode] NVARCHAR (64) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Txt_OPMCode] NVARCHAR (64) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Txt_PrefFormCode] NVARCHAR (100) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ADD CONSTRAINT [CL_Ingredient_UK01] UNIQUE NONCLUSTERED ([Txt_IngrCode] ASC);
go
IF EXISTS
(SELECT 1 FROM sys.tables tab INNER JOIN sys.columns col ON tab.object_id = col.object_id WHERE tab.name = 'MY_TABLE' AND col.name = 'MY_COLUMN')
BEGIN
ALTER TABLE [dbo].[CL_Ingredient] DROP CONSTRAINT [PK_mytable_data];
ALTER TABLE [dbo].[CL_Ingredient] ALTER COLUMN [Txt_OPMCode] NVARCHAR (64) NOT NULL;
ALTER TABLE [dbo].[CL_Ingredient] ADD CONSTRAINT PK_mytable_data PRIMARY KEY (fkid, data);
END
GO
--------------*************************************************************************************************
--------------*************************************************************************************************
--------------*************************************************************************************************

ALTER TABLE dbo.tblDiary_events SET (LOCK_ESCALATION = TABLE)


ALTER TABLE dbo.Tmp_tblDiary SET (LOCK_ESCALATION = TABLE)

IF EXISTS(SELECT * FROM dbo.tblDiary)


EXEC('INSERT INTO dbo.Tmp_tblDiary (Diary_ID, Date, Diary_event_type_ID, Notes, Expected_call_volumes, Expected_duration, Skill_affected)
SELECT Diary_ID, Date, Diary_event_type_ID, CONVERT(varchar(MAX), Notes), Expected_call_volumes, Expected_duration, CONVERT(smallint, Skill_affected)
FROM dbo.tblDiary WITH (HOLDLOCK TABLOCKX)')
GO
SET IDENTITY_INSERT dbo.Tmp_tblDiary OFF
GO
DROP TABLE dbo.tblDiary
GO
EXECUTE sp_rename N'dbo.Tmp_tblDiary', N'tblDiary', 'OBJECT'
GO
ALTER TABLE dbo.tblDiary ADD CONSTRAINT
PK_tblDiary PRIMARY KEY NONCLUSTERED
(
Diary_ID
) WITH( PAD_INDEX = OFF, FILLFACTOR = 86, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON T3_Data_2

GO
CREATE UNIQUE CLUSTERED INDEX tblDiary_ID ON dbo.tblDiary
(
Diary_ID
) WITH( PAD_INDEX = OFF, FILLFACTOR = 86, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON T3_Data_2
GO
CREATE NONCLUSTERED INDEX tblDiary_date ON dbo.tblDiary
(
Date
) WITH( PAD_INDEX = OFF, FILLFACTOR = 86, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON T3_Data_2
GO
ALTER TABLE dbo.tblDiary WITH NOCHECK ADD CONSTRAINT
FK_tblDiary_tblDiary_events FOREIGN KEY
(
Diary_event_type_ID
) REFERENCES dbo.tblDiary_events
(
Diary_event_ID
) ON UPDATE CASCADE
ON DELETE CASCADE

GO

Online index operations are not available in every edition


Disabling an index prevents user access to the index, and for clustered indexes to the underlying table data. The index definition remains in metadata, and index statistics are kept on nonclustered
indexes. Disabling a nonclustered or clustered index on a view physically deletes the index data. Disabling a clustered index on a table prevents access to the data; the data still remains in the table, but
is unavailable for data manipulation language (DML) operations until the index is dropped or rebuilt.
In This Topic
 Before you begin:
Limitations and Restrictions
Security
 To disable an index, using:
SQL Server Management Studio
Transact-SQL

Before You Begin


Limitations and Restrictions
 The index is not maintained while it is disabled.
 The query optimizer does not consider the disabled index when creating query execution plans. Also, queries that reference the disabled index with a table hint fail.
 You cannot create an index that uses the same name as an existing disabled index.
 A disabled index can be dropped.
 When disabling a unique index, the PRIMARY KEY or UNIQUE constraint and all FOREIGN KEY constraints that reference the indexed columns from other tables are also disabled. When disabling
a clustered index, all incoming and outgoing FOREIGN KEY constraints on the underlying table are also disabled. The constraint names are listed in a warning message when the index is
disabled. After rebuilding the index, all constraints must be manually enabled by using the ALTER TABLE CHECK CONSTRAINT statement.
 Nonclustered indexes are automatically disabled when the associated clustered index is disabled. They cannot be enabled until either the clustered index on the table or view is enabled or the
clustered index on the table is dropped. Nonclustered indexes must be explicitly enabled, unless the clustered index was enabled by using the ALTER INDEX ALL REBUILD statement.
 The ALTER INDEX ALL REBUILD statement rebuilds and enables all disabled indexes on the table, except for disabled indexes on views. Indexes on views must be enabled in a separate ALTER
INDEX ALL REBUILD statement.
 Disabling a clustered index on a table also disables all clustered and nonclustered indexes on views that reference that table. These indexes must be rebuilt just as those on the referenced table.
 The data rows of the disabled clustered index cannot be accessed except to drop or rebuild the clustered index.
 You can rebuild a disabled nonclustered index online when the table does not have a disabled clustered index. However, you must always rebuild a disabled clustered index offline if you use
either the ALTER INDEX REBUILD or CREATE INDEX WITH DROP_EXISTING statement. For more information about online index operations, see Perform Index Operations Online.
 The CREATE STATISTICS statement cannot be successfully executed on a table that has a disabled clustered index.
 The AUTO_CREATE_STATISTICS database option creates new statistics on a column when the index is disabled and the following conditions exist:
o AUTO_CREATE_STATISTICS is set to ON
o There are no existing statistics for the column.
o Statistics are required during query optimization.
 If a clustered index is disabled, DBCC CHECKDB cannot return information about the underlying table; instead, the statement reports that the clustered index is disabled. DBCC
INDEXDEFRAG cannot be used to defragment a disabled index; the statement fails with an error message. You can use DBCC DBREINDEX to rebuild a disabled index.
 Creating a new clustered index enables previously disabled nonclustered indexes. For more information, see Enable Indexes and Constraints.
Security
Permissions
To execute ALTER INDEX, at a minimum, ALTER permission on the table or view is required.

Using SQL Server Management Studio


To disable an index
1. In Object Explorer, click the plus sign to expand the database that contains the table on which you want to disable an index.
2. Click the plus sign to expand the Tables folder.
3. Click the plus sign to expand the table on which you want to disable an index.
4. Click the plus sign to expand the Indexes folder.
5. Right-click the index you want to disable and select Disable.
6. In the Disable Indexes dialog box, verify that the correct index is in the Indexes to disable grid and click OK.
To disable all indexes on a table
1. In Object Explorer, click the plus sign to expand the database that contains the table on which you want to disable the indexes.
2. Click the plus sign to expand the Tables folder.
3. Click the plus sign to expand the table on which you want to disable the indexes.
4. Right-click the Indexes folder and select Disable All.
5. In the Disable Indexes dialog box, verify that the correct indexes are in the Indexes to disable grid and click OK. To remove an index from the Indexes to disable grid, select the index and
then press the Delete key.
The following information is available in the Disable Indexes dialog box:
Index Name
Displays the name of the index. During execution, this column also displays an icon representing the status.
Table Name
Displays the name of the table or view that the index was created on.
Index Type
Displays the type of the index: Clustered, Nonclustered, Spatial, or XML.
Status
Displays the status of the disable operation. Possible values after execution are:
 Blank
Prior to execution Status is blank.
 In progress
Disabling of the indexes has been started but is not complete.
 Success
The disable operation completed successfully.
Error
An error was encountered during the index disable operation, and the operation did not complete successfully.
 Stopped
The disable of the index was not completed successfully because the user stopped the operation.
Message
Provides the text of error messages during the disable operation. During execution, errors appear as hyperlinks. The text of the hyperlinks describes the body of the error. The Message column is rarely
wide enough to read the full message text. There are two ways to get the full text:
 Move the mouse pointer over the message cell to display a ToolTip with the error text.
 Click the hyperlink to display a dialog box displaying the full error.

Using Transact-SQL
To disable an index
1. In Object Explorer, connect to an instance of Database Engine.
2. On the Standard bar, click New Query.
3. Copy and paste the following example into the query window and click Execute.
4. USE AdventureWorks2012;
5. GO
6. -- disables the IX_Employee_OrganizationLevel_OrganizationNode index
7. -- on the HumanResources.Employee table
8. ALTER INDEX IX_Employee_OrganizationLevel_OrganizationNode ON HumanResources.Employee
9. DISABLE;
10.
To disable all indexes on a table
1. In Object Explorer, connect to an instance of Database Engine.
2. On the Standard bar, click New Query.
3. Copy and paste the following example into the query window and click Execute.
4. USE AdventureWorks2012;
5. GO
6. -- Disables all indexes on the HumanResources.Employee table.
7. ALTER INDEX ALL ON HumanResources.Employee
8. DISABLE;

SORT_IN_TEMPDB Option For Indexes

Applies To: SQL Server 2016


When you create or rebuild an index, by setting the SORT_IN_TEMPDB option to ON you can direct the SQL Server Database Engine to use tempdbto store the intermediate sort results that are used to
build the index. Although this option increases the amount of temporary disk space that is used to create an index, the option could reduce the time that is required to create or rebuild an index
when tempdb is on a set of disks different from that of the user database. For more information about tempdb, see Configure the index create memory Server Configuration Option.

Phases of Index Building


As the Database Engine builds an index, it goes through the following phases:
 The Database Engine first scans the data pages of the base table to retrieve key values and builds an index leaf row for each data row. When the internal sort buffers have been filled with leaf
index entries, the entries are sorted and written to disk as an intermediate sort run. The Database Engine then resumes the data page scan until the sort buffers are again filled. This pattern of
scanning multiple data pages followed by sorting and writing a sort run continues until all the rows of the base table have been processed.
In a clustered index, the leaf rows of the index are the data rows of the table; therefore, the intermediate sort runs contain all the data rows. In a nonclustered index, the leaf rows may contain
nonkey columns, but are generally smaller than a clustered index. If the index keys are large, or there are several nonkey columns included in the index, a nonclustered sort run can be large. For
more information about including nonkey columns, see Create Indexes with Included Columns.
 The Database Engine merges the sorted runs of index leaf rows into a single, sorted stream. The sort merge component of the Database Engine starts with the first page of each sort run, finds
the lowest key in all the pages, and passes that leaf row to the index create component. The next lowest key is processed, and then the next, and so on. When the last leaf index row is extracted
from a sort run page, the process shifts to the next page from that sort run. When all the pages in a sort run extent have been processed, the extent is freed. As each leaf index row is passed to
the index create component, it is included in a leaf index page in the buffer. Each leaf page is written as it is filled. As leaf pages are written, the Database Engine also builds the upper levels of
the index. Each upper level index page is written when it is filled.

SORT_IN_TEMPDB Option
When SORT_IN_TEMPDB is set to OFF, the default, the sort runs are stored in the destination filegroup. During the first phase of creating the index, the alternating reads of the base table pages and
writes of the sort runs move the disk read/write heads from one area of the disk to another. The heads are in the data page area as the data pages are scanned. They move to an area of free space when
the sort buffers fill and the current sort run has to be written to disk, and then move back to the data page area as the table page scan is resumed. The read/write head movement is greater in the
second phase. At that time the sort process is typically alternating reads from each sort run area. Both the sort runs and the new index pages are built in the destination filegroup. This means that at the
same time the Database Engine is spreading reads across the sort runs, it has to periodically jump to the index extents to write new index pages as they are filled.
If the SORT_IN_TEMPDB option is set to ON and tempdb is on a separate set of disks from the destination filegroup, during the first phase, the reads of the data pages occur on a different disk from the
writes to the sort work area in tempdb. This means the disk reads of the data keys generally continue more serially across the disk, and the writes to the tempdb disk also are generally serial, as do the
writes to build the final index. Even if other users are using the database and accessing separate disk addresses, the overall pattern of reads and writes are more efficient when SORT_IN_TEMPDB is
specified than when it is not.
The SORT_IN_TEMPDB option may improve the contiguity of index extents, especially if the CREATE INDEX operation is not being processed in parallel. The sort work area extents are freed on a
somewhat random basis with regard to their location in the database. If the sort work areas are contained in the destination filegroup, as the sort work extents are freed, they can be acquired by the
requests for extents to hold the index structure as it is built. This can randomize the locations of the index extents to a degree. If the sort extents are held separately in tempdb, the sequence in which
they are freed has no effect on the location of the index extents. Also, when the intermediate sort runs are stored in tempdb instead of the destination filegroup, there is more space available in the
destination filegroup. This increases the chances that index extents will be contiguous.
The SORT_IN_TEMPDB option affects only the current statement. No metadata records that the index was or was not sorted in tempdb. For example, if you create a nonclustered index using the
SORT_IN_TEMPDB option, and at a later time create a clustered index without specifying the option, the Database Engine does not use the option when it re-creates the nonclustered
index.

Note

If a sort operation is not required or if the sort can be performed in memory, the SORT_IN_TEMPDB option is ignored.

Disk Space Requirements


When you set the SORT_IN_TEMPDB option to ON, you must have sufficient free disk space available in tempdb to hold the intermediate sort runs, and enough free disk space in the destination
filegroup to hold the new index. The CREATE INDEX statement fails if there is insufficient free space and there is some reason the databases cannot autogrow to acquire more space, such as no space on
the disk or autogrow is set to off.
If SORT_IN_TEMPDB is set to OFF, the available free disk space in the destination filegroup must be roughly the size of the final index. During the first phase, the sort runs are built and require about the
same amount of space as the final index. During the second phase, each sort run extent is freed after it has been processed. This means that sort run extents are freed at about the same rate at which
extents are acquired to hold the final index pages; therefore, the overall space requirements do not greatly exceed the size of the final index. One side effect of this is that if the amount of free space is
very close to the size of the final index, the Database Engine will generally reuse the sort run extents very quickly after they are freed. Because the sort run extents are freed in a somewhat random
manner, this reduces the continuity of the index extents in this scenario. If SORT_IN_TEMPDB is set to OFF, the continuity of the index extents is improved if there is sufficient free space available in the
destination filegroup that the index extents can be allocated from a contiguous pool instead of from the freshly deallocated sort run extents.
When you create a nonclustered index, you must have available as free space:
 If SORT_IN_TEMPDB is set to ON, there must be sufficient free space in tempdb to store the sort runs, and sufficient free space in the destination filegroup to store the final index structure. The
sort runs contain the leaf rows of the index.
 If SORT_IN_TEMPDB is set to OFF, the free space in the destination filegroup must be large enough to store the final index structure. The continuity of the index extends may be improved if
more free space is available.
When you create a clustered index on a table that does not have nonclustered indexes, you must have available as free space:
 If SORT_IN_TEMPDB is set to ON, there must be sufficient free space in tempdb to store the sort runs. These include the data rows of the table. There must be sufficient free space in the
destination filegroup to store the final index structure. This includes the data rows of the table and the index B-tree. You may have to adjust the estimate for factors such as having a large key
size or a fill factor with a low value.
 If SORT_IN_TEMPDB is set to OFF, the free space in the destination filegroup must be large enough to store the final table. This includes the index structure. The continuity of the table and index
extents may be improved if more free space is available.
When you create a clustered index on a table that has nonclustered indexes, you must have available as free space:
 If SORT_IN_TEMPDB is set to ON, there must be sufficient free space in tempdb to store the collection of sort runs for the largest index, typically the clustered index, and sufficient free space in
the destination filegroup to store the final structures of all the indexes. This includes the clustered index that contains the data rows of the table.
 If SORT_IN_TEMPDB is set to OFF, the free space in the destination filegroup must be large enough to store the final table. This includes the structures of all the indexes. The continuity of the
table and index extents may be improved if more free space is available.

Related Tasks
CREATE INDEX (Transact-SQL)
Reorganize and Rebuild Indexes

Modify an Index

Applies To: SQL Server 2016


This topic describes how to modify an index in SQL Server 2016 by using SQL Server Management Studio or Transact-SQL.

Important

Indexes created as the result of a PRIMARY KEY or UNIQUE constraint cannot be modified by using this method. Instead, the constraint must be modified.
In This Topic
 To modify an index, using:
SQL Server Management Studio
Transact-SQL
Using SQL Server Management Studio
To modify an index
1. In Object Explorer, connect to an instance of the SQL Server Database Engine and then expand that instance.
2. Expand Databases, expand the database in which the table belongs, and then expand Tables.
3. Expand the table in which the index belongs and then expand Indexes.
4. Right-click the index that you want to modify and then click Properties.
5. In the Index Properties dialog box, make the desired changes. For example, you can add or remove a column from the index key, or change the setting of an index option.
To modify index columns
1. To add, remove, or change the position of an index column, select the General page from the Index Properties dialog box.

Using Transact-SQL
To modify an index
1. Connect to the Database Engine.
2. From the Standard bar, click New Query.
3. Copy and paste the following example into the query window and click Execute. This example drops and re-creates an existing index on the ProductID column of
the Production.WorkOrder table by using the DROP_EXISTING option. The options FILLFACTOR and PAD_INDEX are also set.

Transact-SQL

USE AdventureWorks2012;
GO
CREATE NONCLUSTERED INDEX IX_WorkOrder_ProductID
ON Production.WorkOrder(ProductID)
WITH (FILLFACTOR = 80,
PAD_INDEX = ON,
DROP_EXISTING = ON);
GO
The following example uses ALTER INDEX to set several options on the index AK_SalesOrderHeader_SalesOrderNumber.

Transact-SQL

USE AdventureWorks2012;
GO
ALTER INDEX AK_SalesOrderHeader_SalesOrderNumber ON
Sales.SalesOrderHeader
SET (
STATISTICS_NORECOMPUTE = ON,
IGNORE_DUP_KEY = ON,
ALLOW_PAGE_LOCKS = ON
) ;
GO
To modify index columns
1. To add, remove, or change the position of an index column, you must drop and recreate the index.

ALTER INDEX { index_name | ALL } ON <object>


{
REBUILD {
[ PARTITION = ALL ] [ WITH ( <rebuild_index_option> [ ,...n ] ) ]
| [ PARTITION = partition_number [ WITH ( <single_partition_rebuild_index_option> ) [ ,...n ] ]
}
| DISABLE
| REORGANIZE [ PARTITION = partition_number ] [ WITH ( <reorganize_option> ) ]
| SET ( <set_index_option> [ ,...n ] )
}
[ ; ]

<object> ::=
{
[ database_name. [ schema_name ] . | schema_name. ]
table_or_view_name
}

<rebuild_index_option > ::=


{
PAD_INDEX = { ON | OFF }
| FILLFACTOR = fillfactor
| SORT_IN_TEMPDB = { ON | OFF }
| IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
| STATISTICS_INCREMENTAL = { ON | OFF }
| ONLINE = {
ON [ (
WAIT_AT_LOW_PRIORITY ( MAX_DURATION = <time> [ MINUTES ] ,
ABORT_AFTER_WAIT = { NONE | SELF | BLOCKERS } )
) ]
| OFF }
| ALLOW_ROW_LOCKS = { ON | OFF }
| ALLOW_PAGE_LOCKS = { ON | OFF }
| MAXDOP = max_degree_of_parallelism
| COMPRESSION_DELAY = {0 | delay [Minutes]}
| DATA_COMPRESSION = { NONE | ROW | PAGE | COLUMNSTORE | COLUMNSTORE_ARCHIVE }
[ ON PARTITIONS ( {<partition_number> [ TO <partition_number>] } [ , ...n ] ) ]
}

<single_partition_rebuild_index_option> ::=
{
SORT_IN_TEMPDB = { ON | OFF }
| MAXDOP = max_degree_of_parallelism
| DATA_COMPRESSION = { NONE | ROW | PAGE | COLUMNSTORE | COLUMNSTORE_ARCHIVE} }
| ONLINE = { ON [ ( <low_priority_lock_wait> ) ] | OFF }
}

<reorganize_option>::=
{
LOB_COMPACTION = { ON | OFF }
| COMPRESS_ALL_ROW_GROUPS = { ON | OFF}
}

<set_index_option>::=
{
ALLOW_ROW_LOCKS = { ON | OFF }
| ALLOW_PAGE_LOCKS = { ON | OFF }
| IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
| COMPRESSION_DELAY= {0 | delay [Minutes]}
}

<low_priority_lock_wait>::=
{
WAIT_AT_LOW_PRIORITY ( MAX_DURATION = <time> [ MINUTES ] ,
ABORT_AFTER_WAIT = { NONE | SELF | BLOCKERS } )
}

-- Syntax for Azure SQL Database

ALTER INDEX { index_name | ALL } ON <object>


{
REBUILD [ WITH ( <rebuild_index_option> [ ,...n ] ) ]
| DISABLE
| REORGANIZE [ PARTITION = partition_number ] [ WITH ( <reorganize_option> ) ]
| SET ( <set_index_option> [ ,...n ] )
}
[ ; ]

<object> ::=
{
[ database_name. [ schema_name ] . | schema_name. ]
table_or_view_name
}

<rebuild_index_option > ::=


{
IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
| ONLINE = { ON | OFF }
| DATA_COMPRESSION = { NONE | ROW | PAGE }
| COMPRESSION_DELAY= {0 | delay [Minutes]}
}

<reorganize_option>::=
{
LOB_COMPACTION = { ON | OFF }
| COMPRESS_ALL_ROW_GROUPS = { ON | OFF}
}

<set_index_option>::=
{
IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
}

-- Syntax for Azure SQL Data Warehouse and Parallel Data Warehouse

ALTER INDEX { index_name | ALL }


ON [ schema_name. ] table_name
{
REBUILD {
[ PARTITION = ALL [ WITH ( <rebuild_index_option> ) ] ]
| [ PARTITION = partition_number [ WITH ( <single_partition_rebuild_index_option> )] ]
}
| DISABLE
| REORGANIZE [ PARTITION = partition_number ]
}
[;]

<rebuild_index_option > ::=


{
DATA_COMPRESSION = { COLUMNSTORE | COLUMNSTORE_ARCHIVE }
[ ON PARTITIONS ( {<partition_number> [ TO <partition_number>] } [ , ...n ] ) ]
}

<single_partition_rebuild_index_option > ::=


{
DATA_COMPRESSION = { COLUMNSTORE | COLUMNSTORE_ARCHIVE }
}

Arguments
index_name
Is the name of the index. Index names must be unique within a table or view but do not have to be unique within a database. Index names must follow the rules of identifiers.
ALL
Specifies all indexes associated with the table or view regardless of the index type. Specifying ALL causes the statement to fail if one or more indexes are in an offline or read-only filegroup or the
specified operation is not allowed on one or more index types. The following table lists the index operations and disallowed index types.

Specifying ALL with this operation Fails if the table has one or more

REBUILD WITH ONLINE = ON XML index

Spatial index

Columnstore index : Applies to:SQL Server 2012 through SQL Server 2016, SQL
Database V12.

REBUILD PARTITION = partition_number Nonpartitioned index, XML index, spatial index, or disabled index

REORGANIZE Indexes with ALLOW_PAGE_LOCKS set to OFF


Specifying ALL with this operation Fails if the table has one or more

REORGANIZE PARTITION Nonpartitioned index, XML index, spatial index, or disabled index
= partition_number

IGNORE_DUP_KEY = ON XML index

Spatial index

Columnstore index : Applies to:SQL Server 2012 through SQL Server 2016, SQL
Database V12.

ONLINE = ON XML index

Spatial index

Columnstore index : Applies to:SQL Server 2012 through SQL Server 2016, SQL
Database V12.

Warning

For more detailed information about index operations that can be performed online, see Guidelines for Online Index Operations.
If ALL is specified with PARTITION = partition_number, all indexes must be aligned. This means that they are partitioned based on equivalent partition functions. Using ALL with PARTITION causes all
index partitions with the same partition_number to be rebuilt or reorganized. For more information about partitioned indexes, see Partitioned Tables and Indexes.
database_name
Is the name of the database.
schema_name
Is the name of the schema to which the table or view belongs.
table_or_view_name
Is the name of the table or view associated with the index. To display a report of the indexes on an object, use the sys.indexes catalog view.
Windows Azure SQL Database supports the three-part name format database_name.[schema_name].table_or_view_name when the database_name is the current database or the database_name is
tempdb and the table_or_view_name starts with #.
REBUILD [ WITH (<rebuild_index_option> [ ,... n]) ]
Specifies the index will be rebuilt using the same columns, index type, uniqueness attribute, and sort order. This clause is equivalent to DBCC DBREINDEX. REBUILD enables a disabled index. Rebuilding a
clustered index does not rebuild associated nonclustered indexes unless the keyword ALL is specified. If index options are not specified, the existing index option values stored in sys.indexes are applied.
For any index option whose value is not stored in sys.indexes, the default indicated in the argument definition of the option applies.
If ALL is specified and the underlying table is a heap, the rebuild operation has no effect on the table. Any nonclustered indexes associated with the table are rebuilt.
The rebuild operation can be minimally logged if the database recovery model is set to either bulk-logged or simple.

Note

When you rebuild a primary XML index, the underlying user table is unavailable for the duration of the index operation.

Applies to: SQL Server 2012 through SQL Server 2016, SQL Database V12.

For columnstore indexes, the rebuild operation:


1. Does not use the sort order.
2. Acquires an exclusive lock on the table or partition while the rebuild occurs. The data is “offline” and unavailable during the rebuild, even when using NOLOCK, RCSI, or SI.
3. Re-compresses all data into the columnstore. Two copies of the columnstore index exist while the rebuild is taking place. When the rebuild is finished, SQL Server deletes the original
columnstore index.
For more information about rebuilding columnstore indexes, see Columnstore Indexes Defragmentation
PARTITION

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies that only one partition of an index will be rebuilt or reorganized. PARTITION cannot be specified if index_name is not a partitioned index.
PARTITION = ALL rebuilds all partitions.

Warning

Creating and rebuilding nonaligned indexes on a table with more than 1,000 partitions is possible, but is not supported. Doing so may cause degraded performance or excessive memory
consumption during these operations. We recommend using only aligned indexes when the number of partitions exceed 1,000.
partition_number
Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Is the partition number of a partitioned index that is to be rebuilt or reorganized. partition_number is a constant expression that can reference variables. These include user-defined type variables or
functions and user-defined functions, but cannot reference a Transact-SQL statement. partition_number must exist or the statement fails.
WITH (<single_partition_rebuild_index_option>)

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

SORT_IN_TEMPDB, MAXDOP, and DATA_COMPRESSION are the options that can be specified when you rebuild a single partition (PARTITION = n). XML indexes cannot be specified in a single partition
rebuild operation.
DISABLE
Marks the index as disabled and unavailable for use by the Database Engine. Any index can be disabled. The index definition of a disabled index remains in the system catalog with no underlying index
data. Disabling a clustered index prevents user access to the underlying table data. To enable an index, use ALTER INDEX REBUILD or CREATE INDEX WITH DROP_EXISTING. For more information,
see Disable Indexes and Constraints and Enable Indexes and Constraints.
REORGANIZE a rowstore index
For rowstore indexes, REORGANIZE specifies to reorganize the index leaf level. The REORGANIZE operation is:
 Always performed online. This means long-term blocking table locks are not held and queries or updates to the underlying table can continue during the ALTER INDEX REORGANIZE transaction.
 Not allowed for a disabled index
 Not allowed when ALLOW_PAGE_LOCKS is set to OFF
 Not rolled back when it is performed within a transaction and the transaction is rolled back.
REORGANIZE WITH ( LOB_COMPACTION = { ON | OFF } )
Applies to rowstore indexes.
LOB_COMPACTION = ON
 Specifies to compact all pages that contain data of these large object (LOB) data types: image, text, ntext, varchar(max), nvarchar(max), varbinary(max), and xml. Compacting this data can reduce
the data size on disk.
 For a clustered index, this compacts all LOB columns that are contained in the table.
 For a nonclustered index, this compacts all LOB columns that are nonkey (included) columns in the index.
 REORGANIZE ALL performs LOB_COMPACTION on all indexes. For each index, this compacts all LOB columns in the clustered index, underlying table, or included columns in a nonclustered
index..
LOB_COMPACTION = OFF
 Pages that contain large object data are not compacted.
 OFF has no effect on a heap.
REORGANIZE a columnstore index
REORGANIZE is performed online.
For columnstore indexes, REORGANIZE compresses each CLOSED delta rowgroup into the columnstore as a compressed rowgroup.
 REORGANIZE is not required in order to move CLOSED delta rowgroups into compressed rowgroups. The background tuple-mover (TM) process wakes up periodically to compress CLOSED
delta rowgroups. We recommend using REORGANIZE when tuple-mover is falling behind. REORGANIZE can compress rowgroups more aggressively.
 To compress all OPEN and CLOSED rowgroups, see the REBUILD WITH (COMPRESS_ALL_ROW_GROUPS) option in this section.
For columnstore indexes in SQL Server (starting with 2016) and Azure SQL Database, REORGANIZE performs the following additional defragmentation optimizations online:
 Physically removes rows from a rowgroup when 10% or more of the rows have been logically deleted. The deleted bytes are reclaimed on the physical media. For example, if a compressed row
group of 1 million rows has 100K rows deleted, SQL Server will remove the deleted rows and recompress the rowgroup with 900k rows. It saves on the storage by removing deleted rows.
 Combines one or more compressed rowgroups to increase rows per rowgroup up to the maximum of 1,024,576 rows. For example, if you bulk import 5 batches of 102,400 rows you will get 5
compressed rowgroups. If you run REORGANIZE, these rowgroups will get merged into 1 compressed rowgroup of size 512,000 rows. This assumes there were no dictionary size or memory
limitations.
 For rowgroups in which 10% or more of the rows have been logically deleted, SQL Server will try to combine this rowgroup with one or more rowgroups. For example, rowgroup 1 is compressed
with 500,000 rows and rowgroup 21 is compressed with the maximum of 1,048,576 rows. Rowgroup 21 has 60% of the rows deleted which leaves 409,830 rows. SQL Server favors combining
these two rowgroups to compress a new rowgroup that has 909,830 rows.
REORGANIZE WITH ( COMPRESS_ALL_ROW_GROUPS = { ON | OFF } )
In SQL Server (starting with 2016) and Azure SQL Database, the COMPRESS_ALL_ROW_GROUPS provides a way to force OPEN or CLOSED delta rowgroups into the columnstore. With this option, it is
not necessary to rebuild the columnstore index to empty the delta rowgroups. This, combined with the other remove and merge defragmentation features makes it no longer necessary to rebuild the
index in most situations.
 ON forces all rowgroups into the columnstore, regardless of size and state (CLOSED or OPEN).
 OFF forces all CLOSED rowgroups into the columnstore.
SET ( <set_index option> [ ,... n] )
Specifies index options without rebuilding or reorganizing the index. SET cannot be specified for a disabled index.
PAD_INDEX = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies index padding. The default is OFF.


ON
The percentage of free space that is specified by FILLFACTOR is applied to the intermediate-level pages of the index. If FILLFACTOR is not specified at the same time PAD_INDEX is set to ON, the fill
factor value stored in sys.indexes is used.
OFF or fillfactor is not specified
The intermediate-level pages are filled to near capacity. This leaves sufficient space for at least one row of the maximum size that the index can have, based on the set of keys on the intermediate pages.
For more information, see CREATE INDEX (Transact-SQL).
FILLFACTOR = fillfactor

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies a percentage that indicates how full the Database Engine should make the leaf level of each index page during index creation or alteration. fillfactor must be an integer value from 1 to 100. The
default is 0. Fill factor values 0 and 100 are the same in all respects.
An explicit FILLFACTOR setting applies only when the index is first created or rebuilt. The Database Engine does not dynamically keep the specified percentage of empty space in the pages. For more
information, see CREATE INDEX (Transact-SQL).
To view the fill factor setting, use sys.indexes.

Important

Creating or altering a clustered index with a FILLFACTOR value affects the amount of storage space the data occupies, because the Database Engine redistributes the data when it
creates the clustered index.
SORT_IN_TEMPDB = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether to store the sort results in tempdb. The default is OFF.
ON
The intermediate sort results that are used to build the index are stored in tempdb. If tempdb is on a different set of disks than the user database, this may reduce the time needed to create an index.
However, this increases the amount of disk space that is used during the index build.
OFF
The intermediate sort results are stored in the same database as the index.
If a sort operation is not required, or if the sort can be performed in memory, the SORT_IN_TEMPDB option is ignored.
For more information, see SORT_IN_TEMPDB Option For Indexes.
IGNORE_DUP_KEY = { ON | OFF }
Specifies the error response when an insert operation attempts to insert duplicate key values into a unique index. The IGNORE_DUP_KEY option applies only to insert operations after the index is created
or rebuilt. The default is OFF.
ON
A warning message will occur when duplicate key values are inserted into a unique index. Only the rows violating the uniqueness constraint will fail.
OFF
An error message will occur when duplicate key values are inserted into a unique index. The entire INSERT operation will be rolled back.
IGNORE_DUP_KEY cannot be set to ON for indexes created on a view, non-unique indexes, XML indexes, spatial indexes, and filtered indexes.
To view IGNORE_DUP_KEY, use sys.indexes.
In backward compatible syntax, WITH IGNORE_DUP_KEY is equivalent to WITH IGNORE_DUP_KEY = ON.
STATISTICS_NORECOMPUTE = { ON | OFF }
Specifies whether distribution statistics are recomputed. The default is OFF.
ON
Out-of-date statistics are not automatically recomputed.
OFF
Automatic statistics updating are enabled.
To restore automatic statistics updating, set the STATISTICS_NORECOMPUTE to OFF, or execute UPDATE STATISTICS without the NORECOMPUTE clause.

Important

Disabling automatic recomputation of distribution statistics may prevent the query optimizer from picking optimal execution plans for queries that involve the table.
STATISTICS_INCREMENTAL = { ON | OFF }
When ON, the statistics created are per partition statistics. When OFF, the statistics tree is dropped and SQL Server re-computes the statistics. The default is OFF.
If per partition statistics are not supported the option is ignored and a warning is generated. Incremental stats are not supported for following statistics types:
 Statistics created with indexes that are not partition-aligned with the base table.
 Statistics created on Always On readable secondary databases.
 Statistics created on read-only databases.
 Statistics created on filtered indexes.
 Statistics created on views.
 Statistics created on internal tables.
 Statistics created with spatial indexes or XML indexes.

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

ONLINE = { ON | OFF } <as applies to rebuild_index_option>


Specifies whether underlying tables and associated indexes are available for queries and data modification during the index operation. The default is OFF.
For an XML index or spatial index, only ONLINE = OFF is supported, and if ONLINE is set to ON an error is raised.

Note

Online index operations are not available in every edition of Microsoft SQL Server. For a list of features that are supported by the editions of SQL Server, see Features Supported by the
Editions of SQL Server 2016.
ON
Long-term table locks are not held for the duration of the index operation. During the main phase of the index operation, only an Intent Share (IS) lock is held on the source table. This allows queries or
updates to the underlying table and indexes to continue. At the start of the operation, a Shared (S) lock is very briefly held on the source object. At the end of the operation, an S lock is very briefly held
on the source if a nonclustered index is being created, or an SCH-M (Schema Modification) lock is acquired when a clustered index is created or dropped online, or when a clustered or nonclustered
index is being rebuilt. ONLINE cannot be set to ON when an index is being created on a local temporary table.
OFF
Table locks are applied for the duration of the index operation. An offline index operation that creates, rebuilds, or drops a clustered, spatial, or XML index, or rebuilds or drops a nonclustered index,
acquires a Schema modification (Sch-M) lock on the table. This prevents all user access to the underlying table for the duration of the operation. An offline index operation that creates a nonclustered
index acquires a Shared (S) lock on the table. This prevents updates to the underlying table but allows read operations, such as SELECT statements.
For more information, see How Online Index Operations Work.
Indexes, including indexes on global temp tables, can be rebuilt online with the following exceptions:
 XML indexes
 Indexes on local temp tables
 A subset of a partitioned index (An entire partitioned index can be rebuilt online.)
 SQL Database prior to V12, and SQL Server prior to SQL Server 2012, do not permit the ONLINE option for clustered index build or rebuild operations when the base table
contains varchar(max) or varbinary(max) columns.
ALLOW_ROW_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether row locks are allowed. The default is ON.


ON
Row locks are allowed when accessing the index. The Database Engine determines when row locks are used.
OFF
Row locks are not used.
ALLOW_PAGE_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether page locks are allowed. The default is ON.


ON
Page locks are allowed when you access the index. The Database Engine determines when page locks are used.
OFF
Page locks are not used.

Note

An index cannot be reorganized when ALLOW_PAGE_LOCKS is set to OFF.


MAXDOP =max_degree_of_parallelism

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12 (Performance Levels P2 and P3 only).

Overrides the max degree of parallelism configuration option for the duration of the index operation. For more information, see Configure the max degree of parallelism Server Configuration Option .
Use MAXDOP to limit the number of processors used in a parallel plan execution. The maximum is 64 processors.

Important

Although the MAXDOP option is syntactically supported for all XML indexes, for a spatial index or a primary XML index, ALTER INDEX currently uses only a single processor.
max_degree_of_parallelism can be:
1
Suppresses parallel plan generation.
>1
Restricts the maximum number of processors used in a parallel index operation to the specified number.
0 (default)
Uses the actual number of processors or fewer based on the current system workload.
For more information, see Configure Parallel Index Operations.

Note

Parallel index operations are not available in every edition of Microsoft SQL Server. For a list of features that are supported by the editions of SQL Server, see Features Supported by
the Editions of SQL Server 2016.
COMPRESSION_DELAY = { 0 |duration [Minutes] }
This feature will be available after SQL Server 2016 CTP 3.3
For a disk-based table, delay specifies the minimum number of minutes a delta rowgroup in the CLOSED state must remain in the delta rowgroup before SQL Server can compress it into the compressed
rowgroup. Since disk-based tables don't track insert and update times on individual rows, SQL Server applies the delay to delta rowgroups in the CLOSED state.
The default is 0 minutes.
The default is 0 minutes.
For recommendations on when to use COMPRESSION_DELAY, see Columnstore Indexes for Real-Time Operational Analytics.
DATA_COMPRESSION
Specifies the data compression option for the specified index, partition number, or range of partitions. The options are as follows:
NONE
Index or specified partitions are not compressed. This does not apply to columnstore indexes.
ROW
Index or specified partitions are compressed by using row compression. This does not apply to columnstore indexes.
PAGE
Index or specified partitions are compressed by using page compression. This does not apply to columnstore indexes.
COLUMNSTORE
Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

Applies only to columnstore indexes, including both nonclustered columnstore and clustered columnstore indexes. COLUMNSTORE specifies to decompress the index or specified partitions that are
compressed with the COLUMNSTORE_ARCHIVE option. When the data is restored, it will continue to be compressed with the columnstore compression that is used for all columnstore indexes.
COLUMNSTORE_ARCHIVE

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

Applies only to columnstore indexes, including both nonclustered columnstore and clustered columnstore indexes. COLUMNSTORE_ARCHIVE will further compress the specified partition to a smaller
size. This can be used for archival, or for other situations that require a smaller storage size and can afford more time for storage and retrieval.
For more information about compression, see Data Compression.
ON PARTITIONS ( { <partition_number_expression> | <range> } [,...n] )

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies the partitions to which the DATA_COMPRESSION setting applies. If the index is not partitioned, the ON PARTITIONS argument will generate an error. If the ON PARTITIONS clause is not
provided, the DATA_COMPRESSION option applies to all partitions of a partitioned index.
<partition_number_expression> can be specified in the following ways:
 Provide the number for a partition, for example: ON PARTITIONS (2).
 Provide the partition numbers for several individual partitions separated by commas, for example: ON PARTITIONS (1, 5).
 Provide both ranges and individual partitions: ON PARTITIONS (2, 4, 6 TO 8).
<range> can be specified as partition numbers separated by the word TO, for example: ON PARTITIONS (6 TO 8).
To set different types of data compression for different partitions, specify the DATA_COMPRESSION option more than once, for example:
REBUILD WITH
(
DATA_COMPRESSION = NONE ON PARTITIONS (1),
DATA_COMPRESSION = ROW ON PARTITIONS (2, 4, 6 TO 8),
DATA_COMPRESSION = PAGE ON PARTITIONS (3, 5)
);

ONLINE = { ON | OFF } <as applies to single_partition_rebuild_index_option>


Specifies whether an index or an index partition of an underlying table can be rebuild online or offline. If REBUILD is performed online (ON) the data in this table is available for queries and data
modification during the index operation. The default is OFF.
ON
Long-term table locks are not held for the duration of the index operation. During the main phase of the index operation, only an Intent Share (IS) lock is held on the source table. A S-lock on the table
is required in the beginning of the index rebuild and a Sch-M lock on the table at the end of the online index rebuild. Although both locks are short metadata locks, especially the Sch-M lock must wait
for all blocking transactions to be completed. During the wait time the Sch-M lock blocks all other transactions that wait behind this lock when accessing the same table.

Note

Online index rebuild can set the low_priority_lock_wait options described later in this section.
OFF
Table locks are applied for the duration of the index operation. This prevents all user access to the underlying table for the duration of the operation.
WAIT_AT_LOW_PRIORITY

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

An online index rebuild has to wait for blocking operations on this table. WAIT_AT_LOW_PRIORITY indicates that the online index rebuild operation will wait for low priority locks, allowing other
operations to proceed while the online index build operation is waiting. Omitting the WAIT AT LOW PRIORITY option is equivalent to WAIT_AT_LOW_PRIORITY (MAX_DURATION = 0 minutes,
ABORT_AFTER_WAIT = NONE).
MAX_DURATION = time [MINUTES ]

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

The wait time (an integer value specified in minutes) that the online index rebuild locks will wait with low priority when executing the DDL command. If the operation is blocked for
the MAX_DURATION time, one of the ABORT_AFTER_WAIT actions will be executed. MAX_DURATION time is always in minutes, and the word MINUTES can be omitted.
ABORT_AFTER_WAIT = [NONE | SELF | BLOCKERS } ]
Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

NONE
Continue waiting for the lock with normal (regular) priority.
SELF
Exit the online index rebuild DDL operation currently being executed without taking any action.
BLOCKERS
Kill all user transactions that block the online index rebuild DDL operation so that the operation can continue. The BLOCKERS option requires the login to have ALTER ANY CONNECTION permission.

Remarks
ALTER INDEX cannot be used to repartition an index or move it to a different filegroup. This statement cannot be used to modify the index definition, such as adding or deleting columns or changing
the column order. Use CREATE INDEX with the DROP_EXISTING clause to perform these operations.
When an option is not explicitly specified, the current setting is applied. For example, if a FILLFACTOR setting is not specified in the REBUILD clause, the fill factor value stored in the system catalog will
be used during the rebuild process. To view the current index option settings, use sys.indexes.

Note

The values for ONLINE, MAXDOP, and SORT_IN_TEMPDB are not stored in the system catalog. Unless specified in the index statement, the default value for the option is used.
On multiprocessor computers, just like other queries do, ALTER INDEX REBUILD automatically uses more processors to perform the scan and sort operations that are associated with modifying the index.
When you run ALTER INDEX REORGANIZE, with or without LOB_COMPACTION, the max degree of parallelism value is a single threaded operation. For more information, see Configure Parallel Index
Operations.
An index cannot be reorganized or rebuilt if the filegroup in which it is located is offline or set to read-only. When the keyword ALL is specified and one or more indexes are in an offline or read-only
filegroup, the statement fails.

Rebuilding Indexes
Rebuilding an index drops and re-creates the index. This removes fragmentation, reclaims disk space by compacting the pages based on the specified or existing fill factor setting, and reorders the index
rows in contiguous pages. When ALL is specified, all indexes on the table are dropped and rebuilt in a single transaction. FOREIGN KEY constraints do not have to be dropped in advance. When indexes
with 128 extents or more are rebuilt, the Database Engine defers the actual page deallocations, and their associated locks, until after the transaction commits.
Rebuilding or reorganizing small indexes often does not reduce fragmentation. The pages of small indexes are sometimes stored on mixed extents. Mixed extents are shared by up to eight objects, so
the fragmentation in a small index might not be reduced after reorganizing or rebuilding it.
Starting with SQL Server 2012, statistics are not created by scanning all the rows in the table when a partitioned index is created or rebuilt. Instead, the query optimizer uses the default sampling
algorithm to generate statistics. To obtain statistics on partitioned indexes by scanning all the rows in the table, use CREATE STATISTICS or UPDATE STATISTICS with the FULLSCAN clause.
In earlier versions of SQL Server, you could sometimes rebuild a nonclustered index to correct inconsistencies caused by hardware failures. In SQL Server 2008 and later, you may still be able to repair
such inconsistencies between the index and the clustered index by rebuilding a nonclustered index offline. However, you cannot repair nonclustered index inconsistencies by rebuilding the index online,
because the online rebuild mechanism will use the existing nonclustered index as the basis for the rebuild and thus persist the inconsistency. Rebuilding the index offline can sometimes force a scan of
the clustered index (or heap) and so remove the inconsistency. To assure a rebuild from the clustered index, drop and recreate the non-clustered index. As with earlier versions, we recommend
recovering from inconsistencies by restoring the affected data from a backup; however, you may be able to repair the index inconsistencies by rebuilding the nonclustered index offline. For more
information, see DBCC CHECKDB (Transact-SQL).
To rebuild a clustered columnstore index, SQL Server:
1. Acquires an exclusive lock on the table or partition while the rebuild occurs. The data is “offline” and unavailable during the rebuild.
2. Defragments the columnstore by physically deleting rows that have been logically deleted from the table; the deleted bytes are reclaimed on the physical media.
3. Reads all data from the original columnstore index, including the deltastore. It combines the data into new rowgroups, and compresses the rowgroups into the columnstore.
4. Requires space on the physical media to store two copies of the columnstore index while the rebuild is taking place. When the rebuild is finished, SQL Server deletes the original clustered
columnstore index.

Reorganizing Indexes
Reorganizing an index uses minimal system resources. It defragments the leaf level of clustered and nonclustered indexes on tables and views by physically reordering the leaf-level pages to match the
logical, left to right, order of the leaf nodes. Reorganizing also compacts the index pages. Compaction is based on the existing fill factor value. To view the fill factor setting, use sys.indexes.
When ALL is specified, relational indexes, both clustered and nonclustered, and XML indexes on the table are reorganized. Some restrictions apply when specifying ALL, see the definition for ALL in the
Arguments section.
For more information, see Reorganize and Rebuild Indexes.

Disabling Indexes
Disabling an index prevents user access to the index, and for clustered indexes, to the underlying table data. The index definition remains in the system catalog. Disabling a nonclustered index or
clustered index on a view physically deletes the index data. Disabling a clustered index prevents access to the data, but the data remains unmaintained in the B-tree until the index is dropped or rebuilt.
To view the status of an enabled or disabled index, query the is_disabled column in the sys.indexes catalog view.
If a table is in a transactional replication publication, you cannot disable any indexes that are associated with primary key columns. These indexes are required by replication. To disable an index, you
must first drop the table from the publication. For more information, see Publish Data and Database Objects.
Use the ALTER INDEX REBUILD statement or the CREATE INDEX WITH DROP_EXISTING statement to enable the index. Rebuilding a disabled clustered index cannot be performed with the ONLINE
option set to ON. For more information, see Disable Indexes and Constraints.

Setting Options
You can set the options ALLOW_ROW_LOCKS, ALLOW_PAGE_LOCKS, IGNORE_DUP_KEY and STATISTICS_NORECOMPUTE for a specified index without rebuilding or reorganizing that index. The modified
values are immediately applied to the index. To view these settings, use sys.indexes. For more information, see Set Index Options.
Row and Page Locks Options
When ALLOW_ROW_LOCKS = ON and ALLOW_PAGE_LOCK = ON, row-level, page-level, and table-level locks are allowed when you access the index. The Database Engine chooses the appropriate lock
and can escalate the lock from a row or page lock to a table lock.
When ALLOW_ROW_LOCKS = OFF and ALLOW_PAGE_LOCK = OFF, only a table-level lock is allowed when you access the index.
If ALL is specified when the row or page lock options are set, the settings are applied to all indexes. When the underlying table is a heap, the settings are applied in the following ways:

ALLOW_ROW_LOCKS = ON To the heap and any associated nonclustered indexes.


or OFF

ALLOW_PAGE_LOCKS = ON To the heap and any associated nonclustered indexes.


ALLOW_PAGE_LOCKS = Fully to the nonclustered indexes. This means that all page locks are not allowed on the nonclustered indexes. On the heap, only the shared (S), update
OFF (U) and exclusive (X) locks for the page are not allowed. The Database Engine can still acquire an intent page lock (IS, IU or IX) for internal purposes.

Online Index Operations


When rebuilding an index and the ONLINE option is set to ON, the underlying objects, the tables and associated indexes, are available for queries and data modification. You can also rebuild online a
portion of an index residing on a single partition. Exclusive table locks are held only for a very short amount of time during the alteration process.
Reorganizing an index is always performed online. The process does not hold locks long term and, therefore, does not block queries or updates that are running.
You can perform concurrent online index operations on the same table or table partition only when doing the following:
 Creating multiple nonclustered indexes.
 Reorganizing different indexes on the same table.
 Reorganizing different indexes while rebuilding nonoverlapping indexes on the same table.
All other online index operations performed at the same time fail. For example, you cannot rebuild two or more indexes on the same table concurrently, or create a new index while rebuilding an
existing index on the same table.
For more information, see Perform Index Operations Online.
WAIT_AT_LOW_PRIORITY
In order to execute the DDL statement for an online index rebuild, all active blocking transactions running on a particular table must be completed. When the online index rebuild executes, it blocks all
new transactions that are ready to start execution on this table. Although the duration of the lock for online index rebuild is very short, waiting for all open transactions on a given table to complete and
blocking the new transactions to start, might significantly affect the throughput, causing a workload slow down or timeout, and significantly limit access to the underlying table.
The WAIT_AT_LOW_PRIORITY option allows DBA's to manage the S-lock and Sch-M locks required for online index rebuilds and allows them to select one of 3 options. In all 3 cases, if during the wait
time ( (MAX_DURATION = n [minutes]) ) there are no blocking activities, the online index rebuild is executed immediately without waiting and the DDL statement is completed.

Spatial Index Restrictions


When you rebuild a spatial index, the underlying user table is unavailable for the duration of the index operation because the spatial index holds a schema lock.
The PRIMARY KEY constraint in the user table cannot be modified while a spatial index is defined on a column of that table. To change the PRIMARY KEY constraint, first drop every spatial index of the
table. After modifying the PRIMARY KEy constraint, you can re-create each of the spatial indexes.
In a single partition rebuild operation, you cannot specify any spatial indexes. However, you can specify spatial indexes in a complete partition rebuild.
To change options that are specific to a spatial index, such as BOUNDING_BOX or GRID, you can either use a CREATE SPATIAL INDEX statement that specifies DROP_EXISTING = ON, or drop the spatial
index and create a new one. For an example, see CREATE SPATIAL INDEX (Transact-SQL).

Data Compression
For a more information about data compression, see Data Compression.
To evaluate how changing PAGE and ROW compression will affect a table, an index, or a partition, use the sp_estimate_data_compression_savingsstored procedure.
The following restrictions apply to partitioned indexes:
 When you use ALTER INDEX ALL ..., you cannot change the compression setting of a single partition if the table has nonaligned indexes.
 The ALTER INDEX <index> ... REBUILD PARTITION ... syntax rebuilds the specified partition of the index.
 The ALTER INDEX <index> ... REBUILD WITH ... syntax rebuilds all partitions of the index.

Statistics
When you execute ALTER INDEX ALL … on a table, only the statistics associates with indexes are updated. Automatic or manual statistics created on the table (instead of an index) are not updated.

Permissions
To execute ALTER INDEX, at a minimum, ALTER permission on the table or view is required.

Version Notes
 Azure SQL Database does not use filegroup and filestream options.
 Columnstore indexes are not available prior to SQL Server 2012.

Examples: Columnstore Indexes


These examples apply to columnstore indexes.
REORGANIZE demo
This example demonstrates how the ALTER INDEX REORGANIZE command works. It creates a table that has multiple rowgroups, and then demonstrates how REORGANIZE merges the rowgroups.
-- Create a database
CREATE DATABASE [ columnstore ];
GO

-- Create a rowstore staging table


CREATE TABLE [ staging ] (
AccountKey int NOT NULL,
AccountDescription nvarchar (50),
AccountType nvarchar(50),
AccountCodeAlternateKey int
)

-- Insert 10 million rows into the staging table.


DECLARE @loop int
DECLARE @AccountDescription varchar(50)
DECLARE @AccountKey int
DECLARE @AccountType varchar(50)
DECLARE @AccountCode int

SELECT @loop = 0
BEGIN TRAN
WHILE (@loop < 300000)
BEGIN
SELECT @AccountKey = CAST (RAND()*10000000 as int);
SELECT @AccountDescription = 'accountdesc ' + CONVERT(varchar(20), @AccountKey);
SELECT @AccountType = 'AccountType ' + CONVERT(varchar(20), @AccountKey);
SELECT @AccountCode = CAST (RAND()*10000000 as int);

INSERT INTO staging VALUES (@AccountKey, @AccountDescription, @AccountType, @AccountCode);

SELECT @loop = @loop + 1;


END
COMMIT

-- Create a table for the clustered columnstore index

CREATE TABLE cci_target (


AccountKey int NOT NULL,
AccountDescription nvarchar (50),
AccountType nvarchar(50),
AccountCodeAlternateKey int
)

-- Convert the table to a clustered columnstore index named inxcci_cci_target;


CREATE CLUSTERED COLUMNSTORE INDEX idxcci_cci_target ON cci_target;
Use the TABLOCK option to insert rows in parallel. Starting with SQL Server 2016, the INSERT INTO operation can run in parallel when TABLOCK is used.
INSERT INTO cci_target WITH (TABLOCK)
SELECT TOP 300000 * FROM staging;

Run this command to see the OPEN delta rowgroups. The number of rowgroups depends on the degree of parallelism.
SELECT *
FROM sys.dm_db_column_store_row_group_physical_stats
WHERE object_id = object_id('cci_target');

Run this command to force all CLOSED and OPEN rowgroups into the columnstore.
ALTER INDEX idxcci_cci_target ON cci_target REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);

Run this command again and you will see that smaller rowgroups are merged into one compressed rowgroup.
ALTER INDEX idxcci_cci_target ON cci_target REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);

A. Compress CLOSED delta rowgroups into the columnstore


This example uses the REORGANIZE option to compresses each CLOSED delta rowgroup into the columnstore as a compressed rowgroup. This is not necessary, but is useful when the tuple-mover is not
compressing CLOSED rowgroups fast enough.
-- Uses AdventureWorksDW
-- REORGANIZE all partitions
ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REORGANIZE;

-- REORGANIZE a specific partition


ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REORGANIZE PARTITION = 0;

B. Compress all OPEN AND CLOSED delta rowgroups into the columnstore
Does not apply to: SQL Server 2012 and 2014
Starting with SQL Server 2016, you can run REORGANIZE WITH ( COMPRESS_ALL_ROW_GROUPS =ON ) to compress each OPEN and CLOSED delta rowgroup into the columnstore as a compressed
rowgroup. This empties the deltastores and forces all rows to get compressed into the columnstore. This is useful especially after performing many insert operations since these operations store the
rows in one or more deltastores.
REORGANIZE combines rowgroups to fill rowgroups up to a maximum number of rows <= 1,024,576. Therefore, when you compress all OPEN and CLOSED rowgroups you won't end up with lots of
compressed rowgroups that only have a few rows in them. You want rowgroups to be as full as possible to reduce the compressed size and improve query performance.
-- Uses AdventureWorksDW2016CTP3
-- Move all OPEN and CLOSED delta rowgroups into the columnstore.
ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);

-- For a specific partition, move all OPEN AND CLOSED delta rowgroups into the columnstore
ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REORGANIZE PARTITION = 0 WITH (COMPRESS_ALL_ROW_GROUPS = ON);

C. Defragment a columnstore index online


Does not apply to: SQL Server 2012 and 2014.
Starting with SQL Server 2016, REORGANIZE does more than compress delta rowgroups into the columnstore. It also performs online defragmentation. First, it reduces the size of the columnstore by
physically removing deleted rows when 10% or more of the rows in a rowgroup have been deleted. Then, it combines rowgroups together to form larger rowgroups that have up to the maximum of
1,024,576 rows per rowgroups. All rowgroups that are changed get re-compressed.

Note

Starting with SQL Server 2016, rebuilding a columnstore index is no longer necessary in most situations since REORGANIZE physically removes deleted rows and merges rowgroups.
The COMPRESS_ALL_ROW_GROUPS option forces all OPEN or CLOSED delta rowgroups into the columnstore which previously could only be done with a rebuild.
REORGANIZE is online and occurs in the background so queries can continue as the operation happens.
-- Uses AdventureWorks
-- Defragment by physically removing rows that have been logically deleted from the table, and merging rowgroups.
ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REORGANIZE;

D. Rebuild a clustered columnstore index offline


Applies to: SQL Server 2012, SQL Server 2014
Starting with SQL Server 2016, we recommend using ALTER INDEX REORGANIZE instead of ALTER INDEX REBUILD.

Note

In SQL Server 2012 and 2014, REORGANIZE is only used to compress CLOSED rowgroups into the columnstore. The only way to perform defragmentation operations and to force
all delta rowgroups into the columnstore is to rebuild the index.
This example shows how to rebuild a clustered columnstore index and force all delta rowgroups into the columnstore. This first step prepares a table FactInternetSales2 with a clustered columnstore
index and inserts data from the first four columns.
-- Uses AdventureWorksDW

CREATE TABLE dbo.FactInternetSales2 (


ProductKey [int] NOT NULL,
OrderDateKey [int] NOT NULL,
DueDateKey [int] NOT NULL,
ShipDateKey [int] NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX cci_FactInternetSales2


ON dbo.FactInternetSales2;

INSERT INTO dbo.FactInternetSales2


SELECT ProductKey, OrderDateKey, DueDateKey, ShipDateKey
FROM dbo.FactInternetSales;

SELECT * FROM sys.column_store_row_groups;

The results show there is one OPEN rowgroup, which means SQL Server will wait for more rows to be added before it closes the rowgroup and moves the data to the columnstore. This next statement
rebuilds the clustered columnstore index, which forces all rows into the columnstore.
ALTER INDEX cci_FactInternetSales2 ON FactInternetSales2 REBUILD;
SELECT * FROM sys.column_store_row_groups;

The results of the SELECT statement show the rowgroup is COMPRESSED, which means the column segments of the rowgroup are now compressed and stored in the columnstore.
E. Rebuild a partition of a clustered columnstore index offline
Use this for: SQL Server 2012, SQL Server 2014
To rebuild a partition of a large clustered columnstore index, use ALTER INDEX REBUILD with the partition option. This example rebuilds partition 12. Starting with SQL Server 2016, we recommend
replacing REBUILD with REORGANIZE.
ALTER INDEX cci_fact3
ON fact3
REBUILD PARTITION = 12;

F. Change a clustered columstore index to use archival compression


Does not apply to: SQL Server 2012
You can choose to reduce the size of a clustered columnstore index even further by using the COLUMNSTORE_ARCHIVE data compression option. This is practical for older data that you want to keep
on cheaper storage. We recommend only using this on data that is not accessed often since decomrpess is slower than with the normal COLUMNSTORE compression.
The following example rebuilds a clustered columnstore index to use archival compression, and then shows how to remove the archival compression. The final result will use only columnstore
compression.
--Prepare the example by creating a table with a clustered columnstore index.
CREATE TABLE SimpleTable (
ProductKey [int] NOT NULL,
OrderDateKey [int] NOT NULL,
DueDateKey [int] NOT NULL,
ShipDateKey [int] NOT NULL
);

CREATE CLUSTERED INDEX cci_SimpleTable ON SimpleTable (ProductKey);

CREATE CLUSTERED COLUMNSTORE INDEX cci_SimpleTable


ON SimpleTable
WITH (DROP_EXISTING = ON);

--Compress the table further by using archival compression.


ALTER INDEX cci_SimpleTable ON SimpleTable
REBUILD
WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE);

--Remove the archive compression and only use columnstore compression.


ALTER INDEX cci_SimpleTable ON SimpleTable
REBUILD
WITH (DATA_COMPRESSION = COLUMNSTORE);
GO

Examples: rowstore indexes


A. Rebuilding an index
The following example rebuilds a single index on the Employee table in the AdventureWorks2012 database.
ALTER INDEX PK_Employee_EmployeeID ON HumanResources.Employee REBUILD;

B. Rebuilding all indexes on a table and specifying options


The following example specifies the keyword ALL. This rebuilds all indexes associated with the table Production.Product in the AdventureWorks2012 database. Three options are specified.

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

ALTER INDEX ALL ON Production.Product


REBUILD WITH (FILLFACTOR = 80, SORT_IN_TEMPDB = ON, STATISTICS_NORECOMPUTE = ON);

The following example adds the ONLINE option including the low priority lock option, and adds the row compression option.

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

ALTER INDEX ALL ON Production.Product


REBUILD WITH
(
FILLFACTOR = 80,
SORT_IN_TEMPDB = ON,
STATISTICS_NORECOMPUTE = ON,
ONLINE = ON ( WAIT_AT_LOW_PRIORITY ( MAX_DURATION = 4 MINUTES, ABORT_AFTER_WAIT = BLOCKERS ) ),
DATA_COMPRESSION = ROW
);

D. Reorganizing an index with LOB compaction


The following example reorganizes a single clustered index in the AdventureWorks2012 database. Because the index contains a LOB data type in the leaf level, the statement also compacts all pages that
contain the large object data. Note that specifying the WITH (LOB_COMPACTION) option is not required because the default value is ON.
ALTER INDEX PK_ProductPhoto_ProductPhotoID ON Production.ProductPhoto REORGANIZE WITH (LOB_COMPACTION);

E. Setting options on an index


The following example sets several options on the index AK_SalesOrderHeader_SalesOrderNumber in the AdventureWorks2012 database.

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

ALTER INDEX AK_SalesOrderHeader_SalesOrderNumber ON


Sales.SalesOrderHeader
SET (
STATISTICS_NORECOMPUTE = ON,
IGNORE_DUP_KEY = ON,
ALLOW_PAGE_LOCKS = ON
) ;
GO

F. Disabling an index
The following example disables a nonclustered index on the Employee table in the AdventureWorks2012 database.
ALTER INDEX IX_Employee_ManagerID ON HumanResources.Employee DISABLE;

G. Disabling constraints
The following example disables a PRIMARY KEY constraint by disabling the PRIMARY KEY index in the AdventureWorks2012 database. The FOREIGN KEY constraint on the underlying table is
automatically disabled and warning message is displayed.
ALTER INDEX PK_Department_DepartmentID ON HumanResources.Department DISABLE;

The result set returns this warning message.


Warning: Foreign key 'FK_EmployeeDepartmentHistory_Department_DepartmentID'
on table 'EmployeeDepartmentHistory' referencing table 'Department'
was disabled as a result of disabling the index 'PK_Department_DepartmentID'.
H. Enabling constraints
The following example enables the PRIMARY KEY and FOREIGN KEY constraints that were disabled in Example F.
The PRIMARY KEY constraint is enabled by rebuilding the PRIMARY KEY index.
ALTER INDEX PK_Department_DepartmentID ON HumanResources.Department REBUILD;

The FOREIGN KEY constraint is then enabled.


ALTER TABLE HumanResources.EmployeeDepartmentHistory
CHECK CONSTRAINT FK_EmployeeDepartmentHistory_Department_DepartmentID;
GO

I. Rebuilding a partitioned index


The following example rebuilds a single partition, partition number 5, of the partitioned index IX_TransactionHistory_TransactionDate in the AdventureWorks2012 database. Partition 5 is rebuilt online
and the 10 minutes wait time for the low priority lock applies separately to every lock acquired by index rebuild operation. If during this time the lock cannot be obtained to complete index rebuild,
the rebuild operation statement is aborted.

Applies to: SQL Server 2014 through SQL Server 2016, SQL Database V12.

-- Verify the partitioned indexes.


SELECT *
FROM sys.dm_db_index_physical_stats (DB_ID(),OBJECT_ID(N'Production.TransactionHistory'), NULL , NULL, NULL);
GO
--Rebuild only partition 5.
ALTER INDEX IX_TransactionHistory_TransactionDate
ON Production.TransactionHistory
REBUILD Partition = 5
WITH (ONLINE = ON (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 10 minutes, ABORT_AFTER_WAIT = SELF)));
GO

J. Changing the compression setting of an index


The following example rebuilds an index on a nonpartitioned rowstore table.
ALTER INDEX IX_INDEX1
ON T1
REBUILD
WITH (DATA_COMPRESSION = PAGE);
GO

For additional data compression examples, see Data Compression.

Examples: Azure SQL Data Warehouse and Parallel Data Warehouse


K. Basic Syntax
ALTER INDEX index1 ON table1 REBUILD;

ALTER INDEX ALL ON table1 REBUILD;

ALTER INDEX ALL ON dbo.table1 REBUILD;


-- Syntax for SQL Server

CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name


ON <object> ( column [ ASC | DESC ] [ ,...n ] )
[ INCLUDE ( column_name [ ,...n ] ) ]
[ WHERE <filter_predicate> ]
[ WITH ( <relational_index_option> [ ,...n ] ) ]
[ ON { partition_scheme_name ( column_name )
| filegroup_name
| default
}
]
[ FILESTREAM_ON { filestream_filegroup_name | partition_scheme_name | "NULL" } ]

[ ; ]

<object> ::=
{
[ database_name. [ schema_name ] . | schema_name. ]
table_or_view_name
}

<relational_index_option> ::=
{
PAD_INDEX = { ON | OFF }
| FILLFACTOR = fillfactor
| SORT_IN_TEMPDB = { ON | OFF }
| IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
| STATISTICS_INCREMENTAL = { ON | OFF }
| DROP_EXISTING = { ON | OFF }
| ONLINE = { ON | OFF }
| ALLOW_ROW_LOCKS = { ON | OFF }
| ALLOW_PAGE_LOCKS = { ON | OFF }
| MAXDOP = max_degree_of_parallelism
| DATA_COMPRESSION = { NONE | ROW | PAGE}
[ ON PARTITIONS ( { <partition_number_expression> | <range> }
[ , ...n ] ) ]
}

<filter_predicate> ::=
<conjunct> [ AND <conjunct> ]

<conjunct> ::=
<disjunct> | <comparison>

<disjunct> ::=
column_name IN (constant ,...n)

<comparison> ::=
column_name <comparison_op> constant

<comparison_op> ::=
{ IS | IS NOT | = | <> | != | > | >= | !> | < | <= | !< }

<range> ::=
<partition_number_expression> TO <partition_number_expression>

Backward Compatible Relational Index


Important The backward compatible relational index syntax structure will be removed in a future version of SQL Server.
Avoid using this syntax structure in new development work, and plan to modify applications that currently use the
feature. Use the syntax structure specified in <relational_index_option> instead.

CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name


ON <object> ( column_name [ ASC | DESC ] [ ,...n ] )
[ WITH <backward_compatible_index_option> [ ,...n ] ]
[ ON { filegroup_name | "default" } ]

<object> ::=
{
[ database_name. [ owner_name ] . | owner_name. ]
table_or_view_name
}

<backward_compatible_index_option> ::=
{
PAD_INDEX
| FILLFACTOR = fillfactor
| SORT_IN_TEMPDB
| IGNORE_DUP_KEY
| STATISTICS_NORECOMPUTE
| DROP_EXISTING
}

-- Syntax for Azure SQL Database

CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name


ON <object> ( column [ ASC | DESC ] [ ,...n ] )
[ INCLUDE ( column_name [ ,...n ] ) ]
[ WHERE <filter_predicate> ]
[ WITH ( <relational_index_option> [ ,...n ] ) ]
[ ; ]

<object> ::=
{
[ database_name. [ schema_name ] . | schema_name. ]
table_or_view_name
}
<relational_index_option> ::=
{
| IGNORE_DUP_KEY = { ON | OFF }
| STATISTICS_NORECOMPUTE = { ON | OFF }
| DROP_EXISTING = { ON | OFF }
| ONLINE = { ON | OFF }
| DATA_COMPRESSION = { NONE | ROW | PAGE}
}

<filter_predicate> ::=
<conjunct> [ AND <conjunct> ]

<conjunct> ::=
<disjunct> | <comparison>

<disjunct> ::=
column_name IN (constant ,…)

<comparison> ::=
column_name <comparison_op> constant

<comparison_op> ::=
{ IS | IS NOT | = | <> | != | > | >= | !> | < | <= | !< }

-- Syntax for Azure SQL Data Warehouse and Parallel Data Warehouse

CREATE [ CLUSTERED | NONCLUSTERED ] INDEX index_name


ON [ database_name . [ schema ] . | schema . ] table_name
( { column [ ASC | DESC ] } [ ,...n ] )
WITH ( DROP_EXISTING = { ON | OFF } )
[;]

Arguments
UNIQUE
Creates a unique index on a table or view. A unique index is one in which no two rows are permitted to have the same index key value. A clustered index on a view
must be unique.
The Database Engine does not allow creating a unique index on columns that already include duplicate values, whether or not IGNORE_DUP_KEY is set to ON. If
this is tried, the Database Engine displays an error message. Duplicate values must be removed before a unique index can be created on the column or columns.
Columns that are used in a unique index should be set to NOT NULL, because multiple null values are considered duplicates when a unique index is created.
CLUSTERED
Creates an index in which the logical order of the key values determines the physical order of the corresponding rows in a table. The bottom, or leaf, level of the
clustered index contains the actual data rows of the table. A table or view is allowed one clustered index at a time.
A view with a unique clustered index is called an indexed view. Creating a unique clustered index on a view physically materializes the view. A unique clustered
index must be created on a view before any other indexes can be defined on the same view. For more information, see Create Indexed Views.
Create the clustered index before creating any nonclustered indexes. Existing nonclustered indexes on tables are rebuilt when a clustered index is created.
If CLUSTERED is not specified, a nonclustered index is created.

Note

Because the leaf level of a clustered index and the data pages are the same by definition, creating a clustered index and using the
ON partition_scheme_name or ON filegroup_name clause effectively moves a table from the filegroup on which the table was created to the new
partition scheme or filegroup. Before creating tables or indexes on specific filegroups, verify which filegroups are available and that they have
enough empty space for the index.
In some cases creating a clustered index can enable previously disabled indexes. For more information, see Enable Indexes and Constraints and Disable Indexes and
Constraints.
NONCLUSTERED
Creates an index that specifies the logical ordering of a table. With a nonclustered index, the physical order of the data rows is independent of their indexed order.
Each table can have up to 999 nonclustered indexes, regardless of how the indexes are created: either implicitly with PRIMARY KEY and UNIQUE constraints, or
explicitly with CREATE INDEX.
For indexed views, nonclustered indexes can be created only on a view that has a unique clustered index already defined.
The default is NONCLUSTERED.
index_name
Is the name of the index. Index names must be unique within a table or view but do not have to be unique within a database. Index names must follow the rules
of identifiers.
column
Is the column or columns on which the index is based. Specify two or more column names to create a composite index on the combined values in the specified
columns. List the columns to be included in the composite index, in sort-priority order, inside the parentheses after table_or_view_name.
Up to 32 columns can be combined into a single composite index key. All the columns in a composite index key must be in the same table or view. The maximum
allowable size of the combined index values is 900 bytes for a clustered index, or 1,700 for a nonclustered index. The limits are 16 columns and 900 bytes for
versions before SQL Database V12 and SQL Server 2016.
Columns that are of the large object (LOB) data types ntext, text, varchar(max), nvarchar(max), varbinary(max), xml, or image cannot be specified as key
columns for an index. Also, a view definition cannot include ntext, text, or image columns, even if they are not referenced in the CREATE INDEX statement.
You can create indexes on CLR user-defined type columns if the type supports binary ordering. You can also create indexes on computed columns that are defined
as method invocations off a user-defined type column, as long as the methods are marked deterministic and do not perform data access operations. For more
information about indexing CLR user-defined type columns, see CLR User-defined Types.
[ ASC | DESC ]
Determines the ascending or descending sort direction for the particular index column. The default is ASC.
INCLUDE (column [ ,... n ] )
Specifies the non-key columns to be added to the leaf level of the nonclustered index. The nonclustered index can be unique or non-unique.
Column names cannot be repeated in the INCLUDE list and cannot be used simultaneously as both key and non-key columns. Nonclustered indexes always contain
the clustered index columns if a clustered index is defined on the table. For more information, see Create Indexes with Included Columns.
All data types are allowed except text, ntext, and image. The index must be created or rebuilt offline (ONLINE = OFF) if any one of the specified non-key columns
are varchar(max), nvarchar(max), or varbinary(max) data types.
Computed columns that are deterministic and either precise or imprecise can be included columns. Computed columns derived
from image, ntext, text, varchar(max), nvarchar(max), varbinary(max), and xml data types can be included in non-key columns as long as the computed
column data types is allowable as an included column. For more information, see Indexes on Computed Columns.
For information on creating an XML index, see CREATE XML INDEX (Transact-SQL).
WHERE <filter_predicate>
Creates a filtered index by specifying which rows to include in the index. The filtered index must be a nonclustered index on a table. Creates filtered statistics for
the data rows in the filtered index.
The filter predicate uses simple comparison logic and cannot reference a computed column, a UDT column, a spatial data type column, or a hierarchyID data type
column. Comparisons using NULL literals are not allowed with the comparison operators. Use the IS NULL and IS NOT NULL operators instead.
Here are some examples of filter predicates for the Production.BillOfMaterials table:
WHERE StartDate > '20000101' AND EndDate <= '20000630'
WHERE ComponentID IN (533, 324, 753)
WHERE StartDate IN ('20000404', '20000905') AND EndDate IS NOT NULL
Filtered indexes do not apply to XML indexes and full-text indexes. For UNIQUE indexes, only the selected rows must have unique index values. Filtered indexes do
not allow the IGNORE_DUP_KEY option.
ON partition_scheme_name(column_name)

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies the partition scheme that defines the filegroups onto which the partitions of a partitioned index will be mapped. The partition scheme must exist within
the database by executing either CREATE PARTITION SCHEME or ALTER PARTITION SCHEME. column_name specifies the column against which a partitioned index
will be partitioned. This column must match the data type, length, and precision of the argument of the partition function that partition_scheme_name is
using. column_name is not restricted to the columns in the index definition. Any column in the base table can be specified, except when partitioning a UNIQUE
index, column_name must be chosen from among those used as the unique key. This restriction allows the Database Engine to verify uniqueness of key values
within a single partition only.

Note

When you partition a non-unique, clustered index, the Database Engine by default adds the partitioning column to the list of clustered index keys, if it
is not already specified. When partitioning a non-unique, nonclustered index, the Database Engine adds the partitioning column as a non-key
(included) column of the index, if it is not already specified.
If partition_scheme_name or filegroup is not specified and the table is partitioned, the index is placed in the same partition scheme, using the same partitioning
column, as the underlying table.

Note

You cannot specify a partitioning scheme on an XML index. If the base table is partitioned, the XML index uses the same partition scheme as the
table.
For more information about partitioning indexes, Partitioned Tables and Indexes.
ON filegroup_name

Applies to: SQL Server 2008 through SQL Server 2016.

Creates the specified index on the specified filegroup. If no location is specified and the table or view is not partitioned, the index uses the same filegroup as the
underlying table or view. The filegroup must already exist.
ON "default"

Applies to: SQL Server 2008 through SQL Server 2016.

Creates the specified index on the default filegroup.


The term default, in this context, is not a keyword. It is an identifier for the default filegroup and must be delimited, as in ON "default" or ON [default]. If "default"
is specified, the QUOTED_IDENTIFIER option must be ON for the current session. This is the default setting. For more information, see SET QUOTED_IDENTIFIER
(Transact-SQL).
[ FILESTREAM_ON { filestream_filegroup_name | partition_scheme_name | "NULL" } ]
Applies to: SQL Server 2008 through SQL Server 2016.

Specifies the placement of FILESTREAM data for the table when a clustered index is created. The FILESTREAM_ON clause allows FILESTREAM data to be moved to a
different FILESTREAM filegroup or partition scheme.
filestream_filegroup_name is the name of a FILESTREAM filegroup. The filegroup must have one file defined for the filegroup by using a CREATE
DATABASE or ALTER DATABASE statement; otherwise, an error is raised.
If the table is partitioned, the FILESTREAM_ON clause must be included and must specify a partition scheme of FILESTREAM filegroups that uses the same partition
function and partition columns as the partition scheme for the table. Otherwise, an error is raised.
If the table is not partitioned, the FILESTREAM column cannot be partitioned. FILESTREAM data for the table must be stored in a single filegroup that is specified in
the FILESTREAM_ON clause.
FILESTREAM_ON NULL can be specified in a CREATE INDEX statement if a clustered index is being created and the table does not contain a FILESTREAM column.
For more information, see FILESTREAM (SQL Server).
<object>::=
Is the fully qualified or nonfully qualified object to be indexed.
database_name
Is the name of the database.
schema_name
Is the name of the schema to which the table or view belongs.
table_or_view_name
Is the name of the table or view to be indexed.
The view must be defined with SCHEMABINDING to create an index on it. A unique clustered index must be created on a view before any nonclustered index is
created. For more information about indexed views, see the Remarks section.
Beginning with SQL Server 2016, the object can be a table stored with a clustered columnstore index.
Azure SQL Database supports the three-part name format database_name.[schema_name].object_name when the database_name is the current database or
the database_name is tempdb and the object_name starts with #.
<relational_index_option>::=
Specifies the options to use when you create the index.
PAD_INDEX = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies index padding. The default is OFF.


ON
The percentage of free space that is specified by fillfactor is applied to the intermediate-level pages of the index.
OFF or fillfactor is not specified
The intermediate-level pages are filled to near capacity, leaving sufficient space for at least one row of the maximum size the index can have, considering the set of
keys on the intermediate pages.
The PAD_INDEX option is useful only when FILLFACTOR is specified, because PAD_INDEX uses the percentage specified by FILLFACTOR. If the percentage specified
for FILLFACTOR is not large enough to allow for one row, the Database Engine internally overrides the percentage to allow for the minimum. The number of rows
on an intermediate index page is never less than two, regardless of how low the value of fillfactor.
In backward compatible syntax, WITH PAD_INDEX is equivalent to WITH PAD_INDEX = ON.
FILLFACTOR =fillfactor

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies a percentage that indicates how full the Database Engine should make the leaf level of each index page during index creation or rebuild. fillfactor must be
an integer value from 1 to 100. If fillfactor is 100, the Database Engine creates indexes with leaf pages filled to capacity.
The FILLFACTOR setting applies only when the index is created or rebuilt. The Database Engine does not dynamically keep the specified percentage of empty space
in the pages. To view the fill factor setting, use the sys.indexes catalog view.

Important

Creating a clustered index with a FILLFACTOR less than 100 affects the amount of storage space the data occupies because the Database Engine
redistributes the data when it creates the clustered index.
For more information, see Specify Fill Factor for an Index.
SORT_IN_TEMPDB = { ON | OFF }
Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether to store temporary sort results in tempdb. The default is OFF.
ON
The intermediate sort results that are used to build the index are stored in tempdb. This may reduce the time required to create an index if tempdb is on a
different set of disks than the user database. However, this increases the amount of disk space that is used during the index build.
OFF
The intermediate sort results are stored in the same database as the index.
In addition to the space required in the user database to create the index, tempdb must have about the same amount of additional space to hold the intermediate
sort results. For more information, see SORT_IN_TEMPDB Option For Indexes.
In backward compatible syntax, WITH SORT_IN_TEMPDB is equivalent to WITH SORT_IN_TEMPDB = ON.
IGNORE_DUP_KEY = { ON | OFF }
Specifies the error response when an insert operation attempts to insert duplicate key values into a unique index. The IGNORE_DUP_KEY option applies only to
insert operations after the index is created or rebuilt. The option has no effect when executing CREATE INDEX, ALTER INDEX, or UPDATE. The default is OFF.
ON
A warning message will occur when duplicate key values are inserted into a unique index. Only the rows violating the uniqueness constraint will fail.
OFF
An error message will occur when duplicate key values are inserted into a unique index. The entire INSERT operation will be rolled back.
IGNORE_DUP_KEY cannot be set to ON for indexes created on a view, non-unique indexes, XML indexes, spatial indexes, and filtered indexes.
To view IGNORE_DUP_KEY, use sys.indexes.
In backward compatible syntax, WITH IGNORE_DUP_KEY is equivalent to WITH IGNORE_DUP_KEY = ON.
STATISTICS_NORECOMPUTE = { ON | OFF}
Specifies whether distribution statistics are recomputed. The default is OFF.
ON
Out-of-date statistics are not automatically recomputed.
OFF
Automatic statistics updating are enabled.
To restore automatic statistics updating, set the STATISTICS_NORECOMPUTE to OFF, or execute UPDATE STATISTICS without the NORECOMPUTE clause.

Important

Disabling automatic recomputation of distribution statistics may prevent the query optimizer from picking optimal execution plans for queries
involving the table.
In backward compatible syntax, WITH STATISTICS_NORECOMPUTE is equivalent to WITH STATISTICS_NORECOMPUTE = ON.
STATISTICS_INCREMENTAL = { ON | OFF }
When ON, the statistics created are per partition statistics. When OFF, the statistics tree is dropped and SQL Server re-computes the statistics. The default is OFF.
If per partition statistics are not supported the option is ignored and a warning is generated. Incremental stats are not supported for following statistics types:
 Statistics created with indexes that are not partition-aligned with the base table.
 Statistics created on Always On readable secondary databases.
 Statistics created on read-only databases.
 Statistics created on filtered indexes.
 Statistics created on views.
 Statistics created on internal tables.
 Statistics created with spatial indexes or XML indexes.
DROP_EXISTING = { ON | OFF }
Is an option to drop and rebuild the existing clustered or nonclustered index with modified column specifications, and keep the same name for the index. The
default is OFF.
ON
Specifies to drop and rebuild the existing index, which must have the same name as the parameter index_name.
OFF
Specifies not to drop and rebuild the existing index. SQL Server displays an error if the specified index name already exists.
With DROP_EXISTING, you can change:
 A nonclustered rowstore index to a clustered rowstore index.

With DROP_EXISTING, you cannot change:


 A clustered rowstore index to a nonclustered rowstore index.
 A clustered columnstore index to any type of rowstore index.
In backward compatible syntax, WITH DROP_EXISTING is equivalent to WITH DROP_EXISTING = ON.
ONLINE = { ON | OFF }
Specifies whether underlying tables and associated indexes are available for queries and data modification during the index operation. The default is OFF.

Note

Online index operations are not available in every edition of MicrosoftSQL Server. For a list of features that are supported by the editions of SQL
Server, see Features Supported by the Editions of SQL Server 2016.
ON
Long-term table locks are not held for the duration of the index operation. During the main phase of the index operation, only an Intent Share (IS) lock is held on
the source table. This enables queries or updates to the underlying table and indexes to proceed. At the start of the operation, a Shared (S) lock is held on the
source object for a very short period of time. At the end of the operation, for a short period of time, an S (Shared) lock is acquired on the source if a nonclustered
index is being created; or an SCH-M (Schema Modification) lock is acquired when a clustered index is created or dropped online and when a clustered or
nonclustered index is being rebuilt. ONLINE cannot be set to ON when an index is being created on a local temporary table.
OFF
Table locks are applied for the duration of the index operation. An offline index operation that creates, rebuilds, or drops a clustered index, or rebuilds or drops a
nonclustered index, acquires a Schema modification (Sch-M) lock on the table. This prevents all user access to the underlying table for the duration of the
operation. An offline index operation that creates a nonclustered index acquires a Shared (S) lock on the table. This prevents updates to the underlying table but
allows read operations, such as SELECT statements.
For more information, see How Online Index Operations Work.
Indexes, including indexes on global temp tables, can be created online with the following exceptions:
 XML index
 Index on a local temp table.
 Initial unique clustered index on a view.
 Disabled clustered indexes.
 Clustered index if the underlying table contains LOB data types: image, ntext, text, and spatial types.
 varchar(max) and varbinary(max) columns cannot be part of an index. In SQL Server (beginning with SQL Server 2012) and in SQL Database V12, when a
table contains varchar(max) or varbinary(max) columns, a clustered index containing other columns, can be built or rebuilt using the ONLINE option.
SQL Database does not permit the ONLINE option when the base table contains varchar(max) or varbinary(max)columns.
For more information, see Perform Index Operations Online.
ALLOW_ROW_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether row locks are allowed. The default is ON.


ON
Row locks are allowed when accessing the index. The Database Engine determines when row locks are used.
OFF
Row locks are not used.
ALLOW_PAGE_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether page locks are allowed. The default is ON.


ON
Page locks are allowed when accessing the index. The Database Engine determines when page locks are used.
OFF
Page locks are not used.
MAXDOP = max_degree_of_parallelism

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12 (Performance Levels P2 and P3 only).

Overrides the Configure the max degree of parallelism Server Configuration Option configuration option for the duration of the index operation. Use MAXDOP to
limit the number of processors used in a parallel plan execution. The maximum is 64 processors.
max_degree_of_parallelism can be:
1
Suppresses parallel plan generation.
>1
Restricts the maximum number of processors used in a parallel index operation to the specified number or fewer based on the current system workload.
0 (default)
Uses the actual number of processors or fewer based on the current system workload.
For more information, see Configure Parallel Index Operations.

Note

Parallel index operations are not available in every edition of MicrosoftSQL Server. For a list of features that are supported by the editions of SQL
Server, see Features Supported by the Editions of SQL Server 2016.
DATA_COMPRESSION
Specifies the data compression option for the specified index, partition number, or range of partitions. The options are as follows:
NONE
Index or specified partitions are not compressed.
ROW
Index or specified partitions are compressed by using row compression.
PAGE
Index or specified partitions are compressed by using page compression.
For more information about compression, see Data Compression.
ON PARTITIONS ( { <partition_number_expression> | <range> } [ ,...n ] )

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies the partitions to which the DATA_COMPRESSION setting applies. If the index is not partitioned, the ON PARTITIONS argument will generate an error. If
the ON PARTITIONS clause is not provided, the DATA_COMPRESSION option applies to all partitions of a partitioned index.
<partition_number_expression> can be specified in the following ways:
 Provide the number for a partition, for example: ON PARTITIONS (2).
 Provide the partition numbers for several individual partitions separated by commas, for example: ON PARTITIONS (1, 5).
 Provide both ranges and individual partitions, for example: ON PARTITIONS (2, 4, 6 TO 8).
<range> can be specified as partition numbers separated by the word TO, for example: ON PARTITIONS (6 TO 8).
To set different types of data compression for different partitions, specify the DATA_COMPRESSION option more than once, for example:
REBUILD WITH
(
DATA_COMPRESSION = NONE ON PARTITIONS (1),
DATA_COMPRESSION = ROW ON PARTITIONS (2, 4, 6 TO 8),
DATA_COMPRESSION = PAGE ON PARTITIONS (3, 5)
);

Remarks
The CREATE INDEX statement is optimized like any other query. To save on I/O operations, the query processor may choose to scan another index instead of
performing a table scan. The sort operation may be eliminated in some situations. On multiprocessor computers CREATE INDEX can use more processors to
perform the scan and sort operations associated with creating the index, in the same way as other queries do. For more information, see Configure Parallel Index
Operations.
The create index operation can be minimally logged if the database recovery model is set to either bulk-logged or simple.
Indexes can be created on a temporary table. When the table is dropped or the session ends, the indexes are dropped.
Indexes support extended properties.

Clustered Indexes
Creating a clustered index on a table (heap) or dropping and re-creating an existing clustered index requires additional workspace to be available in the database
to accommodate data sorting and a temporary copy of the original table or existing clustered index data. For more information about clustered indexes, see Create
Clustered Indexes.

Nonclustered Indexes
Beginning with SQL Server 2016, you can create a nonclustered index on a table stored as a clustered columnstore index. If you first create a nonclustered index on
a table stored as a heap or clustered index, the index will persist if you later convert the table to a clustered columnstore index. It is also not necessary to drop the
nonclustered index when you rebuild the clustered columnstore index.
Limitations and Restrictions:
 The FILESTREAM_ON option is not valid when you create a nonclustered index on a table stored as a clustered columnstore index.

Unique Indexes
When a unique index exists, the Database Engine checks for duplicate values each time data is added by a insert operations. Insert operations that would generate
duplicate key values are rolled back, and the Database Engine displays an error message. This is true even if the insert operation changes many rows but causes
only one duplicate. If an attempt is made to enter data for which there is a unique index and the IGNORE_DUP_KEY clause is set to ON, only the rows violating the
UNIQUE index fail.

Partitioned Indexes
Partitioned indexes are created and maintained in a similar manner to partitioned tables, but like ordinary indexes, they are handled as separate database objects.
You can have a partitioned index on a table that is not partitioned, and you can have a nonpartitioned index on a table that is partitioned.
If you are creating an index on a partitioned table, and do not specify a filegroup on which to place the index, the index is partitioned in the same manner as the
underlying table. This is because indexes, by default, are placed on the same filegroups as their underlying tables, and for a partitioned table in the same partition
scheme that uses the same partitioning columns. When the index uses the same partition scheme and partitioning column as the table, the index is aligned with
the table.

Warning

Creating and rebuilding nonaligned indexes on a table with more than 1,000 partitions is possible, but is not supported. Doing so may cause degraded
performance or excessive memory consumption during these operations. We recommend using only aligned indexes when the number of partitions
exceed 1,000.
When partitioning a non-unique, clustered index, the Database Engine by default adds any partitioning columns to the list of clustered index keys, if not already
specified.
Indexed views can be created on partitioned tables in the same manner as indexes on tables. For more information about partitioned indexes, see Partitioned
Tables and Indexes.
In SQL Server 2016, statistics are not created by scanning all the rows in the table when a partitioned index is created or rebuilt. Instead, the query optimizer uses
the default sampling algorithm to generate statistics. To obtain statistics on partitioned indexes by scanning all the rows in the table, use CREATE STATISTICS or
UPDATE STATISTICS with the FULLSCAN clause.

Filtered Indexes
A filtered index is an optimized nonclustered index, suited for queries that select a small percentage of rows from a table. It uses a filter predicate to index a
portion of the data in the table. A well-designed filtered index can improve query performance, reduce storage costs, and reduce maintenance costs.
Required SET Options for Filtered Indexes
The SET options in the Required Value column are required whenever any of the following conditions occur:
 Create a filtered index.
 INSERT, UPDATE, DELETE, or MERGE operation modifies the data in a filtered index.
 The filtered index is used by the query optimizer to produce the query plan.

Default Default
SET options Required value Default server value
OLE DB and ODBC value DB-Library value

ANSI_NULLS ON ON ON OFF

ANSI_PADDING ON ON ON OFF

ANSI_WARNINGS* ON ON ON OFF

ARITHABORT ON ON OFF OFF

CONCAT_NULL_YIELDS_NULL ON ON ON OFF

NUMERIC_ROUNDABORT OFF OFF OFF OFF

QUOTED_IDENTIFIER ON ON ON OFF

 *Setting ANSI_WARNINGS to ON implicitly sets ARITHABORT to ON when the database compatibility level is set to 90 or higher. If the database
compatibility level is set to 80 or earlier, the ARITHABORT option must explicitly be set to ON.
If the SET options are incorrect, the following conditions can occur:
 The filtered index is not created.
 The Database Engine generates an error and rolls back INSERT, UPDATE, DELETE, or MERGE statements that change data in the index.
 Query optimizer does not consider the index in the execution plan for any Transact-SQL statements.
For more information about Filtered Indexes, see Create Filtered Indexes.

Spatial Indexes
For information about spatial indexes, see CREATE SPATIAL INDEX (Transact-SQL) and Spatial Indexes Overview.

XML Indexes
For information about XML indexes see, CREATE XML INDEX (Transact-SQL) and XML Indexes (SQL Server).

Index Key Size


The maximum size for an index key is 900 bytes for a clustered index and 1,700 bytes for a nonclustered index. (Before SQL Database V12 and SQL Server 2016 the
limit was always 900 bytes.) Indexes on varchar columns that exceed the byte limit can be created if the existing data in the columns do not exceed the limit at the
time the index is created; however, subsequent insert or update actions on the columns that cause the total size to be greater than the limit will fail. The index key
of a clustered index cannot contain varchar columns that have existing data in the ROW_OVERFLOW_DATA allocation unit. If a clustered index is created on
a varchar column and the existing data is in the IN_ROW_DATA allocation unit, subsequent insert or update actions on the column that would push the data off-
row will fail.
Nonclustered indexes can include non-key columns in the leaf level of the index. These columns are not considered by the Database Engine when calculating the
index key size . For more information, see Create Indexes with Included Columns.

Note

When tables are partitioned, if the partitioning key columns are not already present in a non-unique clustered index, they are added to the index by the
Database Engine. The combined size of the indexed columns (not counting included columns), plus any added partitioning columns cannot exceed
1800 bytes in a non-unique clustered index.

Computed Columns
Indexes can be created on computed columns. In addition, computed columns can have the property PERSISTED. This means that the Database Engine stores the
computed values in the table, and updates them when any other columns on which the computed column depends are updated. The Database Engine uses these
persisted values when it creates an index on the column, and when the index is referenced in a query.
To index a computed column, the computed column must deterministic and precise. However, using the PERSISTED property expands the type of indexable
computed columns to include:
 Computed columns based on Transact-SQL and CLR functions and CLR user-defined type methods that are marked deterministic by the user.
 Computed columns based on expressions that are deterministic as defined by the Database Engine but imprecise.
Persisted computed columns require the following SET options to be set as shown in the previous section "Required SET Options for Indexed Views".
The UNIQUE or PRIMARY KEY constraint can contain a computed column as long as it satisfies all conditions for indexing. Specifically, the computed column must
be deterministic and precise or deterministic and persisted. For more information about determinism, see Deterministic and Nondeterministic Functions.
Computed columns derived from image, ntext, text, varchar(max), nvarchar(max), varbinary(max), and xml data types can be indexed either as a key or
included non-key column as long as the computed column data type is allowable as an index key column or non-key column. For example, you cannot create a
primary XML index on a computed xml column. If the index key size exceeds 900 bytes, a warning message is displayed.
Creating an index on a computed column may cause the failure of an insert or update operation that previously worked. Such a failure may take place when the
computed column results in arithmetic error. For example, in the following table, although computed column c results in an arithmetic error,
the INSERT statement works.
CREATE TABLE t1 (a int, b int, c AS a/b);
INSERT INTO t1 VALUES (1, 0);

If, instead, after creating the table, you create an index on computed column c, the same INSERT statement will now fail.
CREATE TABLE t1 (a int, b int, c AS a/b);
CREATE UNIQUE CLUSTERED INDEX Idx1 ON t1(c);
INSERT INTO t1 VALUES (1, 0);

For more information, see Indexes on Computed Columns.

Included Columns in Indexes


Non-key columns, called included columns, can be added to the leaf level of a nonclustered index to improve query performance by covering the query. That is, all
columns referenced in the query are included in the index as either key or non-key columns. This allows the query optimizer to locate all the required information
from an index scan; the table or clustered index data is not accessed. For more information, see Create Indexes with Included Columns.

Specifying Index Options


SQL Server 2005 introduced new index options and also modifies the way in which options are specified. In backward compatible syntax, WITH option_name is
equivalent to WITH ( <option_name> = ON ). When you set index options, the following rules apply:
 New index options can only be specified by using WITH (option_name= ON | OFF).
 Options cannot be specified by using both the backward compatible and new syntax in the same statement. For example, specifying
WITH (DROP_EXISTING, ONLINE = ON) causes the statement to fail.
 When you create an XML index, the options must be specified by using WITH (option_name= ON | OFF).

DROP_EXISTING Clause
You can use the DROP_EXISTING clause to rebuild the index, add or drop columns, modify options, modify column sort order, or change the partition scheme or
filegroup.
If the index enforces a PRIMARY KEY or UNIQUE constraint and the index definition is not altered in any way, the index is dropped and re-created preserving the
existing constraint. However, if the index definition is altered the statement fails. To change the definition of a PRIMARY KEY or UNIQUE constraint, drop the
constraint and add a constraint with the new definition.
DROP_EXISTING enhances performance when you re-create a clustered index, with either the same or different set of keys, on a table that also has nonclustered
indexes. DROP_EXISTING replaces the execution of a DROP INDEX statement on the old clustered index followed by the execution of a CREATE INDEX statement
for the new clustered index. The nonclustered indexes are rebuilt once, and then only if the index definition has changed. The DROP_EXISTING clause does not
rebuild the nonclustered indexes when the index definition has the same index name, key and partition columns, uniqueness attribute, and sort order as the
original index.
Whether the nonclustered indexes are rebuilt or not, they always remain in their original filegroups or partition schemes and use the original partition functions. If a
clustered index is rebuilt to a different filegroup or partition scheme, the nonclustered indexes are not moved to coincide with the new location of the clustered
index. Therefore, even the nonclustered indexes previously aligned with the clustered index, they may no longer be aligned with it. For more information about
partitioned index alignment, see.
The DROP_EXISTING clause will not sort the data again if the same index key columns are used in the same order and with the same ascending or descending
order, unless the index statement specifies a nonclustered index and the ONLINE option is set to OFF. If the clustered index is disabled, the CREATE INDEX WITH
DROP_EXISTING operation must be performed with ONLINE set to OFF. If a nonclustered index is disabled and is not associated with a disabled clustered index, the
CREATE INDEX WITH DROP_EXISTING operation can be performed with ONLINE set to OFF or ON.
When indexes with 128 extents or more are dropped or rebuilt, the Database Engine defers the actual page deallocations, and their associated locks, until after the
transaction commits.

ONLINE Option
The following guidelines apply for performing index operations online:
 The underlying table cannot be altered, truncated, or dropped while an online index operation is in process.
 Additional temporary disk space is required during the index operation.
 Online operations can be performed on partitioned indexes and indexes that contain persisted computed columns, or included columns.
For more information, see Perform Index Operations Online.

Row and Page Locks Options


When ALLOW_ROW_LOCKS = ON and ALLOW_PAGE_LOCK = ON, row-, page-, and table-level locks are allowed when accessing the index. The Database Engine
chooses the appropriate lock and can escalate the lock from a row or page lock to a table lock.
When ALLOW_ROW_LOCKS = OFF and ALLOW_PAGE_LOCK = OFF, only a table-level lock is allowed when accessing the index.

Viewing Index Information


To return information about indexes, you can use catalog views, system functions, and system stored procedures.

Data Compression
Data compression is described in the topic Data Compression. The following are key points to consider:
 Compression can allow more rows to be stored on a page, but does not change the maximum row size.
 Non-leaf pages of an index are not page compressed but can be row compressed.
 Each nonclustered index has an individual compression setting, and does not inherit the compression setting of the underlying table.
 When a clustered index is created on a heap, the clustered index inherits the compression state of the heap unless an alternative compression state is
specified.
The following restrictions apply to partitioned indexes:
 You cannot change the compression setting of a single partition if the table has nonaligned indexes.
 The ALTER INDEX <index> ... REBUILD PARTITION ... syntax rebuilds the specified partition of the index.
 The ALTER INDEX <index> ... REBUILD WITH ... syntax rebuilds all partitions of the index.
To evaluate how changing the compression state will affect a table, an index, or a partition, use the sp_estimate_data_compression_savings stored procedure.

Permissions
Requires ALTER permission on the table or view. User must be a member of the sysadmin fixed server role or the db_ddladmin and db_owner fixed database
roles.

Limitations and Restrictions


SQL Data Warehouse and Parallel Data Warehouse, you cannot create:
 A clustered or nonclustered rowstore index on a data warehouse table when a columnstore index already exists. This behavior is different from SMP SQL
Server which allows both rowstore and columnstore indexes to co-exist on the same table.
 You cannot create an index on a view.

Metadata
To view information on existing indexes, you can query the sys.indexes (Transact-SQL) catalog view.

Version Notes
SQL Database does not support filegroup and filestream options.

Examples: all versions


A. Create a simple nonclustered rowstore index
The following example creates a nonclustered index on the VendorID column of the Purchasing.ProductVendor table.
-- Uses AdventureWorks

IF EXISTS (SELECT name FROM sys.indexes WHERE name = N'IX_ProductVendor_VendorID')


DROP INDEX IX_ProductVendor_VendorID ON Purchasing.ProductVendor;

-- Some examples
CREATE INDEX IX_VendorID ON ProductVendor (VendorID);
CREATE INDEX IX_VendorID ON dbo.ProductVendor (VendorID DESC, Name ASC, Address DESC);
CREATE INDEX IX_VendorID ON Purchasing..ProductVendor (VendorID);

B. Create a simple nonclustered rowstore composite index


The following example creates a nonclustered composite index on the SalesQuota and SalesYTD columns of the Sales.SalesPerson table.
-- Uses AdventureWorks

IF EXISTS ( SELECT name FROM sys.indexes WHERE name = N'IX_SalesPerson_SalesQuota_SalesYTD')


DROP INDEX IX_SalesPerson_SalesQuota_SalesYTD ON Sales.SalesPerson ;

CREATE NONCLUSTERED INDEX IX_SalesPerson_SalesQuota_SalesYTD ON Sales.SalesPerson (SalesQuota, SalesYTD);

C. Create an index on a table in another database


The following example creates a non-clustered index on the VendorID column of the ProductVendor table in the Purchasing database.
-- Uses AdventureWorks

CREATE CLUSTERED INDEX IX_ProductVendor_VendorID ON Purchasing..ProductVendor (VendorID);

D. Add a column to an index


The following example creates index IX_FF with two columns from the dbo.FactFinance table. The next statement rebuilds the index with one more column and
keeps the existing name.
-- Uses AdventureWorks

CREATE INDEX IX_FF ON dbo.FactFinance ( FinanceKey ASC, DateKey ASC );

--Rebuild and add the OrganizationKey


CREATE INDEX IX_FF ON dbo.FactFinance ( FinanceKey, DateKey, OrganizationKey DESC)
WITH ( DROP_EXISTING = ON );

Examples: SQL Server, Azure SQL Database


E. Create a unique nonclustered index
The following example creates a unique nonclustered index on the Name column of the Production.UnitMeasure table in the AdventureWorks2012 database.
The index will enforce uniqueness on the data inserted into the Name column.
IF EXISTS (SELECT name from sys.indexes
WHERE name = N'AK_UnitMeasure_Name')
DROP INDEX AK_UnitMeasure_Name ON Production.UnitMeasure;
GO
CREATE UNIQUE INDEX AK_UnitMeasure_Name
ON Production.UnitMeasure(Name);

The following query tests the uniqueness constraint by attempting to insert a row with the same value as that in an existing row.
--Verify the existing value.
SELECT Name FROM Production.UnitMeasure WHERE Name = N'Ounces';
GO
INSERT INTO Production.UnitMeasure (UnitMeasureCode, Name, ModifiedDate)
VALUES ('OC', 'Ounces', GetDate());

The resulting error message is:


Server: Msg 2601, Level 14, State 1, Line 1
Cannot insert duplicate key row in object 'UnitMeasure' with unique index 'AK_UnitMeasure_Name'. The statement has been
terminated.

F. Use the IGNORE_DUP_KEY option


The following example demonstrates the effect of the IGNORE_DUP_KEY option by inserting multiple rows into a temporary table first with the option set
to ON and again with the option set to OFF. A single row is inserted into the #Test table that will intentionally cause a duplicate value when the second multiple-
row INSERT statement is executed. A count of rows in the table returns the number of rows inserted.
CREATE TABLE #Test (C1 nvarchar(10), C2 nvarchar(50), C3 datetime);
GO
CREATE UNIQUE INDEX AK_Index ON #Test (C2)
WITH (IGNORE_DUP_KEY = ON);
GO
INSERT INTO #Test VALUES (N'OC', N'Ounces', GETDATE());
INSERT INTO #Test SELECT * FROM Production.UnitMeasure;
GO
SELECT COUNT(*)AS [Number of rows] FROM #Test;
GO
DROP TABLE #Test;
GO

Here are the results of the second INSERT statement.


Server: Msg 3604, Level 16, State 1, Line 5 Duplicate key was ignored.

Number of rows
--------------
38

Notice that the rows inserted from the Production.UnitMeasure table that did not violate the uniqueness constraint were successfully inserted. A warning was
issued and the duplicate row ignored, but the entire transaction was not rolled back.
The same statements are executed again, but with IGNORE_DUP_KEY set to OFF.
CREATE TABLE #Test (C1 nvarchar(10), C2 nvarchar(50), C3 datetime);
GO
CREATE UNIQUE INDEX AK_Index ON #Test (C2)
WITH (IGNORE_DUP_KEY = OFF);
GO
INSERT INTO #Test VALUES (N'OC', N'Ounces', GETDATE());
INSERT INTO #Test SELECT * FROM Production.UnitMeasure;
GO
SELECT COUNT(*)AS [Number of rows] FROM #Test;
GO
DROP TABLE #Test;
GO

Here are the results of the second INSERT statement.


Server: Msg 2601, Level 14, State 1, Line 5
Cannot insert duplicate key row in object '#Test' with unique index
'AK_Index'. The statement has been terminated.

Number of rows
--------------
1

Notice that none of the rows from the Production.UnitMeasure table were inserted into the table even though only one row in the table violated
the UNIQUE index constraint.
G. Using DROP_EXISTING to drop and re-create an index
The following example drops and re-creates an existing index on the ProductID column of the Production.WorkOrder table in the AdventureWorks2012
database by using the DROP_EXISTING option. The options FILLFACTOR and PAD_INDEX are also set.
CREATE NONCLUSTERED INDEX IX_WorkOrder_ProductID
ON Production.WorkOrder(ProductID)
WITH (FILLFACTOR = 80,
PAD_INDEX = ON,
DROP_EXISTING = ON);
GO

H. Create an index on a view


The following example creates a view and an index on that view. Two queries are included that use the indexed view.
--Set the options to support indexed views.
SET NUMERIC_ROUNDABORT OFF;
SET ANSI_PADDING, ANSI_WARNINGS, CONCAT_NULL_YIELDS_NULL, ARITHABORT,
QUOTED_IDENTIFIER, ANSI_NULLS ON;
GO
--Create view with schemabinding.
IF OBJECT_ID ('Sales.vOrders', 'view') IS NOT NULL
DROP VIEW Sales.vOrders ;
GO
CREATE VIEW Sales.vOrders
WITH SCHEMABINDING
AS
SELECT SUM(UnitPrice*OrderQty*(1.00-UnitPriceDiscount)) AS Revenue,
OrderDate, ProductID, COUNT_BIG(*) AS COUNT
FROM Sales.SalesOrderDetail AS od, Sales.SalesOrderHeader AS o
WHERE od.SalesOrderID = o.SalesOrderID
GROUP BY OrderDate, ProductID;
GO
--Create an index on the view.
CREATE UNIQUE CLUSTERED INDEX IDX_V1
ON Sales.vOrders (OrderDate, ProductID);
GO
--This query can use the indexed view even though the view is
--not specified in the FROM clause.
SELECT SUM(UnitPrice*OrderQty*(1.00-UnitPriceDiscount)) AS Rev,
OrderDate, ProductID
FROM Sales.SalesOrderDetail AS od
JOIN Sales.SalesOrderHeader AS o ON od.SalesOrderID=o.SalesOrderID
AND ProductID BETWEEN 700 and 800
AND OrderDate >= CONVERT(datetime,'05/01/2002',101)
GROUP BY OrderDate, ProductID
ORDER BY Rev DESC;
GO
--This query can use the above indexed view.
SELECT OrderDate, SUM(UnitPrice*OrderQty*(1.00-UnitPriceDiscount)) AS Rev
FROM Sales.SalesOrderDetail AS od
JOIN Sales.SalesOrderHeader AS o ON od.SalesOrderID=o.SalesOrderID
AND DATEPART(mm,OrderDate)= 3
AND DATEPART(yy,OrderDate) = 2002
GROUP BY OrderDate
ORDER BY OrderDate ASC;
GO
I. Create an index with included (non-key) columns
The following example creates a nonclustered index with one key column (PostalCode) and four non-key columns
(AddressLine1, AddressLine2, City, StateProvinceID). A query that is covered by the index follows. To display the index that is selected by the query
optimizer, on the Querymenu in SQL Server Management Studio, select Display Actual Execution Plan before executing the query.
IF EXISTS (SELECT name FROM sys.indexes
WHERE name = N'IX_Address_PostalCode')
DROP INDEX IX_Address_PostalCode ON Person.Address;
GO
CREATE NONCLUSTERED INDEX IX_Address_PostalCode
ON Person.Address (PostalCode)
INCLUDE (AddressLine1, AddressLine2, City, StateProvinceID);
GO
SELECT AddressLine1, AddressLine2, City, StateProvinceID, PostalCode
FROM Person.Address
WHERE PostalCode BETWEEN N'98000' and N'99999';
GO

J. Create a partitioned index


The following example creates a nonclustered partitioned index on TransactionsPS1, an existing partition scheme in the AdventureWorks2012 database. This
example assumes the partitioned index sample has been installed.

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

IF EXISTS (SELECT name FROM sys.indexes


WHERE name = N'IX_TransactionHistory_ReferenceOrderID'
AND object_id = OBJECT_ID(N'Production.TransactionHistory'))
DROP INDEX IX_TransactionHistory_ReferenceOrderID
ON Production.TransactionHistory;
GO
CREATE NONCLUSTERED INDEX IX_TransactionHistory_ReferenceOrderID
ON Production.TransactionHistory (ReferenceOrderID)
ON TransactionsPS1 (TransactionDate);
GO

K. Creating a filtered index


The following example creates a filtered index on the Production.BillOfMaterials table in the AdventureWorks2012 database. The filter predicate can include
columns that are not key columns in the filtered index. The predicate in this example selects only the rows where EndDate is non-NULL.
IF EXISTS (SELECT name FROM sys.indexes
WHERE name = N'FIBillOfMaterialsWithEndDate'
AND object_id = OBJECT_ID(N'Production.BillOfMaterials'))
DROP INDEX FIBillOfMaterialsWithEndDate
ON Production.BillOfMaterials;
GO
CREATE NONCLUSTERED INDEX "FIBillOfMaterialsWithEndDate"
ON Production.BillOfMaterials (ComponentID, StartDate)
WHERE EndDate IS NOT NULL;

L. Create a compressed index


The following example creates an index on a nonpartitioned table by using row compression.
CREATE NONCLUSTERED INDEX IX_INDEX_1
ON T1 (C2)
WITH ( DATA_COMPRESSION = ROW ) ;
GO

The following example creates an index on a partitioned table by using row compression on all partitions of the index.
CREATE CLUSTERED INDEX IX_PartTab2Col1
ON PartitionTable1 (Col1)
WITH ( DATA_COMPRESSION = ROW ) ;
GO

The following example creates an index on a partitioned table by using page compression on partition 1 of the index and row compression on
partitions 2 through 4 of the index.
CREATE CLUSTERED INDEX IX_PartTab2Col1
ON PartitionTable1 (Col1)
WITH (DATA_COMPRESSION = PAGE ON PARTITIONS(1),
DATA_COMPRESSION = ROW ON PARTITIONS (2 TO 4 ) ) ;
GO

Examples: Azure SQL Data Warehouse and Parallel Data Warehouse


M. Basic syntax
CREATE INDEX IX_VendorID
ON ProductVendor (VendorID);
CREATE INDEX IX_VendorID
ON dbo.ProductVendor (VendorID DESC, Name ASC, Address DESC);
CREATE INDEX IX_VendorID
ON Purchasing..ProductVendor (VendorID);

N. Create a non-clustered index on a table in the current database


The following example creates a non-clustered index on the VendorID column of the ProductVendor table.
CREATE INDEX IX_ProductVendor_VendorID
ON ProductVendor (VendorID);

O. Create a clustered index on a table in another database


The following example creates a non-clustered index on the VendorID column of the ProductVendor table in the Purchasing database.
CREATE CLUSTERED INDEX IX_ProductVendor_VendorID
ON Purchasing..ProductVendor (VendorID);

P. Add a column to an index


The following example creates index IX_FF with two columns from the dbo.FactFinance table. The next statement demonstrates rebuilding that index with the same
name and one more column.
-- Uses AdventureWorks

CREATE INDEX IX_FF ON dbo.FactFinance (


FinanceKey ASC, DateKey ASC );

--Rebuild and add the OrganizationKey


CREATE INDEX IX_FF ON dbo.FactFinance (
FinanceKey, DateKey, OrganizationKey DESC)
WITH ( DROP_EXISTING = ON );

See Also
ALTER INDEX (Transact-SQL)
CREATE PARTITION FUNCTION (Transact-SQL)
CREATE PARTITION SCHEME (Transact-SQL)
CREATE SPATIAL INDEX (Transact-SQL)
CREATE STATISTICS (Transact-SQL)
CREATE TABLE (Transact-SQL)
CREATE XML INDEX (Transact-SQL)
Data Types (Transact-SQL)
DBCC SHOW_STATISTICS (Transact-SQL)
DROP INDEX (Transact-SQL)
XML Indexes (SQL Server)
sys.indexes (Transact-SQL)
sys.index_columns (Transact-SQL)
sys.xml_indexes (Transact-SQL)
EVENTDATA (Transact-SQL)
Community Additions
ADD
Errata: O. Create a clustered index on a table in another database
The current text says:

"The following example creates a non-clustered index on the VendorID column of the ProductVendor table in the Purchasing database."

CREATE SPATIAL INDEX (Transact-SQL)

THIS TOPIC APPLIES TO: SQL Server (starting with 2008) Azure SQL Database Azure SQL Data Warehouse Parallel Data Warehouse
Creates a spatial index on a specified table and column in SQL Server. An index can be created before there is data in the table. Indexes can be created on tables or
views in another database by specifying a qualified database name. Spatial indexes require the table to have a clustered primary key. For information about spatial
indexes, see Spatial Indexes Overview.

Transact-SQL Syntax Conventions

Syntax
-- SQL Server Syntax

CREATE SPATIAL INDEX index_name


ON <object> ( spatial_column_name )
{
<geometry_tessellation> | <geography_tessellation>
}
[ ON { filegroup_name | "default" } ]
[;]

<object> ::=
[ database_name. [ schema_name ] . | schema_name. ] table_name

<geometry_tessellation> ::=
{
<geometry_automatic_grid_tessellation>
| <geometry_manual_grid_tessellation>
}

<geometry_automatic_grid_tessellation> ::=
{
[ USING GEOMETRY_AUTO_GRID ]
WITH (
<bounding_box>
[ [,] <tessellation_cells_per_object> [ ,…n] ]
[ [,] <spatial_index_option> [ ,…n] ]
)
}

<geometry_manual_grid_tessellation> ::=
{
[ USING GEOMETRY_GRID ]
WITH (
<bounding_box>
[ [,]<tessellation_grid> [ ,…n] ]
[ [,]<tessellation_cells_per_object> [ ,…n] ]
[ [,]<spatial_index_option> [ ,…n] ]
)
}

<geography_tessellation> ::=
{
<geography_automatic_grid_tessellation> | <geography_manual_grid_tessellation>
}

<geography_automatic_grid_tessellation> ::=
{
[ USING GEOGRAPHY_AUTO_GRID ]
[ WITH (
[ [,] <tessellation_cells_per_object> [ ,…n] ]
[ [,] <spatial_index_option> ]
) ]
}

<geography_manual_grid_tessellation> ::=
{
[ USING GEOGRAPHY_GRID ]
[ WITH (
[ <tessellation_grid> [ ,…n] ]
[ [,] <tessellation_cells_per_object> [ ,…n] ]
[ [,] <spatial_index_option> [ ,…n] ]
) ]
}

<bounding_box> ::=
{
BOUNDING_BOX = ( {
xmin, ymin, xmax, ymax
| <named_bb_coordinate>, <named_bb_coordinate>, <named_bb_coordinate>, <named_bb_coordinate>
} )
}

<named_bb_coordinate> ::= { XMIN = xmin | YMIN = ymin | XMAX = xmax | YMAX=ymax }

<tesselation_grid> ::=
{
GRIDS = ( { <grid_level> [ ,...n ] | <grid_size>, <grid_size>, <grid_size>, <grid_size> }
)
}
<tesseallation_cells_per_object> ::=
{
CELLS_PER_OBJECT = n
}

<grid_level> ::=
{
LEVEL_1 = <grid_size>
| LEVEL_2 = <grid_size>
| LEVEL_3 = <grid_size>
| LEVEL_4 = <grid_size>
}

<grid_size> ::= { LOW | MEDIUM | HIGH }

<spatial_index_option> ::=
{
PAD_INDEX = { ON | OFF }
| FILLFACTOR = fillfactor
| SORT_IN_TEMPDB = { ON | OFF }
| IGNORE_DUP_KEY = OFF
| STATISTICS_NORECOMPUTE = { ON | OFF }
| DROP_EXISTING = { ON | OFF }
| ONLINE = OFF
| ALLOW_ROW_LOCKS = { ON | OFF }
| ALLOW_PAGE_LOCKS = { ON | OFF }
| MAXDOP = max_degree_of_parallelism
| DATA_COMPRESSION = { NONE | ROW | PAGE }
}

-- Windows Azure SQL Database Syntax

CREATE SPATIAL INDEX index_name


ON <object> ( spatial_column_name )
{
[ USING <geometry_grid_tessellation> ]
WITH ( <bounding_box>
[ [,] <tesselation_parameters> [,... n ] ]
[ [,] <spatial_index_option> [,... n ] ] )
| [ USING <geography_grid_tessellation> ]
[ WITH ( [ <tesselation_parameters> [,... n ] ]
[ [,] <spatial_index_option> [,... n ] ] ) ]
}

[ ; ]

<object> ::=
{
[database_name. [schema_name ] . | schema_name. ]
table_name
}

<geometry_grid_tessellation> ::=
{ GEOMETRY_GRID }

<bounding_box> ::=
BOUNDING_BOX = ( {
xmin, ymin, xmax, ymax
| <named_bb_coordinate>, <named_bb_coordinate>, <named_bb_coordinate>, <named_bb_coordinate>
} )

<named_bb_coordinate> ::= { XMIN = xmin | YMIN = ymin | XMAX = xmax | YMAX=ymax }

<tesselation_parameters> ::=
{
GRIDS = ( { <grid_density> [ ,... n ] | <density>, <density>, <density>, <density> } )
| CELLS_PER_OBJECT = n
}

<grid_density> ::=
{
LEVEL_1 = <density>
| LEVEL_2 = <density>
| LEVEL_3 = <density>
| LEVEL_4 = <density>
}

<density> ::= { LOW | MEDIUM | HIGH }

<geography_grid_tessellation> ::=
{ GEOGRAPHY_GRID }

<spatial_index_option> ::=
{
IGNORE_DUP_KEY = OFF
| STATISTICS_NORECOMPUTE = { ON | OFF }
| DROP_EXISTING = { ON | OFF }
| ONLINE = OFF
}

Arguments
index_name
Is the name of the index. Index names must be unique within a table but do not have to be unique within a database. Index names must follow the rules
of identifiers.
ON <object> ( spatial_column_name )
Specifies the object (database, schema, or table) on which the index is to be created and the name of spatial column.
spatial_column_name specifies the spatial column on which the index is based. Only one spatial column can be specified in a single spatial index definition;
however, multiple spatial indexes can be created on a geometry or geography column.
USING
Indicates the tessellation scheme for the spatial index. This parameter uses the type-specific value, shown in the following table:

Data type of column Tessellation scheme

geometry GEOMETRY_GRID

geometry GEOMETRY_AUTO_GRID

geography GEOGRAPY_GRID

geography GEOGRAPHY_AUTO_GRID

A spatial index can be created only on a column of type geometry or geography. Otherwise, an error is raised. Also, if an invalid parameter for a given type is
passed, an error is raised.
For information about how SQL Server implements tessellation, see Spatial Indexes Overview.
ON filegroup_name
Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Creates the specified index on the specified filegroup. If no location is specified and the table is not partitioned, the index uses the same filegroup as the
underlying table. The filegroup must already exist.
ON "default"

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Creates the specified index on the default filegroup.


The term default, in this context, is not a keyword. It is an identifier for the default filegroup and must be delimited, as in ON "default" or ON [default]. If "default" is
specified, the QUOTED_IDENTIFIER option must be ON for the current session. This is the default setting. For more information, see SET QUOTED_IDENTIFIER
(Transact-SQL).
<object>::=
Is the fully qualified or non-fully qualified object to be indexed.
database_name
Is the name of the database.
schema_name
Is the name of the schema to which the table belongs.
table_name
Is the name of the table to be indexed.
Windows Azure SQL Database supports the three-part name format database_name.[schema_name].object_name when the database_name is the current database
or the database_name is tempdb and the object_name starts with #.
USING Options
GEOMETRY_GRID
Specifies the geometry grid tessellation scheme that you are using. GEOMETRY_GRID can be specified only on a column of the geometry data type.
GEOMETRY_GRID allows for manual adjusting of the tessellation scheme.
GEOMETRY_AUTO_GRID

Applies to: SQL Server 2012 through SQL Server 2016, SQL Database V12.

Can be specified only on a column of the geometry data type. This is the default for this data type and does not need to be specified.
GEOGRAPHY_GRID
Specifies the geography grid tessellation scheme. GEOGRAPHY_GRID can be specified only on a column of the geography data type.
GEOGRAPHY_AUTO_GRID

Applies to: SQL Server 2012 through SQL Server 2016, SQL Database V12.

Can be specified only on a column of the geography data type. This is the default for this data type and does not need to be specified.
WITH Options
BOUNDING_BOX
Specifies a numeric four-tuple that defines the four coordinates of the bounding box: the x-min and y-min coordinates of the lower-left corner, and the x-max and
y-max coordinates of the upper-right corner.
xmin
Specifies the x-coordinate of the lower-left corner of the bounding box.
ymin
Specifies the y-coordinate of the lower-left corner of the bounding box.
xmax
Specifies the x-coordinate of the upper-right corner of the bounding box.
ymax
Specifies the y-coordinate of the upper-right corner of the bounding box.
XMIN = xmin
Specifies the property name and value for the x-coordinate of the lower-left corner of the bounding box.
YMIN =ymin
Specifies the property name and value for the y-coordinate of the lower-left corner of the bounding box.
XMAX =xmax
Specifies the property name and value for the x-coordinate of the upper-right corner of the bounding box.
YMAX =ymax
Specifies the property name and value for the y-coordinate of upper-right corner of the bounding box
Bounding-box coordinates apply only within a USING GEOMETRY_GRID clause.
xmax must be greater than xmin and ymax must be greater than ymin. You can specify any valid float value representation, assuming
that: xmax > xmin and ymax > ymin. Otherwise the appropriate errors are raised.
There are no default values.
The bounding-box property names are case-insensitive regardless of the database collation.
To specify property names, you must specify each of them once and only once. You can specify them in any order. For example, the following clauses are
equivalent:
 BOUNDING_BOX =( XMIN =xmin, YMIN =ymin, XMAX =xmax, YMAX =ymax )
 BOUNDING_BOX =( XMIN =xmin, XMAX =xmax, YMIN =ymin, YMAX =ymax)
GRIDS
Defines the density of the grid at each level of a tessellation scheme. When GEOMETRY_AUTO_GRID and GEOGRAPHY_AUTO_GRID are selected, this option is
disabled.
For information about tessellation, see Spatial Indexes Overview.
The GRIDS parameters are as follows:
LEVEL_1
Specifies the first-level (top) grid.
LEVEL_2
Specifies the second-level grid.
LEVEL_3
Specifies the third-level grid.
LEVEL_4
Specifies the fourth-level grid.
LOW
Specifies the lowest possible density for the grid at a given level. LOW equates to 16 cells (a 4x4 grid).
MEDIUM
Specifies the medium density for the grid at a given level. MEDIUM equates to 64 cells (an 8x8 grid).
HIGH
Specifies the highest possible density for the grid at a given level. HIGH equates to 256 cells (a 16x16 grid).
Using level names allows you to specify the levels in any order and to omit levels. If you use the name for any level, you must use the name of any other level that
you specify. If you omit a level, its density defaults to MEDIUM.
If an invalid density is specified, an error is raised.
CELLS_PER_OBJECT =n
Specifies the number of tessellation cells per object that can be used for a single spatial object in the index by the tessellation process. n can be any integer
between 1 and 8192, inclusive. If an invalid number is passed or the number is larger than the maximum number of cells for the specified tessellation, an error is
raised.
CELLS_PER_OBJECT has the following default values:

USING option Default Cells per Object

GEOMETRY_GRID 16

GEOMETRY_AUTO_GRID 8

GEOGRAPHY_GRID 16

GEOGRAPHY_AUTO_GRID 12

At the top level, if an object covers more cells than specified by n, the indexing uses as many cells as necessary to provide a complete top-level tessellation. In such
cases, an object might receive more than the specified number of cells. In this case, the maximum number is the number of cells generated by the top-level grid,
which depends on the density.
The CELLS_PER_OBJECT value is used by the cells-per-object tessellation rule. For information about the tessellation rules, see Spatial Indexes Overview.
PAD_INDEX = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies index padding. The default is OFF.


ON
Indicates that the percentage of free space that is specified by fillfactor is applied to the intermediate-level pages of the index.
OFF or fillfactor is not specified
Indicates that the intermediate-level pages are filled to near capacity, leaving sufficient space for at least one row of the maximum size the index can have,
considering the set of keys on the intermediate pages.
The PAD_INDEX option is useful only when FILLFACTOR is specified, because PAD_INDEX uses the percentage specified by FILLFACTOR. If the percentage specified
for FILLFACTOR is not large enough to allow for one row, the Database Engine internally overrides the percentage to allow for the minimum. The number of rows
on an intermediate index page is never less than two, regardless of how low the value of fillfactor.
FILLFACTOR =fillfactor
Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies a percentage that indicates how full the Database Engine should make the leaf level of each index page during index creation or rebuild. fillfactor must be
an integer value from 1 to 100. The default is 0. If fillfactor is 100 or 0, the Database Engine creates indexes with leaf pages filled to capacity.

Note

Fill factor values 0 and 100 are the same in all respects.
The FILLFACTOR setting applies only when the index is created or rebuilt. The Database Engine does not dynamically keep the specified percentage of empty space
in the pages. To view the fill factor setting, use the sys.indexes catalog view.

Important

Creating a clustered index with a FILLFACTOR less than 100 affects the amount of storage space the data occupies because the Database Engine
redistributes the data when it creates the clustered index.
For more information, see Specify Fill Factor for an Index.
SORT_IN_TEMPDB = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether to store temporary sort results in tempdb. The default is OFF.
ON
The intermediate sort results that are used to build the index are stored in tempdb. This may reduce the time required to create an index if tempdb is on a different
set of disks than the user database. However, this increases the amount of disk space that is used during the index build.
OFF
The intermediate sort results are stored in the same database as the index.
In addition to the space required in the user database to create the index, tempdb must have about the same amount of additional space to hold the intermediate
sort results. For more information, see SORT_IN_TEMPDB Option For Indexes.
IGNORE_DUP_KEY =OFF
Has no effect for spatial indexes because the index type is never unique. Do not set this option to ON, or else an error is raised.
STATISTICS_NORECOMPUTE = { ON | OFF}
Specifies whether distribution statistics are recomputed. The default is OFF.
ON
Out-of-date statistics are not automatically recomputed.
OFF
Automatic statistics updating are enabled.
To restore automatic statistics updating, set the STATISTICS_NORECOMPUTE to OFF, or execute UPDATE STATISTICS without the
NORECOMPUTE clause.

Important

Disabling automatic recomputation of distribution statistics may prevent the query optimizer from picking optimal execution plans for queries
involving the table.
DROP_EXISTING = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies that the named, preexisting spatial index is dropped and rebuilt. The default is OFF.
ON
The existing index is dropped and rebuilt. The index name specified must be the same as a currently existing index; however, the index definition can be modified.
For example, you can specify different columns, sort order, partition scheme, or index options.
OFF
An error is displayed if the specified index name already exists.
The index type cannot be changed by using DROP_EXISTING.
ONLINE =OFF
Specifies that underlying tables and associated indexes are not available for queries and data modification during the index operation. In this version of SQL Server,
online index builds are not supported for spatial indexes. If this option is set to ON for a spatial index, an error is raised. Either omit the ONLINE option or set
ONLINE to OFF.
An offline index operation that creates, rebuilds, or drops a spatial index, acquires a Schema modification (Sch-M) lock on the table. This prevents all
user access to the underlying table for the duration of the operation.

Note

Online index operations are not available in every edition of SQL Server. For a list of features that are supported by the editions of SQL Server,
see Features Supported by the Editions of SQL Server 2016.
ALLOW_ROW_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether row locks are allowed. The default is ON.


ON
Row locks are allowed when accessing the index. The Database Engine determines when row locks are used.
OFF
Row locks are not used.
ALLOW_PAGE_LOCKS = { ON | OFF }

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Specifies whether page locks are allowed. The default is ON.


ON
Page locks are allowed when accessing the index. The Database Engine determines when page locks are used.
OFF
Page locks are not used.
MAXDOP =max_degree_of_parallelism

Applies to: SQL Server 2008 through SQL Server 2016, SQL Database V12.

Overrides the max degree of parallelism


configuration option for the duration of the index operation. Use MAXDOP to limit the number of processors
used in a parallel plan execution. The maximum is 64 processors.

Important

Although the MAXDOP option is syntactically supported, CREATE SPATIAL INDEX currently always uses only a single processor.
max_degree_of_parallelism can be:
1
Suppresses parallel plan generation.
>1
Restricts the maximum number of processors used in a parallel index operation to the specified number or fewer based on the current system workload.
0 (default)
Uses the actual number of processors or fewer based on the current system workload.
For more information, see Configure Parallel Index Operations.

Note
Parallel index operations are not available in every edition of MicrosoftSQL Server. For a list of features that are supported by the editions of SQL
Server, see Features Supported by the Editions of SQL Server 2016.
DATA_COMPRESSION = {NONE | ROW | PAGE}

Applies to: SQL Server 2012 through SQL Server 2016, SQL Database V12.

Determines the level of data compression used by the index.


NONE
No compression used on data by the index
ROW
Row compression used on data by the index
PAGE
Page compression used on data by the index

Remarks
Every option can be specified only once per CREATE SPATIAL INDEX statement. Specifying a duplicate of any option raises an error.
You can create up to 249 spatial indexes on each spatial column in a table. Creating more than one spatial index on specific spatial column can be
useful, for example, to index different tessellation parameters in a single column.

Important

There are a number of other restrictions on creating a spatial index. For more information, see Spatial Indexes Overview.
An index build cannot make use of available process parallelism.

Methods Supported on Spatial Indexes


Under certain conditions, spatial indexes support a number of set-oriented geometry methods. For more information, see Spatial Indexes Overview.

Spatial Indexes and Partitioning


By default, if a spatial index is created on a partitioned table, the index is partitioned according to the partition scheme of the table. This assures that index data
and the related row are stored in the same partition.
In this case, to alter the partition scheme of the base table, you would have to drop the spatial index before you can repartition the base table. To avoid this
restriction, when you are creating a spatial index, you can specify the "ON filegroup" option. For more information, see "Spatial Indexes and Filegroups," later in this
topic.

Spatial Indexes and Filegroups


By default, spatial indexes are partitioned to the same filegroups as the table on which the index is specified. This can be overridden by using the filegroup
specification:
[ ON { filegroup_name | "default" } ]
If you specify a filegroup for a spatial index, the index is placed on that filegroup, regardless of the partitioning scheme of the table.

Catalog Views for Spatial Indexes


The following catalog views are specific to spatial indexes:
sys.spatial_indexes
Represents the main index information of the spatial indexes.
sys.spatial_index_tessellations
Represents the information about the tessellation scheme and parameters of each of the spatial indexes.

Additional Remarks About Creating Indexes


For more information about creating indexes, see the "Remarks" section in CREATE INDEX (Transact-SQL).

Permissions
The user must have ALTER permission on the table or view, or be a member of the sysadmin fixed server role or the db_ddladmin and db_owner fixed database
roles.

Examples
A. Creating a spatial index on a geometry column
The following example creates a table named SpatialTable that contains a geometry type column, geometry_col. The example then creates a spatial
index, SIndx_SpatialTable_geometry_col1, on the geometry_col. The example uses the default tessellation scheme and specifies the bounding box.
CREATE TABLE SpatialTable(id int primary key, geometry_col geometry);
CREATE SPATIAL INDEX SIndx_SpatialTable_geometry_col1
ON SpatialTable(geometry_col)
WITH ( BOUNDING_BOX = ( 0, 0, 500, 200 ) );

B. Creating a spatial index on a geometry column


The following example creates a second spatial index, SIndx_SpatialTable_geometry_col2, on the geometry_col in the SpatialTable table. The
example specifies GEOMETRY_GRID as the tessellation scheme. The example also specifies the bounding box, different densities on different grid levels, and 64
cells per object. The example also sets the index padding to ON.
CREATE SPATIAL INDEX SIndx_SpatialTable_geometry_col2
ON SpatialTable(geometry_col)
USING GEOMETRY_GRID
WITH (
BOUNDING_BOX = ( xmin=0, ymin=0, xmax=500, ymax=200 ),
GRIDS = (LOW, LOW, MEDIUM, HIGH),
CELLS_PER_OBJECT = 64,
PAD_INDEX = ON );

C. Creating a spatial index on a geometry column


The following example creates a third spatial index, SIndx_SpatialTable_geometry_col3, on the geometry_col in the SpatialTable table. The example
uses the default tessellation scheme. The example specifies the bounding box and uses different cell densities on the third and fourth levels, while using the default
number of cells per object.
CREATE SPATIAL INDEX SIndx_SpatialTable_geometry_col3
ON SpatialTable(geometry_col)
WITH (
BOUNDING_BOX = ( 0, 0, 500, 200 ),
GRIDS = ( LEVEL_4 = HIGH, LEVEL_3 = MEDIUM ) );

D. Changing an option that is specific to spatial indexes


The following example rebuilds the spatial index created in the preceding example, SIndx_SpatialTable_geography_col3, by specifying a
new LEVEL_3 density with DROP_EXISTING = ON.
CREATE SPATIAL INDEX SIndx_SpatialTable_geography_col3
ON SpatialTable(geography_col)
WITH ( BOUNDING_BOX = ( 0, 0, 500, 200 ),
GRIDS = ( LEVEL_3 = LOW ),
DROP_EXISTING = ON );

E. Creating a spatial index on a geography column


The following example creates a table named SpatialTable2 that contains a geography type column, geography_col. The example then creates a spatial
index, SIndx_SpatialTable_geography_col1, on the geography_col. The example uses the default parameters values of the GEOGRAPHY_AUTO_GRID
tessellation scheme.
CREATE TABLE SpatialTable2(id int primary key, object GEOGRAPHY);
CREATE SPATIAL INDEX SIndx_SpatialTable_geography_col1
ON SpatialTable2(object);

Note

For geography grid indexes, a bounding box cannot be specified.

F. Creating a spatial index on a geography column


The following example creates a second spatial index, SIndx_SpatialTable_geography_col2, on the geography_col in the SpatialTable2 table. The
example specifies GEOGRAPHY_GRID as the tessellation scheme. The example also specifies different grid densities on different levels and 64 cells per object. The
example also sets the index padding to ON.
CREATE SPATIAL INDEX SIndx_SpatialTable_geography_col2
ON SpatialTable2(object)
USING GEOGRAPHY_GRID
WITH (
GRIDS = (MEDIUM, LOW, MEDIUM, HIGH ),
CELLS_PER_OBJECT = 64,
PAD_INDEX = ON );

G. Creating a spatial index on a geography column


The example then creates a third spatial index, SIndx_SpatialTable_geography_col3, on the geography_col in the SpatialTable2 table. The example
uses the default tessellation scheme, GEOGRAPHY_GRID, and the default CELLS_PER_OBJECT value (16).
CREATE SPATIAL INDEX SIndx_SpatialTable_geography_col3
ON SpatialTable2(object)
WITH ( GRIDS = ( LEVEL_3 = HIGH, LEVEL_2 = HIGH ) );

The Black Art Of Spatial Index Tuning In SQL Server

For my inaugural blog post I have decided to discuss some investigations I have been making recently into tuning spatial indexes in SQL Server.

I have found that while there is a lot of information on teh interwebs about the theory of SQL Server spatial indexes, and the utilities that can be used to tune
them, there is very little guidance on what spatial index settings are best suited for different data scenarios, and there is only limited information about the
characteristics to consider when tuning these indexes.

As an attempt to provide some guidance I have investigated configuring and tuning spatial indexes on three different spatial data sets which I have discussed as
case studies, but before jumping straight into these I think it would be wise to discuss some background aspects to spatial indexing in SQL Server.

SQL Server Spatial Index Theory


I'm not going to get too much into the theory of spatial indexes in SQL server, or the details of creating spatial indexes because there is a lot of documentation on
this subject, but I will be referring to aspects of spatial index theory throughout this post. The best starting point for delving into this area is the SQL Server
Spatial Indexing Overview documentation. Another great source of information is Isaac Kunen's Blog.

A SQL Server spatial index is basically a grid of cells that cover the extent of the spatial data in a table which allows spatial queries to easily filter out prospective
records and lookup smaller subsets of records to see if they match the spatial criteria. e.g. If a spatial query is trying to find all the records that intersect a
particular rectangle, then a spatial index can improve the performance by only executing the intersection criteria on records that are contained in a grid cell(s)
that the criteria rectangle resides in.

Rather than just being a single layer of cells over the extent of the spatial data, the index is actually a layered grid which is four levels deep (see the image from
the SQL Server documentation below). Each cell in level 1 of the grid is divided into cells that make up level 2, and so on for level 3 and 4.

Each level of the grid can be divided into a different number of cells based on a setting of Low (4x4 or 16 cells), Medium (8x8 or 64 cells), or High (16x16 or 256
cells). This is what controls the granularity of the spatial index.

Geography vs. Geometry

An interesting thing to note about the differences between Geography and Geometry data types is that spatial indexes for Geography types do not have a
bounding box specified. This means that Geography indexes span the entire globe from -180 to 180 longitude and -90 to 90 latitude. This means that the
granularity specified for a Geography index divides the entire globe into the desired number of cells.

Geometry indexes have a bounding box specified, so the administrator can define the grid granularity in conjunction with a known data extent, meaning that the
administrator has a lot more control over the equivalent "ground" size of each grid cell.

Because of this limitation I see the Geometry index as having a major advantage over the Geography index. I'm not entirely sure of the reason for this constraint
on Geography indexes, perhaps there is an assumption that data sets using geographic coordinates (longitude, latitude) are usually large scale data sets spanning
large extents around the earth, but in practice I don't think this is always the case, which will become evident in my last case study.

Index Distribution

In SQL Server a primary key index has high selectivity because each indexed value is unique, which means that performance is relatively similar across the
distribution of keys in the index regardless of the selection criteria.

It is a different story with non-unique indexes, because there may not be the same level of selectivity across the entire distribution of values in the index, e.g. A
column containing surnames of users would undoubtedly have low selectivity when searching for names such as "smith" or "jones", but potentially have higher
selectivity when searching for names such as "kerouac" or "zimmerman". These indexes show us that the performance of the index has a high correlation with
the criteria we are using to search with.

This same consideration needs to be applied when we create spatial indexes. When calculating the desired settings for a spatial index, not only is it important to
make sure that the index is granular enough to efficiently index our records, it is also important that we have a rough idea on the range of spatial selections that
users will be performing on our data set.

Based on the four levels of index grid, and three settings of granularity of each grid, it can be seen that the resulting number of cells in the grid will be the same if
the index is defined as High, Medium, Medium, Medium (HMMM) as it would if the index was defined as MMMH, but the difference in performance will be
dependent on the spatial criteria being applied, i.e. A large sized rectangular criteria filter may perform better using the MMMH index over the HMMM index
because it may be able to select fewer grid cells in level 1 to analyse before drilling into the level 2 grid cells.
Spatial Index Tuning

I found a great source of information on spatial index tuning on Bob Beauchemin's Blog which highlights the following stored procedures for investigating spatial
index characteristics:

sp_help_spatial_geography_index

sp_help_spatial_geometry_index

This stored procedures accept as input parameters

Table Name

Index Name

Flag indicating whether to return verbose output

A geography/geometry instance representing the selection extent (refer to the index distribution discussion above)

In Bob's Blog he discusses the interesting characteristics being the primary and internal filter efficiency, as well as the statistics on the overall number of rows in
the index, broken down as an average number of index rows per row of data.

The primary filter is the spatial index, and the internal filter seems to be an optimised version of the spatial index. The spatial filter's purpose is to reduce the
number of times the query's spatial criteria is compared against records in the table, by executing it against cells in the index and looking up the data that resides
in these cells.

In this post on Bob Beauchemin's Blog he discusses the output values from the tuning stored procedures, showing that the primary index alone will result in
selection of a records that may meet the spatial criteria, including a number of false positives, while the optimised internal filter will perform a conservative
selection of the bare minimum of records that meet the spatial criteria, and it is the difference between these selections that the query engine will use to
compare the spatial criteria against the data records to determine whether they meet the query criteria.

So the most efficient spatial indexes will have a large percentage of the records being determined from the primary and internal filters (i.e. high efficiency
percentages), with only a small number of surplus records needing to be checked individually.

Keep in mind that if we make the index quite granular, i.e. have a large number of small grid cells, we may be able to improve the efficiency percentage of the
primary and internal filters, but the index itself will need to analyse a larger number of grid cells to see if those match the query criteria, and once the matching
cells are determined, the index will possibly need to look-up the data records from more index leaf nodes.

From these observations we can see that the perfect spatial index will be defined such that the number of grid cells in the index is minimised, while still providing
a granularity of the index grid to allow selections to be made across the entire data set, in such a way that the index filter efficiency will be maximised across the
broad range of spatial queries being performed.

Wow, that's a mouthful, breaking that down, basically we want the index to be as efficient as possible while trying to minimise the number of rows in the index.

Index Granularity

As discussed in the Index Distribution section above, the main consideration for the spatial index settings comes down to having a good idea of the types of
spatial queries that will be getting performed. e.g. If the majority of spatial selections are for view extents, and they are mostly higher scale selections, then it
might be appropriate to have an index grid density that is relatively sparse compared to the spatial density of the data in the table. If on the other hand you are
doing a lot of selections of small numbers of records, perhaps within a buffer of another feature, then you may want to have an index grid density that is fairly
similar to the density of your data.
This leads to us needing to know a little more about the statistics of our data. Typical indexes in SQL Server use statistics in their indexes to determine how
selective particular data values are across the range of data in the table, so in a sense we are doing something similar in getting to know the distribution of our
data to make an informed decision on the settings for our spatial index.

In my case studies below I performed spatial analysis of my data sets to determine the maximum density of data, and made decisions on my spatial indexes using
that information. I did this using a T-SQL script that splits the data extent into a number of blocks, and counts the number of records in each block looking for the
maximum number. It then calculates a relative density compared to the extent size using the number of records in the block with the maximum number.

Spatial Index Hints

Just briefly I wanted to mention that when performing my analysis of spatial indexes in SQL Server I found that I had to always define an index hint to force the
spatial queries to use the index I had defined. I have read that this may be due to the version of SQL Server I am using, but I thought it would be useful to make a
note of so that anyone having problems with the spatial index not being used can be aware that this might be a cause.

The image below shows my query plan without an index hint, showing that a table scan (in this case the clustered primary key) will be performed to check each
record to see if it matches the criteria expression.

The image below shows the same query with an index hint defined, showing part of the query plan that indicates a spatial index seek will be employed.

Case Studies

I have a background in cadastral surveying, so I have used three data sets that each represent a layer of cadastral land boundaries, as land parcel polygons:

Data Set 1 - Local Municipality - approximately 15km x 15km, with 14000 records

Data Set 2 - Local Municipality - approximately 40km x 40km, with 66000 records

Data Set 3 - State - approximately 1700km x 2200km, with 2.5 million records

For each of the data sets I created four separate selections in different locations within the data set, and then wrote a script that would execute the four
selections a hundred times and then average the time taken to select the records. The number of features selected is not exactly the same for each data set, but
is relatively similar enough to make comparisons.

For each of the data sets, the scenarios that I had in mind were spatial selections of urban or semi-urban land parcels, so three of the four spatial selections in
the tests select a relatively small number of records, i.e. akin to selecting the land parcels in a street, or subdivision etc. The fourth selection is like a selection for
a view extent, of a number of thousand parcels, i.e. around 1:10000 scale.

Data Set 1

This data set represents a small regional local government authority. The extent of the data is about 15km x 15km (10 miles). Regardless of the small size of the
area, the majority of land parcels are typical residential sized allotments.

The data set was based on a projected map grid datum, so I was able to create each land parcel as a geometry data type. This meant that I was able to define my
spatial indexes with a bounding box, and be in more control of the size of the resulting index grid cells.
The data set has an average parcel size of 10000sqm, with 80% of parcels being under 2000sqm (residential land parcel size), with the total area of those 80% of
parcels being 0.64% of the area of the bounding box of the data set, and being 6.78% of the total area of all parcels.

This gives you an indication of the relative density of the data, and that the majority of the small spatial selections that will be performed on the data will be on
parcels that are very small compared to the extent of the data set.

I calculated the highest area of spatial density to be 1:238400 (comparison to the size of the overall extent of the data set), which is a lot higher than the number
of records in the data set (approx 14000).

I made a decision that I wanted an index grid that would be similar to the highest record density, so I figured that a grid of MLLL, or 64x16x16x16 (equating to
262144 grid cells) would be the ideal, or a permutation of those settings, i.e. LMLL, LLML, LLLM. I also executed my test script against other index configurations
as well to test my hypothesis, e.g. LLLL, HLLL, etc.

My test selections were performed with an STIntersects criteria expression with different sized rectangles in different locations in the area. The results being:

Test 1 Count: 56 features

Test 2 Count: 124 features

Test 3 Count: 110 features

Test 4 Count: 7016 features

The results for each index are shown below, with the average time taken to perform each query, and the filter efficiency of each selection, along with the number
of rows in the index.

Index Settings

Index Performance

No Index

Test 1

0.1081s

Test 2

0.1091s

Test 3

0.1080s

Test 4

0.1266s

MLLL

Total_Primary_Index_Rows 87980

Total_Primary_Index_Pages 264

Average_Number_Of_Index_Rows_Per_Base_Row 6

Test 1

0.0049s

Internal_Filter_Efficiency 94.64%

Primary_Filter_Efficiency 67.46%

Test 2

0.0072s

Internal_Filter_Efficiency 82.25%

Primary_Filter_Efficiency 92.53%
Test 3

0.0049s

Internal_Filter_Efficiency 88.18%

Primary_Filter_Efficiency 88.70%

Test 4

0.0433s

Internal_Filter_Efficiency 88.18%

Primary_Filter_Efficiency 88.70%

LMLL

Total_Primary_Index_Rows 87955

Total_Primary_Index_Pages 263

Average_Number_Of_Index_Rows_Per_Base_Row 6

Test 1

0.0039s

Internal_Filter_Efficiency 94.64%

Primary_Filter_Efficiency 67.46%

Test 2

0.0082s

Internal_Filter_Efficiency 82.25%

Primary_Filter_Efficiency 92.53%

Test 3

0.0040s

Internal_Filter_Efficiency 88.18%

Primary_Filter_Efficiency 88.70%

Test 4

0.0417s

Internal_Filter_Efficiency 88.54%

Primary_Filter_Efficiency 98.63%

LLML

Total_Primary_Index_Rows 87020

Total_Primary_Index_Pages 261

Average_Number_Of_Index_Rows_Per_Base_Row 6

Test 1

0.0051s

Internal_Filter_Efficiency 94.64%

Primary_Filter_Efficiency 67.46%

Test 2

0.0074s
Internal_Filter_Efficiency 78.22%

Primary_Filter_Efficiency 89.20%

Test 3

0.0050s

Internal_Filter_Efficiency 87.27%

Primary_Filter_Efficiency 82.08%

Test 4

0.0416s

Internal_Filter_Efficiency 87.10%

Primary_Filter_Efficiency 98.24%

LLLL

Total_Primary_Index_Rows 53354

Total_Primary_Index_Pages 161

Average_Number_Of_Index_Rows_Per_Base_Row 3

Test 1

0.0047s

Internal_Filter_Efficiency 73.21%

Primary_Filter_Efficiency 58.33%

Test 2

0.0070s

Internal_Filter_Efficiency 79.83%

Primary_Filter_Efficiency 93.23%

Test 3

0.0042s

Internal_Filter_Efficiency 79.09%

Primary_Filter_Efficiency 69.18%

Test 4

0.0506s

Internal_Filter_Efficiency 73.07%

Primary_Filter_Efficiency 97.55%

HLLL

Total_Primary_Index_Rows 157928

Total_Primary_Index_Pages 473

Average_Number_Of_Index_Rows_Per_Base_Row 11

Test 1

0.0048s

Internal_Filter_Efficiency 100%

Primary_Filter_Efficiency 71.79%

Test 2
0.0103s

Internal_Filter_Efficiency 85.48%

Primary_Filter_Efficiency 96.12%

Test 3

0.0053s

Internal_Filter_Efficiency 90%

Primary_Filter_Efficiency 94.01%

Test 4

0.0496s

Internal_Filter_Efficiency 95.89%

Primary_Filter_Efficiency 98.40%

The results show that creating an index with a granularity equivalent to the most dense area of my data resulted in the best performance. The LMLL index
provided the best filter efficiency across all four selections, but the timed performance was slightly worse for one of the smaller selections, but this time
difference is negligible in the context of the entire query time.

You can see that the HLLL index actually provided a much better primary and internal filter efficiency than the MLLL permutations, but because this index results
in twice the number of rows of the MLLL indices, the performance time is longer, as I discussed in the Spatial Index Tuning section.

Conversely, you can see from the LLLL index that the number of index rows is significantly less, but the primary and internal filter efficiency is worse, because the
granularity of the grid cells does not support the queries that we are performing, so the query engine is having to resort to comparing more of the data against
the spatial criteria, and hence the efficiency is poor.

One thing to note about this data set is that it is so small (14000 records) that the performance with no index is still quite good, with the table scan only taking
around 0.1s to check each record in the table to see if it matches the spatial criteria (this is why the time is very similar for each test). It is worth noting though
that by adding the spatial index the large selection query performance is 3 times faster, and the smaller selections are 20 times faster.

Data Set 2

This data set represents a medium sized suburban local government authority. The extent of the data is about 40km x 40km (25 miles). The data has a number
of medium to high density suburban areas, and there are large portions within the extent of data set that are not utilised as land parcels, leading to the data set
having areas of higher density parcels relative to the size of the extent of the data set.

Like the previous data set, this data was based on a projected map grid datum, so I was able to create each land parcel as a geometry data type. This meant that I
was able to define my index with a bounding box, and be in more control of the size of the resulting index grid cells.

This data set has an average parcel size of 8000sqm, with 90% of parcels being under 2000sqm, with the total area of those 90% of parcels being 2.78% of the
area of the extents of the data set, and being 7.73% of total area of all parcels.

I calculated the highest area of spatial density to be 1:1189200, which is a lot higher than the number of records in the data set (approx 66000).

Again, basing my decision for the index grid size to be similar to the highest record density, I decided on a grid size of MMLL (64x64x16x16) or HLLL
(256x16x16x16) to equate to 1048576 grid cells.

The spatial selection test results are shown below:

Test 1 Count: 82 features

Test 2 Count: 167 features


Test 3 Count: 116 features

Test 4 Count: 6226 features

Index Settings

Index Performance

No Index

Test 1

0.4770s

Test 2

0.4755s

Test 3

0.4784s

Test 4

0.4958s

MMLL

Total_Primary_Index_Rows 274349

Total_Primary_Index_Pages 820

Average_Number_Of_Index_Rows_Per_Base_Row 4

Test 1

0.0046s

Internal_Filter_Efficiency 50%

Primary_Filter_Efficiency 84.53%

Test 2

0.0072s

Internal_Filter_Efficiency 91.01%

Primary_Filter_Efficiency 97.66%

Test 3

0.0048s

Internal_Filter_Efficiency 77.58%

Primary_Filter_Efficiency 76.31%

Test 4

0.0417s

Internal_Filter_Efficiency 84.90%

Primary_Filter_Efficiency 96.94%

MLML

Total_Primary_Index_Rows 273834

Total_Primary_Index_Pages 818

Average_Number_Of_Index_Rows_Per_Base_Row 4
Test 1

0.0043s

Internal_Filter_Efficiency 50%

Primary_Filter_Efficiency 84.53%

Test 2

0.0079s

Internal_Filter_Efficiency 91.01%

Primary_Filter_Efficiency 97.66%

Test 3

0.0047s

Internal_Filter_Efficiency 77.58%

Primary_Filter_Efficiency 76.31%

Test 4

0.0421s

Internal_Filter_Efficiency 84.86%

Primary_Filter_Efficiency 96.90%

HLLL

Total_Primary_Index_Rows 274443

Total_Primary_Index_Pages 820

Average_Number_Of_Index_Rows_Per_Base_Row 4

Test 1

0.0051s

Internal_Filter_Efficiency 50%

Primary_Filter_Efficiency 84.53%

Test 2

0.0076s

Internal_Filter_Efficiency 91.01%

Primary_Filter_Efficiency 97.66%

Test 3

0.0053s

Internal_Filter_Efficiency 77.58%

Primary_Filter_Efficiency 76.31%

Test 4

0.0403s

Internal_Filter_Efficiency 84.90%

Primary_Filter_Efficiency 96.94%

MMML
Total_Primary_Index_Rows 489475

Total_Primary_Index_Pages 1459

Average_Number_Of_Index_Rows_Per_Base_Row 7

Test 1

0.0051s

Internal_Filter_Efficiency 92.41%

Primary_Filter_Efficiency 97.14%

Test 2

0.0100s

Internal_Filter_Efficiency 86.82%

Primary_Filter_Efficiency 95.42%

Test 3

0.0054s

Internal_Filter_Efficiency 82.92%

Primary_Filter_Efficiency 88.17%

Test 4

0.0386s

Internal_Filter_Efficiency 91.37%

Primary_Filter_Efficiency 87.87%

As we can see again, by making the index granularity similar to the density of the most dense data in the data set, we have defined the best performing spatial
index - MMLL. As I mentioned above, the grid cell granularity of MMLL is the same as HLLL. What is interesting to note is that the primary and internal filter
efficiency is exactly the same between MMLL and HLLL, and the number of index rows is very similar as well, but the time taken to perform the larger selections
is slightly faster for the HLLL index. It may be that more of the solution may be getting determined by grid levels 1 or 2 and not having to resort to lower grid
levels. When using the verbose output setting on the stored procedure, there are results such as Total_Number_Of_ObjectCells_In_Level1_In_Index that will
give a breakdown of each grid level, and can be analysed to see what the object/index cell distribution is across each of the levels.

One of the interesting results in indexes MMLL and HLLL is the efficiency values in test 1. The internal and primary filter efficiency is quite poor. I checked this
selection and found that the selection rectangle actually passed through most of the features that were being selected. This means that a high percentage of
features needed to be analysed to see if they fit the criteria. One thing we may be able to read into this is that the granularity of the index grid may not be small
enough, given than our optimal choice in the first data set resulted in an average index rows per data row of 6, and this choice was 4, perhaps we don't have the
granularity we need. Since the selection was so small, and the performance time quite fast anyway, I ignored this result as an outlier, considering it an anomaly
of my selection choice.

Another interesting result was the use of the MMML index, which results in twice the number of index rows, but results in an average number of index rows per
data row that is closer to our choice from data set 1 - 7. Looking at the filter efficiency we can see that there has been a big improvement for test 1, although the
performance time has not improved. Again we can see this as the trade-off between efficiency based on grid granularity, and number of index rows that need to
be analysed. If over time we found the MMLL index being inefficient for other selections, we may need to think about using the MMML index, depending on how
it was performing.

It is interesting to note with this data set having 66000 records, that the time to perform a table scan has increased significantly on the time encountered in data
set 1, with indexed results being between 10 and 100 times faster.

Data Set 3

This data set represents the cadastral boundaries for the Australian state of Queensland. For overseas readers, you may recall Queensland being in the news
earlier in the year due to severe flooding in the south east, and Cyclone Yasi in the north. Queensland is the 3rd most populated state of Australia, and 2nd in
area to Western Australia. The state is 1700km (1000 miles) wide, by 2200km (1400 miles) in length. Most of the population is centred around the south east
corner of the state (containing the vast majority of land parcels in the state), with a number of populated regional centres located along the coast.

This data set has an average parcel size of 654150sqm, with 80% of parcels being under 2000sqm, with the total area of those 90% of parcels being 0.033% of the
area of the extents of the data set, and being 0.071% of total area of all parcels.

To create a spatial index that will cater for the diversity of spatial density for this data set is quite challenging, but as I mentioned in the discussion on Geography
vs Geometry, I struck a limitation with this data set that constrained what I could achieve generally. This data set is in geographic coordinates (longitude,
latitude), so I am not able to define a bounding box for the spatial index. Instead I was only able to try to make the grid as granular as I could to improve the
types of selections I desired.

As with the other two data sets, I calculated the highest area of spatial density to be 1:508659200, which is a lot higher than the number of records in the data
set (approx 2.5 million), which if using geometry data I could have created a HHHM (256x256x256x64) 1073741824 grid cell index, but using this granularity for
the geography index provided poor efficiency (see below). I was only able to define one level higher in the number of grid cells - HHHH (256x256x256x256) with
4294967296 grid cells, and while this did improve upon the previous settings, was still quite poor performing in terms of efficiency.

The spatial selection test results are shown below:

Test 1 Count: 79 features

Test 2 Count: 264 features

Test 3 Count: 117 features

Test 4 Count: 6297 features

Index Settings

Index Performance

No Index

Test 1

34.8497s

Test 2

34.9102s

Test 3

34.8761s

Test 4

34.9194s

HHHL

Total_Primary_Index_Rows 3378393

Total_Primary_Index_Pages 10054

Average_Number_Of_Index_Rows_Per_Base_Row 0
Test 1

0.0397s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 3.97%

Test 2

0.0565s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 8.96%

Test 3

0.0343s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 6.92%

Test 4

0.3844s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 31.26%

HHHM

Total_Primary_Index_Rows 3882914

Total_Primary_Index_Pages 11554

Average_Number_Of_Index_Rows_Per_Base_Row 0

Test 1

0.0228s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 6.86%

Test 2

0.0197s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 29.01%

Test 3

0.0207s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 11.83%

Test 4

0.2183s

Internal_Filter_Efficiency 2.79%

Primary_Filter_Efficiency 49.22%

HHHH

Total_Primary_Index_Rows 4582733

Total_Primary_Index_Pages 13637

Average_Number_Of_Index_Rows_Per_Base_Row 0
Test 1

0.0162s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 11.70%

Test 2

0.0167s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 38.59%

Test 3

0.0144s

Internal_Filter_Efficiency 0%

Primary_Filter_Efficiency 19.05%

Test 4

0.1785s

Internal_Filter_Efficiency 9.30%

Primary_Filter_Efficiency 61.30%

As discussed above, the use of the Geography data type has constrained our choices with this data set, so HHHH is the natural choice for our index, based on it
having the best filter efficiency, and unfortunately not really having any more granular settings at out disposal, i.e. HHHH is the most detailed index grid we can
define.

When performing the selections on this data set with no index, the time taken to perform a table scan was around 35s, so the improvement in using the index
was from 200 to 2000 times faster.

Unfortunately the lack of control over the granularity of the grid means that a lot of processing would be occurring in the database to perform each query, so I
would expect performance to degrade quickly for this data set in a multi user environment.

While this data set was interesting to use from an analysis perspective, in an enterprise situation I would expect that the data would be tiled into logical regions,
with queries incorporating one or more tiles depending on the size of the query. In this way a tile would act as a kind of preliminary index.

Conclusion

Like any system implementation I think that applying a spatial index is something that needs to be reviewed and tweaked over the lifetime of the data set.
Depending on the type of data being kept, the data may incrementally change over time, possibly requiring tweaking of the index, but regardless of this, you will
also learn more about the queries that users are performing against the data as time goes by, and this knowledge will help you design you spatial index.

Remember that your spatial index will have a profound effect on the performance of your database, especially in a multi user scenario, so the index should be
designed well to meet the needs of the queries being performed.

Another suggestion that I haven't spoken of so far is that like typical SQL Server indexes, it is possible to have multiple spatial indexes on the same table, so if
necessary you could have an index that is utilised by larger queries, such as view extents at a high scale, and another for granular spatial queries, and then use
index hints to swap between the two when performing different operations. This would split the processing and potentially improve the performance in a multi
user environment.

The following are a summary of my main tips from what I have learned:

Always define a spatial index, no matter what the settings - don't get hung up on needing the perfect index from the start, as I have shown having any index
improves spatial queries over no index at all. As I said you can improve your index over time as you get to understand your users, and your data.
Make sure that your index is being used by checking the query plan.

Determine the density of your data, or at least be able to estimate the density of your desired index grid cells.

Get some information about the types of spatial selections users will be performing - perhaps log all user selections so that you can test your spatial indexes with
these queries.

Use the tuning stored procedures - be objective about filter efficiency vs. number index rows, because an imbalance in either will affect your query performance,
and therefore load on the server in a multi user environment.

My final comment would be that were you have the choice to use Geometry or Geography data, choose Geometry, if only for the control over the spatial index. I
see the index limitation with the Geography data type to be a major stumbling block, especially in situations such as my third case study. I have heard that future
versions of SQL Server will have the ability to have indexes with deeper grid levels, so this may alleviate the situation I have, so I look forward to seeing this new
functionality.

Posted by stoolrossa

Email This

BlogThis!

Share to Twitter

Share to Facebook

Share to Pinterest

Labels: index, spatial, SQL Server, tuning

5 comments:

AnonymousSeptember 12, 2013 at 8:03 PM

Amazing post, learned a lot about something i never really new about in SQL server (always something new to learn right?). Theres a nice page here too:

SQL Index tuning tips

Reply

AnonymousMarch 23, 2015 at 1:32 PM

Great Info

Reply

AnonymousNovember 19, 2015 at 12:27 AM

Hi

Awesome post. I have a question though, I didnt get why you couldn't use geometry on the dcdb? I have to maintain a points dataset with geotagged photos in
Qld, am I doing something ridiculously wrong by loading them up into a geometry feature class (they're photos geotagged from the gps in their exif)

Regards

Reply

Replies

stoolrossaNovember 19, 2015 at 4:03 AM

Thanks for your feedback. It's been a while since I did this analysis/post so your are testing my memory a little :)

I think one of the reasons I tested the two smaller datasets as geometry features and the large dataset as geography features was because my existing data was
already in coordinate systems that fit that approach, i.e. the smaller datasets were in grid coordinates and the large was in longitude/latitude.
In addition to this it was also an opportunity to test the differences in configuring indexes for both geometry and geography shapes. In hindsight, yes I could have
done a second test of the large dataset in grid coordinates and compared the performance difference, but note it would need to have been a different projection
than the smaller data sets are in (MGA56) otherwise I wouldn't have been able to include all the data, i.e. Queensland is covered by 3 zones for the MGA
projection (54, 55, 56).

So no, there is nothing that will technically prevent you from storing your longitude/latitude data as coordinates as a geometry, but you must remember that
since these are polar coordinates, you would need to do your own distance calculations etc taking into considerations the curvature of the earth, i.e. the arc
traced out by the great circle connecting your points etc. The other consideration is crossing the 180E/180W longitude line, but if your data is limited to
Queensland only, then this wont be a problem.

Later versions of SQL Server also have more capabilities for defining parameters of the spatial index, so even if you did use the geography type you could still
define an efficient spatial index.

Problem

As each year goes by the uptime requirement for our databases gets larger and larger, meaning the downtime
that we have to do maintenance on our databases is getting smaller and smaller. This tip will look at a
feature that was introduced in SQL Server 2005 that allows us to leave our indexes online and accessible
while they are being rebuilt.

Solution

SQL Server Online Index Rebuild Background Information

There are a few things I want to mention regarding online index rebuilds before we get into the details. First,
I want to make sure you understand the difference between rebuilding and reorganizing an index. This tip is
going to focus on rebuilds only. If you would like to learn more about the differences between the two
operations you can read more here. Second, we should note that in SQL Server 2005 the online option for
index rebuilds is only available in Enterprise edition while in SQL Server 2008 it is available in Enterprise,
Developer, and Evaluation editions. Finally, this option is not available if:

 the index is an XML index


 the index is a Spatial index
 the index is on a local temp table
 the index is clustered and the table contains a LOB database column(s)
 the index is no clustered and the index itself contains a LOB database column(s)

SQL Server ALTER INDEX Syntax

The syntax for rebuilding indexes is very simple, we just add the "WITH ONLINE=ON" clause to the ALTER
INDEX command. Here are a couple of examples. The first rebuilds a single index on a table and the second rebuilds all
the indexes on the table. You can read more on rebuilding indexes here.

ALTER INDEX [IX_Test] ON [dbo].[Test] REBUILD WITH (ONLINE = ON);


ALTER INDEX ALL ON [dbo].[Test] REBUILD WITH (ONLINE = ON);

Performance of SQL Server Online vs. Offline Index Rebuilds

To test the performance of rebuilding indexes online I wanted to use a fairly large table so the rebuild would
take at least a minute or two. I used a table that had about 20,000,000 records in it and two indexes, one
clustered primary key and one no clustered single column index. Here is the complete table and index
definition.

CREATE TABLE [dbo].[Test] (


[PKID] [int] NOT NULL,
[IndCol] [int] NOT NULL,
[Col1] [int] NULL,
[Col2] [int] NULL,
[Col3] [datetime] NULL,
[Col4] [varchar](1000) NULL,
[Col5] [timestamp] NOT NULL,
[Col6] [int] NULL,
[Col7] [varchar](200) NULL,
[Col8] [int] NULL,
CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED
(
[PKID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
CREATE NONCLUSTERED INDEX [IX_Test] ON [dbo].[Test]
(
[IndCol] ASC
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF,
DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

For this performance test I first compared the traditional OFFLINE index rebuild with using the ONLINE
parameter for both a clustered and a no clustered index rebuild. This initial baseline test used the following
commands and were run without any other concurrent activity on the table.

ALTER INDEX [PK_Test] ON [dbo].[Test] REBUILD WITH (ONLINE=ON)


ALTER INDEX [IX_Text] ON [dbo].[Test] REBUILD WITH (ONLINE=ON)
ALTER INDEX [PK_Test] ON [dbo].[Test] REBUILD WITH (ONLINE=OFF)
ALTER INDEX [IX_Text] ON [dbo].[Test] REBUILD WITH (ONLINE=OFF)

Note that each of the commands was run 5 times and an average taken of each metric. Below are the results of the tests:

OFFLINE ONLINE

CPU DURATION CPU DURATION


READS WRITES READS WRITES
(ms) (ms) (ms) (ms)

CLUSTERED 17828 841991 225377 21825 44063 3704666 508581 93890

NONCLUSTERED 20078 308781 25868 24160 19610 660834 65703 32045

It's pretty obvious from these results that the ONLINE index rebuild does not perform even close to as well
as when run in OFFLINE mode as it uses considerably more resources to complete the rebuild. The extra
reads and writes can be attributed to the second copy of the index that SQL Server creates during the initial
phase of the index rebuild. More on this later.

Next, let's take a look at what happens when there is some activity (inserts/updates/selects) on the table while the index is being rebuilt. To simulate
activity on the table while the rebuild is running I opened 3 other sessions with each running one statement per second. One ran a simple select, one
ran an insert and the last session ran a random update which would update about 600 records per statement. Here are the results of that test.

WITH ACTIVITY NO ACTIVITY

CPU DURATION CPU DURATION


READS WRITES READS WRITES
(ms) (ms) (ms) (ms)

CLUSTERED 85938 59243675 1583512 527312 44063 3704666 508581 93890

NONCLUSTERED 19735 665451 65738 34053 19610 660834 65703 32045

These results are quite interesting. Looking first at the no clustered index rebuild, we see that it uses only
marginally more resources with concurrent activity on the table. The clustered index however, uses much
more resources with activity on the table, taking almost 5 times longer to complete. This is probably due to
the fact that it is basically maintaining two copies of the index (and data since it's clustered) while the
rebuild is running and other processes are accessing the table.

SQL Server Transaction Log Usage

Another factor to take into consideration when running index rebuilds with the ONLINE option ON is the
additional space required in the transaction log. To see just how much extra space is used I took a backup
right before my rebuild command to clear out the log. I then ran each rebuild command and between each
rebuild I took another transaction log backup. Below are the results. You can see in both cases, clustered or
no clustered indexes, each online operation generates more redo than a normal OFFLINE rebuild.

LOG SPACE USED (MB)

OFFLINE ONLINE

CLUSTERED 1801 2131

NONCLUSTERED 215 262

Extra Disk Space Required During SQL Server Online Index Rebuilds

The first place where more disk space is required is the data file in which the index resides. During the initial phase of
the online rebuild enough space for a second copy of this index is required since SQL Server creates a snapshot of the
index. Also, for clustered indexes, a temporary mapping index is created which determines the records to modify in the
new index when changes occur in the original index during the rebuild phase. Once the rebuild process is complete this
index is dropped during the final phase. A good explanation of these phases can be found here. There is also extra
space required in the version store portion of the tempdb. During an online rebuild you can query
the sys.dm_db_file_space_usage DMV to see how many pages are currently reserved. In my test cases I found that
during the clustered index rebuild the version store required about 1880 pages. This number would be completely
dependent on the nature of the activity on your table during the rebuild. I also noticed that for no clustered index
rebuilds the version store is only used if you are updating a column that is a part of the index. In cases where I updated
a column that was not a part of the index being rebuilt (as in the examples in this tip), the version store was not used
at all.

SQL Server Locks Acquired with SQL Server Online Index Rebuilds

This link also shows exactly which types of locks are acquired during the online index rebuild. The locks in the
preparation and build phase, IS and S, are there mainly to ensure that another process does not get an exclusive lock
on the object while the index is being rebuilt. The last lock acquired during the final phase is a Sch-M lock. This
schema modification lock blocks all other concurrent access to the table, but it is only held for a very short period of
time while the old index is dropped and the metadata updated.

Summary

We can see from the above tests that ONLINE index rebuilds do require many more resources and take much
longer to complete than its old OFFLINE counterpart. For those of us that have available downtime in our
systems, it's probably a better idea to perform these maintenance activities during this window, but for those
of us that do not have that luxury, ONLINE index rebuilds are a very handy feature.

Next Steps
 Investigate effects of using SORT_IN_TEMPDB parameter
 How to identify which indexes need to be rebuilt
 Check out these index related tips:
o Fragmentation and Index Maintenance
o Indexing

SERVER – Disable Clustered Index and Data Insert. The same reader has asked me the difference between ALTER INDEX ALL REBUILD and ALTER
INDEX REBUILD along with disabled clustered index.

Instead of writing a big theory, we will go over the demo right away. Here are the steps that we intend to follow.

1) Create Clustered and Nonclustered Index


2) Disable Clustered and Nonclustered Index
3) Enable – a) All Indexes, b) Clustered Index

USE tempdb
GO
-- Drop Table if Exists
IF EXISTS (SELECT * FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'[dbo].[TableName]') AND type IN(N'U'))
DROP TABLE [dbo].[TableName] GO
-- Create Table
CREATE TABLE [dbo].[TableName](
[ID] [int] NOT NULL,
[FirstCol] [varchar](50) NULL
)
GO
-- Create Clustered Index
ALTER TABLE[TableName]ADD CONSTRAINT [PK_TableName] PRIMARY KEY CLUSTERED
([ID] ASC)
GO
-- Create Nonclustered Index
CREATE UNIQUE NONCLUSTERED INDEX [IX_NonClustered_TableName] ON [dbo].[TableName] ([FirstCol]ASC)
GO
-- Check that all the indexes are enabled
SELECT OBJECT_NAME(OBJECT_ID), Name, type_desc, is_disabled
FROM sys.indexes
WHERE OBJECT_NAME(OBJECT_ID) = 'TableName'
GO

Now let us disable both the indexes.

-- Disable Indexes
-- Disable Nonclustered Index
ALTER INDEX [IX_NonClustered_TableName] ON [dbo].[TableName] DISABLE
GO
-- Disable Clustered Index
ALTER INDEX [PK_TableName] ON [dbo].[TableName] DISABLE
GO
-- Check that all the indexes are disabled
SELECT OBJECT_NAME(OBJECT_ID), Name, type_desc, is_disabled
FROM sys.indexes
WHERE OBJECT_NAME(OBJECT_ID) = 'TableName'
GO
Next, let us rebuild all the indexes and see the output.

-- Test 1: ALTER INDEX ALL REBUILD


-- Rebuliding should work fine
ALTER INDEX ALL ON [dbo].[TableName] REBUILD
GO
-- Check that all the indexes are enabled
SELECT OBJECT_NAME(OBJECT_ID), Name, type_desc, is_disabled
FROM sys.indexes
WHERE OBJECT_NAME(OBJECT_ID) = 'TableName'
GO

Now, once again disable indexes for the second test.


-- Disable Indexes
-- Disable Nonclustered Index
ALTER INDEX [IX_NonClustered_TableName] ON [dbo].[TableName] DISABLE
GO
-- Disable Clustered Index
ALTER INDEX [PK_TableName] ON [dbo].[TableName] DISABLE
GO
-- Check that all the indexes are disabled
SELECT OBJECT_NAME(OBJECT_ID), Name, type_desc, is_disabled
FROM sys.indexes
WHERE OBJECT_NAME(OBJECT_ID) = 'TableName'
GO

Next, let us build only the clustered index and see the output of all the indexes.
-- Test 2: ALTER INDEX REBUILD
-- Rebuliding should work fine
ALTER INDEX [PK_TableName] ON [dbo].[TableName] REBUILD
GO
-- Check that only clustered index is enabled
SELECT OBJECT_NAME(OBJECT_ID), Name, type_desc, is_disabled
FROM sys.indexes
WHERE OBJECT_NAME(OBJECT_ID) = 'TableName'
GO

Let us do final clean up.


-- Clean up
DROP TABLE [TableName] GO

From the example, it is very clear that if you have built only clustered index when the nonclustered index is disabled, it still remains disabled. Do let me
know if the idea is clear.

Related commands:

CREATE INDEX
CREATE PARTITION SCHEME
Data Types
DBCC SHOW_STATISTICS
DROP INDEX
DBCC DBREINDEX - use to re-index with older versions (SQL Server 2000 and earlier)
sys.indexes
sys.index_columns
sys.dm_db_index_physical_stats
sys.xml_indexes
EVENTDATA
Equivalent Oracle command: ALTER INDEX

Why, when and how to rebuild and reorganize SQL Server indexes

The purpose of SQL Server index is pretty much the same as in its distant relative – the book index – it allows you to get to the information quickly,
but instead of navigating through the book, it indexes a SQL Server database.
SQL Server indexes are created on a column level in both tables and views. Its aim is to provide a “quick to locate” data based on the values within
indexed columns. If an index is created on the primary key, whenever a search for a row of data based on one of the primary key values is
performed, the SQL Server will locate searched value in the index, and then use that index to locate the entire row of data. This means that the SQL
Server does not have to perform a full table scan when searching for particular row, which is much more performance intensive task –consuming
more time, and using more SQL Server resources.

Relational indexes can be created even before there is data in the specified table, or even on tables and views in another database.

CREATE INDEX MyIndex ON MyTable (Column1);

More on CREATE INDEX Transact-SQL can be found on the MSDN.

After indexes are created, they will undergo automatic maintenance by the SQL Server Database Engine whenever insert, update or delete
operations are executed on the underlying data.

Even so, these automatic modifications will continuously scatter the information in the index throughout the database – fragmenting the index over
time. The result – indexes now have pages where logical ordering (based on the key value) differs from the physical ordering inside the data file. This
means that there is a high percentage of free space on the index pages, and that SQL Server has to read higher number of pages when scanning
each index. Also, ordering of pages that belong to the same index gets scrambled and this adds more work to the SQL Server when reading an index
– especially in IO terms.

The Index fragmentation impact on the SQL Server can range from decreased efficiency of queries – for servers with low performance impact, all the
way to the point where SQL Server completely stops using indexes and resorts to the last-straw solution – full table scans for each and every query.
As mentioned before, full table scans will drastically impact SQL Server performance and this is final alarm to remedy index fragmentation on the
SQL Server.

The solution to fragmented indexes is to rebuild or reorganize indexes.

But, before considering maintenance of indexes, it is important to answer two main questions:

1. What is the degree of fragmentation?

2. What is the appropriate action? Reorganize or rebuild?

Detecting fragmentation

Generally, in order to solve any problem, it is essential to first and foremost locate it, and isolate affected area before applying the correct remedy.

Fragmentation can be easily detected by running the system function sys.dm_db_index_physical_statswhich returns the size and the fragmentation
information for the data and indexes of tables or views in SQL Server. It can be run only against a specific index in the table or view, all indexes in the
specific table or view, or vs. all indexes in all databases:

The results returned after running the procedures include following information:

 avg_fragmentation_in_percent – average percent of incorrect pages in the index


 fragment_count – number of fragments in index

 avg_fragment_size_in_pages – average number of pages in one fragment in an index

Analyzing detection results

After the fragmentation has been detected, the next step is to determine its impact on the SQL Server and wheatear or what course of action to
take.

There is no exact information on the minimal amount of fragmentation that affects the SQL Server in specific way to cause performance congestion,
especially since the SQL Server environments greatly vary from one system to another.

However, there is a generally accepted solution based on the percent of fragmentation (avg_fragmentation_in_percent column from the previously
described sys.dm_db_index_physical_stats function)

 Fragmentation is less than 10% – no de-fragmentation is required. It is generally accepted that in majority of environments index
fragmentation less than 10% in negligible and its performance impact on the SQL Server is minimal.
 Fragmentation is between 10-30% – it is suggested to perform index reorganization
 Fragmentation is higher than 30% – it is suggested to perform index rebuild

Here is the reasoning behind the thresholds above which will help you to determine if you should perform index rebuild or index reorganization:

Index reorganization is a process where the SQL Server goes through existing index, and cleans it up. Index rebuild is a heavy-duty process where
index is deleted and then recreated from scratch with entirely new structure, free from all piled up fragments and empty-space pages.

While index reorganization is a pure cleanup operation which leaves system state as it is without locking-out affected tables and views, the rebuild
process locks affected table for the whole rebuild period, which may result in long down-times that could not be acceptable in some environments.

With this in mind, it is clear that the index rebuild is a process with ‘stronger’ solution, but it comes with a price – possible long locks on affected
indexed tables.

On the other side, index reorganization is a ‘lightweight’ process that will solve the fragmentation in a less effective way – since cleaned index will
always be second to the new one fully made from scratch. But reorganizing index is much better from the efficiency standpoint, since it does not
lock affected indexed table during the course of operation.

Servers with regular maintenance periods (e.g. regular maintenance over weekend) should almost always opt for the index rebuild, regardless of the
fragmentation percent, since these environments will hardly be affected by the table lock-outs imposed by index rebuilds due to regular and long
maintenance periods.

How to reorganize and rebuild index:

Using SQL Server Management Studio:

1. In the Object Explorer pane navigate to and expand the SQL Server, and then the Databases node
2. Expand the specific database with fragmented index

3. Expand the Tables node, and the table with fragmented index

4. Expand the specific table

5. Expand the Indexes node

6. Right-click on the fragmented index and select Rebuild or Reorganize option in the context menu (depending on the desired action):

7. Click the OK button and wait for the process to complete

Reorganize indexes in a table using Transact-SQL

Provide appropriate database and table details and execute following code in SQL Server Management Studio to reorganize all indexes on a specific
table:

USE MyDatabase;
GO

ALTER INDEX ALL ON MyTable REORGANIZE;


GO

Rebuild indexes in a table using Transact-SQL

Provide appropriate database and table details and execute following code in SQL Server Management Studio to rebuild all indexes on a specific
table:

USE MyDatabase;
GO

ALTER INDEX ALL ON MyTable REBUILD;


GO

Rebuild or reorganize indexes with ApexSQL Backup

Final option when rebuilding or reorganizing SQL Server indexes is to use ApexSQL Backup – a tool for database jobs management and automation.

ApexSQL Backup allows users to perform and schedule index de-fragmentation jobs from user-friendly graphical user interface. In addition to
automation features, which allow the user to perform regular maintenance on their indexes, ApexSQL Backup allows rebuilding/reorganizing indexes
on multiple tables/views at once. Additionally, ApexSQL Backup can send an email notification on job completion. Additionally, all scheduled jobs, as
well as completed activities can be inspected and altered from the comprehensive view screens.

To schedule index reorganize/rebuild job with ApexSQL Backup, do the following:

1. Start the application


2. In the main ribbon choose appropriate index defragmentation job:

3. Provide SQL Server and database details, choose tables and views to be included in the defragmentation job and click the OK button:

4. To provide schedule details, click on the Schedules radio button, and chose schedule frequency and duration, and then click
the Create button:
5. To complete the schedule, click on the Create button:

With this, the reorganize/rebuild job has been created and will be executed as per specified schedule. All scheduled jobs can be seen in
the Schedules pane.
Q: I’ve learned that my clustering key (i.e., the columns on which I defined my clustered index) should be unique, narrow, static, and ever-
increasing. However, my clustering key is on a GUID. Although a GUID is unique, static, and relatively narrow, I’d like to change my
clustering key, and therefore change my clustered index definition. How can I change the definition of a clustered index?

A: This question is much more complex than it seems, and the process you follow is going to depend on whether the clustered index is
enforcing a primary key constraint. In SQL Server 2000, the DROP_EXISTING clause was added to let you change the definition of the
clustered index without causing all of the nonclustered indexes to be rebuilt twice. The first rebuild is because when you drop a clustered
index, the table reverts to being a heap, so all of the lookup references in the nonclustered indexes must be changed from the clustering key to
the row identifier (RID), as I described in the answer to the previous question. The second nonclustered index rebuild is because when you
build the clustered index again, all nonclustered indexes must use the new clustering key.

Related: What Happens if I Drop a Clustered Index?

To reduce this obvious churn on the nonclustered indexes (along with the associated table locking and transaction log generation), SQL Server
2000 included the DROP_EXISTING clause so that the clustering key could be changed and the nonclustered indexes would need to be
rebuilt only once (to use the new clustering key).

However, the bad news is that the DROP_EXISTING clause can be used to change only indexes that aren’t enforcing a primary key or unique
key constraint (i.e., only indexes created using a CREATE INDEX statement). And, in many cases, when GUIDs are used as the primary key,
the primary key constraint definition might have been created without specifying the index type. When the index type isn’t specified, SQL
Server defaults to creating a clustered index to enforce the primary key. You can choose to enforce the primary key with a nonclustered index
by explicitly stating the index type at definition, but the default index type is a clustered index if one doesn’t already exist. (Note that if a
clustered index already exists and the index type isn’t specified, SQL Server will still allow the primary key to be created; it will be enforced
using a nonclustered index.)

Clustering on a key such as a GUID can result in a lot of fragmentation. However, the level of fragmentation also depends on how the GUIDs
are being generated. Often, GUIDs are generated at the client or using a function (either the newid() function or
the newsequentialid() function) at the server. Using the client or the newid() function to generate GUIDs creates random inserts in the
structure that’s now ordered by these GUIDs—because it’s the clustering key. As a result of the performance problems caused by the
fragmentation, you might want to change your clustering key or even just change the function (if it’s server side). If the GUID is being
generated using a DEFAULT constraint, then you might have the option to change the function behind the constraint from
the newid() function to the newsequentialid() function. Although the newsequentialid()function doesn’t guarantee perfect contiguity or a
gap-free sequence, it generally creates values greater than any prior generated. (Note that there are cases when the base value that’s used is
regenerated. For example, if the server is restarted, a new starting value, which might be lower than the current value, will be generated.)
Even with these exceptions, the fragmentation within this clustered index will be drastically reduced.

Related: Leverage Clustered Indexes to Avoid Bookmark Lookups

So, if you still want to change the definition of the clustered index and the clustered index is being used to enforce your table’s primary key,
it’s not going to be a simple process. And, this process should be done when users aren’t allowed to connect the database, otherwise data
integrity problems can occur. Additionally, if you’re changing the clustering key to use a different column(s), then you’ll also need to
remember to recreate your primary key to be enforced by a nonclustered index instead. Here’s the process to follow to change the definition of
a clustered index:

1. Disable all the table’s nonclustered indexes so that they aren’t automatically rebuilt when the clustered index is dropped in step 3. Because this is
likely to be a one-time operation, use the query in Listing 1 (with the desired table name) to generate the ALTER INDEX statements.

Listing 1: Code to Generate the ALTER INDEX Statements


SELECT
DISABLE_STATEMENT =
N'ALTER INDEX '
+ QUOTENAME(si.[name], N']')
+ N' ON '
+ QUOTENAME(sch.[name], N']')
+ N'.'
+ QUOTENAME(OBJECT_NAME(so.[object_id]), N']')
+ N' DISABLE'
, ENABLE_STATEMENT =
N'ALTER INDEX '
+ QUOTENAME(si.[name], N']')
+ N' ON '
+ QUOTENAME(sch.[name], N']')
+ N'.'
+ QUOTENAME(OBJECT_NAME(so.[object_id]), N']')
+ N' REBUILD'
FROM sys.indexes AS si
JOIN sys.objects AS so
ON si.[object_id] = so.[object_id]
JOIN sys.schemas AS sch
ON so.[schema_id] = sch.[schema_id]
WHERE si.[object_id] = object_id('tablename')
AND si.[index_id] > 1

Note that you should use the column for DISABLE_STATEMENTS to disable the nonclustered indexes, and be sure to keep the enable information handy
because you’ll need it to rebuild the nonclustered indexes after you’ve created the new clustered index.
2. Disable any foreign key constraints. This is where you want to be careful if there are users using the database. In addition, this is also where you
might want to use the following query to change the database to be restricted to only DBO use:

ALTER DATABASE DatabaseName


SET RESTRICTED_USER
WITH ROLLBACK AFTER 5

The ROLLBACK AFTER n clause at the end of the ALTER DATABASE statement lets you terminate user connections and put the database
into a restricted state for modifications. As for automating the disabling of foreign key constraints, I leveraged some of the code
from sp_fkeys and significantly altered it to generate the DISABLE command (similarly to how we did this in step 1 for disabling nonclustered
indexes), which Listing 2 shows.

Listing 2: Code to Generate the DISABLE Command


SELECT
DISABLE_STATEMENT =
N'ALTER TABLE '
+ QUOTENAME(convert(sysname, schema_name(o2.schema_id)), N']')
+ N'.'
+ QUOTENAME(convert(sysname, o2.name), N']')
+ N' NOCHECK CONSTRAINT '
+ QUOTENAME(convert(sysname, object_name(f.object_id)), N']')
, ENABLE_STATEMENT =
N'ALTER TABLE '
+ QUOTENAME(convert(sysname, schema_name(o2.schema_id)), N']')
+ N'.'
+ QUOTENAME(convert(sysname, o2.name), N']')
+ N' WITH CHECK CHECK CONSTRAINT '
+ QUOTENAME(convert(sysname, object_name(f.object_id)), N']')
, RECHECK_CONSTRAINT =
N'SELECT OBJECTPROPERTY(OBJECT_ID('
+ QUOTENAME(convert(sysname, object_name(f.object_id)), N'''')
+ N'), ''CnstIsNotTrusted'')'
FROM
sys.objects AS o1,
sys.objects AS o2,
sys.columns AS c1,
sys.columns AS c2,
sys.foreign_keys AS f
INNER JOIN sys.foreign_key_columns AS k
ON (k.constraint_object_id = f.object_id)
INNER JOIN sys.indexes AS i
ON (f.referenced_object_id = i.object_id
AND f.key_index_id = i.index_id)
WHERE
o1.[object_id] = object_id('tablename')
AND i.name = 'Primary key Name'
AND o1.[object_id] = f.referenced_object_id
AND o2.[object_id] = f.parent_object_id
AND c1.[object_id] = f.referenced_object_id
AND c2.[object_id] = f.parent_object_id
AND c1.column_id = k.referenced_column_id
AND c2.column_id = k.parent_column_id
ORDER BY 1, 2, 3

Use the column for DISABLE_STATEMENTS to disable the foreign key constraints, and keep the remaining information handy because you’ll
need it to reenable and recheck the data, as well as verify the foreign key constraints after you’ve recreated the primary key as a unique
nonclustered index.

1. Drop the constraint-based clustered index using the following query:


2.
3. ALTER TABLE schema.tablename
4. DROP CONSTRAINT ConstraintName
5. Create the new clustered index. The new clustered index can be constraint-based or a regular CREATE INDEX statement. However, the clustering
key (the key definition that defines the clustered index) should be unique, narrow, static, and ever-increasing. And although we’ve started to discuss some
aspects of how to choose a good clustering key, this is an incredibly difficult discussion to have in one article. To learn more, check out my posts about the
clustering key.

6. Create the primary key as a constraint-based nonclustered index. Because nonclustered indexes use the clustering key, you should always create
nonclustered indexes after creating the clustered index, as the following statement shows:

7.
8. ALTER TABLE schema.tablename
9. ADD CONSTRAINT ConstraintName
10. PRIMARY KEY NONCLUSTERED (key definition)
11.
12. Recreate the foreign key constraints. First, use the ENABLE_STATEMENT generated in step 2 to re-enable and recheck all of the foreign keys. In
this case, you’ll want to make sure to recheck the data as well using the WITH CHECK clause. However, this is likely to be a one-time thing, so as long as
you have kept the information from step 2, you should be able to recreate the foreign key constraints relatively easily.

13. Once completed, make sure that all of the constraints are considered “trusted” by using the RECHECK_CONSTRAINT statements that were
generated in step 2.

14. Rebuild all of the nonclustered indexes (this is how you enable them again). Use the ENABLE_STATEMENT created in step 1. Rebuilding a
nonclustered index is the only way to enable them.

SQL Server 2014 CTP1 introduces extensions to online operation options that will good news for companies hosting very large databases that require little to no
downtime.

To set the context, imagine that you’re using SQL Server 2012 Enterprise Edition for the online index management and index partitioning features and you attempt the
following index rebuild on a partitioned table:
ALTER INDEX [PK_FactInternetSales_SalesOrderNumber_SalesOrderLineNumber]
ON [dbo].[FactInternetSales]
REBUILD PARTITION = ALL
WITH (ONLINE= ON);

Testing this in SQL Server 2012, we are able to rebuild all partitions online without error. But what if we want to specify a specific partition instead of all partitions?

ALTER INDEX [PK_FactInternetSales_SalesOrderNumber_SalesOrderLineNumber]


ON [dbo].[FactInternetSales]
REBUILD PARTITION = 1
WITH (ONLINE= ON);

Attempting this in SQL Server 2012 or earlier, you’ll see the following error message:

Msg 155, Level 15, State 1, Line 4


'ONLINE' is not a recognized ALTER INDEX REBUILD PARTITION option.

But starting with SQL Server 2014 (as of CTP1), online single partition index operations are now supported. And this is certainly a big deal for very large table
maintenance scenarios where you would prefer, or indeed must break your overall maintenance into smaller pieces over a period of time. You may also want to do
partition-level maintenance for only those partitions that actually require it – for example, those partitions that actually exceed a specific fragmentation level.

To test out this SQL Server 2014 CTP1 functionality I used the AdventureWorksDW2012 with a version of FactInternetSales that contains 61,847,552 rows, and
partitioned by the ShipDate column.

Rebuilding all partitions online for the table using PARTITION = ALL in my test environment took 3 minutes and 23 seconds. Regarding overall duration, my tests were
for indexes that weren’t all that fragmented, so the 3 minutes and 23 second duration represents an average duration over a few tests. Also keep in mind that I didn’t
have competing workloads running at the time, so the online rebuild is happening without having to compete with other significant workloads against the index in
question.
The query execution plan shape for the online index rebuild using PARTITION = ALL was as follows:

Execution plan for online rebuild of all partitions


Notice that the operations are parallel-enabled except for the Constant Scan operator. In the query execution plan you can see 39 rows in the outer reference Constant
Scan that are being passed to the Distribute Streams operator and then driving the Nested Loop.

The significance of the 39 rows? The following query validates the maximum partition number count from sys.dm_db_partition_stats. For my test environment, the
result was 39 for the maximum partition number, matching what I saw for the Constant Scan actual rows:
SELECT MAX([partition_number]) AS [max_partition_number]
FROM [sys].[dm_db_partition_stats]
WHERE [object_id] = OBJECT_ID('FactInternetSales');

Now you’ll also notice the Online Index Insert operator in the previous plan. Removing the ONLINE = ON option from my ALTER INDEX REBUILD (making it an offline
operation), and keeping the PARTITION = ALL option, the only change was having an “Index Insert” operator instead of an “Online Index Insert” in the query execution
plan – and also a reduction in duration, where my test showed a 1 minute and 9 seconds execution duration compared to the online 3 minutes and 23 seconds.
I then tested an online rebuild of one partition with 5,678,080 rows in it instead (remember the total table row count is 61,847,552 rows). For this test, the overall
duration took exactly 1 minute and had the following query execution plan shape:

Execution plan for online rebuild of a single partition


The first observation is that this is a serial plan. Also note that I said I picked one partition out of the original 39, although that particular partition did represent ~ 9% of
rows in the table overall. Also notice that the Constant Scan shows 1 row instead of 39, as I would expect.

What about the duration of a single partition, offline rebuild? In my test environment, this took 11 seconds compared to the online rebuild 1 minute. The query
execution plan shape for the offline rebuild of a single partition was as follows:
Execution plan for offline rebuild of a single partition
Notice there is no Constant Scan or associated Nested Loops process and also notice that this plan now has parallel operators in it vs. the previous serial plan, even
though they are both doing a Clustered Index Scan for 5,678,080 rows. Also doing a keyword search of “partition” in the XML plan text for the single partition offline
parallel index operation didn’t result in any matches – compared to the serial plan, online single partition index operation which had Partitioned = “true” for the
Clustered Index Scan and Online Index Insert physical operators.

Back to the main exploration…

Can I pick a few, but not all partitions in a single execution? Unfortunately not.

The ALTER INDEX and ALTER TABLE commands have the PARTITION = ALL argument and then PARTITION = <partition number> argument, but not the ability to list
multiple partitions for a single rebuild operation. I’m not complaining too loudly about this though, as I’m happy to have the ability to rebuild a single partition online
and it isn’t terribly complicated to execute the operation once for each rebuild, however the cumulative impact to duration was something I wanted to explore further.
How long would it take to rebuild all 39 partitions separately and online versus the PARTITION = ALL duration of 3 minutes and 23 seconds?
We know that a benefit of online rebuilds is the ability to still access the associated table or index during the index operation. But in exchange for that online operation,
we’ll lose the performance edge of the rebuild compared to an offline rebuild. And what I wanted to know next was how a one-by-one partition online rebuild would
perform versus the PARTITION = ALL alternative.
Executing 39 separate rebuild operations (one rebuild for each unique partition), the total execution duration was 9 minutes and 54 seconds compared to the PARTITION
= ALL which took 3 minutes and 23 seconds, so clearly the piecemeal approach is cumulatively not as fast as an online rebuild of all partitions in one statement. While I
was able to do one partition at a time, the over-arching benefit is the ability to break apart our maintenance activities over time and keep access to the objects as they
are being rebuilt, but if you’re looking for a shorter rebuild window, offline options are still the fastest, followed by online for PARTITION = ALL and then in last place,
doing one partition at a time.
The following table recaps the duration comparisons – and again, these tests were based on SQL Server 2014 CTP1 and a very specific table size and VM guest
configuration, so pay more attention to the relative durations across tests rather than the durations themselves:

Test Description Duration


Offline rebuild of all partitions 1:09
Online rebuild of all partitions 3:23
Online rebuild of one partition 1:00
Offline rebuild of one partition 0:11
Online rebuild of all partitions, one partition at a time 9:54

Now there are other aspects to explore on this subject as well. Just because an operation is online doesn’t mean that there aren’t a few moments (or longer) where locks
are still be held on the targeted object. Index operations still have locking behavior for online operations – and SQL Server 2014 has provided options for this as well
which I’ll explore in a separate post.