Automate INDEX rebuild based on fragmentation results? - sql

Is it possible to add a maintenance job to check indexes fragmentation. If greater than 50% then rebuild those indexes automatically ?
Index size can vary from 100MB to 10GB.
SQL 2005.
Thank you.

I use this script . Please note I would advise you reading up about the dmv I am using here they are a hidden gem in SQL2005+.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
CREATE TABLE #FragmentedIndexes
(
DatabaseName SYSNAME
, SchemaName SYSNAME
, TableName SYSNAME
, IndexName SYSNAME
, [Fragmentation%] FLOAT
)
INSERT INTO #FragmentedIndexes
SELECT
DB_NAME(DB_ID()) AS DatabaseName
, ss.name AS SchemaName
, OBJECT_NAME (s.object_id) AS TableName
, i.name AS IndexName
, s.avg_fragmentation_in_percent AS [Fragmentation%]
FROM sys.dm_db_index_physical_stats(db_id(),NULL, NULL, NULL, 'SAMPLED') s
INNER JOIN sys.indexes i ON s.[object_id] = i.[object_id]
AND s.index_id = i.index_id
INNER JOIN sys.objects o ON s.object_id = o.object_id
INNER JOIN sys.schemas ss ON ss.[schema_id] = o.[schema_id]
WHERE s.database_id = DB_ID()
AND i.index_id != 0
AND s.record_count > 0
AND o.is_ms_shipped = 0
DECLARE #RebuildIndexesSQL NVARCHAR(MAX)
SET #RebuildIndexesSQL = ''
SELECT
#RebuildIndexesSQL = #RebuildIndexesSQL +
CASE
WHEN [Fragmentation%] > 30
THEN CHAR(10) + 'ALTER INDEX ' + QUOTENAME(IndexName) + ' ON '
+ QUOTENAME(SchemaName) + '.'
+ QUOTENAME(TableName) + ' REBUILD;'
WHEN [Fragmentation%] > 10
THEN CHAR(10) + 'ALTER INDEX ' + QUOTENAME(IndexName) + ' ON '
+ QUOTENAME(SchemaName) + '.'
+ QUOTENAME(TableName) + ' REORGANIZE;'
END
FROM #FragmentedIndexes
WHERE [Fragmentation%] > 10
DECLARE #StartOffset INT
DECLARE #Length INT
SET #StartOffset = 0
SET #Length = 4000
WHILE (#StartOffset < LEN(#RebuildIndexesSQL))
BEGIN
PRINT SUBSTRING(#RebuildIndexesSQL, #StartOffset, #Length)
SET #StartOffset = #StartOffset + #Length
END
PRINT SUBSTRING(#RebuildIndexesSQL, #StartOffset, #Length)
EXECUTE sp_executesql #RebuildIndexesSQL
DROP TABLE #FragmentedIndexes
Also keep in mind that this script can run a while and block access to your tables. Unless you have Enterprise editions SQL can LOCK the table when rebuilding the index. This will block all queries to that table using the index till the index defrag is finished. Thus it is not advised to run index rebuild during operational hours only during maintenance windows. If you are running enterprise edition you can use the ONLINE=ON option to defrag indexes online. This will use more space but your tables wont be blocked/locked during the defrag operation.
Shout if you need more information.
UPDATED:
If you are running this query on a smaller database you can probably use the 'DETAILED' parameter in the call to sys.dm_db_index_physical_stats. This is probably a more detailed examination of the indexes. The discussion in the comments will also point out that on much larger tables it is probably worth doing a SAMPLED scan as this will help reduce the time needed to do the index scan.

In case, you were thinking of avoiding to create any temp tables and parsing the string to create a list of SQL strings. Here is an efficient way of accomplishing it:
USE databasename
GO
DECLARE #Queryresult NVARCHAR(4000)
SET #Queryresult=''
SELECT
#Queryresult=#Queryresult + 'ALTER INDEX ' + QUOTENAME(i.name) + ' ON '
+ QUOTENAME('dbo') + '.'
+ QUOTENAME(OBJECT_NAME(i.OBJECT_ID)) + ' REBUILD;'
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') ss
INNER JOIN sys.indexes i ON i.OBJECT_ID = ss.OBJECT_ID AND i.index_id = ss.index_id
INNER JOIN sys.objects o ON ss.object_id = o.object_id
WHERE ss.avg_fragmentation_in_percent > 50
AND ss.record_count > 0
AND o.is_ms_shipped = 0 --Excludes any objects created as a part of SQL Server installation
AND ss.index_id > 0 --Excludes heap indexes
EXEC sp_executesql #Queryresult

yes, you can.
You can get the fragmented indexes using this query:
SELECT OBJECT_NAME(i.OBJECT_ID) AS TableName,
i.name AS IndexName,
indexstats.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') indexstats
INNER JOIN sys.indexes i ON i.OBJECT_ID = indexstats.OBJECT_ID
AND i.index_id = indexstats.index_id
WHERE indexstats.avg_fragmentation_in_percent > 20
and based on the result just build a command to recreate them
I would wrap everything on a Stored Procedure and call it from a SQL Server Job
FYI, 50% is a very big fragmentation. I would go with less.

You can use sys.dm_db_index_physical_stats to get information about your index fragmentation (see the avg_fragmentation_in_percent column). Then you can do an alter index with the rebuild clause whenever your threshold is reached.

Related

Rebuild or Reorganize indexes

I want to reorganize or rebuild indexes. I know the sql to perform this action on all indexes in a table is
alter index all on table_name reorganize;
But I only want to rebuild or reorganize if fragmentation percentage on each index is between a certain range. Is this possible to do?
Here is a procedure what I wrote
CREATE PROCEDURE dbo.ixRebuild #fillfactor int = 100, #Force bit = 0, #Schema varchar(255) = NULL, #Table varchar(255) = NULL, #PkOnly bit = 0
AS
/*
* ixRebuild
* Rebuild all indices in a database.
* Indices with >30% fragmentation are rebuilt.
* Indices with >6% fragmentation are just reorganised.
*
* The default fill factor is 100%.
*
* Required permissions are:
* GRANT VIEW DATABASE STATE TO <user>
* GRANT ALTER TO <user>
* GRANT EXEC ON ixRebuild TO <user>
*
* Created 17/9/08 by rwb.
*/
BEGIN
DECLARE #db int
DECLARE #tab varchar(256)
DECLARE #ix int
DECLARE #ixName varchar(256)
DECLARE #frag float
DECLARE #cmd varchar(1024)
DECLARE #type int
DECLARE c CURSOR FAST_FORWARD FOR
SELECT DISTINCT s.database_id, s.index_id, i.name,
Convert(float, s.avg_fragmentation_in_percent),
ss.name + '.' + t.name AS tableName,
i.type
FROM sys.dm_db_index_physical_stats(Db_Id(), NULL, NULL, NULL, NULL) s
INNER JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id
INNER JOIN sys.tables t ON i.object_id = t.object_id
INNER JOIN sys.schemas ss ON t.schema_id = ss.schema_id
WHERE (#Schema IS NULL OR ss.name = #Schema)
AND (#Table IS NULL OR t.name = #Table)
AND (#PkOnly = 0 OR i.is_primary_key = 1)
AND (
#Force = 1
OR (
avg_fragmentation_in_percent > 6
AND page_count > 100 -- rebuilding small indices does nothing
AND i.name IS NOT NULL -- for tables with no PK
)
)
-- DISTINCT because ys.dm_db_index_physical_stats
-- contains a row for each part of a partitioned index on a partitioned table.
OPEN c
FETCH NEXT FROM c INTO #db, #ix, #ixName, #frag, #tab, #type
WHILE ##Fetch_Status = 0
BEGIN
PRINT Db_Name( #db ) + ' / ' + #tab + ' / ' + #ixName + ' ' + Cast(#frag as varchar(16))
SET #cmd = ''
IF #frag < 10.0 AND #Force = 0
BEGIN
SET #cmd = 'ALTER INDEX ' + #ixName + ' ON ' + #tab + ' REORGANIZE'
END
ELSE
BEGIN
SET #cmd = 'ALTER INDEX ' + #ixName + ' ON ' + #tab +
CASE
WHEN #type IN (1, 2) THEN ' REBUILD WITH (FILLFACTOR = ' + Cast(#fillfactor AS varchar(4)) + ')'
ELSE ''
END
END
RAISERROR(#cmd, 0, 1) WITH NOWAIT;
EXEC (#cmd)
FETCH NEXT FROM c INTO #db, #ix, #ixName, #frag, #tab, #type
END
CLOSE c
DEALLOCATE c
END
Generally rebuilding all indexes of a table is a bad idea, because some can have no fragmentation, while some other can be horribly bloated !
Rebuiliding index is a heavy process and is blocking the access of the table the time to do so, except in ONLINE mode available only with the Enterprise version.
So you need to rebuild only under certains circumstances... Authors (which I think due to my old age in RDBMS, I am a part of them...) agree to say that small tables don't care and a percentage of fragmentation under 30 does not matter.
I alway add some complexity over these ideas, with another point : "big" rows of table or indexes (1600 bytes is a max for an index) will appear always to be fragmented, but are not... This because evaluating a frag percent take in account a percent of free space on a page and with a 8 Kb page, a natural no-recoverable space will stay inside the page (20% max for an index and 49 % max for a table).
So a good practice is to mix all those considerations to build a proper code to rebuild or defrag all objects includes in your database.
As an example, a short code to do so, can be :
DECLARE #SQL NVARCHAR(max) = N'';
SELECT #SQL = #SQL + CASE WHEN i.name IS NULL
THEN N'ALTER TABLE [' + s.name + '].[' + o.name + '] REBUILD;'
WHEN avg_fragmentation_in_percent > 30
THEN N'ALTER INDEX [' + i.name + '] ON [' + s.name + '].[' + o.name + '] REBUILD;'
ELSE N'ALTER INDEX [' + i.name + '] ON [' + s.name + '].[' + o.name + '] REORGANIZE;'
END
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) AS ips
JOIN sys.objects AS o ON ips.object_id = o.object_id
JOIN sys.schemas AS s ON o.schema_id = s.schema_id
JOIN sys.indexes AS i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.page_count > 64 AND avg_fragmentation_in_percent > 10;
EXEC (#SQL);

Diff / Delta script: ideas on streamlining it?

I'm pulling data from a remote DB into a local MS SQL Server DB, the criteria being whether the PK is higher than I have in my data warehouse or whether the edit date is within a range that I provide with an argument. That works super fast so I am happy with it. However, when I attempt to sync this delta table into my data warehouse it takes quite a long time.
Here's my SPROC:
ALTER PROCEDURE [dbo].[sp_Sync_Delta_Table]
#tableName varchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql as varchar(4000)
-- Delete rows in MIRROR database where ID exists in the DELTA database
SET #sql = 'Delete from [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] Where [ID] in (Select [ID] from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + '])'
EXEC(#sql)
-- Insert all deltas
SET #sql = 'Insert Into [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] Select * from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + ']'
EXEC(#sql)
END
It works, but I think it takes way too long. For example: inserting 3590 records from the DELTA table into the MIRROR table containing 3,600,761 took over 25 minutes.
Can anyone give me a hint on how I can make this job easier on SSMS? I'm using 2008 R2, btw.
Thanks again!
Nate
The issue is likely the time required to do a table scan on the 3,600,761 to see if the new records are unique.
First of all, let's confirm that the primary key (ID) on the target table is the clustered index and increasing.
SELECT s.name, o.name, i.name, i.type_desc, ic.key_ordinal, c.name
FROM sys.objects o
JOIN sys.columns c ON (c.object_id = o.object_id)
JOIN sys.schemas s ON (s.schema_id = o.schema_id)
JOIN sys.indexes i ON (i.object_id = o.object_id)
JOIN sys.index_columns ic ON (ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.column_id = c.column_id)
WHERE o.name = '[table_name]'
If the index is not an ascending integer, it is possible that the inserts are causing lots of page splits.
Second, what other objects does that insert affect. Are there triggers, materialized views, or non-clustered indexes?
Third, do you have
My suggestion would be to stage the data on the mirror server in a local table. It can be as simple as as:
SET #sql = 'SELECT INTO * [MIRROR].[dbo].[' + #tableName + '_Staging] from [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + ']'
EXEC(#sql)
After that add a clustered primary key to the table.
SET #sql = 'ALTER TABLE [MIRROR].[dbo].[' + #tableName + '_Staging] ADD CONSTRAINT [PK_' + #tableName + '] PRIMARY KEY CLUSTERED (Id ASC)'
EXEC(#sql)
At this point, try inserting the data into the real table. The optimizer should be much more helpful this time.
Change the delete portion to:
SET #sql = 'Delete tbl1 from [SERVER-WHS].[MIRROR].[dbo].[' + #tableName
+ '] tbl1 inner join [SERVER-DELTA].[DELTAS].[dbo].[' + #tableName + '] tbl2 on tbl1.[ID] = tbl2.[ID]'
In future use INNER JOIN instead of IN with Sub Query.

How to fetch the row count for all tables in a SQL SERVER database [duplicate]

This question already has answers here:
Query to list number of records in each table in a database
(23 answers)
Closed 8 years ago.
I am searching for a SQL Script that can be used to determine if there is any data (i.e. row count) in any of the tables of a given database.
The idea is to re-incarnate the database in case there are any rows existing (in any of the database).
The database being spoken of is Microsoft SQL SERVER.
Could someone suggest a sample script?
The following SQL will get you the row count of all tables in a database:
CREATE TABLE #counts
(
table_name varchar(255),
row_count int
)
EXEC sp_MSForEachTable #command1='INSERT #counts (table_name, row_count) SELECT ''?'', COUNT(*) FROM ?'
SELECT table_name, row_count FROM #counts ORDER BY table_name, row_count DESC
DROP TABLE #counts
The output will be a list of tables and their row counts.
If you just want the total row count across the whole database, appending:
SELECT SUM(row_count) AS total_row_count FROM #counts
will get you a single value for the total number of rows in the whole database.
If you want to by pass the time and resources it takes to count(*) your 3million row tables. Try this per SQL SERVER Central by Kendal Van Dyke.
Row Counts Using sysindexes
If you're using SQL 2000 you'll need to use sysindexes like so:
-- Shows all user tables and row counts for the current database
-- Remove OBJECTPROPERTY function call to include system objects
SELECT o.NAME,
i.rowcnt
FROM sysindexes AS i
INNER JOIN sysobjects AS o ON i.id = o.id
WHERE i.indid < 2 AND OBJECTPROPERTY(o.id, 'IsMSShipped') = 0
ORDER BY o.NAME
If you're using SQL 2005 or 2008 querying sysindexes will still work but Microsoft advises that sysindexes may be removed in a future version of SQL Server so as a good practice you should use the DMVs instead, like so:
-- Shows all user tables and row counts for the current database
-- Remove is_ms_shipped = 0 check to include system objects
-- i.index_id < 2 indicates clustered index (1) or hash table (0)
SELECT o.name,
ddps.row_count
FROM sys.indexes AS i
INNER JOIN sys.objects AS o ON i.OBJECT_ID = o.OBJECT_ID
INNER JOIN sys.dm_db_partition_stats AS ddps ON i.OBJECT_ID = ddps.OBJECT_ID
AND i.index_id = ddps.index_id
WHERE i.index_id < 2 AND o.is_ms_shipped = 0 ORDER BY o.NAME
Works on Azure, doesn't require stored procs.
SELECT t.name AS table_name
,s.row_count AS row_count
FROM sys.tables t
JOIN sys.dm_db_partition_stats s
ON t.OBJECT_ID = s.OBJECT_ID
AND t.type_desc = 'USER_TABLE'
AND t.name NOT LIKE '%dss%' --Exclude tables created by SQL Data Sync for Azure.
AND s.index_id IN (0, 1)
ORDER BY table_name;
Credit.
This one looks better than the others I think.
USE [enter your db name here]
GO
SELECT SCHEMA_NAME(A.schema_id) + '.' +
--A.Name, SUM(B.rows) AS 'RowCount' Use AVG instead of SUM
A.Name, AVG(B.rows) AS 'RowCount'
FROM sys.objects A
INNER JOIN sys.partitions B ON A.object_id = B.object_id
WHERE A.type = 'U'
GROUP BY A.schema_id, A.Name
GO
Short and sweet
sp_MSForEachTable 'DECLARE #t AS VARCHAR(MAX);
SELECT #t = CAST(COUNT(1) as VARCHAR(MAX))
+ CHAR(9) + CHAR(9) + ''?'' FROM ? ; PRINT #t'
Output:
SELECT
sc.name +'.'+ ta.name TableName, SUM(pa.rows) RowCnt
FROM
sys.tables ta
INNER JOIN sys.partitions pa
ON pa.OBJECT_ID = ta.OBJECT_ID
INNER JOIN sys.schemas sc
ON ta.schema_id = sc.schema_id
WHERE ta.is_ms_shipped = 0 AND pa.index_id IN (1,0)
GROUP BY sc.name,ta.name
ORDER BY SUM(pa.rows) DESC
SQL Server 2005 or later gives quite a nice report showing table sizes - including row counts etc. It's in Standard Reports - and it is Disc Usage by Table.
Programmatically, there's a nice solution at:
http://www.sqlservercentral.com/articles/T-SQL/67624/
Don't use SELECT COUNT(*) FROM TABLENAME, since that is a resource intensive operation. One should use SQL Server Dynamic Management Views or System Catalogs to get the row count information for all tables in a database.
I would make a minor change to Frederik's solution. I would use the sp_spaceused system stored procedure which will also include data and index sizes.
declare c_tables cursor fast_forward for
select table_name from information_schema.tables
open c_tables
declare #tablename varchar(255)
declare #stmt nvarchar(2000)
declare #rowcount int
fetch next from c_tables into #tablename
while ##fetch_status = 0
begin
select #stmt = 'sp_spaceused ' + #tablename
exec sp_executesql #stmt
fetch next from c_tables into #tablename
end
close c_tables
deallocate c_tables
Here's a dynamic SQL approach that also gives you the schema as well:
DECLARE #sql nvarchar(MAX)
SELECT
#sql = COALESCE(#sql + ' UNION ALL ', '') +
'SELECT
''' + s.name + ''' AS ''Schema'',
''' + t.name + ''' AS ''Table'',
COUNT(*) AS Count
FROM ' + QUOTENAME(s.name) + '.' + QUOTENAME(t.name)
FROM sys.schemas s
INNER JOIN sys.tables t ON t.schema_id = s.schema_id
ORDER BY
s.name,
t.name
EXEC(#sql)
If needed, it would be trivial to extend this to run over all databases in the instance (join to sys.databases).
select all rows from the information_schema.tables view, and issue a count(*) statement for each entry that has been returned from that view.
declare c_tables cursor fast_forward for
select table_name from information_schema.tables
open c_tables
declare #tablename varchar(255)
declare #stmt nvarchar(2000)
declare #rowcount int
fetch next from c_tables into #tablename
while ##fetch_status = 0
begin
select #stmt = 'select #rowcount = count(*) from ' + #tablename
exec sp_executesql #stmt, N'#rowcount int output', #rowcount=#rowcount OUTPUT
print N'table: ' + #tablename + ' has ' + convert(nvarchar(1000),#rowcount) + ' rows'
fetch next from c_tables into #tablename
end
close c_tables
deallocate c_tables
This is my favorite solution for SQL 2008 , which puts the results into a "TEST" temp table that I can use to sort and get the results that I need :
SET NOCOUNT ON
DBCC UPDATEUSAGE(0)
DROP TABLE #t;
CREATE TABLE #t
(
[name] NVARCHAR(128),
[rows] CHAR(11),
reserved VARCHAR(18),
data VARCHAR(18),
index_size VARCHAR(18),
unused VARCHAR(18)
) ;
INSERT #t EXEC sp_msForEachTable 'EXEC sp_spaceused ''?'''
SELECT * INTO TEST FROM #t;
DROP TABLE #t;
SELECT name, [rows], reserved, data, index_size, unused FROM TEST \
WHERE ([rows] > 0) AND (name LIKE 'XXX%')
SELECT
SUM(sdmvPTNS.row_count) AS [DBRows]
FROM
sys.objects AS sOBJ
INNER JOIN sys.dm_db_partition_stats AS sdmvPTNS
ON sOBJ.object_id = sdmvPTNS.object_id
WHERE
sOBJ.type = 'U'
AND sOBJ.is_ms_shipped = 0
AND sdmvPTNS.index_id < 2
GO

How can I drop all indexes in a SQL database with one command?

So, how can I drop all indexes in a SQL database with one command? I have this command that will get me all the 20 or so drop statements, but how can I run all of those drop statements from this "result set"?
select * from vw_drop_idnex;
Another variation that gives me the same list is:
SELECT 'DROP INDEX ' + ix.Name + ' ON ' + OBJECT_NAME(ID) AS QUERYLIST
FROM sysindexes ix
WHERE ix.Name IS NOT null and ix.Name like '%pre_%'
I tried to do "exec(select cmd from vw_drop_idnex)" and it didn't work. I am looking for something that works like a for loop and runs the queries one by one.
-----------------------
With Rob Farleys help, final draft of the script is:
declare #ltr nvarchar(1024);
SELECT #ltr = ( select 'alter table '+o.name+' drop constraint '+i.name+';'
from sys.indexes i join sys.objects o on i.object_id=o.object_id
where o.type<>'S' and is_primary_key=1
FOR xml path('') );
exec sp_executesql #ltr;
declare #qry nvarchar(1024);
select #qry = (select 'drop index '+o.name+'.'+i.name+';'
from sys.indexes i join sys.objects o on i.object_id=o.object_id
where o.type<>'S' and is_primary_key<>1 and index_id>0
for xml path(''));
exec sp_executesql #qry
You're very close.
declare #qry nvarchar(max);
select #qry =
(SELECT 'DROP INDEX ' + quotename(ix.name) + ' ON ' + quotename(object_schema_name(object_id)) + '.' + quotename(OBJECT_NAME(object_id)) + '; '
FROM sys.indexes ix
WHERE ix.Name IS NOT null and ix.Name like '%prefix_%'
for xml path(''));
exec sp_executesql #qry
this worked for me
we skip sys indexes and for constraints
declare #qry nvarchar(max);
select #qry = (
select 'IF EXISTS(SELECT * FROM sys.indexes WHERE name='''+ i.name +''' AND object_id = OBJECT_ID(''['+s.name+'].['+o.name+']'')) drop index ['+i.name+'] ON ['+s.name+'].['+o.name+']; '
from sys.indexes i
inner join sys.objects o on i.object_id=o.object_id
inner join sys.schemas s on o.schema_id = s.schema_id
where o.type<>'S' and is_primary_key<>1 and index_id>0
and s.name!='sys' and s.name!='sys' and is_unique_constraint=0
for xml path(''));
exec sp_executesql #qry
From: Stephen Hill's Bloggie
DECLARE #indexName VARCHAR(128)
DECLARE #tableName VARCHAR(128)
DECLARE [indexes] CURSOR FOR
SELECT [sysindexes].[name] AS [Index],
[sysobjects].[name] AS [Table]
FROM [sysindexes]
INNER JOIN [sysobjects]
ON [sysindexes].[id] = [sysobjects].[id]
WHERE [sysindexes].[name] IS NOT NULL
AND [sysobjects].[type] = 'U'
--AND [sysindexes].[indid] > 1
OPEN [indexes]
FETCH NEXT FROM [indexes] INTO #indexName, #tableName
WHILE ##FETCH_STATUS = 0
BEGIN
--PRINT 'DROP INDEX [' + #indexName + '] ON [' + #tableName + ']'
Exec ('DROP INDEX [' + #indexName + '] ON [' + #tableName + ']')
FETCH NEXT FROM [indexes] INTO #indexName, #tableName
END
CLOSE [indexes]
DEALLOCATE [indexes]
GO
None of the answers quite suited my needs.
I needed one that will also drop indexes that backup unique or primary constraints (except if these can't be dropped as they back up a foreign key)
DECLARE #SqlScript NVARCHAR(MAX);
SELECT #SqlScript =
(
SELECT
'
BEGIN TRY
'+ CASE WHEN 1 IN (i.is_primary_key, i.is_unique_constraint) THEN
'
ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id)) + '.' + QUOTENAME(t.name) + ' DROP CONSTRAINT ' + QUOTENAME(i.name) + ';'
else
'
DROP INDEX ' + QUOTENAME(i.name) + ' ON ' + QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id)) + '.' + QUOTENAME(t.name)
END+'
END TRY
BEGIN CATCH
RAISERROR(''Could not drop %s on table %s'', 0,1, ' + QUOTENAME(i.name, '''') + ', ' + QUOTENAME(t.name, '''') + ')
END CATCH
'
FROM sys.indexes i
JOIN sys.tables t ON i.object_id = t.object_id
WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED' )
ORDER BY t.object_id, i.index_id DESC
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)');
--Return script that will be run
SELECT #SqlScript AS [processing-instruction(x)]
FOR XML PATH('');
EXEC (#SqlScript);
Minor improvements to the accepted answer that I had to make in my own case, mostly to account for schemas:
declare #qry nvarchar(4000);
select #qry = (select 'drop index ['+s.name+'].['+o.name+'].['+i.name+'];'
from sys.indexes i join sys.objects o on i.object_id=o.object_id join sys.schemas s on o.schema_id=s.schema_id
where o.type<>'S' and is_primary_key<>1 and index_id>0 and s.name<>'sys'
for xml path(''));
exec sp_executesql #qry
Also: In my case it couldn't run in one go because the script becomes longer than 4000 characters. The only way I could think of to deal with that was to put a "top 20" on the inner select and execute it multiple times.
The "Final Draft" that OP posts as part of his question errors out if there are already zero indexes on any table in the DB. I needed to fix that.
Also, I wanted more control over the process than dropping all indexes on all tables, so I wrote the following stored proc to do it one table at a time:
CREATE PROCEDURE [dbo].[sp_DropAllIndexesOnTable]
#TableName varchar(1000)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #DropCommand1 nvarchar(4000)
DECLARE #DropCommand2 nvarchar(4000)
--Create Temp Table to hold the names and table names of all Clustered Indexes
SELECT o.name AS TableName, i.name AS IndexName
INTO #AllClustered
FROM sys.indexes i
INNER JOIN sys.objects o ON i.object_id=o.object_id
WHERE o.type <> 'S'
AND is_primary_key = 1
--Create Temp Table to hold the names and table names of all NonClustered Indexes
SELECT o.name AS TableName, i.name AS IndexName
INTO #AllNonClustered
FROM sys.indexes i
INNER JOIN sys.objects o ON i.object_id=o.object_id
WHERE o.type <> 'S'
AND is_primary_key <> 1
AND index_id > 0
--Create DROP CONSTRAINT command for the Primary Key Constraint if there is one
SELECT #DropCommand1 = ( SELECT 'ALTER TABLE dbo.['+ TableName +'] DROP CONSTRAINT ['+ IndexName +'];'
FROM #AllClustered
WHERE TableName = #TableName
FOR xml path('') );
--Create DROP INDEX command for the indexes on the table if there are any
SELECT #DropCommand2 = ( SELECT 'DROP INDEX [' + IndexName + '] ON dbo.['+ TableName +'];'
FROM #AllNonClustered
WHERE TableName = #TableName
FOR xml path('') );
IF (#DropCommand1 IS NULL AND #DropCommand2 IS NULL) PRINT 'No action taken, zero indexes found on table ' + #TableName
IF #DropCommand1 IS NOT NULL EXEC sp_executesql #DropCommand1
IF #DropCommand2 IS NOT NULL EXEC sp_executesql #DropCommand2
DROP TABLE IF EXISTS #AllClustered
DROP TABLE IF EXISTS #AllNonClustered
END
GO
I cycle through the specific tables in my DB which I want to drop indexes on using a loop, and I drop the indexes by calling this proc with the table name, and recreate better ones right after. This way, only one table at a time has no indexes.
The reason I do this and the number of tables I do it on would make your head spin, but I definitely needed a proc like this!
SELECT 'DROP INDEX [' + IX.NAME + '] ON ' + OBJECT_NAME(IX.OBJECT_ID) + '; '
FROM SYS.INDEXES IX
JOIN SYS.OBJECTS O ON IX.OBJECT_ID = O.OBJECT_ID
INNER JOIN SYS.SCHEMAS S ON O.SCHEMA_ID = S.SCHEMA_ID
WHERE
IX.NAME IS NOT NULL
AND O.TYPE <> 'S'
AND IS_PRIMARY_KEY <> 1
AND INDEX_ID > 0
AND S.NAME != 'SYS' AND S.NAME!= 'SYS' AND IS_UNIQUE_CONSTRAINT = 0
Modify conditions according to your needs
If u want to delete PK constraints, you will get this message if you try to drop index:
An explicit DROP INDEX is not allowed on index... It is being used for PRIMARY KEY constraint enforcement.
Then, use this...
SELECT 'ALTER TABLE [' + O.NAME + '] DROP CONSTRAINT ' + IX.NAME + '; '
FROM SYS.INDEXES IX
JOIN SYS.OBJECTS O ON IX.OBJECT_ID = O.OBJECT_ID
INNER JOIN SYS.SCHEMAS S ON O.SCHEMA_ID = S.SCHEMA_ID
WHERE
IX.NAME IS NOT NULL
AND O.TYPE <> 'S'
AND INDEX_ID > 0
AND S.NAME != 'SYS' AND S.NAME!= 'SYS'

Preserve SQL Indexes While Altering Column Datatype

I have a smalldatetime column that I need to alter to be a datetime column. This is something that will be part of an install process, so it cannot be a manual procedure. Unfortunately, the column has a few indexes and a not null constraint on it. The indexes are performance related and would need to be retained only using the new data type. Is it possible to write a statement that will allow me to retain the relevant information while still altering the column datatype? If so, how can this be done?
You can not change the datatype from smalldatetime to datetime with the indexes, unique constraints, foreign key constraints or check constraints in place. You will have to drop them all prior to changing the type. Then:
alter table T alter column TestDate datetime not null
Then recreate the constraints and indexes that still apply.
Some different approaches for generating the drop and creates:
1) If you have given explicit names to all indexes and constraints then your installer can run a static script in each environment (dev, test, user acceptance testing, performance testing, etc, production.)
To generate this explicit script you can:
a) Use SSMS (or with SQL Server 2000, enterprise manager) to script the create and drop statements.
b) Work from you source code repository to discover the names and definitions of the dependent objects and put together the appropriate static script.
c) Attempt to run the alter statement. See what it fails on. Look up the definitions and hand write the drop and create. (Personally, this would be great for writing the drop, not so good at the create.)
2) If you have not given explicit names to all indexes and constraints, then your installer will have to query the data dictionary for the appropriate names and use dynamic SQL to run the drops, in the correct order, prior to the alter column statement and then the creates, in the correct order, after the alter column.
This will be simpler if you know that there are no constraints, and just indexes.
There may be tools or libraries that already know how to do this.
Also, if this is a packaged application, you may not be assured that the local DBAs have not added indexes.
NOTE: If there is a unique constraint, it will have built an index, which you will not be able to drop with DROP INDEX.
If you are just changing the size, the Index will still remain on the table.
If you are changing the data type, then you will get an error message stating that objects depend on the column that you are trying to change and therefore you will not be able to change it.
You can script out the indexes in question manually or via script. In SSMS, right click the table and script out the object in question.
If you want programatic index scripting, here is a stored proc that I have been using that I got from an ex colleague of mine.
Drop Proc ScriptIndex
GO
Create Proc ScriptIndex
#TableName VarChar (Max),
#IndexScript VarChar (Max) OUTPUT
AS
-- Get all existing indexes, EXCEPT the primary keys
DECLARE cIX CURSOR FOR
SELECT OBJECT_NAME(SI.Object_ID), SI.Object_ID, SI.Name, SI.Index_ID
FROM Sys.Indexes SI
LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
ON SI.Name = TC.CONSTRAINT_NAME
AND OBJECT_NAME(SI.Object_ID) = TC.TABLE_NAME
WHERE 1=1
AND OBJECT_NAME(SI.Object_ID) = #TableName
AND TC.CONSTRAINT_NAME IS NULL
AND OBJECTPROPERTY(SI.Object_ID, 'IsUserTable') = 1
ORDER BY OBJECT_NAME(SI.Object_ID), SI.Index_ID
DECLARE #IxTable SYSNAME
DECLARE #IxTableID INT
DECLARE #IxName SYSNAME
DECLARE #IxID INT
-- Loop through all indexes
OPEN cIX
FETCH NEXT FROM cIX INTO #IxTable, #IxTableID, #IxName, #IxID
WHILE (##FETCH_STATUS = 0)
BEGIN
DECLARE #IXSQL NVARCHAR(4000)
DECLARE #PKSQL NVARCHAR(4000)
SET #PKSQL = ''
SET #IXSQL = 'CREATE '
-- Check if the index is unique
IF (INDEXPROPERTY(#IxTableID, #IxName, 'IsUnique') = 1)
SET #IXSQL = #IXSQL + 'UNIQUE '
-- Check if the index is clustered
IF (INDEXPROPERTY(#IxTableID, #IxName, 'IsClustered') = 1)
SET #IXSQL = #IXSQL + 'CLUSTERED '
SET #IXSQL = #IXSQL + 'INDEX ' + #IxName + ' ON ' + #IxTable + '('
-- Get all columns of the index
DECLARE cIxColumn CURSOR FOR
SELECT SC.Name
FROM Sys.Index_Columns IC
JOIN Sys.Columns SC ON IC.Object_ID = SC.Object_ID AND IC.Column_ID = SC.Column_ID
WHERE IC.Object_ID = #IxTableID AND Index_ID = #IxID
ORDER BY IC.Index_Column_ID
DECLARE #IxColumn SYSNAME
DECLARE #IxFirstColumn BIT SET #IxFirstColumn = 1
-- Loop throug all columns of the index and append them to the CREATE statement
OPEN cIxColumn
FETCH NEXT FROM cIxColumn INTO #IxColumn
WHILE (##FETCH_STATUS = 0)
BEGIN
IF (#IxFirstColumn = 1)
SET #IxFirstColumn = 0
ELSE
SET #IXSQL = #IXSQL + ', '
SET #IXSQL = #IXSQL + #IxColumn
FETCH NEXT FROM cIxColumn INTO #IxColumn
END
CLOSE cIxColumn
DEALLOCATE cIxColumn
SET #IXSQL = #IXSQL + ')'
-- Print out the CREATE statement for the index
PRINT #IXSQL
FETCH NEXT FROM cIX INTO #IxTable, #IxTableID, #IxName, #IxID
END
CLOSE cIX
DEALLOCATE cIX
GO
Declare #TableName VarChar (Max), #IndexScript VarChar (Max)
Exec ScriptIndex 'Client', #IndexScript OUTPUT
Print #IndexScript
EDIT: It depends on the original and changed datatype.
If you try to alter a column from varchar to nvarchar, it will fail.
Whereas, if you alter column from varchar(16) to varchar(32), it will succeed.
--Disable Index
ALTER INDEX MyIndex ON MyTable DISABLE
GO
-- Change column datatype
--Enable Index
ALTER INDEX MyIndex ON MyTable REBUILD
GO
If you change the type of a column, then all indexes that use that column will have to be rebuilt.
But unless you have huge volumes of data (or run 24/7), rebuilding indexes is no big deal. Just schedule a maintenance window.
The best thing to do is to create a procedure that returns the index script of a given table / column. So you can remove the indexes just from the column being altered and not all indexes from the table, whereas creating indices can be somewhat expensive.
Stores the result of the procedure in a datatable
Delete the indices of the column
Modify your column
Rebuild the indexes stored in the datatable
-- objective : Generates indices scripting using specified column
-- Parameters :
-- #Tabela -> Name of the table that the column belongs to
-- #Coluna -> Name of the column that will be searched for the indices to generate the script
--Use: proc_ScriptIndexColumn 'TableName', 'ColumnName'
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
Create Proc proc_ScriptIndexColumn (#Tabela VARCHAR(4000), #Coluna VARCHAR(4000))
AS
BEGIN
DECLARE #isql_key VARCHAR(4000),
#isql_incl VARCHAR(4000),
#tableid INT,
#indexid INT
DECLARE #tablename VARCHAR(4000),
#indexname VARCHAR(4000)
DECLARE #isunique INT,
#isclustered INT,
#indexfillfactor INT
DECLARE #srsql VARCHAR(MAX)
DECLARE #ScriptsRetorno TABLE
(Script VARCHAR(MAX))
DECLARE index_cursor CURSOR
FOR
SELECT tablename = OBJECT_NAME(i.[object_id]),
tableid = i.[object_id],
indexid = i.index_id,
indexname = i.name,
isunique = i.is_unique,
CASE I.type_desc
WHEN 'CLUSTERED' THEN 1
ELSE 0
END AS isclustered,
indexfillfactor = i.fill_factor
FROM sys.indexes AS i
INNER JOIN SYSOBJECTS AS O
ON I.[object_id] = O.ID
INNER JOIN sys.index_columns AS ic
ON (ic.column_id > 0
AND (ic.key_ordinal > 0
OR ic.partition_ordinal = 0
OR ic.is_included_column != 0
))
AND ( ic.index_id = CAST(i.index_id AS INT)
AND ic.object_id = i.[object_id]
)
INNER JOIN sys.columns AS sc
ON sc.object_id = ic.object_id
AND sc.column_id = ic.column_id
WHERE O.XTYPE = 'U'
AND i.typE = 2 /*Non clustered*/
AND i.is_unique = 0
AND i.is_hypothetical = 0
AND UPPER(OBJECT_NAME(i.[object_id])) = UPPER(#Tabela)
AND UPPER(sc.name) = UPPER(#Coluna)
OPEN index_cursor
FETCH NEXT FROM index_cursor INTO #tablename,#tableid, #indexid,#indexname ,
#isunique ,#isclustered , #indexfillfactor
WHILE ##fetch_status <> -1
BEGIN
SELECT #isql_key = '',
#isql_incl = ''
SELECT #isql_key = CASE ic.is_included_column
WHEN 0 THEN CASE ic.is_descending_key
WHEN 1 THEN #isql_key +COALESCE(sc.name, '') +
' DESC, '
ELSE #isql_key + COALESCE(sc.name, '')
+ ' ASC, '
END
ELSE #isql_key
END,
--include column
#isql_incl = CASE ic.is_included_column
WHEN 1 THEN CASE ic.is_descending_key
WHEN 1 THEN #isql_incl +
COALESCE(sc.name, '') +
', '
ELSE #isql_incl + COALESCE(sc.name, '')
+ ', '
END
ELSE #isql_incl
END
FROM sysindexes i
INNER JOIN sys.index_columns AS ic
ON (
ic.column_id > 0
AND (
ic.key_ordinal > 0
OR ic.partition_ordinal = 0
OR ic.is_included_column != 0
)
)
AND (ic.index_id = CAST(i.indid AS INT) AND ic.object_id = i.id)
INNER JOIN sys.columns AS sc
ON sc.object_id = ic.object_id
AND sc.column_id = ic.column_id
WHERE i.indid > 0
AND i.indid < 255
AND (i.status & 64) = 0
AND i.id = #tableid
AND i.indid = #indexid
ORDER BY
i.name,
CASE ic.is_included_column
WHEN 1 THEN ic.index_column_id
ELSE ic.key_ordinal
END
IF LEN(#isql_key) > 1
SET #isql_key = LEFT(#isql_key, LEN(#isql_key) -1)
IF LEN(#isql_incl) > 1
SET #isql_incl = LEFT(#isql_incl, LEN(#isql_incl) -1)
SET #srsql = 'CREATE ' + 'INDEX [' + #indexname + ']' + ' ON [' + #tablename
+ '] '
SET #srsql = #srsql + '(' + #isql_key + ')'
IF (#isql_incl <> '')
SET #srsql = #srsql + ' INCLUDE(' + #isql_incl + ')'
IF (#indexfillfactor <> 0)
SET #srsql = #srsql + ' WITH ( FILLFACTOR = ' + CONVERT(VARCHAR(10), #indexfillfactor)
+ ')'
FETCH NEXT FROM index_cursor INTO #tablename,#tableid,#indexid,#indexname,
#isunique ,#isclustered , #indexfillfactor
INSERT INTO #ScriptsRetorno
VALUES
(#srsql)
END
CLOSE index_cursor
DEALLOCATE index_cursor
SELECT *
FROM #ScriptsRetorno
RETURN ##ERROR
END