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 @MaxRows
Bereich 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:
- 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 = @Cursor
für jedes Id
In @Ids
die folgenden Zeilen gelesen und dann das Ergebnis zusammengeführt und sortiert werden.
Die Effizienz hängt dann von der relativen @Cursor
Wertposition 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 @Cursor
entweder 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 .
- 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 = @Cursor
dann 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.
- 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.
quelle
Value
Spalte. @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.Antworten:
Eine Lösung besteht darin, dass sich die Clientanwendung das Maximum
rowversion
pro ID merkt . Der benutzerdefinierte Tabellentyp würde sich ändern zu:Die Abfrage in der Prozedur kann dann neu geschrieben werden, um das
APPLY
Muster zu verwenden (siehe meine SQLServerCentral-Artikel Teil 1 und Teil 2 - kostenlose Anmeldung erforderlich). Der Schlüssel zu einer guten Leistung liegt hierORDER BY
darin, dass ungeordnetes Vorabrufen des Joins mit verschachtelten Schleifen vermieden wird . DiesRECOMPILE
ist erforderlich, damit der Optimierer die Kardinalität der Tabellenvariablen zur Kompilierungszeit sehen kann (was wahrscheinlich zu einem wünschenswerten parallelen Plan führt).Sie sollten einen Abfrageplan nach der Ausführung wie diesen erhalten (der geschätzte Plan ist seriell):
quelle
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.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:
Natürlich müssen wir sicherstellen, dass VersionNumber mit eins beginnt und keine Lücken aufweist. Dies ist leicht mit Einschränkungen zu tun.
quelle
VersionNumber
? In beiden Fällen kann ich nicht sehen, wie das bei der Frage helfen wird. Könnten Sie das näher erläutern?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:
Stellen Sie Ihre neue BigInt PK ( DataID ) so ein, dass ein Clustered-Index verwendet wird:
Dies wird:
Erstellen Sie einen Non-Clustered-Index um (Datum, ID).
Dieser Wille:
Erstellen Sie einen nicht gruppierten Index für (RV, ID).
Dieser Wille:
quelle