Database
 sql >> Database >  >> RDS >> Database

Un altro motivo per evitare sp_updatestats

In precedenza ho scritto sul blog perché non amo sp_updatestats. Di recente ho trovato un altro motivo per cui non è mio amico. TL;DR:non aggiorna le statistiche sulle visualizzazioni indicizzate. Ora, la documentazione non afferma che lo sia, quindi non ci sono bug qui. La documentazione MSDN afferma chiaramente:

Esegue UPDATE STATISTICS su tutte le tabelle interne e definite dall'utente nel database corrente.

Ma... quanti di voi hanno pensato alle visualizzazioni indicizzate e si sono chiesti se quelle sono state aggiornate? Ammetto di no. Dimentico le viste indicizzate, il che è un peccato perché possono essere davvero potenti se usate in modo appropriato. Possono anche essere un incubo da svelare durante la risoluzione dei problemi, ma non ho intenzione di discuterne l'uso oggi. Voglio solo che tu sappia che non vengono aggiornati da sp_updatestats e che tu veda quali opzioni hai.

Configurazione

Poiché le World Series sono appena terminate, utilizzeremo il database di Baseball per i nostri test. Puoi scaricarlo dalla pagina Risorse SQLskills. Una volta ripristinato, creeremo una copia della tabella dbo.Players, denominata dbo.PlayerInfo, caricheremo alcune migliaia di righe al suo interno, quindi creeremo una vista indicizzata che unisce la nostra nuova tabella alla tabella di PitchingPost:

USE [BaseballData];
GO
 
CREATE TABLE [dbo].[PlayerInfo](
	[lahmanID] [int] NOT NULL,
	[playerID] [varchar](10) NULL DEFAULT (NULL),
	[managerID] [varchar](10) NULL DEFAULT (NULL),
	[hofID] [varchar](10) NULL DEFAULT (NULL),
	[birthYear] [int] NULL DEFAULT (NULL),
	[birthMonth] [int] NULL DEFAULT (NULL),
	[birthDay] [int] NULL DEFAULT (NULL),
	[birthCountry] [varchar](50) NULL DEFAULT (NULL),
	[birthState] [varchar](2) NULL DEFAULT (NULL),
	[birthCity] [varchar](50) NULL DEFAULT (NULL),
	[deathYear] [int] NULL DEFAULT (NULL),
	[deathMonth] [int] NULL DEFAULT (NULL),
	[deathDay] [int] NULL DEFAULT (NULL),
	[deathCountry] [varchar](50) NULL DEFAULT (NULL),
	[deathState] [varchar](2) NULL DEFAULT (NULL),
	[deathCity] [varchar](50) NULL DEFAULT (NULL),
	[nameFirst] [varchar](50) NULL DEFAULT (NULL),
	[nameLast] [varchar](50) NULL DEFAULT (NULL),
	[nameNote] [varchar](255) NULL DEFAULT (NULL),
	[nameGiven] [varchar](255) NULL DEFAULT (NULL),
	[nameNick] [varchar](255) NULL DEFAULT (NULL),
	[weight] [int] NULL DEFAULT (NULL),
	[height] [int] NULL,
	[bats] [varchar](1) NULL DEFAULT (NULL),
	[throws] [varchar](1) NULL DEFAULT (NULL),
	[debut] [varchar](10) NULL DEFAULT (NULL),
	[finalGame] [varchar](10) NULL DEFAULT (NULL),
	[college] [varchar](50) NULL DEFAULT (NULL),
	[lahman40ID] [varchar](9) NULL DEFAULT (NULL),
	[lahman45ID] [varchar](9) NULL DEFAULT (NULL),
	[retroID] [varchar](9) NULL DEFAULT (NULL),
	[holtzID] [varchar](9) NULL DEFAULT (NULL),
	[bbrefID] [varchar](9) NULL DEFAULT (NULL),
PRIMARY KEY CLUSTERED 
([lahmanID] ASC) ON [PRIMARY]
) ON [PRIMARY];
GO
 
INSERT INTO [dbo].[PlayerInfo]
           ([lahmanID]
           ,[playerID]
           ,[managerID]
           ,[hofID]
           ,[birthYear]
           ,[birthMonth]
           ,[birthDay]
           ,[birthCountry]
           ,[birthState]
           ,[birthCity]
           ,[deathYear]
           ,[deathMonth]
           ,[deathDay]
           ,[deathCountry]
           ,[deathState]
           ,[deathCity]
           ,[nameFirst]
           ,[nameLast]
           ,[nameNote]
           ,[nameGiven]
           ,[nameNick]
           ,[weight]
           ,[height]
           ,[bats]
           ,[throws]
           ,[debut]
           ,[finalGame]
           ,[college]
           ,[lahman40ID]
           ,[lahman45ID]
           ,[retroID]
           ,[holtzID]
           ,[bbrefID])
SELECT [lahmanID]
           ,[playerID]
           ,[managerID]
           ,[hofID]
           ,[birthYear]
           ,[birthMonth]
           ,[birthDay]
           ,[birthCountry]
           ,[birthState]
           ,[birthCity]
           ,[deathYear]
           ,[deathMonth]
           ,[deathDay]
           ,[deathCountry]
           ,[deathState]
           ,[deathCity]
           ,[nameFirst]
           ,[nameLast]
           ,[nameNote]
           ,[nameGiven]
           ,[nameNick]
           ,[weight]
           ,[height]
           ,[bats]
           ,[throws]
           ,[debut]
           ,[finalGame]
           ,[college]
           ,[lahman40ID]
           ,[lahman45ID]
           ,[retroID]
           ,[holtzID]
           ,[bbrefID]
FROM [dbo].[Players]
WHERE [lahmanID] <= 10000;
 
CREATE VIEW [PlayerPostSeason]
WITH SCHEMABINDING
AS
	SELECT 
		[p].[lahmanID], 
		[p].[nameFirst], 
		[p].[nameLast], 
		[p].[debut], 
		[p].[finalGame], 
		[pp].[yearID], 
		[pp].[round], 
		[pp].[teamID], 
		[pp].[W], 
		[pp].[L], 
		[pp].[G]
	FROM [dbo].[PlayerInfo] [p]
	JOIN [dbo].[PitchingPost] [pp] ON [p].[playerID] = [pp].[playerID];
 
CREATE UNIQUE CLUSTERED INDEX [CI_PlayerPostSeason] ON [PlayerPostSeason] ([lahmanID], [yearID], [round]);
 
CREATE NONCLUSTERED INDEX [NCI_PlayerPostSeason_Name] ON [PlayerPostSeason] ([nameFirst], [nameLast]);

Se controlliamo le statistiche per gli indici cluster e non cluster, vediamo che esistono:

DBCC SHOW_STATISTICS ('PlayerPostSeason', CI_PlayerPostSeason) WITH STAT_HEADER;
GO
DBCC SHOW_STATISTICS ('PlayerPostSeason', NCI_PlayerPostSeason_Name) WITH STAT_HEADER;
GO

Statistiche di visualizzazione dell'indice dopo la creazione iniziale

Ora inseriremo più righe in PlayerInfo:

INSERT INTO [dbo].[PlayerInfo]
           ([lahmanID]
           ,[playerID]
           ,[managerID]
           ,[hofID]
           ,[birthYear]
           ,[birthMonth]
           ,[birthDay]
           ,[birthCountry]
           ,[birthState]
           ,[birthCity]
           ,[deathYear]
           ,[deathMonth]
           ,[deathDay]
           ,[deathCountry]
           ,[deathState]
           ,[deathCity]
           ,[nameFirst]
           ,[nameLast]
           ,[nameNote]
           ,[nameGiven]
           ,[nameNick]
           ,[weight]
           ,[height]
           ,[bats]
           ,[throws]
           ,[debut]
           ,[finalGame]
           ,[college]
           ,[lahman40ID]
           ,[lahman45ID]
           ,[retroID]
           ,[holtzID]
           ,[bbrefID])
SELECT [lahmanID]
           ,[playerID]
           ,[managerID]
           ,[hofID]
           ,[birthYear]
           ,[birthMonth]
           ,[birthDay]
           ,[birthCountry]
           ,[birthState]
           ,[birthCity]
           ,[deathYear]
           ,[deathMonth]
           ,[deathDay]
           ,[deathCountry]
           ,[deathState]
           ,[deathCity]
           ,[nameFirst]
           ,[nameLast]
           ,[nameNote]
           ,[nameGiven]
           ,[nameNick]
           ,[weight]
           ,[height]
           ,[bats]
           ,[throws]
           ,[debut]
           ,[finalGame]
           ,[college]
           ,[lahman40ID]
           ,[lahman45ID]
           ,[retroID]
           ,[holtzID]
           ,[bbrefID]
FROM [dbo].[Players]
WHERE [lahmanID] > 10000;

E se controlliamo sys.dm_db_stats_properties, possiamo vedere le modifiche alla riga:

SELECT  
	[sch].[name] AS [Schema],
	[so].[name] AS [ObjectName],
	[so].[type] AS [ObjectType],
    [ss].[name] AS [Statistic],
    [sp].[last_updated] AS [StatsLastUpdated] ,
    [sp].[rows] AS [RowsInTable] ,
    [sp].[rows_sampled] AS [RowsSampled] ,
    [sp].[modification_counter] AS [RowModifications]
FROM [sys].[objects] [so]
JOIN [sys].[stats] [ss] ON [so].[object_id] = [ss].[object_id]
JOIN [sys].[schemas] [sch] ON [so].[schema_id] = [sch].[schema_id]
OUTER APPLY [sys].[dm_db_stats_properties]([so].[object_id],
                                                   [ss].[stats_id]) sp
WHERE [so].[name] = 'PlayerPostSeason';

Righe modificate nella vista indicizzata, tramite sys.dm_db_stats_properties

E solo per divertimento, se controlliamo sys.sysindexes, possiamo vedere le modifiche anche lì:

SELECT  [so].[name], [si].[name], [si].[rowcnt], [si].[rowmodctr]
FROM [sys].[sysindexes] [si]
JOIN [sys].[objects] [so] ON [si].[id] = [so].[object_id]
WHERE [so].[name] = 'PlayerPostSeason';

Righe modificate nella vista indicizzata, tramite sys.sysindexes

Ora sys.sysindexes è deprecato, ma se ricordi dal mio post precedente, è ciò che sp_updatestats usa per vedere cosa è stato modificato. Ma... l'elenco degli oggetti per sys.indexes è guidato dalla query su sys.objects, che, se ricordi, filtra sulle tabelle utente ("U") e sulle tabelle interne ("IT"). Non include le visualizzazioni ("V") in quel filtro. Pertanto, quando eseguiamo sp_updatestats e controlliamo l'output (non incluso per brevità), non viene menzionata la nostra vista PlayerPostSeason.

Pertanto, se disponi di viste indicizzate e ti affidi a sp_updatestats per aggiornare le statistiche, le statistiche delle viste non vengono aggiornate. Tuttavia, suppongo che la maggior parte di voi abbia l'opzione Statistiche di aggiornamento automatico abilitata per i propri database. Questo è positivo, perché con questa opzione, le statistiche di visualizzazione si aggiorneranno se sono state invalidate. Sappiamo di aver apportato oltre 2000 modifiche agli indici su PlayerPostSeason. Se eseguiamo una query in base a un nome selettivo, il nostro piano di query dovrebbe utilizzare l'indice NCI_PlayerPostSeason_Name e poiché le statistiche non sono aggiornate, dovrebbero essere aggiornate. Controlliamo:

SELECT *
FROM [PlayerPostSeason]
WHERE [nameFirst] = 'Madison';
GO

Piano di query da SELECT rispetto a un indice non cluster

Possiamo vedere nel piano che è stato utilizzato l'indice non cluster NCI_PlayerPostSeason_Name e se controlliamo le statistiche:

Statistiche dopo l'aggiornamento automatico

In effetti, le statistiche per l'indice non cluster sono state aggiornate. Ma ovviamente non vogliamo fare affidamento sull'aggiornamento automatico per gestire le statistiche, vogliamo essere proattivi. Abbiamo due opzioni:

  • Attività di manutenzione
  • Script personalizzato

L'attività di manutenzione delle statistiche di aggiornamento fa aggiornare le statistiche di visualizzazione. Questo non viene specificato in nessuna parte dell'interfaccia utente, ma se creiamo un piano di manutenzione con l'attività di aggiornamento delle statistiche e lo eseguiamo, le statistiche per la vista indicizzata vengono aggiornate. Lo svantaggio dell'attività di manutenzione delle statistiche di aggiornamento è che si tratta di un approccio a mazza. Aggiorna tutti statistiche, indipendentemente dal fatto che sia necessario (è quasi quanto sp_updatestats). Preferisco uno script personalizzato, in cui SQL Server aggiorna solo ciò che è stato modificato. Se non ti piace girare la tua sceneggiatura, puoi usare la sceneggiatura di Ola Hallengren. È comune aggiornare le statistiche durante le ricostruzioni e le riorganizzazioni dell'indice. Ad esempio, con lo script di Ola nel lavoro di SQL Agent avresti:

sqlcmd -E -S $(ESCAPE_SQUOTE(SRVR)) -d master -Q "EXECUTE [dbo].[IndexOptimize] @Databases ='BaseballData', @FragmentationLow =NULL, @FragmentationMedium ='INDEX_REORGANIZE', @FragmentationHigh ='INDEX_REBUILD ', @FragmentationLevel1 =5, @FragmentationLevel2 =30, @UpdateStatistics ='TUTTO', @OnlyModifiedStatistics ='Y', @LogToTable ='Y'" –b

Con questa opzione, se le statistiche sono state modificate, verranno aggiornate e se controlliamo la procedura memorizzata [dbo].[IndexOptimize] possiamo vedere dove Ola verifica le modifiche:

        -- Has the data in the statistics been modified since the statistics was last updated?
        IF @CurrentStatisticsID IS NOT NULL AND @UpdateStatistics IS NOT NULL AND @OnlyModifiedStatistics = 'Y'
        BEGIN
          SET @CurrentCommand10 = ''
          IF @LockTimeout IS NOT NULL SET @CurrentCommand10 = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
          IF (@Version >= 10.504000 AND @Version < 11) OR @Version >= 11.03000
          BEGIN
            SET @CurrentCommand10 = @CurrentCommand10 + 'USE ' + QUOTENAME(@CurrentDatabaseName) 
              + '; IF EXISTS(SELECT * FROM sys.dm_db_stats_properties (@ParamObjectID, @ParamStatisticsID) 
                   WHERE modification_counter > 0) BEGIN SET @ParamStatisticsModified = 1 END'
          END
          ELSE
          BEGIN
            SET @CurrentCommand10 = @CurrentCommand10 + 'IF EXISTS(SELECT * FROM ' 
              + QUOTENAME(@CurrentDatabaseName) + '.sys.sysindexes sysindexes 
              WHERE sysindexes.[id] = @ParamObjectID AND sysindexes.[indid] = @ParamStatisticsID 
              AND sysindexes.[rowmodctr] <> 0) BEGIN SET @ParamStatisticsModified = 1 END'
          END

Per le versioni che supportano il DMF sys.dm_db_stats_properties, Ola controlla le statistiche che sono state modificate e per le versioni che non supportano il nuovo DMF sys.dm_db_stats_properties, la tabella di sistema sys.sysindexes viene controllata. La mia unica lamentela qui è che lo script si comporta allo stesso modo di sp_updatestats:se almeno una riga è stata modificata, la statistica verrà aggiornata.

Se non hai intenzione di scrivere il tuo codice per la gestione delle statistiche, ti consiglio di attenersi allo script di Ola. Ma se vuoi indirizzare un po' di più i tuoi aggiornamenti, ti consiglio di usare sys.dm_db_stats_properties. Questo DMF è disponibile solo per SQL Server 2008R2 SP2 e versioni successive e SQL Server 2012 SP1 e versioni successive, quindi se utilizzi una versione precedente, dovrai usare sys.indexes. Ma per quelli di voi che hanno accesso a sys.dm_db_stats_properties, ecco una query per iniziare:

SELECT
	[sch].[name] AS [Schema],
	[so].[name] AS [ObjectName],
	[so].[type] AS [ObjectType],
	[ss].[name] AS [Statistic],
	[sp].[last_updated] AS [StatsLastUpdated] ,
	[sp].[rows] AS [RowsInTable] ,
	[sp].[rows_sampled] AS [RowsSampled] ,
	CAST(100 * [sp].[rows_sampled] / [sp].[rows] AS DECIMAL (18, 2)) AS [PercentSampled],
	[sp].[modification_counter] AS [RowModifications] ,
	CAST(100 * [sp].[modification_counter] / [sp].[rows] AS DECIMAL(18, 2)) AS [PercentChange]
FROM [sys].[objects] AS [so]
INNER JOIN [sys].[stats] AS [ss] ON [so].[object_id] = [ss].[object_id]
INNER JOIN [sys].[schemas] AS [sch] ON [so].[schema_id] = [sch].[schema_id]
OUTER APPLY [sys].[dm_db_stats_properties]([so].[object_id], [ss].[stats_id]) AS [sp]
WHERE [so].[type] IN ('U','V')
AND ((CAST(100 * [sp].[modification_counter] / [sp].[rows] AS DECIMAL(18,2)) >= 10.0))
ORDER BY CAST(100 * [sp].[modification_counter] / [sp].[rows] AS DECIMAL(18, 2)) DESC;

Nota che con sys.objects filtriamo su tabelle e viste; potresti modificarlo per includere le tabelle di sistema. È quindi possibile modificare il predicato per recuperare solo le righe in base alla percentuale di righe modificate, o forse una combinazione di percentuale di modifica e numero di righe (per tabelle con milioni o miliardi di righe, tale percentuale potrebbe essere inferiore rispetto alle tabelle piccole).

Riepilogo

Il messaggio da portare a casa qui è abbastanza chiaro:non consiglio di usare sp_updatestats per gestire le statistiche. Le statistiche vengono aggiornate quando una o più righe vengono modificate (che è una soglia estremamente bassa per l'aggiornamento delle statistiche) e le statistiche per le visualizzazioni indicizzate non aggiornato. Questo non è un metodo completo ed efficiente per gestire le statistiche... e l'attività di aggiornamento delle statistiche in un piano di manutenzione non è molto migliore. Aggiorna le statistiche di visualizzazione indicizzata, ma aggiorna ogni statistica, indipendentemente dalle modifiche. Uno script personalizzato è davvero la strada da percorrere, ma capisci che lo script di Ola Hallengren, se stai aggiornando in base alla modifica, si aggiorna anche quando è stata modificata solo la riga (ma almeno ottiene le viste indicizzate). Alla fine, per il miglior controllo, cerca di eseguire il tuo script per la gestione delle statistiche. Ti ho dato la query di base per iniziare. Se puoi bloccare un paio d'ore per esercitarti con la scrittura T-SQL e poi testarlo, avrai uno script personalizzato funzionante pronto per i tuoi database prima che arrivino le vacanze.