Merkwürdiges Verhalten bei Stichprobengrößen für Statistikaktualisierungen

25

Ich habe versucht, Stichprobenschwellenwerte mit Statistikaktualisierungen auf SQL Server (2012) zu untersuchen, und dabei ein merkwürdiges Verhalten festgestellt. Grundsätzlich scheint die Anzahl der abgetasteten Zeilen unter Umständen zu variieren - auch bei gleichem Datensatz.

Ich führe diese Abfrage aus:

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

Wenn ich mir die Ausgabe von SHOW_STATISTICS ansehe, stelle ich fest, dass sich die Anzahl der erfassten Zeilen bei jeder vollständigen Ausführung ändert (dh, die Tabelle wird gelöscht, neu erstellt und neu gefüllt).

Beispielsweise:

Zeilen abgetastet

  • 318618
  • 319240
  • 324198
  • 314154

Ich habe erwartet, dass diese Zahl jedes Mal dieselbe ist, wenn die Tabelle identisch ist. Übrigens bekomme ich dieses Verhalten nicht, wenn ich nur die Daten lösche und sie erneut einfüge.

Es ist keine kritische Frage, aber ich würde gerne verstehen, was los ist.

Matthew McGiffen
quelle
2
Wie viele Dateien in der Dateigruppe, in die Sie einfügen? Ich habe es 2016 ein paar Mal versucht und beide Male wurde die Tabelle in 3584 Seiten mit 279 Zeilen und 1 mit 64 Seiten aufgeteilt. Die zwei verschiedenen Stichprobengrößen, die ich sah, waren 314712 und 315270 - beides exakte Vielfache von 279.
Martin Smith
1
@ JoeObbish - Es liest immer ganze Seiten AFAIK, also war ich nicht überrascht. Aus irgendeinem Grund dachte ich, dass die Zahlen in der Frage nicht zu diesem Muster passen. Aber sie haben die Mathematik überarbeitet. 318618 = 1142*279, 319240 = 1144*279 + 64, 324198=1162*279Und 314154=1126so ist die Varianz der Anzahl der Seiten , abgetastet.
Martin Smith
@MartinSmithNur die eine Datei - die 279 Abbildung ist interessant, ich verstehe immer gerne die beteiligten Muster
Matthew McGiffen

Antworten:

26

Hintergrund

Daten für das Statistikobjekt werden mit einer Anweisung der Form gesammelt:

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)

Sie können diese Anweisung mit Extended Events oder Profiler ( SP:StmtCompleted) sammeln .

Abfragen zur Generierung von Statistiken greifen häufig auf die Basistabelle zu (anstatt auf einen nicht gruppierten Index), um das Clustering von Werten zu vermeiden, das normalerweise auf nicht gruppierten Indexseiten auftritt.

Die Anzahl der abgetasteten Zeilen hängt von der Anzahl der zum Abtasten ausgewählten ganzen Seiten ab. Jede Seite der Tabelle ist entweder ausgewählt oder nicht. Alle Zeilen auf ausgewählten Seiten tragen zur Statistik bei.

Zufällige Zahlen

SQL Server verwendet einen Zufallszahlengenerator, um zu entscheiden, ob eine Seite qualifiziert ist oder nicht. Der in diesem Fall verwendete Generator ist der Lehmer-Zufallszahlengenerator mit folgenden Parameterwerten:

X next = X seed * 7 5 mod (2 31 - 1)

Der Wert von wird berechnet als die Summe von:Xseed

  • Der niedrige ganzzahlige Teil der ( bigint) Basistabelle ist partition_idz

    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;
  • Der in der REPEATABLEKlausel angegebene Wert

    • Für die Stichprobe UPDATE STATISTICSist der REPEATABLEWert 1.
    • Dieser Wert wird im m_randomSeedElement der internen Debugging-Informationen der Zugriffsmethode angezeigt, die in Ausführungsplänen angezeigt werden, wenn beispielsweise das Ablaufverfolgungsflag 8666 aktiviert ist<Field FieldName="m_randomSeed" FieldValue="1" />

Für SQL Server 2012 erfolgt diese Berechnung in sqlmin!UnOrderPageScanner::StartScan:

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

Dabei [rcx+30h]enthält memory at die niedrigen 32 Bits der Partitions-ID und memory at [rcx+2Ch]den verwendeten REPEATABLEWert.

Der Zufallszahlengenerator wird später in derselben Methode initialisiert sqlmin!RandomNumGenerator::Init, wobei die Anweisung aufgerufen wird:

imul    r9d,r9d,41A7h

... multipliziert den 41A7Startwert mit dem Hexadezimalwert (16807 Dezimal = 7 5 ), wie in der obigen Gleichung gezeigt.

Spätere Zufallszahlen (für einzelne Seiten) werden mit dem gleichen Basiscode wie in generiert sqlmin!UnOrderPageScanner::SetupSubScanner.

StatMan

Für die StatManoben gezeigte Beispielabfrage werden dieselben Seiten wie für die T-SQL-Anweisung gesammelt:

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

Dies entspricht der Ausgabe von:

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

Edge-Fall

Eine Konsequenz der Verwendung des Zufallszahlengenerators von MINSTD Lehmer ist, dass die Startwerte Null und int.max nicht verwendet werden sollten, da dies dazu führt, dass der Algorithmus eine Folge von Nullen erzeugt (Auswahl jeder Seite).

Der Code erkennt Null und verwendet in diesem Fall einen Wert aus der Systemuhr als Startwert. Dies ist nicht der Fall, wenn der Startwert int.max ( 0x7FFFFFFF= 2 31 - 1) ist.

Wir können dieses Szenario konstruieren, da der anfängliche REPEATABLEStartwert als die Summe der niedrigen 32 Bits der Partitions-ID und des Werts berechnet wird. Der REPEATABLEWert, der dazu führt, dass der Startwert int.max ist und daher jede Seite für die Stichprobe ausgewählt wird, ist:

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;

Das in ein vollständiges Beispiel umwandeln:

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

Dadurch wird jede Zeile auf jeder Seite ausgewählt, unabhängig davon, was in der TABLESAMPLEKlausel steht (sogar null Prozent).

Paul White sagt GoFundMonica
quelle
11

Das ist eine ausgezeichnete Frage! Ich fange mit dem an, was ich sicher weiß, und gehe dann zur Spekulation über. Viele Details dazu in meinem Blogbeitrag hier .

Sampled Stats-Updates werden TABLESAMPLEim Hintergrund verwendet. Es ist ziemlich einfach, online Dokumentation darüber zu finden. Ich bin jedoch der Meinung, dass es nicht allgemein bekannt ist, dass die von zurückgegebenen Zeilen TABLESAMPLEteilweise vom hobt_idObjekt abhängen . Wenn Sie das Objekt löschen und neu erstellen, erhalten Sie ein neues Objekt, hobt_idsodass die durch Zufallsstichproben zurückgegebenen Zeilen unterschiedlich sind.

Wenn Sie die Daten löschen und erneut einfügen, bleiben diese unverändert hobt_id. Solange die Daten auf dieselbe Weise auf der Festplatte angeordnet sind (ein Allokationsreihenfolge-Scan liefert dieselben Ergebnisse in derselben Reihenfolge), sollten sich die abgetasteten Daten nicht ändern.

Sie können auch die Anzahl der untersuchten Zeilen ändern, indem Sie den Clustered-Index für die Tabelle neu erstellen. Beispielsweise:

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

Ich glaube, der Grund dafür ist, dass SQL Server den Clustered-Index anstelle des Nonclustered-Index durchsucht, wenn er Stichprobenstatistiken für einen Index sammelt. Ich denke auch, dass es einen verborgenen (für diejenigen von uns, die die verborgenen Statistik-Aktualisierungsabfragen verfolgen) Wert gibt, REPEATABLEder mit verwendet wird TABLESAMPLE. Ich habe nichts davon bewiesen, aber es erklärt, warum sich Ihr Histogramm und die untersuchten Zeilen bei einer Neuerstellung des Clustered-Index ändern.

Joe Obbish
quelle