web-dev-qa-db-ja.com

統計更新のサンプルサイズによる奇妙な動作

SQL Server(2012)で統計情報の更新を使用してサンプリングしきい値の調査に取り組み、奇妙な動作に気づきました。基本的に、同じデータセットであっても、サンプリングされる行数は状況によって異なるようです。

私はこのクエリを実行します:

--Drop table if exists
IF (OBJECT_ID('dbo.Test')) IS NOT NULL DROP TABLE dbo.Test;

--Create Table for Testing
CREATE TABLE dbo.Test(Id INT IDENTITY(1,1) CONSTRAINT PK_Test PRIMARY KEY CLUSTERED, TextValue VARCHAR(20) NULL);

--Insert enough data so we have more than 8Mb (the threshold at which sampling kicks in)
INSERT INTO dbo.Test(TextValue) 
SELECT TOP 1000000 'blahblahblah'
FROM sys.objects a, sys.objects b, sys.objects c, sys.objects d;  

--Create Index on TextValue
CREATE INDEX IX_Test_TextValue ON dbo.Test(TextValue);

--Update Statistics without specifying how many rows to sample
UPDATE STATISTICS dbo.Test IX_Test_TextValue;

--View the Statistics
DBCC SHOW_STATISTICS('dbo.Test', IX_Test_TextValue) WITH STAT_HEADER;

SHOW_STATISTICSの出力を見ると、「サンプリングされた行」が実行のたびに変化することがわかります(つまり、テーブルが削除され、再作成され、再入力されます)。

例えば:

サンプリングされた行

  • 318618
  • 319240
  • 324198
  • 314154

テーブルが同一であるため、この数値は毎回同じになると予想していました。ちなみに、データを削除して再挿入しただけでは、この動作は発生しません。

それは重要な質問ではありませんが、私は何が起こっているのかを理解することに興味があります。

25

バックグラウンド

統計オブジェクトのデータは、次の形式のステートメントを使用して収集されます。

SELECT 
    StatMan([SC0], [SC1], [SB0000]) 
FROM 
(
    SELECT TOP 100 PERCENT 
        [SC0], [SC1], STEP_DIRECTION([SC0]) OVER (ORDER BY NULL) AS [SB0000]
    FROM 
    (
        SELECT 
            [TextValue] AS [SC0], 
            [Id] AS [SC1] 
        FROM [dbo].[Test] 
            TABLESAMPLE SYSTEM (2.223684e+001 PERCENT) 
            WITH (READUNCOMMITTED) 
    ) AS _MS_UPDSTATS_TBL_HELPER 
    ORDER BY 
        [SC0], 
        [SC1], 
        [SB0000] 
) AS _MS_UPDSTATS_TBL
OPTION (MAXDOP 1)

このステートメントは、拡張イベントまたはプロファイラー(SP:StmtCompleted)で収集できます。

統計生成クエリは、非クラスター化インデックスページで自然に発生する値のクラスター化を回避するために、(非クラスター化インデックスではなく)ベーステーブルにアクセスすることがよくあります。

サンプリングされる行数は、サンプリングするために選択されたページ全体の数によって異なります。テーブルの各ページが選択されているか、選択されていません。選択したページのすべての行が統計に寄与します。

乱数

SQL Serverは、乱数ジェネレータを使用して、ページが適格かどうかを判断します。このインスタンスで使用されるジェネレーターは Lehmer乱数ジェネレーター であり、パラメーター値は以下のとおりです。

バツ = Xシード * 75 mod(231 -1)

Xseedの値は、次の合計として計算されます。

  • bigint)ベーステーブルのpartition_idの低整数部分。

    SELECT
        P.[partition_id] & 0xFFFFFFFF
    FROM sys.partitions AS P
    WHERE
        P.[object_id] = OBJECT_ID(N'dbo.Test', N'U')
        AND P.index_id = 1;
    
  • REPEATABLE句で指定された値

    • サンプリングされたUPDATE STATISTICSの場合、REPEATABLEの値は1です。
    • この値は、トレースフラグ8666が有効な場合に実行プランに表示されるアクセスメソッドの内部デバッグ情報のm_randomSeed要素で公開されます(例:<Field FieldName="m_randomSeed" FieldValue="1" />)。

SQL Server 2012の場合、この計算はsqlmin!UnOrderPageScanner::StartScanで行われます。

mov     edx,dword ptr [rcx+30h]
add     edx,dword ptr [rcx+2Ch]

ここで、[rcx+30h]のメモリにはパーティションIDの下位32ビットが含まれ、[rcx+2Ch]のメモリには使用中のREPEATABLE値が含まれます。

乱数ジェネレータは後で同じメソッドで初期化され、sqlmin!RandomNumGenerator::Initを呼び出します。

imul    r9d,r9d,41A7h

...シードに41A7 hex(16807 16進数= 7)を乗算します5)上記の式に示すように。

後で(個々のページの)乱数は、sqlmin!UnOrderPageScanner::SetupSubScannerにインラインされた同じ基本コードを使用して生成されます。

StatMan

上記のStatManクエリの例では、T-SQLステートメントと同じページが収集されます。

SELECT 
    COUNT_BIG(*) 
FROM dbo.Test AS T 
    TABLESAMPLE SYSTEM (2.223684e+001 PERCENT)  -- Same sample %
    REPEATABLE (1)                              -- Always 1 for statman
    WITH (INDEX(0));                            -- Scan base object

これは次の出力と一致します。

SELECT 
    DDSP.rows_sampled
FROM sys.stats AS S
CROSS APPLY sys.dm_db_stats_properties(S.[object_id], S.stats_id) AS DDSP
WHERE 
    S.[object_id] = OBJECT_ID(N'dbo.Test', N'U')
    AND S.[name] = N'IX_Test_TextValue';

エッジケース

MINSTD Lehmer乱数ジェネレータを使用すると、シード値0とint.maxを使用しないでください。これにより、アルゴリズムによって一連のゼロが生成されます(すべてのページを選択)。

コードはゼロを検出し、その場合、シードとしてシステムの「クロック」からの値を使用します。シードがint.max(0x7FFFFFFF = 231 -1)。

初期シードはパーティションIDの下位32ビットとREPEATABLE値の合計として計算されるため、このシナリオを設計できます。シードがint.maxになるREPEATABLEの値は、サンプルとして選択されるすべてのページです。

SELECT
    0x7FFFFFFF - (P.[partition_id] & 0xFFFFFFFF)
FROM sys.partitions AS P
WHERE
    P.[object_id] = OBJECT_ID(N'dbo.Test', N'U')
    AND P.index_id = 1;

それを完全な例に組み込む:

DECLARE @SQL nvarchar(4000) = 
    N'
    SELECT
        COUNT_BIG(*) 
    FROM dbo.Test AS T 
        TABLESAMPLE (0 PERCENT) 
        REPEATABLE (' +
        (
            SELECT TOP (1)
                CONVERT(nvarchar(11), 0x7FFFFFFF - P.[partition_id] & 0xFFFFFFFF)
            FROM sys.partitions AS P
            WHERE
                P.[object_id] = OBJECT_ID(N'dbo.Test', N'U')
                AND P.index_id = 1
        ) + ')
        WITH (INDEX(0));';

PRINT @SQL;
--EXECUTE (@SQL);

これにより、TABLESAMPLE句の内容に関係なく、すべてのページのすべての行が選択されます(ゼロパーセントでも)。

26
Paul White 9

これはすばらしい質問です。私は確かに私が知っていることから始めて、そして推測に移ります。これに関する多くの詳細は私のブログ投稿 here にあります。

サンプル統計更新では、裏でTABLESAMPLEを使用します。そのドキュメントをオンラインで簡単に見つけることができます。ただし、TABLESAMPLEによって返される行がオブジェクトのhobt_idに部分的に依存していることはよく知られていないと思います。オブジェクトをドロップして再作成すると、新しいhobt_idが取得されるため、ランダムサンプリングによって返される行は異なります。

データを削除して再挿入しても、hobt_idは同じままです。データがディスク上で同じように配置されている限り(割り当て順序スキャンが同じ結果を同じ順序で返す)、サンプリングされたデータは変更されません。

テーブルでクラスター化インデックスを再構築することにより、サンプリングされる行数を変更することもできます。例えば:

UPDATE STATISTICS dbo.Test IX_Test_TextValue;

DBCC SHOW_STATISTICS('dbo.Test', IX_Test_TextValue) WITH STAT_HEADER; -- 273862 rows

ALTER INDEX PK_Test on Test REBUILD;

UPDATE STATISTICS dbo.Test IX_Test_TextValue;

DBCC SHOW_STATISTICS('dbo.Test', IX_Test_TextValue) WITH STAT_HEADER; -- 273320 rows

これが発生する理由については、SQL Serverがインデックスのサンプル統計を収集するときに、非クラスター化インデックスではなくクラスター化インデックスをスキャンするためだと思います。また、REPEATABLEで使用されるTABLESAMPLEには、(非表示の統計更新クエリをトレースする)非表示の値があると思います。それはまだ証明されていませんが、ヒストグラムとサンプリングされた行がクラスター化インデックスの再構築によって変化する理由を説明しています。

11
Joe Obbish

ページごとにランダムな確率を割り当てるという点でTABLESAMPLEがどのように機能するかを忘れました。 - Martin Smith

Itzik Ben-Ganによる Microsoft SQL Server 2008内:T-SQLクエリ でこれを見て、コメントとして追加できないので、ここに投稿します。他の人にとっても興味深いと思います。

enter image description here

Rojiによる TABLESAMPLEを使用したサンプリング も参照してください。 P.トーマス。

3
sepupic