web-dev-qa-db-ja.com

どのくらいの頻度で統計を更新する必要がありますか?

どのくらいの頻度で統計を更新する必要がありますか? 「めったにない」とは何ですか? 「頻度が高すぎる」はどのくらいの頻度ですか?

答えは、データベース、ユーザー、データなどによって「異なります」です。

したがって、私は統計がどのように見えるかを2つの表に記録しようとしました。はい、どうぞ:

DROP TABLE /*IF EXISTS */ dbo.dm_db_stats_histogram
DROP TABLE /*IF EXISTS */ dbo.dm_db_stats_properties
go
CREATE TABLE dbo.dm_db_stats_properties(
  dm_db_stats_propertiesID INT IDENTITY(1,1) NOT NULL constraint PK_dm_db_stats_properties PRIMARY KEY CLUSTERED,
  DatabaseId INT NOT NULL,
  object_id int NOT NULL,
  stats_id  int NOT NULL,
  last_updated  DATETIME2 NOT NULL,
  rows  BIGINT NOT NULL,
  rows_sampled  BIGINT NOT NULL,
  steps int NOT NULL,
  unfiltered_rows   BIGINT NOT NULL,
  modification_counter  BIGINT NOT NULL,
  persisted_sample_percent  FLOAT  NULL
  , SampleDate DATETIME2 NOT NULL CONSTRAINT df_dm_db_stats_properties_SampleDate DEFAULT SYSUTCDATETIME()
)
GO
ALTER TABLE dbo.dm_db_stats_properties ADD StatsName NVARCHAR(128) NOT NULL CONSTRAINT df_dm_db_stats_properties_StatsName DEFAULT ('')

GO
CREATE TABLE dbo.dm_db_stats_histogram(
  dm_db_stats_histogramID INT IDENTITY(1,1) NOT NULL constraint PK_dm_db_stats_histogram PRIMARY KEY CLUSTERED,
  dm_db_stats_propertiesID INT NOT NULL, 
  object_id int NOT NULL,
  stats_id  int NOT NULL,
  step_number   int NOT NULL,
  range_high_key    sql_variant NOT NULL,
  range_rows    real    NOT NULL,
  equal_rows    real    NOT NULL,
  distinct_range_rows   bigint  NOT NULL,
  average_range_rows    REAL NOT NULL
)
go
ALTER TABLE dbo.dm_db_stats_histogram ADD CONSTRAINT fk_dm_db_stats_properties FOREIGN KEY(dm_db_stats_propertiesID) REFERENCES dbo.dm_db_stats_properties(dm_db_stats_propertiesID)
ALTER TABLE dbo.dm_db_stats_histogram ALTER COLUMN range_high_key SQL_VARIANT NULL
GO

そして、ここに私が統計を記録するために使用するコードがあります:

SET NOCOUNT ON
BEGIN TRY
  DROP TABLE #Stat_Header
END TRY
BEGIN CATCH
END CATCH
CREATE TABLE #Stat_Header (Name sysname, Updated DATETIME, Rows BIGINT, Rows_Sampled BIGINT, Steps SMALLINT, Density REAL, AverageKeyLength INT, StringIndex varchar(10)
, FilterExpression varchar(8000), unfiltered_rows bigint, persisted_sample_percent float)

BEGIN TRY
  DROP TABLE #Histogram
END TRY
BEGIN CATCH
END CATCH
CREATE TABLE #Histogram (Step_Number INT IDENTITY(1,1), range_high_key SQL_VARIANT, range_rows REAL NOT NULL, equal_rows    REAL NOT NULL, distinct_range_rows BIGINT NOT NULL, average_range_rows REAL NOT NULL)


DECLARE TableCursor CURSOR LOCAL STATIC FOR 
    SELECT t.name AS TableName, sc.name AS SchemaName
    FROM sys.tables t 
    INNER JOIN sys.schemas sc ON sc.schema_id = t.schema_id
    ORDER BY sc.name, t.name

DECLARE @sql NVARCHAR(MAX) = '', @TableName VARCHAR(100), @SchemaName VARCHAR(100), @loopCounter INT =0
SELECT GETDATE() AS StartDate
OPEN TableCursor
WHILE 1 =1 BEGIN
    FETCH TableCursor INTO @TableName, @SchemaName
    IF @@fetch_status <> 0 BREAK

    SELECT @sql = 'declare @Scope_Identity int = 0, @RowCount int
    SET NOCOUNT ON' + CHAR(13)
    SELECT @sql += '
    TRUNCATE TABLE #Stat_Header
    TRUNCATE TABLE #Histogram 
    INSERT INTO #Stat_Header(Name, Updated, Rows, Rows_Sampled, Steps, Density, AverageKeyLength, StringIndex, FilterExpression, unfiltered_rows/*, persisted_sample_percent*/)
    exec (''DBCC SHOW_STATISTICS ("' + @SchemaName + '.' + @TableName + '", "' + s.name +'") with STAT_HEADER'')
    INSERT INTO dbo.dm_db_stats_properties(databaseid, object_id, stats_id, last_updated, rows, rows_sampled, steps, unfiltered_rows, modification_counter, persisted_sample_percent, SampleDate, StatsName)
    SELECT db_id(), ' + LTRIM(t.object_id) + ', ' + LTRIM(s.stats_id) +', coalesce(sh.Updated, ''2000-01-01''), isnull(sh.rows,0), isnull(sh.Rows_Sampled,0), isnull(sh.steps,0), isnull(sh.unfiltered_rows,0), 0, sh.persisted_sample_percent, cast(''' + LTRIM(SYSUTCDATETIME()) + ''' as datetime2(7)), ''' + s.name + '''
    FROM #Stat_Header sh
    LEFT JOIN dbo.dm_db_stats_properties sp ON sp.object_id=' + LTRIM(t.object_id) + ' AND sp.stats_id=' + LTRIM(s.stats_id) + ' AND sh.Updated=sp.last_updated
    WHERE sp.dm_db_stats_propertiesID IS NULL
    SELECT @Scope_Identity = SCOPE_IDENTITY(), @RowCount=@@ROWCOUNT
    IF @RowCount>0 BEGIN 
        --raiserror (''here'', 10, 1) with nowait
        INSERT INTO #Histogram(range_high_key, range_rows, equal_rows, distinct_range_rows, average_range_rows)
        exec (''DBCC SHOW_STATISTICS ("' + @SchemaName + '.' + @TableName + '", "' + s.name +'") with HISTOGRAM'')
        INSERT INTO dbo.dm_db_stats_histogram(dm_db_stats_propertiesID, object_id, stats_id, step_number, range_high_key, range_rows, equal_rows, distinct_range_rows, average_range_rows)
        SELECT @Scope_Identity, ' + LTRIM(t.object_id) + ', ' + LTRIM(s.stats_id) +', h.Step_Number, h.range_high_key, h.range_rows, h.equal_rows, h.distinct_range_rows, h.average_range_rows
        FROM #Histogram h
    END 
    raiserror (''table = ' + @TableName + ', ' + s.name + ', rc= %i '', 10, 1, @RowCount) with nowait
    waitfor delay ''00:00:01''
    '
    FROM sys.stats AS s
    INNER JOIN sys.tables t ON t.object_id = s.object_id
    INNER JOIN sys.schemas sc ON sc.schema_id = t.schema_id
    WHERE t.name=@TableName
    AND sc.name = @SchemaName
    IF @loopCounter < 1 EXEC dbo.LongPrint @String=@sql
    SET @loopCounter +=1
    EXEC sp_executesql @sql
    --BREAK
END
DEALLOCATE TableCursor
SELECT GETDATE() AS StopDate

SQL Server 2016 + 17で新しいDMVを使用するソリューションもあります

exec sp_foreachdb @command = N'
use ?
DECLARE @SampleDate DATETIME2 = SYSUTCDATETIME()

  INSERT INTO master.dbo.dm_db_stats_properties(DatabaseID, object_id, stats_id, last_updated, rows, rows_sampled, steps, unfiltered_rows, modification_counter, persisted_sample_percent, SampleDate, StatsName)
  SELECT db_id() as DatabaseID, s.object_id, s.stats_id, sp.last_updated, sp.rows, sp.rows_sampled, sp.steps, sp.unfiltered_rows, sp.modification_counter, sp.persisted_sample_percent, @SampleDate, s.name
  FROM ?.sys.stats AS s
  INNER JOIN ?.sys.tables t ON t.object_id = s.object_id
  INNER JOIN ?.sys.schemas sc ON sc.schema_id = t.schema_id
  CROSS APPLY ?.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
  LEFT JOIN master.dbo.dm_db_stats_properties T1 ON T1.object_id = s.object_id AND T1.stats_id = s.stats_id AND T1.last_updated=sp.last_updated
  WHERE sp.last_updated IS NOT NULL
  AND T1.last_updated IS NULL
  select @@rowcount as r1

INSERT INTO master.dbo.dm_db_stats_histogram(dm_db_stats_propertiesID, object_id, stats_id, step_number, range_high_key, range_rows, equal_rows, distinct_range_rows, average_range_rows)
SELECT sp.dm_db_stats_propertiesID, sp.object_id, sp.stats_id, hist.step_number, hist.range_high_key, hist.range_rows, hist.equal_rows, hist.distinct_range_rows, hist.average_range_rows
FROM master.dbo.dm_db_stats_properties sp
CROSS APPLY ?.sys.dm_db_stats_histogram(sp.[object_id], sp.stats_id) AS hist
WHERE sp.SampleDate = @SampleDate
  select @@rowcount as r2

', @exclude_list='tempdb, model', @print_dbname=1


私の本当の質問

今、私が収集したデータに基づいて、クエリをどのように書くのですか?
a)サンプリング期間中に大きく変化しない(変化しない)テーブル、インデックス、列はどれですか?
b)WITH FULLSCANコマンドの恩恵を受けるテーブル、インデックス、列はどれですか?

b)WITH FULLSCANコマンドの恩恵を受けるテーブル、インデックス、列はどれですか?

私の観点からは、あなたはその質問に答えるための適切なデータを収集していません。データベースの統計を分析するだけで改善できる点を探している場合、FULLSCANの代わりにサンプリングされた統計を使用することで引き起こされる可能性のあるクエリパフォーマンスの問題は2つしか考えられません。

  1. 密度は少なくとも1桁ずれています。

    一部のデータ分布は、サンプリングされたデータをヒストグラムに変換するときにSQL Serverが行ういくつかの仮定に適していません。このような状況では、密度が10倍、100倍、またはそれ以上も低下する可能性があります。これは、統計オブジェクトで密度ベクトルを使用するクエリのパフォーマンスの問題を引き起こす可能性があります。

    サンプリングされたすべての統計の密度情報を保存し、FULLSCANを使用して関連するすべての列の統計を収集し、2つの結果セット間の密度を比較することで、考えられる問題を検索できます。不正確すぎるものはすべて、統計を完全に収集することのメリットを享受するための候補です。

  2. クエリは 昇順キーの問題 に対して脆弱です。

    SQL Server 2008がタグとしてリストされているので、これはまだあなたに関係があるかもしれません。行が挿入された日時を格納する列について考えます。非常に最近のデータを探してその列をフィルターするクエリがある場合、それらはヒストグラムの外のデータを検索している可能性があります。レガシーCEを使用すると、カーディナリティの見積もりが非常に低くなり、クエリのパフォーマンスの問題が発生する可能性があります。

    これはFULLSCAN統計で対処できますが、私には少しやり過ぎに感じられます。関連するデータタイプ(VARCHARの昇順キーについて心配する必要がないことを願う)のすべての統計に関する統計を数回収集し、最大の高キーがどのように変化するかを確認できます。

上記の両方の問題について、サンプリングされた統計だけを見てもプログラムでそれらを見つける方法は考えられません。そのため、質問に答えるための適切なデータを収集していないとおっしゃいました。

私の意見に関心がある場合、統計のメンテナンスを本当に最小限に抑える方法は、十分に実行されていないクエリのワークロードを分析し、慎重に根本原因分析を行って、統計の問題が原因となっている時期を特定し、正確なタイプの統計を特定することです問題、そして最後にそれに応じて統計メンテナンスジョブを調整します。

6
Joe Obbish

私自身のステップ

私はこれにかなりの時間を費やしましたが、これまでのところ最も有用なのは、ステップ数が最も大きいテーブル/列/インデックスを表示するクエリです。

drop table /* if exists */ #inv
create table #inv(dm_db_stats_propertiesID int, DatabaseID int, object_id int, stats_id int, Steps varchar(100), dm_db_stats_propertiesIDs varchar(400)
, DatabaseName sysname, StatsName varchar(100) null, TableName varchar(100) null, ColumnName varchar(100) null, minSteps int, maxSteps int)

insert into #inv( dm_db_stats_propertiesID, DatabaseID, object_id, stats_id, Steps, dm_db_stats_propertiesIDs, DatabaseName, StatsName, minSteps, maxSteps)
SELECT dm_db_stats_propertiesID, DatabaseID, object_id, stats_id, Steps, dm_db_stats_propertiesIDs, a.Name as DatabaseName, StatsName, minSteps, maxSteps
from (
    SELECT top (10000) L.Name, sp.*
    , stuff((select ', ' +ltrim(steps) from dbo.dm_db_stats_properties s2 where s2.DatabaseID=sp.DatabaseID and s2.object_id = sp.object_id and s2.stats_id=sp.stats_id for xml path('')),1,2,'') as Steps2
    , stuff((select ', ' + ltrim(dm_db_stats_propertiesID) from dbo.dm_db_stats_properties s2 where s2.DatabaseID=sp.DatabaseID and s2.object_id = sp.object_id and s2.stats_id=sp.stats_id for xml path('')),1,2,'') as dm_db_stats_propertiesIDs
    , (select count(*) as rowcnt from dbo.dm_db_stats_properties s2 where s2.DatabaseID=sp.DatabaseID and s2.object_id = sp.object_id and s2.stats_id=sp.stats_id ) as rowcnt
    , (select min(Steps) as minSteps from dbo.dm_db_stats_properties s2 where s2.DatabaseID=sp.DatabaseID and s2.object_id = sp.object_id and s2.stats_id=sp.stats_id ) as minSteps
    , (select max(Steps) as maxSteps from dbo.dm_db_stats_properties s2 where s2.DatabaseID=sp.DatabaseID and s2.object_id = sp.object_id and s2.stats_id=sp.stats_id ) as maxSteps
    FROM dbo.dm_db_stats_properties sp
    inner join #List L on sp.DatabaseID=L.DatabaseID and sp.SampleDate>=L.SampleDate
) as a
where a.Steps2 like '%,%' --',%'

exec sp_foreachdb 'update #inv set TableName = t.name /* select * */ from #inv i inner join ?.sys.tables t on i.object_id = t.object_id and (''['' + i.DatabaseName + '']'') = ''?'''

exec sp_foreachdb 'update #inv set ColumnName = c.name from #inv i
inner join ?.sys.columns c on i.object_id=c.object_id and c.column_id = convert(int, convert(varbinary, SUBSTRING(i.StatsName, 9, 8),2)) and (''['' + i.DatabaseName + '']'') = ''?''
where i.StatsName like ''_WA_Sys%''
'

select * , (0.0+maxSteps-minSteps)/maxSteps * 100  as PctChange
from #inv
where minSteps <> maxSteps
and DatabaseName <> 'master'
AND TableName NOT LIKE 'dm_db_stats%'
order by PctChange desc

私が望んでいたものとはほとんど違います。