Оптимизируем базу на MSSQL: определяем фрагментацию индексов
Ничто так не "убивает" базу, как "плохое" индексирование (С)
Создать правильные индексы - это только половина дела. Нужно еще и правильно ими управлять.
В процессе работы с базой, особенно, если данные в ней довольно часто модифицируются/добавляются/удаляются, со временем индексы приходят в некоторую "негодность". Увеличивается их "фрагментарность", ухудшается их влияние на скорость исполнения запросов к БД.
Оптимальным считается, когда уровень фрагментации индекса не превышает 10%, но для поддержания такого показателя необходимо проводить периодическую их дефрагментацию и реорганизацию.
(подробнее про реорганизацию и дефрагментацию)
Основным инструментом в процессе управления фрагментацией индексов выступает функция sys.dm_db_index_physical_stats (подробнее)
Для определения списка индексов с уровнем фрагментарности выше оптимальных 10% в своей работе я воспользовалась вот таким запросом:
@table_name varchar(250) = N'db_name.dbo.tbl_name'
SELECT IndStat.database_id,
IndStat.object_id,
QUOTENAME(s.name) + '.' + QUOTENAME(o.name) AS [object_name],
IndStat.index_id,
QUOTENAME(i.name) AS index_name,
IndStat.avg_fragmentation_in_percent,
IndStat.partition_number,
(SELECT count (*) FROM sys.partitions p
WHERE p.object_id = IndStat.object_id AND p.index_id = IndStat.index_id) AS partition_count
FROM sys.dm_db_index_physical_stats
(DB_ID(@db_name), OBJECT_ID(@table_name), NULL, NULL , 'LIMITED') AS IndStat
INNER JOIN sys.objects AS o ON (IndStat.object_id = o.object_id)
INNER JOIN sys.schemas AS s ON s.schema_id = o.schema_id
INNER JOIN sys.indexes i ON (i.object_id = IndStat.object_id AND i.index_id = IndStat.index_id)
WHERE IndStat.avg_fragmentation_in_percent > 10 AND IndStat.index_id > 0
Если указать @table_name = NULL, тогда мы получим данные по всем таблицам указанной базы.
Если указать и @db_name = NULL - получим информацию по всем таблицам всех баз.
Естественно, для выполнения этого запроса нужно обладать некоторыми правами:
- CONTROL на специфический объект БД.
- VIEW DATABASE STATE для получения информации обо всех объектах определенной БД (@object_id = NULL).
- VIEW SERVER STATE - для получения информации обо всех базах сервера (@database_id = NULL).
Так же перед использованием желательно обновить статистику БД.
Вышеприведенный запрос дает только справочную информацию. Но на основании функции sys.dm_db_index_physical_stats можно построить скрипт или хранимую процедуру, которая будет не только определять список индексов, нуждающихся в перестройке, но и будет сама проводить эту операцию.
Один из вариантов такого скрипта приведен ниже (проводит реорганизацию или перестройку(оффлайн) индексов таблиц текущей базы в зависимости от степени фрагментации):
GO
SET NOCOUNT ON;
DECLARE @objectid int;
DECLARE @indexid int;
DECLARE @partitioncount bigint;
DECLARE @schemaname nvarchar(130);
DECLARE @objectname nvarchar(130);
DECLARE @indexname nvarchar(130);
DECLARE @partitionnum bigint;
DECLARE @partitions bigint;
DECLARE @frag float;
DECLARE @command nvarchar(4000);
DECLARE @dbid smallint;
-- Выбираем индексы с уровнем фрагментации выше 10%
-- Определяем текущую БД
SET @dbid = DB_ID();
SELECT
[object_id] AS objectid,
index_id AS indexid,
partition_number AS partitionnum,
avg_fragmentation_in_percent AS frag, page_count
INTO #work_to_do
FROM sys.dm_db_index_physical_stats (@dbid, NULL, NULL , NULL, N'LIMITED')
WHERE avg_fragmentation_in_percent > 10.0
AND index_id > 0 -- игнорируем heap
AND page_count > 25; -- игнорируем маленькие таблицы
-- объявляем курсор для списка обрабатываемых partition
DECLARE partitions CURSOR FOR SELECT objectid,indexid, partitionnum,frag FROM #work_to_do;
OPEN partitions;
-- цикл по partition
WHILE (1=1)
BEGIN
FETCH NEXT
FROM partitions
INTO @objectid, @indexid, @partitionnum, @frag;
IF @@FETCH_STATUS 0 BREAK;
SELECT @objectname = QUOTENAME(o.name), @schemaname = QUOTENAME(s.name)
FROM sys.objects AS o
JOIN sys.schemas AS s ON s.schema_id = o.schema_id
WHERE o.object_id = @objectid;
SELECT @indexname = QUOTENAME(name)
FROM sys.indexes
WHERE object_id = @objectid AND index_id = @indexid;
SELECT @partitioncount = count (*)
FROM sys.partitions
WHERE object_id = @objectid AND index_id = @indexid;
-- 30% считаем пределом для определения типа обновления индекса.
IF @frag 30.0
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
IF @frag >= 30.0
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD';
IF @partitioncount > 1
SET @command = @command + N' PARTITION=' + CAST(@partitionnum AS nvarchar(10));
EXEC (@command);
PRINT N'Выполнено: ' + @command;
END;
CLOSE partitions;
DEALLOCATE partitions;
-- удаляем временную таблицу
DROP TABLE #work_to_do;
GO
Еще посмотреть примеры скриптов/хранимок можно тут или вот тут.
Операцию по устранению дефрагментации индексов рекомендуется проводить регулярно (например, раз в неделю/месяц - в зависимости от величины операций модификации над хранимыми данными). Индексы, за состоянием которых не следят, могут очень существенно "просадить" производительность БД при исполнении запросов.