Filtern von Daten nach Zeilenversion

8

Ich habe eine SQL-Datentabelle mit der folgenden Struktur:

CREATE TABLE Data(
    Id uniqueidentifier NOT NULL,
    Date datetime NOT NULL,
    Value decimal(20, 10) NULL,
    RV timestamp NOT NULL,
 CONSTRAINT PK_Data PRIMARY KEY CLUSTERED (Id, Date)
)

Die Anzahl der unterschiedlichen IDs reicht von 3000 bis 50000.
Die Größe der Tabelle variiert bis zu über einer Milliarde Zeilen.
Eine ID kann zwischen einigen Zeilen bis zu 5% der Tabelle abdecken.

Die am häufigsten ausgeführte Abfrage in dieser Tabelle lautet:

SELECT Id, Date, Value, RV
FROM Data
WHERE Id = @Id
AND Date Between @StartDate AND @StopDate

Ich muss jetzt das inkrementelle Abrufen von Daten für eine Teilmenge von IDs implementieren, einschließlich Aktualisierungen.
Ich habe dann ein Anforderungsschema verwendet, in dem der Anrufer eine bestimmte Zeilenversion bereitstellt, einen Datenblock abruft und den maximalen Zeilenversionswert der zurückgegebenen Daten für den nachfolgenden Aufruf verwendet.

Ich habe dieses Verfahren geschrieben:

CREATE TYPE guid_list_tbltype AS TABLE (Id uniqueidentifier not null primary key)
CREATE PROCEDURE GetData
    @Ids guid_list_tbltype READONLY,
    @Cursor rowversion,
    @MaxRows int
AS
BEGIN
    SELECT A.* 
    FROM (
        SELECT 
            Data.Id,
            Date,
            Value,
            RV,
            ROW_NUMBER() OVER (ORDER BY RV) AS RN
        FROM Data
             inner join (SELECT Id FROM @Ids) Ids ON Ids.Id = Data.Id
        WHERE RV > @Cursor
    ) A 
    WHERE RN <= @MaxRows
END

Der @MaxRowsBereich liegt zwischen 500.000 und 2.000.000, je nachdem, wie stark der Kunde seine Daten haben möchte.


Ich habe verschiedene Ansätze ausprobiert:

  1. Indizierung auf (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

Mithilfe des Index sucht die Abfrage nach den Zeilen, in denen RV = @Cursorfür jedes IdIn @Idsdie folgenden Zeilen gelesen und dann das Ergebnis zusammengeführt und sortiert werden.
Die Effizienz hängt dann von der relativen @CursorWertposition ab.
Wenn es kurz vor dem Ende der Daten steht (von RV bestellt), erfolgt die Abfrage sofort, und wenn nicht, kann die Abfrage bis zu Minuten dauern (lassen Sie sie niemals bis zum Ende laufen).

Das Problem bei diesem Ansatz ist, dass @Cursorentweder das Ende der Daten erreicht ist und die Sortierung nicht schmerzhaft ist (nicht einmal erforderlich, wenn die Abfrage weniger Zeilen zurückgibt als @MaxRows). Sie liegt auch weiter hinten und die Abfrage muss @MaxRows * LEN(@Ids)Zeilen sortieren .

  1. Indizierung auf Wohnmobilen:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

Mithilfe des Index sucht die Abfrage die Zeile, in der RV = @Cursordann jede Zeile gelesen wird, wobei die nicht angeforderten IDs verworfen werden, bis sie erreicht sind @MaxRows.
Die Effizienz hängt dann vom Prozentsatz der angeforderten IDs ( LEN(@Ids) / COUNT(DISTINCT Id)) und ihrer Verteilung ab.
Mehr angeforderte ID% bedeutet weniger verworfene Zeilen, was effizientere Lesevorgänge bedeutet, weniger angeforderte Id% bedeutet mehr verworfene Zeilen, was mehr Lesevorgänge für die gleiche Anzahl resultierender Zeilen bedeutet.

Das Problem bei diesem Ansatz besteht darin, dass, wenn die angeforderten IDs nur wenige Elemente enthalten, möglicherweise der gesamte Index gelesen werden muss, um die gewünschten Zeilen zu erhalten.

  1. Verwenden von gefiltertem Index oder indizierten Ansichten
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

Oder

    CREATE VIEW vDataClient1 WITH SCHEMABINDING
    AS
    SELECT
        Id,
        Date,
        Value,
        RV
    FROM dbo.Data
    WHERE Id IN (/* list of Ids for specific client*/)
    CREATE UNIQUE CLUSTERED INDEX IDX_IDRV ON vDataClient1(Id, Rv);

Diese Methode ermöglicht perfekt effiziente Indizierungs- und Abfrageausführungspläne, hat jedoch Nachteile: 1. In der Praxis muss ich dynamisches SQL implementieren, um die Indizes oder Ansichten zu erstellen und das Anforderungsverfahren zu ändern, um den richtigen Index oder die richtige Ansicht zu verwenden. 2. Ich muss einen Index oder eine Ansicht des vorhandenen Clients einschließlich des Speichers verwalten. 3. Jedes Mal, wenn ein Client seine Liste der angeforderten IDs ändern muss, muss ich den Index löschen oder anzeigen und neu erstellen.


Ich kann anscheinend keine Methode finden, die meinen Bedürfnissen entspricht.
Ich suche nach besseren Ideen, um das inkrementelle Abrufen von Daten zu implementieren. Diese Ideen könnten bedeuten, dass das anfordernde Schema oder das Datenbankschema überarbeitet werden, obwohl ich einen besseren Indizierungsansatz bevorzugen würde, wenn es einen gibt.

Paciv
quelle
Crosspost mit stackoverflow.com/questions/11586004/… . Ich habe die Oracle-Version im Moment entfernt, weil ich festgestellt habe, dass ORA_ROWSCN nicht indizierbar ist (und kaum durch indizierte materialisierte Ansichten).
Paciv
Wie passt das Datumsfeld hinein? Kann eine Zeile mit einer bestimmten ID und einem bestimmten Datum in der Tabelle aktualisiert werden? Und wenn ja, wird das Datum auch aktualisiert (wie ein zusätzlicher Zeitstempel?)
8kb
Scheint, als ob für den Versuch GetData () die Reihenfolge nach die ID enthalten sollte (Reihenfolge nach RV, ID). Können Sie die Verwendung eines Index von (Rv, Id) kommentieren? Auch die Verwendung von ">" max rowversion aus dem vorherigen Aufruf scheint Datensätze zwischen Chunks zu verpassen, wenn Zeilen dieselbe Zeilenversion haben (ist das nicht möglich?).
Crokusek
@ 8kb: Die Update-Anweisungen, die in der Tabelle ausgeführt werden, ändern nur die ValueSpalte. @crokusek: Wird nicht per Wohnmobil bestellt, ID statt Wohnmobil erhöht nur die Sortierarbeitsbelastung ohne Nutzen, ich verstehe die Gründe für Ihren Kommentar nicht. Nach dem, was ich gelesen habe, sollte RV eindeutig sein, es sei denn, Sie fügen Daten speziell in diese Spalte ein, was die Anwendung nicht tut.
Paciv
Kann der Client die Ergebnisse in der Reihenfolge (Id, Rv) akzeptieren und zusätzlich zum LastRowVersion-Argument ein LastId-Argument angeben, um die RV-Sortierung über IDs hinweg zu eliminieren? Meine vorherigen Kommentare basierten alle auf der Annahme, dass RV Duplikate hatte. Der gefilterte Index pro Client sah interessant aus.
Crokusek

Antworten:

5

Eine Lösung besteht darin, dass sich die Clientanwendung das Maximum rowversionpro ID merkt . Der benutzerdefinierte Tabellentyp würde sich ändern zu:

CREATE TYPE
    dbo.guid_list_tbltype
AS TABLE 
    (
    Id      uniqueidentifier PRIMARY KEY, 
    LastRV  rowversion NOT NULL
    );

Die Abfrage in der Prozedur kann dann neu geschrieben werden, um das APPLYMuster zu verwenden (siehe meine SQLServerCentral-Artikel Teil 1 und Teil 2 - kostenlose Anmeldung erforderlich). Der Schlüssel zu einer guten Leistung liegt hier ORDER BYdarin, dass ungeordnetes Vorabrufen des Joins mit verschachtelten Schleifen vermieden wird . Dies RECOMPILEist erforderlich, damit der Optimierer die Kardinalität der Tabellenvariablen zur Kompilierungszeit sehen kann (was wahrscheinlich zu einem wünschenswerten parallelen Plan führt).

ALTER PROCEDURE dbo.GetData

    @IDs        guid_list_tbltype READONLY,
    @MaxRows    bigint

AS
BEGIN

    SELECT TOP (@MaxRows)
        d.Id,
        d.[Date],
        d.Value,
        d.RV
    FROM @Ids AS i
    CROSS APPLY
    (
        SELECT
            d.*
        FROM dbo.Data AS d
        WHERE
            d.Id = i.Id
            AND d.RV > i.LastRV
    ) AS d
    ORDER BY
        i.Id,
        d.RV
    OPTION (RECOMPILE);

END;

Sie sollten einen Abfrageplan nach der Ausführung wie diesen erhalten (der geschätzte Plan ist seriell):

Abfrageplan

Paul White 9
quelle
Richtig, eine der Lösungen für Designänderungen besteht darin, dass sich der Client die MAX(RV)ID pro ID (oder ein Abonnementsystem, in dem sich die interne Anwendung alle ID / RV-Paare merkt) merkt und ich dieses Muster für einen anderen Client verwende. Eine andere Lösung bestand darin, den Client zu zwingen, immer alle IDs abzurufen (was das Indizierungsproblem trivial macht). Der besondere Bedarf der Frage wird immer noch nicht abgedeckt: Inkrementelles Abrufen einer Teilmenge von IDs mit nur einem vom Client bereitgestellten globalen Zähler.
Paciv
2

Wenn möglich, würde ich die Tabelle neu gestalten. Wenn wir VersionNumber als inkrementelle Ganzzahl ohne Lücken haben können, ist die Aufgabe, den nächsten Block abzurufen, ein völlig trivialer Bereichsscan. Wir brauchen nur den folgenden Index:

CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, VersionNumber) INCLUDE(Date, Value);

Natürlich müssen wir sicherstellen, dass VersionNumber mit eins beginnt und keine Lücken aufweist. Dies ist leicht mit Einschränkungen zu tun.

AK
quelle
Meinen Sie eine globale oder eine lokale ID VersionNumber? In beiden Fällen kann ich nicht sehen, wie das bei der Frage helfen wird. Könnten Sie das näher erläutern?
Paciv
0

Was ich getan hätte:

In diesem Fall sollte Ihre PK ein Identitätsfeld "Ersatzschlüssel" sein, das automatisch inkrementiert wird.
Da Sie bereits in Milliardenhöhe sind, ist es am besten, mit einem BigInt zu fahren.
Nennen wir es DataID .
Dieser Wille:

  • Fügen Sie jedem Datensatz in Ihrem Clustered Index 8 Byte hinzu.
  • Speichern Sie 16 Bytes für jeden Datensatz in jedem nicht gruppierten Index.
  • Was Sie hatten, war ein "natürlicher Schlüssel": ein UniqueIdentifyer (16 Bytes) mit einer DateTime (8 Bytes).
  • Das sind 24 Bytes in jedem Indexdatensatz, um auf den Clustered Index zurückzugreifen!
  • Aus diesem Grund haben wir Ersatzschlüssel als kleinere inkrementierende Ganzzahlen.


Stellen Sie Ihre neue BigInt PK ( DataID ) so ein, dass ein Clustered-Index verwendet wird:
Dies wird:

  • Stellen Sie sicher, dass die zuletzt erstellten Datensätze am Ende platziert werden.
  • Ermöglichen Sie eine schnellere Indizierung mit anderen nicht gruppierten Indizes.
  • Ermöglichen Sie zukünftige Erweiterungen als FK für andere Tabellen.


Erstellen Sie einen Non-Clustered-Index um (Datum, ID).
Dieser Wille:

  • Beschleunigen Sie Ihre am häufigsten verwendeten Abfragen.
  • Sie können "Wert" hinzufügen, dies erhöht jedoch die Größe Ihres Index, wodurch er langsamer wird.
  • Ich würde vorschlagen, es innerhalb und außerhalb des Index zu versuchen, um festzustellen, ob es einen großen Unterschied in der Leistung gibt.
  • Ich würde empfehlen, "Include" nicht zu verwenden, wenn Sie es hinzufügen.
  • Gehen Sie einfach so vor (Datum, ID, Wert) - aber nur, wenn Ihre Tests zeigen, dass dies die Leistung verbessert.


Erstellen Sie einen nicht gruppierten Index für (RV, ID).
Dieser Wille:

  • Halten Sie Ihre Indizes immer so klein wie möglich.
  • Wenn Sie nicht feststellen, dass das Datum und der Wert in Ihren Indizes unglaublich große Leistungssteigerungen aufweisen, sollten Sie diese weglassen, um Speicherplatz zu sparen. Probieren Sie es zuerst ohne sie aus.
  • Wenn Sie Datum oder Wert hinzufügen, verwenden Sie nicht "Einschließen", sondern fügen Sie sie der Reihenfolge des Index hinzu.
  • Dank der Inkrementierung der DataID für neue Einfügungen in Ihre Clustered PK werden Ihre letzten Wohnmobile normalerweise gegen Ende angezeigt (es sei denn, Sie aktualisieren ständig Datenschwaden aus der Vergangenheit).
MikeTeeVee
quelle