Migliora le prestazioni di sys.dm_db_index_physical_stats


14

Durante un lavoro di manutenzione, sto cercando di ottenere un elenco di indici frammentati. Ma la query è estremamente lenta e richiede oltre 30 minuti per l'esecuzione . Penso che ciò sia dovuto a una scansione remota su sys.dm_db_index_physical_stats.

Esiste un modo per accelerare la seguente query:

SELECT
    OBJECT_NAME(i.OBJECT_ID) AS TableName,
    i.name AS TableIndexName
FROM
    sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') phystat 
    INNER JOIN sys.indexes i 
        ON i.OBJECT_ID = phystat.OBJECT_ID AND i.index_id = phystat.index_id 
WHERE 
    phystat.avg_fragmentation_in_percent > 20 
    AND OBJECT_NAME(i.OBJECT_ID) IS NOT NULL
ORDER BY phystat.avg_fragmentation_in_percent DESC

Non sono un DBA e potrei fare un errore evidente nella query sopra, o forse ci sono alcuni indici o statistiche che potrebbero aiutare? Forse è solo la dimensione del database (circa 20 GB con circa 140 tabelle).

Il motivo per cui lo chiedo è che abbiamo solo una finestra molto piccola per la manutenzione durante la notte e questo sta occupando la maggior parte del tempo.

Risposte:


20

'DETAILED' implica a scansione completa di ogni singola pagina dell'indice (o heap). Fallo per ogni tabella e ogni indice secondario, il risultato significa che stai eseguendo una scansione completa del database, end-to-end, e non molto efficiente (ad esempio, non così veloce come il backup lo leggerebbe, per esempio). Il tempo è guidato da:

  • quanto è grande il tuo database
  • quanto velocemente il tuo sottosistema IO deve leggere l'intero database
  • carico concorrenziale aggiuntivo in competizione per il throughput IO

Fondamentalmente, se tutto ciò che hai è una cannuccia (il tuo throughput IO) ci vogliono 30 minuti per bere un secchio (la dimensione del tuo database). Acquista IO più veloce, riduci le dimensioni dei tuoi dati o utilizzaliSAMPLED scansioni.

Detto questo ... 20Gb è piuttosto piccolo. 30 minuti per leggere 20Gb sono un sacco di tempo. Il tuo sottosistema IO è così lento? Hai distribuito su unità da 1 TB consumer da 7200 RPM?


12

Oltre alla raccomandazione di @Remus di utilizzare una SAMPLEDscansione, non so che questa query non può iniziare fino all'avvio della finestra di manutenzione. Perché non precompilare una tabella con i risultati? Se si avvia questa query (supponiamo che una scansione campionata richieda 10 minuti) circa 15-20 minuti prima della finestra di manutenzione e inserisca i risultati in una tabella, i dati sarebbero pronti per l'uso non appena si avvia la finestra di manutenzione e il nel frattempo i dati sottostanti non saranno cambiati molto. Se si evita l'ordinamento e il filtro sulla query originale, dovrebbe completarsi anche più velocemente, ad es

CREATE TABLE dbo.IndexStats
(
  TableName SYSNAME,
  IndexName SYSNAME,
  Frag DECIMAL(5,2)
);
CREATE INDEX x ON dbo.IndexStats(Frag);

Quindi nel tuo primo lavoro notturno (che inizia prima della finestra di manutenzione):

TRUNCATE TABLE dbo.IndexStats;

INSERT dbo.IndexStats
SELECT 
  OBJECT_NAME(i.[object_id]),
  i.name,
  s.avg_fragmentation_in_percent
FROM
  sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') AS s 
INNER JOIN sys.indexes AS i 
ON i.[object_id] = s.[object_id]
AND i.index_id = s.index_id;

DELETE dbo.IndexStats WHERE Frag < 20
  OR TableName IS NULL;

Ora il tuo attuale script di deframmentazione ha già tutte le informazioni necessarie per procedere immediatamente. (È anche possibile concatenare i lavori insieme o forzare quanto sopra per attendere l'ora di inizio della finestra di manutenzione utilizzando WAITFOR TIME.)

Puoi anche prendere in considerazione la possibilità di giocare LIMITEDe vedere come funzionano queste tariffe.


5

Dichiarazione di non responsabilità: questi script sono stati testati su SQL Server 2005/2008. Tuttavia, questo codice e le informazioni sono fornite "COSÌ COME SONO" senza garanzie di alcun tipo, espresse o implicite, incluso ma non limitato alle garanzie implicite o commerciabilità e / o idoneità per uno scopo particolare. Come sempre, testarlo nel proprio ambiente di test prima di provare a distribuire nel proprio ambiente di produzione. Ora che è fuori mano ...

Uno dei problemi che ho riscontrato quando ho a che fare con i DMV dell'indice è che non possono essere correlati. Ciò significa che non è possibile utilizzare CROSS / OUTER APPLY contro di essi, al fine di limitare gli indici su cui si stanno eseguendo le scansioni. Per ovviare a questo, distribuisco una funzione wrapper, per DMV indice fisico e operativo, nel mio database principale:

Fisico:

ALTER FUNCTION [dbo].[tfn_IndexPhysicalStats_select]
(
    @DatabaseID SMALLINT = 0,
    @ObjectID INT = 0,
    @IndexID INT = -1,
    @PartitionNumber INT = 0,
    @Mode NVARCHAR(20) = NULL
)
RETURNS @IndexPhysicalStats TABLE
(
    database_id SMALLINT NOT NULL,
    object_id INT NOT NULL,
    index_id INT NOT NULL,
    partition_number INT NOT NULL,
    index_type_desc NVARCHAR(60) NULL,
    alloc_unit_type_desc NVARCHAR(60) NULL,
    index_depth TINYINT NOT NULL,
    index_level TINYINT NOT NULL,
    avg_fragmentation_in_percent FLOAT NULL,
    fragment_count BIGINT NULL,
    avg_fragment_size_in_pages FLOAT NULL,
    page_count BIGINT NOT NULL,
    avg_page_space_used_in_percent FLOAT NULL,
    record_count BIGINT NULL,
    ghost_record_count BIGINT NULL,
    version_ghost_record_count BIGINT NULL,
    min_record_size_in_bytes INT NULL,
    max_record_size_in_bytes INT NULL,
    avg_record_size_in_bytes FLOAT NULL,
    forwarded_record_count BIGINT NULL
)
AS
BEGIN

    INSERT INTO @IndexPhysicalStats
    (
        database_id,
        object_id,
        index_id,
        partition_number,
        index_type_desc,
        alloc_unit_type_desc,
        index_depth,
        index_level,
        avg_fragmentation_in_percent,
        fragment_count,
        avg_fragment_size_in_pages,
        page_count,
        avg_page_space_used_in_percent,
        record_count,
        ghost_record_count,
        version_ghost_record_count,
        min_record_size_in_bytes,
        max_record_size_in_bytes,
        avg_record_size_in_bytes,
        forwarded_record_count
    )
    SELECT
        ddips.database_id,
        ddips.object_id,
        ddips.index_id,
        ddips.partition_number,
        ddips.index_type_desc,
        ddips.alloc_unit_type_desc,
        ddips.index_depth,
        ddips.index_level,
        ddips.avg_fragmentation_in_percent,
        ddips.fragment_count,
        ddips.avg_fragment_size_in_pages,
        ddips.page_count,
        ddips.avg_page_space_used_in_percent,
        ddips.record_count,
        ddips.ghost_record_count,
        ddips.version_ghost_record_count,
        ddips.min_record_size_in_bytes,
        ddips.max_record_size_in_bytes,
        ddips.avg_record_size_in_bytes,
        ddips.forwarded_record_count
    FROM sys.dm_db_index_physical_stats
    (
        @DatabaseID,
        @ObjectID,
        @IndexID,
        @PartitionNumber,
        @Mode
    ) AS ddips;

    RETURN;
END

operativa:

ALTER FUNCTION [dbo].[tfn_IndexOperationalStats_select]
(
    @DatabaseID SMALLINT = 0,
    @TableID INT = 0,
    @IndexID INT = -1,
    @PartitionNumber INT = 0
)
RETURNS @IndexOperationalStats TABLE
(
    database_id SMALLINT NOT NULL,
    object_id INT NOT NULL,
    index_id INT NOT NULL,
    partition_number INT NOT NULL,
    leaf_insert_count BIGINT NULL,
    leaf_delete_count BIGINT NULL,
    leaf_update_count BIGINT NULL,
    leaf_ghost_count BIGINT NULL,
    nonleaf_insert_count BIGINT NULL,
    nonleaf_delete_count BIGINT NULL,
    nonleaf_update_count BIGINT NULL,
    leaf_allocation_count BIGINT NULL,
    nonleaf_allocation_count BIGINT NULL,
    leaf_page_merge_count BIGINT NULL,
    nonleaf_page_merge_count BIGINT NULL,
    range_scan_count BIGINT NULL,
    singleton_lookup_count BIGINT NULL,
    forwarded_fetch_count BIGINT NULL,
    lob_fetch_in_pages BIGINT NULL,
    lob_fetch_in_bytes BIGINT NULL,
    lob_orphan_create_count BIGINT NULL,
    lob_orphan_insert_count BIGINT NULL,
    row_overflow_fetch_in_pages BIGINT NULL,
    row_overflow_fetch_in_bytes BIGINT NULL,
    column_value_push_off_row_count BIGINT NULL,
    column_value_pull_in_row_count BIGINT NULL,
    row_lock_count BIGINT NULL,
    row_lock_wait_count BIGINT NULL,
    row_lock_wait_in_ms BIGINT NULL,
    page_lock_count BIGINT NULL,
    page_lock_wait_count BIGINT NULL,
    page_lock_wait_in_ms BIGINT NULL,
    index_lock_promotion_attempt_count BIGINT NULL,
    index_lock_promotion_count BIGINT NULL,
    page_latch_wait_count BIGINT NULL,
    page_latch_wait_in_ms BIGINT NULL,
    page_io_latch_wait_count BIGINT NULL,
    page_io_latch_wait_in_ms BIGINT NULL
    PRIMARY KEY CLUSTERED
    (
        database_id ASC,
        object_id ASC,
        index_id ASC,
        partition_number ASC
    )
)
AS
BEGIN
    INSERT INTO @IndexOperationalStats
    (
        database_id,
        object_id,
        index_id,
        partition_number,
        leaf_insert_count,
        leaf_delete_count,
        leaf_update_count,
        leaf_ghost_count,
        nonleaf_insert_count,
        nonleaf_delete_count,
        nonleaf_update_count,
        leaf_allocation_count,
        nonleaf_allocation_count,
        leaf_page_merge_count,
        nonleaf_page_merge_count,
        range_scan_count,
        singleton_lookup_count,
        forwarded_fetch_count,
        lob_fetch_in_pages,
        lob_fetch_in_bytes,
        lob_orphan_create_count,
        lob_orphan_insert_count,
        row_overflow_fetch_in_pages,
        row_overflow_fetch_in_bytes,
        column_value_push_off_row_count,
        column_value_pull_in_row_count,
        row_lock_count,
        row_lock_wait_count,
        row_lock_wait_in_ms,
        page_lock_count,
        page_lock_wait_count,
        page_lock_wait_in_ms,
        index_lock_promotion_attempt_count,
        index_lock_promotion_count,
        page_latch_wait_count,
        page_latch_wait_in_ms,
        page_io_latch_wait_count,
        page_io_latch_wait_in_ms
    )
    SELECT
        ddios.database_id,
        ddios.object_id,
        ddios.index_id,
        ddios.partition_number,
        ddios.leaf_insert_count,
        ddios.leaf_delete_count,
        ddios.leaf_update_count,
        ddios.leaf_ghost_count,
        ddios.nonleaf_insert_count,
        ddios.nonleaf_delete_count,
        ddios.nonleaf_update_count,
        ddios.leaf_allocation_count,
        ddios.nonleaf_allocation_count,
        ddios.leaf_page_merge_count,
        ddios.nonleaf_page_merge_count,
        ddios.range_scan_count,
        ddios.singleton_lookup_count,
        ddios.forwarded_fetch_count,
        ddios.lob_fetch_in_pages,
        ddios.lob_fetch_in_bytes,
        ddios.lob_orphan_create_count,
        ddios.lob_orphan_insert_count,
        ddios.row_overflow_fetch_in_pages,
        ddios.row_overflow_fetch_in_bytes,
        ddios.column_value_push_off_row_count,
        ddios.column_value_pull_in_row_count,
        ddios.row_lock_count,
        ddios.row_lock_wait_count,
        ddios.row_lock_wait_in_ms,
        ddios.page_lock_count,
        ddios.page_lock_wait_count,
        ddios.page_lock_wait_in_ms,
        ddios.index_lock_promotion_attempt_count,
        ddios.index_lock_promotion_count,
        ddios.page_latch_wait_count,
        ddios.page_latch_wait_in_ms,
        ddios.page_io_latch_wait_count,
        ddios.page_io_latch_wait_in_ms
    FROM sys.dm_db_index_operational_stats
    (
        @DatabaseID,
        @TableID,
        @IndexID,
        @PartitionNumber
    ) AS ddios;

    RETURN;
END

Quindi faccio riferimento a questa funzione nei miei lavori di manutenzione indice nel modo seguente:

DECLARE 
    @DDL NVARCHAR(MAX);

DECLARE ddl_cursor CURSOR
FOR
    SELECT
        CONVERT(NVARCHAR(MAX), DDL.DDL) AS DDL
    FROM
    (
        SELECT
            MasterIndexes.SchemaName,
            MasterIndexes.TableName,
            MasterIndexes.IndexName,
            MasterIndexes.DatabaseID,
            MasterIndexes.ObjectID,
            MasterIndexes.IndexID,
            MasterIndexes.PartitionNumber,
            MasterIndexes.type_desc,
            MasterIndexes.is_unique,
            MasterIndexes.is_primary_key,
            MasterIndexes.is_unique_constraint,
            MasterIndexes.fill_factor,
            MasterIndexes.allow_row_locks,
            MasterIndexes.allow_page_locks,
            MasterIndexes.UpdateStatisticsIndicator,
            1 AS SortInTempDB,
            CASE
                WHEN CONVERT(VARCHAR(100), SERVERPROPERTY('edition')) LIKE 'Enterprise Edition%' THEN 1
                ELSE 0
            END AS OnlineIndicator,
            CASE
                WHEN 
                    ips.avg_fragmentation_in_percent BETWEEN CONVERT(FLOAT, 10) AND CONVERT(FLOAT, 30)
                    AND ips.page_count >= 100
                THEN
                    1
                ELSE
                    0
            END AS ReorganizationIndicator,
            CASE
                WHEN
                (
                    ips.avg_fragmentation_in_percent >= 30
                    AND ips.page_count >= 100
                )
                OR
                (
                    ips.avg_fragmentation_in_percent BETWEEN CONVERT(FLOAT, 10) AND CONVERT(FLOAT, 30)
                    AND ips.page_count < 100
                )
                THEN 
                    1
                ELSE
                    0
            END AS RebuildIndicator
        FROM
        (
            SELECT
                s.name AS SchemaName,
                t.name AS TableName,
                ix.name AS IndexName,
                DB_ID() AS DatabaseID,
                ddps.object_id AS ObjectID,
                ddps.index_id AS IndexID,
                ddps.partition_number AS PartitionNumber,
                ix.type_desc,
                ix.is_unique,   
                ix.is_primary_key,
                ix.is_unique_constraint,
                ix.fill_factor, 
                ix.allow_row_locks,
                ix.allow_page_locks,
                1 AS UpdateStatisticsIndicator  
            FROM sys.schemas AS s

                INNER JOIN sys.tables AS t
                    ON s.schema_id = t.schema_id

                    INNER JOIN sys.indexes AS ix
                        ON t.object_id = ix.object_id

                        INNER JOIN sys.dm_db_partition_stats AS ddps
                            ON ix.object_id = ddps.object_id
                            AND ix.index_id = ddps.index_id

                CROSS APPLY master.dbo.tfn_IndexOperationalStats_select
                (
                    DB_ID(),
                    t.object_id,
                    ix.index_id,
                    NULL
                ) AS ios

            WHERE
                CASE
                    WHEN ddps.row_count = 0 THEN 0
                    ELSE
                    (
                        (
                            CONVERT
                            (
                                FLOAT,
                                (
                                    ios.nonleaf_insert_count + 
                                    ios.nonleaf_update_count + 
                                    ios.leaf_insert_count + 
                                    ios.leaf_update_count
                                )
                            ) /
                            CONVERT
                            (
                                FLOAT,
                                ddps.row_count
                            )
                        ) * 100.0
                    ) 
                END >= 10.0
            AND t.is_ms_shipped = 0
            AND t.name NOT LIKE 'MSmerge%'
            AND ix.index_id > 0
        ) AS MasterIndexes

            CROSS APPLY master.dbo.tfn_IndexPhysicalStats_select
            (
                MasterIndexes.DatabaseID,
                MasterIndexes.ObjectID,
                MasterIndexes.IndexID,
                MasterIndexes.PartitionNumber,
                'SAMPLED'
            ) AS ips
    ) AS MasterIndexList    

        CROSS APPLY
        (
            SELECT          
                'ALTER INDEX ' + 
                MasterIndexList.IndexName + 
                ' ON ' + 
                MasterIndexList.SchemaName + '.' + MasterIndexList.TableName +
                ' REBUILD WITH(' + 
                'FILLFACTOR = ' + 
                    CASE
                        WHEN MasterIndexList.fill_factor = 0 THEN '100'
                        ELSE CONVERT(VARCHAR(3), MasterIndexList.fill_factor)
                    END + ', ' +
                'SORT_IN_TEMPDB = ' + 
                    CASE
                        WHEN MasterIndexList.SortInTempDB = 1 THEN 'ON'
                        ELSE 'OFF'
                    END + ', ' +
                'ONLINE = ' + 
                    CASE
                        WHEN MasterIndexList.OnlineIndicator = 1 THEN 'ON'
                        ELSE 'OFF'
                    END + ', ' + 
                'ALLOW_ROW_LOCKS = ' + 
                    CASE
                        WHEN MasterIndexList.[allow_row_locks] = 1 THEN 'ON'
                        ELSE 'OFF'
                    END + ', ' + 
                'ALLOW_PAGE_LOCKS = ' + 
                    CASE
                        WHEN MasterIndexList.[allow_page_locks] = 1 THEN 'ON'
                        ELSE 'OFF'
                    END + ');' AS [DDL],

                1 AS DDLOrdinal

            WHERE MasterIndexList.RebuildIndicator = 1

            UNION ALL
            SELECT          
                'ALTER INDEX ' + 
                MasterIndexList.IndexName + 
                ' ON ' + 
                MasterIndexList.SchemaName + '.' + MasterIndexList.TableName +
                ' REORGANIZE;' AS [DDL],

                2 AS DDLOrdinal

            WHERE MasterIndexList.ReorganizationIndicator = 1

            UNION ALL
            SELECT
                'UPDATE STATISTICS ' + 
                MasterIndexList.SchemaName + '.' + MasterIndexList.TableName + ' ' + 
                MasterIndexList.IndexName + ' ' + 
                'WITH FULLSCAN;' AS [DDL],

                3 AS DDLOrdinal

            WHERE MasterIndexList.UpdateStatisticsIndicator = 1
            AND MasterIndexList.RebuildIndicator = 0
            AND STATS_DATE(MasterIndexList.ObjectID, MasterIndexList.IndexID) <= DATEADD(hh, -20, GETDATE())
        ) AS [DDL]

    ORDER BY
        ObjectID ASC,
        IndexID ASC,
        DDLOrdinal ASC;

OPEN ddl_cursor;

FETCH NEXT FROM ddl_cursor
INTO @DDL;

WHILE @@FETCH_STATUS = 0
BEGIN

    EXECUTE sys.sp_executesql 
        @stmt = @DDL;

    FETCH NEXT FROM ddl_cursor
    INTO @DDL;
END

CLOSE ddl_cursor;
DEALLOCATE ddl_cursor;
GO

Come sempre, il tuo chilometraggio può variare, ma sentiti libero di usare / modificare questi script in base alle tue esigenze.

Buona

opaco


0

Sotto il codec funziona bene su un database di ~ 185 GB.

DECLARE @dbid int

SET @dbid = DB_ID()

select o.name as ObjectName, 
       O.id as ObjectID,
       I.name as IndexName,
       I.index_id as IndexID,
       I.type,
       I.type_Desc,
       ps.avg_fragmentation_in_percent 

from sysobjects O with (nolock)
inner join sys.indexes i with (nolock) ON O.id = i.object_id 
inner join sys.dm_db_index_physical_stats (@dbid,null,null,null,'LIMITED') ps on ps.database_id = @dbid
                                                                             and ps.object_id = O.id 
                                                                             and ps.index_id = I.index_id
where xtype = 'U'
and LEFT(o.name,2) <> 'MS'
and ps.avg_fragmentation_in_percent > 10
order by o.name 

0

Ho notato che la mia durata per la raccolta di informazioni sugli indici frammentati è passata da 3 ore e mezza a 5 minuti facendo un aggiornamento delle statistiche. Aggiorna le statistiche sulle tabelle utilizzando il processo di aggiornamento delle statistiche di Ola Hallengren e questo dovrebbe essere eseguito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.