Database
 sql >> Base de Dados >  >> RDS >> Database

Outro motivo para evitar sp_updatestats


Eu escrevi anteriormente sobre por que eu não amo sp_updatestats. Recentemente, encontrei outro motivo para não ser meu amigo. TL;DR:não atualiza as estatísticas das visualizações indexadas. Agora, a documentação não afirma que sim, então não há bug aqui. A documentação do MSDN afirma claramente:
Executa UPDATE STATISTICS em todas as tabelas internas e definidas pelo usuário no banco de dados atual.
Mas… quantos de vocês pensaram em suas visualizações indexadas e se perguntaram se elas foram atualizadas? Admito que não. Eu esqueço das visualizações indexadas, o que é lamentável porque elas podem ser realmente poderosas quando usadas adequadamente. Eles também podem ser um pesadelo para desvendar quando você estiver solucionando problemas, mas não vou discutir seu uso hoje. Eu só quero que você esteja ciente de que eles não são atualizados pelo sp_updatestats e veja quais opções você tem.

Configuração

Como a World Series acabou de terminar, vamos usar o banco de dados de beisebol para nossos testes. Você pode baixá-lo na página Recursos do SQLskills. Uma vez restaurada, criaremos uma cópia da tabela dbo.Players, chamada dbo.PlayerInfo, carregaremos alguns milhares de linhas nela e criaremos uma visualização indexada que une nossa nova tabela à tabela 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 verificarmos as estatísticas dos índices clusterizados e não clusterizados, veremos que eles existem:
DBCC SHOW_STATISTICS ('PlayerPostSeason', CI_PlayerPostSeason) WITH STAT_HEADER;
GO
DBCC SHOW_STATISTICS ('PlayerPostSeason', NCI_PlayerPostSeason_Name) WITH STAT_HEADER;
GO

Indexar estatísticas de visualização após a criação inicial

Agora vamos inserir mais linhas em 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 verificarmos sys.dm_db_stats_properties, podemos ver as modificações de linha:
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';

Linhas modificadas na exibição indexada, via sys.dm_db_stats_properties

E só por diversão, se verificarmos sys.sysindexes, podemos ver as modificações lá também:
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';

Linhas modificadas na exibição indexada, via sys.sysindexes

Agora sys.sysindexes está obsoleto, mas se você se lembra do meu post anterior, é isso que sp_updatestats usa para ver o que foi modificado. Mas… a lista de objetos para sys.indexes é conduzida pela consulta em sys.objects, que, se você se lembra, filtra em tabelas de usuários ('U') e tabelas internas ('IT'). Não inclui visualizações ('V') nesse filtro. Como tal, quando executamos sp_updatestats e verificamos a saída (não incluída para brevidade), não há menção à nossa visualização PlayerPostSeason.

Portanto, se você tiver visualizações indexadas e estiver contando com sp_updatestats para atualizar suas estatísticas, suas estatísticas de visualização não serão atualizadas. No entanto, acho que a maioria de vocês tem a opção Estatísticas de atualização automática habilitada para seus bancos de dados. Isso é bom, porque com esta opção, as estatísticas de visualização serão atualizadas se forem invalidadas. Sabemos que fizemos mais de 2.000 modificações nos índices do PlayerPostSeason. Se consultarmos por um primeiro nome seletivo, nosso plano de consulta deve usar o índice NCI_PlayerPostSeason_Name e, como as estatísticas estão desatualizadas, elas devem ser atualizadas. Vamos checar:
SELECT *
FROM [PlayerPostSeason]
WHERE [nameFirst] = 'Madison';
GO

Plano de consulta do SELECT em relação ao índice não clusterizado

Podemos ver no plano que o índice não clusterizado NCI_PlayerPostSeason_Name foi usado e, se verificarmos as estatísticas:

Estatísticas após atualização automática

Com certeza, as estatísticas para o índice não clusterizado foram atualizadas. Mas é claro que não queremos confiar na atualização automática para gerenciar as estatísticas, queremos ser proativos. Temos duas opções:
  • Tarefa de manutenção
  • Script personalizado

A tarefa de manutenção de estatísticas de atualização faz atualizar estatísticas de visualização. Isso não é chamado especificamente em nenhum lugar da interface do usuário, mas se criarmos um plano de manutenção com a tarefa de atualização de estatísticas e a executarmos, as estatísticas da exibição indexada serão atualizadas. A desvantagem da tarefa de manutenção de estatísticas de atualização é que é uma abordagem de marreta. Ele atualiza todos estatísticas, independentemente de ser necessário (é quase tão ruim quanto sp_updatestats). Prefiro um script personalizado, onde o SQL Server atualiza apenas o que foi modificado. Se você não quiser rolar seu próprio script, você pode usar o script de Ola Hallengren. É comum atualizar estatísticas como parte de suas recompilações e reorganizações de índice. Por exemplo, com o script de Ola no trabalho do SQL Agent, você teria:
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 ='ALL', @OnlyModifiedStatistics ='Y', @LogToTable ='Y'" –b
Com esta opção, se as estatísticas foram modificadas, elas serão atualizadas, e se verificarmos o procedimento armazenado [dbo].[IndexOptimize] podemos ver onde Ola verifica as modificações:
        -- 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

Para versões que suportam o DMF sys.dm_db_stats_properties, o Ola verifica se há estatísticas que foram modificadas e para versões que não suportam o novo DMF sys.dm_db_stats_properties, a tabela de sistema sys.sysindexes é verificada. Minha única reclamação aqui é que o script se comporta da mesma forma que sp_updatestats:se pelo menos uma linha foi modificada, a estatística será atualizada.

Se você não gosta de escrever seu próprio código para gerenciar estatísticas, recomendo seguir o script de Ola. Mas se você quiser direcionar suas atualizações um pouco mais, recomendo usar sys.dm_db_stats_properties. Esse DMF está disponível apenas para SQL Server 2008R2 SP2 e superior e SQL Server 2012 SP1 e superior, portanto, se você estiver em uma versão inferior, precisará usar sys.indexes. Mas para aqueles com acesso a sys.dm_db_stats_properties, aqui está uma consulta para você começar:
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;

Observe que com sys.objects filtramos em tabelas e visualizações; você pode alterar isso para incluir tabelas do sistema. Você pode então modificar o predicado para recuperar apenas linhas com base na porcentagem de linhas modificadas ou talvez uma combinação de porcentagem de modificação e número de linhas (para tabelas com milhões ou bilhões de linhas, essa porcentagem pode ser menor do que para tabelas pequenas).

Resumo

A mensagem para levar para casa aqui é bem clara:não recomendo usar sp_updatestats para gerenciar estatísticas. As estatísticas são atualizadas quando uma ou mais linhas são alteradas (o que é um limite extremamente baixo para atualizar as estatísticas) e as estatísticas das visualizações indexadas não Atualizada. Este não é um método abrangente e eficiente para gerenciar estatísticas... e a tarefa de atualização de estatísticas em um Plano de Manutenção não é muito melhor. Ele atualiza as estatísticas de visualização indexada, mas atualiza todas estatística, independentemente de modificações. Um script personalizado é realmente o caminho a seguir, mas entenda que o script de Ola Hallengren, se você estiver atualizando com base na modificação, também atualiza quando apenas a linha foi modificada (mas pelo menos obtém as visualizações indexadas). No final, para melhor controle, procure rolar seu próprio script para gerenciar estatísticas. Eu lhe dei a consulta básica para começar. Se você puder reservar algumas horas para praticar sua escrita T-SQL e depois testá-la, você terá um script personalizado funcionando pronto para seus bancos de dados antes das férias chegarem.