sql >> データベース >  >> RDS >> Database

sp_updatestatsを回避するもう1つの理由

    私は以前、sp_updatestatsが好きではない理由についてブログを書きました。私は最近、それが私の友達ではないという別の理由を見つけました。 TL; DR:インデックス付きビューの統計は更新されません。 さて、ドキュメントはそれがそうだと主張していないので、ここにバグはありません。 MSDNのドキュメントには次のように明記されています。

    現在のデータベース内のすべてのユーザー定義テーブルと内部テーブルに対してUPDATESTATISTICSを実行します。

    しかし…インデックスに登録されたビューについて考え、それらが更新されたかどうか疑問に思った人はどれくらいいますか?私はそうしなかったことを認めます。インデックス付きのビューを忘れてしまいました。適切に使用すると非常に強力になる可能性があるため、残念です。また、トラブルシューティングの際に解明するのは悪夢かもしれませんが、今日はそれらの使用について議論するつもりはありません。 sp_updatestatsによって更新されないことを認識し、どのようなオプションがあるかを確認してください。

    セットアップ

    ワールドシリーズが終了したばかりなので、テストには野球データベースを使用します。 SQLskillsリソースページからダウンロードできます。復元したら、dbo.PlayerInfoという名前のdbo.Playersテーブルのコピーを作成し、それに数千行をロードしてから、新しいテーブルを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]);

    クラスター化インデックスと非クラスター化インデックスの統計を確認すると、それらが存在することがわかります。

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

    最初の作成後のインデックスビューの統計

    次に、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;

    また、sys.dm_db_stats_propertiesを確認すると、行の変更を確認できます。

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

    sys.dm_db_stats_propertiesを介して、インデックス付きビューで変更された行

    そして、楽しみのために、sys.sysindexesをチェックすると、そこでの変更も確認できます:

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

    sys.sysindexesを介してインデックス付きビューで変更された行

    現在、sys.sysindexesは非推奨ですが、以前の投稿を思い出してください。これは、sp_updatestatsが変更内容を確認するために使用するものです。しかし…sys.indexesのオブジェクトリストは、sys.objectsに対するクエリによって駆動されます。これは、覚えていれば、ユーザーテーブル(「U」)と内部テーブル(「IT」)をフィルタリングします。そのフィルターにはビュー(「V」)は含まれません。そのため、sp_updatestatsを実行して出力を確認すると(簡潔にするために含まれていません)、PlayerPostSeasonビューについては言及されていません。

    したがって、インデックス付きのビューがあり、統計の更新をsp_updatestatsに依存している場合、ビューの統計は更新されません。ただし、ほとんどの場合、データベースで[統計の自動更新]オプションが有効になっていると思います。このオプションを使用すると、無効にされた場合にビュー統計が更新されるため、これは適切です。 PlayerPostSeasonのインデックスに2000を超える変更を加えたことはわかっています。選択的な名でクエリを実行する場合、クエリプランではNCI_PlayerPostSeason_Nameインデックスを使用する必要があり、統計が古くなっているため、更新する必要があります。確認しましょう:

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

    非クラスター化インデックスに対するSELECTからのクエリプラン

    計画では、NCI_PlayerPostSeason_Name非クラスター化インデックスが使用されていることがわかります。統計を確認すると、次のようになります。

    自動更新後の統計

    案の定、非クラスター化インデックスの統計が更新されました。しかしもちろん、統計を管理するために自動更新に依存するのではなく、積極的に行動したいと考えています。 2つのオプションがあります:

    • メンテナンスタスク
    • カスタムスクリプト

    統計の更新保守タスクは実行します ビュー統計を更新します。これはUIのどこにも特に呼び出されませんが、統計の更新タスクを使用してメンテナンスプランを作成して実行すると、インデックス付きビューの統計が更新されます。更新統計保守タスクの欠点は、それがスレッジハンマーアプローチであるということです。 すべてを更新します 必要かどうかに関係なく、統計(sp_updatestatsとほぼ同じくらい悪い)。 SQLServerが変更されたものだけを更新するカスタムスクリプトが好きです。独自のスクリプトを作成することに興味がない場合は、OlaHallengrenのスクリプトを使用できます。インデックスの再構築と再編成の一環として統計を更新するのが一般的です。たとえば、SQLエージェントジョブにOlaのスクリプトを使用すると、次のようになります。

    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

    このオプションを使用すると、統計が変更されている場合は更新され、[dbo]。[IndexOptimize]ストアドプロシージャをチェックすると、Olaが変更をチェックする場所を確認できます。

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

    sys.dm_db_stats_properties DMFをサポートするバージョンの場合、Olaは変更された統計をチェックし、新しいsys.dm_db_stats_properties DMFをサポートしないバージョンの場合、sys.sysindexesシステムテーブルがチェックされます。ここでの私の唯一の不満は、スクリプトがsp_updatestatsと同じように動作することです。少なくとも1つの行が変更されている場合、統計が更新されます。

    統計を管理するための独自のコードを作成することに興味がない場合は、Olaのスクリプトを使用することをお勧めします。ただし、更新をもう少しターゲットにしたい場合は、sys.dm_db_stats_propertiesを使用することをお勧めします。このDMFは、SQL Server2008R2SP2以降およびSQLServer2012 SP1以降でのみ使用できるため、下位バージョンを使用している場合は、sys.indexesを使用する必要があります。ただし、sys.dm_db_stats_propertiesにアクセスできる方のために、開始するためのクエリを次に示します。

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

    sys.objectsを使用して、テーブルとビューでフィルタリングすることに注意してください。これを変更して、システムテーブルを含めることができます。次に、述語を変更して、変更された行のパーセンテージ、または変更のパーセンテージと行数の組み合わせに基づいて行のみを取得することができます(数百万または数十億の行があるテーブルの場合、そのパーセンテージは小さなテーブルの場合よりも低くなる可能性があります)。

    概要

    ここでの持ち帰りメッセージは非常に明確です。統計を管理するためにsp_updatestatsを使用することはお勧めしません。 1つ以上の行が変更されたときに統計が更新され(これは統計を更新するための非常に低いしきい値です)、インデックス付きビューの統計はではありません。 更新しました。これは、統計を管理するための包括的で効率的な方法ではありません…そして、メンテナンスプランの統計の更新タスクはそれほど良くありません。インデックス付きビューの統計を更新しますが、すべてを更新します 変更に関係なく、統計。カスタムスクリプトは実際に使用する方法ですが、Ola Hallengrenのスクリプトは、変更に基づいて更新する場合、行のみが変更されたときにも更新されることを理解してください(ただし、少なくともインデックス付きのビューを取得します)。最後に、最適な制御を行うには、統計を管理するための独自のスクリプトを作成することを検討してください。開始するための基本クエリを提供しました。数時間ブロックしてT-SQLの記述を練習し、それをテストできる場合は、休日が始まる前に、データベース用に機能するカスタムスクリプトを用意できます。


    1. 自作postgresが壊れた

    2. エンティティフレームワーク6にMySQL接続を使用できません

    3. カーディナリティがパフォーマンスにどのように影響するかをご覧ください

    4. PostgreSQL9.0でのPgbouncerを使用した接続プール