Here’s a quick script to retrieve index fragmentation via powershell. Two actually. This uses sqlpsx for both. Do not run these during the day on your production box (you shouldn’t run them at all unless you test them on a test box first to gauge impact). I like the first one because you can strip off the –dbname parameter if you like and collect all the indexes from all your databases. This uses LIMITED option (::fast), but it’s still pretty heavy-duty. Use at your own risk.
First, here is the powershell-object oriented variant:
Import-Module SqlServer
$outputs = @();
$srv = 'ServerName'
Get-SqlDatabase -sqlserver $srv -dbname 'dbName' | %{
$dbName = $_.Name
Get-SqlTable -Database $_ | where{$_.IsSystemObject -ne $true} | %{
$tableName = $_.Name
foreach($index in $_.Indexes)
{
$index.EnumFragmentation([Microsoft.SqlServer.Management.Smo.FragmentationOption]::Fast) | %{
$output = New-Object -TypeName PSObject -Property @{
DatabaseName = $dbName
TableName = $tableName
IndexName = $index.Name
IndexType = $_.IndexType
AvgFragmentationInPercent = $_.AverageFragmentation
SizeMB = [Math]::Truncate(($_.pages*8)/1024)
FillFactor = $index.FillFactor
}
$outputs += $output
}
}
}
}
$outputs | SELECT DatabaseName, TableName, IndexName, IndexType, AvgFragmentationInPercent, SizeMB, FillFactor | Format-Table -AutoSize
Nice and clean. Unfortunately, if you run this while doing a trace, you’ll see that it’s doing much more work than necessary, but it’s very concise and easy to see/debug.
First, it executes this:
exec sp_executesql N'SELECT
i.name AS [Name]
FROM
sys.tables AS tbl
INNER JOIN sys.indexes AS i ON (i.index_id > @_msparam_0 and i.is_hypothetical = @_msparam_1) AND (i.object_id=tbl.object_id)
WHERE
(tbl.name=@_msparam_2 and SCHEMA_NAME(tbl.schema_id)=@_msparam_3)
ORDER BY
[Name] ASC',N'@_msparam_0 nvarchar(4000),@_msparam_1 nvarchar(4000),@_msparam_2 nvarchar(4000),@_msparam_3 nvarchar(4000)',@_msparam_0=N'0',@_msparam_1=N'0',@_msparam_2=N'TableName',@_msparam_3=N'SchemaName'
Then this:
exec sp_executesql N'
declare @database_id int
select @database_id = db_id()
SELECT
i.name AS [Index_Name],
CAST(i.index_id AS int) AS [Index_ID],
fi.index_depth AS [Depth],
fi.page_count AS [Pages],
fi.record_count AS [Rows],
fi.min_record_size_in_bytes AS [MinimumRecordSize],
fi.max_record_size_in_bytes AS [MaximumRecordSize],
fi.avg_record_size_in_bytes AS [AverageRecordSize],
fi.forwarded_record_count AS [ForwardedRecords],
fi.avg_page_space_used_in_percent AS [AveragePageDensity],
fi.index_type_desc AS [IndexType],
fi.partition_number AS [PartitionNumber],
fi.ghost_record_count AS [GhostRows],
fi.version_ghost_record_count AS [VersionGhostRows],
fi.avg_fragmentation_in_percent AS [AverageFragmentation]
FROM
sys.tables AS tbl
INNER JOIN sys.indexes AS i ON (i.index_id > @_msparam_0 and i.is_hypothetical = @_msparam_1) AND (i.object_id=tbl.object_id)
INNER JOIN sys.dm_db_index_physical_stats(@database_id, NULL, NULL, NULL, ''LIMITED'') AS fi ON fi.object_id=CAST(i.object_id AS int) AND fi.index_id=CAST(i.index_id AS int)
WHERE
(i.name=@_msparam_2)and((tbl.name=@_msparam_3 and SCHEMA_NAME(tbl.schema_id)=@_msparam_4))
ORDER BY
[Index_Name] ASC',N'@_msparam_0 nvarchar(4000),@_msparam_1 nvarchar(4000),@_msparam_2 nvarchar(4000),@_msparam_3 nvarchar(4000),@_msparam_4 nvarchar(4000)',@_msparam_0=N'0',@_msparam_1=N'0',@_msparam_2=N'IndexName',@_msparam_3=N'TableName',@_msparam_4=N'SchemaName'
Essentially, one hit to the database per table, and one per index on said table.
You could use this powershell to do one hit to the database to get all the tables, then only suffer one hit per index:
cls
Import-Module SqlServer
$outputs = @();
$srv = 'ServerName'
Get-SqlDatabase -sqlserver $srv -dbname 'DatabaseName' | %{
$dbName = $_.Name
$idxQuery = "
SELECT
DB_ID() AS DBID,
i.object_id,
OBJECT_NAME(i.object_id) AS TableName,
i.name,
i.index_id,
SUM(dps.used_page_count) * 8
FROM
sys.dm_db_partition_stats dps
INNER JOIN sys.indexes i ON dps.[object_id] = i.[object_id] AND dps.index_id = i.index_id
GROUP BY i.name, i.index_id, i.object_id
"
$indexes = Get-SqlData -sqlserver $srv -dbname $dbName -qry $idxQuery
foreach($row in $indexes)
{
$objectID = $row.object_id
$indexID = $row.index_id
$databaseID = $row.DBID
$idxFragQuery = "
SELECT
i.name,
i.type,
dmf.page_count*8 AS 'size',
ROUND(dmf.avg_fragmentation_in_percent,0) AS avg_fragmentation_in_percent,
i.fill_factor
FROM
sys.dm_db_index_physical_stats($databaseID, $objectID, $indexID, null, 'limited') dmf
INNER JOIN sys.indexes i ON ((i.object_id = dmf.object_id) AND (i.index_id = dmf.index_id))
WHERE
(dmf.alloc_unit_type_desc = 'IN_ROW_DATA')
"
$idxFragRows = Get-SqlData -sqlserver $srv -dbname $dbName -qry $idxFragQuery
$output = New-Object -TypeName PSObject -Property @{
DatabaseName = $dbName
TableName = $idxFragRows.TableName
IndexType = $idxFragRows.type
Size = $idxFragRows.size
AvgFragmentationInPercent = $idxFragRows.avg_fragmentation_in_percent
FillFactor = $idxFragRows.fill_factor
}
$outputs += $output
}
}
$outputs | Format-Table -AutoSize
But, it’s not nearly as clean. I wouldn’t run either of these on a production database during the day, but my preference would be to suffer the performance for the cleaner code, unless you have a gigantic number of tables in your database. YMMV. Use at your own risk.