TOP (1) NACH GRUPPEN eines sehr großen (100.000.000+) Tisches

8

Installieren

Ich habe eine riesige Tabelle mit ~ 115.382.254 Zeilen. Die Tabelle ist relativ einfach und protokolliert Anwendungsprozessvorgänge.

CREATE TABLE [data].[OperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [Size] [bigint] NULL,
    [Begin] [datetime2](7) NULL,
    [End] [datetime2](7) NOT NULL,
    [Date]  AS (isnull(CONVERT([date],[End]),CONVERT([date],'19000101',(112)))) PERSISTED NOT NULL,
    [DataSetCount] [bigint] NULL,
    [Result] [int] NULL,
    [Error] [nvarchar](max) NULL,
    [Status] [int] NULL,
 CONSTRAINT [PK_OperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeviceID] ASC,
    [FileSource] ASC,
    [End] ASC
))

CREATE TABLE [model].[SourceDevice](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
 CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
))

ALTER TABLE [data].[OperationData]  WITH CHECK ADD  CONSTRAINT [FK_OperationData_SourceDevice] FOREIGN KEY([SourceDeviceID])
REFERENCES [model].[SourceDevice] ([ID])

Die Tabelle ist täglich in rund 500 Clustern zusammengefasst.

Partitionen

Geben Sie hier die Bildbeschreibung ein

Außerdem ist die Tabelle von PK gut indiziert, die Statistiken sind aktuell und der INDEXer wird jede Nacht defragmentiert.

Indexbasierte SELECTs sind blitzschnell und wir hatten kein Problem damit.

Problem

Ich muss die letzte (TOP) Zeile von kennen [End]und durch partitionieren [SourceDeciveID]. Um das allerletzte [OperationData]von jedem Quellgerät zu erhalten.

Frage

Ich muss einen Weg finden, dies auf gute Weise zu lösen, ohne die DB an ihre Grenzen zu bringen.


Aufwand 1

Der erste Versuch war offensichtlich GROUP BYoder SELECT OVER PARTITION BYfraglich. Das Problem hier ist auch offensichtlich, jede Abfrage muss über sehr Partitionsreihenfolge scannen / die oberste Zeile finden. Die Abfrage ist also sehr langsam und hat eine sehr hohe E / A-Auswirkung.

Beispielabfrage 1

;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY [SourceDeciveID] ORDER BY [End] DESC) AS rn
   FROM [data].[OperationData]
)
SELECT *
FROM cte
WHERE rn = 1

Beispielabfrage 2

SELECT *
FROM [data].[OperationData] AS d 
CROSS APPLY 
(
   SELECT TOP 1 *
   FROM [data].[OperationData] 
   WHERE [SourceDeciveID] = d.[SourceDeciveID]
   ORDER BY [End] DESC
) AS ds

GESCHEITERT!

Aufwand 2

Ich habe eine Hilfetabelle erstellt, die immer einen Verweis auf die oberste Zeile enthält.

CREATE TABLE [data].[LastOperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [End] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_LastOperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeciveID] ASC
)

ALTER TABLE [data].[LastOperationData]  WITH CHECK ADD  CONSTRAINT [FK_LastOperationData_OperationData] FOREIGN KEY([SourceDeciveID], [FileSource], [End])
REFERENCES [data].[OperationData] ([SourceDeciveID], [FileSource], [End])

Um die Tabelle zu füllen, wurde ein Trigger erstellt, der die Quellzeile immer hinzufügt / aktualisiert, wenn eine höhere [End]Spalte eingefügt wird.

CREATE TRIGGER [data].[OperationData_Last]
   ON  [data].[OperationData]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    MERGE [data].[LastOperationData] AS [target]
    USING (SELECT [SourceDeciveID], [FileSource], [End] FROM inserted) AS [source] ([SourceDeciveID], [FileSource], [End])  
    ON ([target].[SourceDeciveID] = [FileSource].[SourceDeciveID])

    WHEN MATCHED AND [target].[End] < [source].[End] THEN
        UPDATE SET [target].[FileSource] = source.[FileSource], [target].[End] = source.[End]

    WHEN NOT MATCHED THEN  
        INSERT ([SourceDeciveID], [FileSource], [End])  
        VALUES (source.[SourceDeciveID], source.[FileSource], source.[End]);

END

Das Problem hierbei ist, dass es auch einen sehr großen Einfluss auf die E / A hat und ich nicht weiß warum.

Wie Sie hier im Abfrageplan sehen können, wird auch ein Scan über die gesamte [OperationData]Tabelle ausgeführt.

Es hat insgesamt einen enormen Einfluss auf meine DB. Statistiken

GESCHEITERT!

Steffen Mangold
quelle
2
In Ihrem ersten Codeblock kann ich nicht sehen, woher die erste Spalte des Clustered-Index stammt - stimmt das?
George.Palacios
Ja, tut mir leid, SSMS nimmt es nicht in das CREATE TABLESkript auf, aber im Abfrageplan werden die Partitionen angezeigt . Ich werde die Frage bearbeiten.
Steffen Mangold
Kein zusätzlicher Index, da er in dem enthalten ist, von dem PRIMARY KEY CLUSTEREDSie glauben, dass er helfen könnte?
Steffen Mangold
Soryy, das war ein Fehler, ich habe die Namen für die Frage geändert, um klarer zu werden, ich habe sie korrigiert.
Steffen Mangold
@ ypercubeᵀᴹ ja, weil SELECT [SourceID], [Source], [End] FROM insertedeinige wie eine Tabelle auf dem scannen [OperationData].
Steffen Mangold

Antworten:

9

Wenn Sie eine Wertetabelle SourceIDund einen Index für Ihre Haupttabelle haben (SourceID, End) include (othercolumns), verwenden Sie einfach OUTER APPLY.

SELECT d.*
FROM dbo.Sources s
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d
    WHERE d.SourceID = s.SourceID
    ORDER BY d.[End] DESC) d;

Wenn Sie wissen, dass Sie erst nach Ihrer neuesten Partition suchen, können Sie einen Filter für End einfügen, z AND d.[End] > DATEADD(day, -1, GETDATE())

Bearbeiten: Da Ihr Clustered-Index aktiviert ist SourceID, Source, End), fügen Sie Source ebenfalls in Ihre Sources-Tabelle ein und schließen Sie sich dem ebenfalls an. Dann brauchen Sie den neuen Index nicht.

SELECT d.*
FROM dbo.Sources s -- Small table
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d -- Big table quick seeks
    WHERE d.SourceID = s.SourceID
    AND d.Source = s.Source
    AND d.[End] > DATEADD(day, -1, GETDATE()) -- If you’re partitioning on [End], do this for partition elimination
    ORDER BY d.[End] DESC) d;
Rob Farley
quelle
Der Index hat die Abfrage wirklich beschleunigt. Ein zweites Problem ist, dass ein nicht partitionierter Index für eine so große Tabelle nahezu nicht zu pflegen ist. In all unseren "Big-Data" -Tabellen arbeiten wir mit partitioniertem Indexer. Sie können Partition für Partition online verwaltet werden. Sobald der Indexer partitioniert ist, ist das Problem das alte, da er jede Partition durchlaufen muss.
Steffen Mangold
1
@SteffenMangold: Je weniger Daten in einem Index vorhanden sind, desto besser (sofern alles vorhanden ist, was Sie benötigen) und ohne materialisierte Ansichten verfügt der Clustered-Index über die maximal mögliche Datenmenge. Clustered-Indizes sind vorhanden, da das Abrufen aller Daten durch den Schlüssel die Norm ist. In diesem Fall erhalten Sie alle Daten, aber Sie erhalten sie nicht wirklich über den Schlüssel, sondern über einen Teil des Schlüssels. Sie benötigen einen Index, der mit einem Teil des Schlüssels abgefragt werden kann.
jmoreno
Es tut mir wirklich leid, aber es gibt eine SourceTabelle, die auf die sourceIDSpalte verweist . Die Spaltenquelle ist nur ein Dateiname. Es ist ein wenig verwirrend zu benennen. Für jedes SourceGerät (Quell-ID) kann es nur einen einzigen Eintrag für eine Datei source(Spalte) zu einem Zeitstempel geben. Außerdem kann ich keine Partitionseliminierung durchführen, da die neueste EndVersion stark fragmentiert ist. Deshalb habe ich mir die Trigger-Lösung ausgedacht. Ich denke, eine Live-Abfrage wird hier nicht funktionieren.
Steffen Mangold
@ Rob Farley Ich habe die Frage bearbeitet, um klarer zu sein
Steffen Mangold
Bei der Partitionierung werden alle diese Suchvorgänge in jeder Partition ausgeführt. Mit dem zusätzlichen Prädikat können Sie es so gestalten, dass es nicht alle stört und nur einige. Machen Sie es einen Monat, wenn Sie müssen.
Rob Farley