Anteriormente escribí en un blog sobre por qué no me encanta sp_updatestats. Recientemente encontré otra razón por la que no es mi amigo. TL;DR:no actualiza las estadísticas de las vistas indexadas. Ahora, la documentación no afirma que lo haga, por lo que no hay ningún error aquí. La documentación de MSDN establece claramente:
Ejecuta UPDATE STATISTICS en todas las tablas internas y definidas por el usuario en la base de datos actual.Pero... ¿cuántos de ustedes pensaron en sus vistas indexadas y se preguntaron si se actualizaron? Admito que no lo hice. Me olvido de las vistas indexadas, lo cual es desafortunado porque pueden ser realmente poderosas cuando se usan de manera adecuada. También pueden ser una pesadilla para resolver cuando está solucionando problemas, pero no voy a discutir su uso hoy. Solo quiero que tenga en cuenta que sp_updatestats no los actualiza y vea qué opciones tiene.
Configuración
Dado que la Serie Mundial acaba de terminar, vamos a utilizar la base de datos de béisbol para nuestras pruebas. Puede descargarlo desde la página de recursos de SQLskills. Una vez restaurada, crearemos una copia de la tabla dbo.Players, llamada dbo.PlayerInfo, cargaremos unas miles de filas en ella y luego crearemos una vista indexada que una nuestra nueva tabla con la tabla 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]);
Si revisamos las estadísticas de los índices agrupados y no agrupados, vemos que existen:
DBCC SHOW_STATISTICS ('PlayerPostSeason', CI_PlayerPostSeason) WITH STAT_HEADER; GO DBCC SHOW_STATISTICS ('PlayerPostSeason', NCI_PlayerPostSeason_Name) WITH STAT_HEADER; GO
Estadísticas de vista de índice después de la creación inicial
Ahora insertaremos más filas en 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;
Y si revisamos sys.dm_db_stats_properties, podemos ver las modificaciones de fila:
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';
Filas modificadas en la vista indexada, a través de sys.dm_db_stats_properties
Y solo por diversión, si revisamos sys.sysindexes, también podemos ver las modificaciones allí:
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';
Filas modificadas en la vista indexada, a través de sys.sysindexes
Ahora sys.sysindexes está en desuso, pero si recuerda mi publicación anterior, eso es lo que usa sp_updatestats para ver qué se ha modificado. Pero... la lista de objetos para sys.indexes está impulsada por la consulta contra sys.objects, que, si recuerda, filtra las tablas de usuario ('U') y las tablas internas ('IT'). No incluye vistas ('V') en ese filtro. Como tal, cuando ejecutamos sp_updatestats y verificamos el resultado (no incluido por brevedad), no se menciona nuestra vista PlayerPostSeason.
Por lo tanto, si tiene vistas indexadas y confía en sp_updatestats para actualizar sus estadísticas, sus estadísticas de vista no se actualizarán. Sin embargo, supongo que la mayoría de ustedes tiene habilitada la opción Actualización automática de estadísticas para sus bases de datos. Esto es bueno, porque con esta opción, las estadísticas de visualización se actualizarán si han sido invalidadas. Sabemos que hemos realizado más de 2000 modificaciones en los índices de PlayerPostSeason. Si consultamos por un nombre que es selectivo, nuestro plan de consulta debe usar el índice NCI_PlayerPostSeason_Name y, debido a que las estadísticas están desactualizadas, deben actualizarse. Comprobemos:
SELECT * FROM [PlayerPostSeason] WHERE [nameFirst] = 'Madison'; GO
Plan de consulta de SELECT contra índice no agrupado
Podemos ver en el plan que se utilizó el índice no agrupado NCI_PlayerPostSeason_Name, y si revisamos las estadísticas:
Estadísticas después de la actualización automática
Efectivamente, las estadísticas del índice no agrupado se han actualizado. Pero, por supuesto, no queremos depender de la actualización automática para administrar las estadísticas, queremos ser proactivos. Tenemos dos opciones:
- Tarea de mantenimiento
- Guión personalizado
La tarea de mantenimiento de estadísticas de actualización sí actualizar las estadísticas de visualización. Esto no se menciona específicamente en ninguna parte de la interfaz de usuario, pero si creamos un plan de mantenimiento con la tarea de actualización de estadísticas y lo ejecutamos, las estadísticas de la vista indexada se actualizan. El inconveniente de la tarea de mantenimiento de estadísticas de actualización es que es un enfoque de mazo. Actualiza todo estadísticas, independientemente de si es necesario (es casi tan malo como sp_updatestats). Prefiero un script personalizado, donde SQL Server solo actualiza lo que se ha modificado. Si no le gusta rodar su propio guión, puede usar el guión de Ola Hallengren. Es común actualizar las estadísticas como parte de las reconstrucciones y reorganizaciones de su índice. Por ejemplo, con el script de Ola en el trabajo del Agente SQL tendría:
sqlcmd -E -S $(ESCAPE_SQUOTE(SRVR)) -d master -Q "EJECUTAR [dbo].[IndexOptimize] @Databases ='BaseballData', @FragmentationLow =NULL, @FragmentationMedium ='INDEX_REORGANIZE', @FragmentationHigh ='INDEX_REBUILD ', @FragmentationLevel1 =5, @FragmentationLevel2 =30, @UpdateStatistics ='TODO', @OnlyModifiedStatistics ='Y', @LogToTable ='Y'" –bCon esta opción, si se han modificado las estadísticas, se actualizarán, y si comprobamos el procedimiento almacenado [dbo].[IndexOptimize] podemos ver dónde comprueba Ola si hay modificaciones:
-- 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 las versiones que admiten el DMF sys.dm_db_stats_properties, Ola comprueba si hay estadísticas que se hayan modificado, y para las versiones que no admiten el nuevo DMF sys.dm_db_stats_properties, se comprueba la tabla del sistema sys.sysindexes. Mi única queja aquí es que el script se comporta de la misma manera que sp_updatestats:si se ha modificado al menos una fila, la estadística se actualizará.
Si no está interesado en escribir su propio código para administrar las estadísticas, entonces le recomendaría seguir con el script de Ola. Pero si desea orientar sus actualizaciones un poco más, le recomiendo usar sys.dm_db_stats_properties. Este DMF solo está disponible para SQL Server 2008R2 SP2 y versiones posteriores, y SQL Server 2012 SP1 y versiones posteriores, por lo que si tiene una versión anterior, deberá usar sys.indexes. Pero para aquellos de ustedes con acceso a sys.dm_db_stats_properties, aquí hay una consulta para comenzar:
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;
Tenga en cuenta que con sys.objects filtramos en tablas y vistas; puede modificar esto para incluir tablas del sistema. A continuación, puede modificar el predicado para recuperar filas solo en función del porcentaje de filas modificadas, o tal vez una combinación de porcentaje de modificación y número de filas (para tablas con millones o miles de millones de filas, ese porcentaje puede ser menor que para tablas pequeñas).
Resumen
El mensaje para llevar a casa aquí es bastante claro:no recomiendo usar sp_updatestats para administrar estadísticas. Las estadísticas se actualizan cuando una o más filas han cambiado (que es un umbral extremadamente bajo para actualizar las estadísticas) y las estadísticas para las vistas indexadas no actualizado. Este no es un método integral y eficiente para administrar estadísticas... y la tarea de actualizar estadísticas en un Plan de mantenimiento no es mucho mejor. Actualiza las estadísticas de vistas indexadas, pero actualiza cada estadística, independientemente de las modificaciones. Una secuencia de comandos personalizada es realmente el camino a seguir, pero comprenda que la secuencia de comandos de Ola Hallengren, si está actualizando según la modificación, también se actualiza cuando solo se ha modificado la fila (pero al menos obtiene las vistas indexadas). Al final, para obtener el mejor control, busque implementar su propio script para administrar las estadísticas. Te he dado la consulta base para empezar. Si puede reservar un par de horas para practicar su escritura T-SQL y luego probarlo, tendrá un script personalizado en funcionamiento listo para sus bases de datos antes de que lleguen las vacaciones.