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

Ein weiterer Grund, sp_updatestats zu vermeiden

Ich habe zuvor darüber gebloggt, warum ich sp_updatestats nicht liebe. Ich habe kürzlich einen anderen Grund gefunden, warum es nicht mein Freund ist. TL;DR:Es aktualisiert keine Statistiken zu indizierten Aufrufen. Nun, die Dokumentation behauptet nicht, dass dies der Fall ist, also gibt es hier keinen Fehler. In der MSDN-Dokumentation heißt es eindeutig:

Führt UPDATE STATISTICS für alle benutzerdefinierten und internen Tabellen in der aktuellen Datenbank aus.

Aber… wie viele von Ihnen haben über Ihre indizierten Ansichten nachgedacht und sich gefragt, ob diese aktualisiert wurden? Ich gebe zu, ich tat es nicht. Ich vergesse indizierte Ansichten, was bedauerlich ist, da sie bei richtiger Verwendung sehr leistungsfähig sein können. Es kann auch ein Albtraum sein, sie zu enträtseln, wenn Sie Fehler beheben, aber ich werde heute nicht über ihre Verwendung streiten. Ich möchte nur, dass Sie wissen, dass sie nicht von sp_updatestats aktualisiert werden, und sehen, welche Optionen Sie haben.

Einrichtung

Da die World Series gerade zu Ende gegangen ist, werden wir die Baseball-Datenbank für unsere Tests verwenden. Sie können es von der Seite SQLskills-Ressourcen herunterladen. Nach der Wiederherstellung erstellen wir eine Kopie der dbo.Players-Tabelle mit dem Namen dbo.PlayerInfo, laden einige tausend Zeilen hinein und erstellen dann eine indizierte Ansicht, die unsere neue Tabelle mit der PitchingPost-Tabelle verbindet:

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]);

Wenn wir die Statistiken für die geclusterten und nicht geclusterten Indizes überprüfen, sehen wir, dass sie existieren:

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

Indexansichtsstatistiken nach der ursprünglichen Erstellung

Jetzt fügen wir weitere Zeilen in PlayerInfo:

ein
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;

Und wenn wir sys.dm_db_stats_properties überprüfen, können wir die Zeilenänderungen sehen:

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';

Zeilen in der indizierten Ansicht geändert, über sys.dm_db_stats_properties

Und nur zum Spaß, wenn wir sys.sysindexes überprüfen, können wir die Änderungen auch dort sehen:

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';

Zeilen in der indizierten Ansicht geändert, über sys.sysindexes

Jetzt ist sys.sysindexes veraltet, aber wenn Sie sich an meinen vorherigen Beitrag erinnern, verwendet sp_updatestats das, um zu sehen, was geändert wurde. Aber … die Objektliste für sys.indexes wird durch die Abfrage von sys.objects gesteuert, die, wenn Sie sich erinnern, nach Benutzertabellen ('U') und internen Tabellen ('IT') filtert. Ansichten ('V') sind in diesem Filter nicht enthalten. Wenn wir also sp_updatestats ausführen und die Ausgabe überprüfen (der Kürze halber nicht enthalten), wird unsere PlayerPostSeason-Ansicht nicht erwähnt.

Wenn Sie also indizierte Ansichten haben und sich auf sp_updatestats verlassen, um Ihre Statistiken zu aktualisieren, werden Ihre Ansichtsstatistiken nicht aktualisiert. Ich vermute jedoch, dass die meisten von Ihnen die Option „Statistiken automatisch aktualisieren“ für Ihre Datenbanken aktiviert haben. Das ist gut, denn mit dieser Option werden Ansichtsstatistiken aktualisiert, wenn sie ungültig gemacht wurden. Wir wissen, dass wir über 2000 Änderungen an den Indizes auf PlayerPostSeason vorgenommen haben. Wenn wir nach einem selektiven Vornamen abfragen, sollte unser Abfrageplan den NCI_PlayerPostSeason_Name-Index verwenden, und da die Statistiken veraltet sind, sollten sie aktualisiert werden. Prüfen wir:

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

Plan von SELECT gegen Nonclustered-Index abfragen

Wir können im Plan sehen, dass der nicht gruppierte Index NCI_PlayerPostSeason_Name verwendet wurde, und wenn wir die Statistiken überprüfen:

Statistik nach automatischer Aktualisierung

Tatsächlich wurden die Statistiken für den Nonclustered-Index aktualisiert. Aber natürlich wollen wir uns nicht auf die automatische Aktualisierung verlassen, um Statistiken zu verwalten, wir wollen proaktiv sein. Wir haben zwei Möglichkeiten:

  • Wartungsaufgabe
  • Benutzerdefiniertes Skript

Die Wartungsaufgabe Statistik aktualisieren erledigt Ansichtsstatistik aktualisieren. Dies wird nirgendwo in der Benutzeroberfläche ausdrücklich erwähnt, aber wenn wir einen Wartungsplan mit der Aufgabe „Statistiken aktualisieren“ erstellen und ausführen, werden die Statistiken für die indizierte Ansicht aktualisiert. Der Nachteil der Wartungsaufgabe zum Aktualisieren von Statistiken besteht darin, dass es sich um einen Vorschlaghammeransatz handelt. Es aktualisiert alle Statistiken, unabhängig davon, ob es benötigt wird (es ist fast so schlimm wie sp_updatestats). Ich bevorzuge ein benutzerdefiniertes Skript, bei dem SQL Server nur das aktualisiert, was geändert wurde. Wenn Sie kein eigenes Drehbuch schreiben möchten, können Sie das Drehbuch von Ola Hallengren verwenden. Es ist üblich, Statistiken im Rahmen Ihrer Indexneuerstellung und -reorgs zu aktualisieren. Mit Olas Skript im SQL Agent-Job hätten Sie beispielsweise:

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

Wenn Statistiken geändert wurden, werden sie mit dieser Option aktualisiert, und wenn wir die gespeicherte Prozedur [dbo].[IndexOptimize] überprüfen, können wir sehen, wo Ola nach Änderungen sucht:

        -- 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

Bei Versionen, die das DMF sys.dm_db_stats_properties unterstützen, überprüft Ola es auf geänderte Statistiken, und bei Versionen, die das neue DMF sys.dm_db_stats_properties nicht unterstützen, wird die Systemtabelle sys.sysindexes überprüft. Meine einzige Beschwerde hier ist, dass sich das Skript genauso verhält wie sp_updatestats:Wenn mindestens eine Zeile geändert wurde, wird die Statistik aktualisiert.

Wenn Sie keinen eigenen Code zum Verwalten von Statistiken schreiben möchten, würde ich empfehlen, bei Olas Skript zu bleiben. Wenn Sie Ihre Updates jedoch etwas gezielter durchführen möchten, empfehle ich die Verwendung von sys.dm_db_stats_properties. Dieses DMF ist nur für SQL Server 2008R2 SP2 und höher und SQL Server 2012 SP1 und höher verfügbar. Wenn Sie also eine niedrigere Version verwenden, müssen Sie sys.indexes verwenden. Aber für diejenigen unter Ihnen, die Zugriff auf sys.dm_db_stats_properties haben, hier ist eine Abfrage, um Ihnen den Einstieg zu erleichtern:

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;

Beachten Sie, dass wir mit sys.objects nach Tabellen und Views filtern; Sie könnten dies ändern, um Systemtabellen einzuschließen. Sie können das Prädikat dann so ändern, dass nur Zeilen basierend auf dem Prozentsatz der geänderten Zeilen oder vielleicht einer Kombination aus Änderungsprozentsatz und Anzahl der Zeilen abgerufen werden (bei Tabellen mit Millionen oder Milliarden Zeilen kann dieser Prozentsatz niedriger sein als bei kleinen Tabellen).

Zusammenfassung

Die Take-Home-Message hier ist ziemlich klar:Ich empfehle nicht, sp_updatestats zu verwenden, um Statistiken zu verwalten. Statistiken werden aktualisiert, wenn sich eine oder mehrere Zeilen geändert haben (was ein extrem niedriger Schwellenwert für die Aktualisierung von Statistiken ist), und Statistiken für indizierte Ansichten werden nicht aktualisiert Aktualisiert. Dies ist keine umfassende und effiziente Methode zum Verwalten von Statistiken ... und die Aufgabe zum Aktualisieren von Statistiken in einem Wartungsplan ist nicht viel besser. Es aktualisiert die indizierten Ansichtsstatistiken, aber es aktualisiert alle Statistik, unabhängig von Änderungen. Ein benutzerdefiniertes Skript ist wirklich der richtige Weg, aber verstehen Sie, dass das Skript von Ola Hallengren, wenn Sie basierend auf einer Änderung aktualisieren, auch aktualisiert, wenn nur eine Zeile geändert wurde (aber es erhält zumindest die indizierten Ansichten). Am Ende sollten Sie für die beste Kontrolle versuchen, Ihr eigenes Skript zum Verwalten von Statistiken zu erstellen. Ich habe Ihnen die Basisabfrage gegeben, um zu beginnen. Wenn Sie sich ein paar Stunden Zeit nehmen können, um das Schreiben von T-SQL zu üben und es dann auszuprobieren, haben Sie vor den Feiertagen ein funktionierendes benutzerdefiniertes Skript für Ihre Datenbanken parat.