Ich muss in der Lage sein, ein fehlendes Element aus einer Tabelle mit mehreren zehn Millionen Zeilen zu finden und habe einen Primärschlüssel einer BINARY(64)
Spalte (der Eingabewert, aus dem berechnet werden soll). Diese Werte werden meistens der Reihe nach eingefügt, aber gelegentlich möchte ich einen vorherigen Wert wiederverwenden, der gelöscht wurde. Es ist nicht möglich, die gelöschten Datensätze mit einer IsDeleted
Spalte zu ändern , da manchmal eine Zeile eingefügt wird, die viele Millionen Werte vor den derzeit vorhandenen Zeilen liegt. Dies bedeutet, dass die Beispieldaten ungefähr so aussehen würden:
KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF
Das Einfügen aller fehlenden Werte zwischen 0x000000000002
und 0xFFFFFFFFFFFF
ist nicht möglich. Der Zeit- und Raumaufwand wäre unerwünscht. Wenn ich den Algorithmus ausführe, erwarte ich im Wesentlichen, dass er zurückkehrt 0x000000000003
. Dies ist die erste Öffnung.
Ich habe einen binären Suchalgorithmus in C # entwickelt, der die Datenbank nach jedem Wert an der Position i
abfragt und testet, ob dieser Wert erwartet wird. Für den Kontext mein schrecklicher Algorithmus: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Dieser Algorithmus würde beispielsweise 26-27 SQL-Abfragen für eine Tabelle mit 100.000.000 Elementen ausführen. (Das scheint nicht viel zu sein, aber es wird sehr häufig vorkommen .) Derzeit enthält diese Tabelle ungefähr 50.000.000 Zeilen, und die Leistung macht sich bemerkbar .
Mein erster alternativer Gedanke ist, dies in eine gespeicherte Prozedur zu übersetzen, aber das hat seine eigenen Hürden. (Ich muss einen BINARY(64) + BINARY(64)
Algorithmus sowie eine Reihe anderer Dinge schreiben .) Dies wäre schmerzhaft, aber nicht unmöglich. Ich habe auch überlegt, den Übersetzungsalgorithmus basierend auf zu implementieren ROW_NUMBER
, aber ich habe ein wirklich schlechtes Bauchgefühl. (A BIGINT
ist für diese Werte bei weitem nicht groß genug.)
Ich bin bereit für andere Vorschläge, da ich dies wirklich brauche, um so schnell wie möglich zu sein. Für das, was es wert ist, ist die einzige Spalte, die von der C # -Abfrage ausgewählt wird, die KeyCol
, die anderen sind für diesen Teil irrelevant.
Für das, was es wert ist, lautet die aktuelle Abfrage, die den entsprechenden Datensatz abruft, wie folgt:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
Wo <VALUE>
ist der vom Algorithmus gelieferte Index? Ich hatte auch noch kein BIGINT
Problem damit OFFSET
, aber ich werde es tun. (Nur 50.000.000 Zeilen im Moment bedeuten, dass nie nach einem Index über diesem Wert gefragt wird, aber irgendwann wird er über den BIGINT
Bereich hinausgehen .)
Einige zusätzliche Daten:
- Bei Löschungen
gap:sequential
beträgt das Verhältnis ungefähr1:20
; - Die letzten 35.000 Zeilen in der Tabelle haben Werte>
BIGINT
maximal;
quelle
delete
es in Zukunft eine Chance, einen Trigger auf die Tabelle zu setzen, der die jetzt verfügbare Binärdatei in einer separaten Tabelle (z. B.create table available_for_reuse(id binary64)
) ablegt, insbesondere angesichts der Notwendigkeit, diese Suche sehr häufig durchzuführen ?mynameisebrown
was bedeuten würde, dass Sie erhalten würdenmynameisebrowo
, was Sie würde nicht wollen, wennabc
verfügbar ist.select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only
aus?SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol]
, was immer zurückkehrt1
.Antworten:
Joe hat bereits die meisten Punkte erreicht, die ich gerade eine Stunde lang getippt habe. Zusammenfassend:
KeyCol
Werte <bigint
max (9.2e18) ausgehen. Daher sollten Konvertierungen (falls erforderlich) von / nachbigint
kein Problem darstellen, solange Sie die Suche auf beschränkenKeyCol <= 0x00..007FFFFFFFFFFFFFFF
Also, was tun?
Lassen Sie uns die Suchidee (wiederholt, CPU-intensiv, Brute Force) für eine Minute auf Eis legen und das Gesamtbild betrachten.
Was ich vorschlagen möchte, sind einige Ergänzungen zum Datenmodell ...
KeyCol
Werten protokolliert, z.available_for_use(KeyCol binary(64) not null primary key)
KeyCol
werden (möglicherweise wird ein gespeicherter "Top-off" -Prozess erstellt?) [z. B. Joesselect/top/row_number()
Abfrage aktualisieren , um einetop 100000
] auszuführenavailable_for_use
Fall, dass Ihnen jemals die Werte ausgehenKeyCol
Werte in unsere neue Tabelle einfügt,available_for_use
wenn eine Zeile aus der Haupttabelle gelöscht wirdKeyCol
Spalte dann eine neue / geänderte UPDATE - Trigger auf der> main_table <auch unsere neue Tabelle haltenavailable_for_use
aktualisiertKeyCol
Wert zu suchen, den Sieselect min(KeyCol) from available_for_use
(offensichtlich gibt es ein bisschen mehr, da a) Sie für Parallelitätsprobleme codieren müssen - möchten Sie nicht, dass 2 Kopien Ihres Prozesses denselbenmin(KeyCol)
und b) Sie greifen mussmin(KeyCol)
aus der Tabelle löschen ; Dies sollte relativ einfach zu codieren sein, möglicherweise als gespeicherter Prozess, und kann bei Bedarf in einem anderen Q & A behandelt werden.select min(KeyCol)
Prozess keine verfügbaren Zeilen findet, können Sie Ihren Top-Off-Prozess starten, um einen neuen Stapel von Zeilen zu generierenMit diesen vorgeschlagenen Änderungen am Datenmodell:
available_for_use
Tabelle zu überwachen , um sicherzustellen, dass Ihnen nie die neuen Werte ausgehenJa, die vorgeschlagene
available_for_use
Tabelle ist nur eine Tabelle mit vorgenerierten Werten für den nächsten Schlüssel. und ja, es besteht die Möglichkeit von Konflikten, wenn der 'nächste' Wert ermittelt wird, aber jeder Konflikt a) kann leicht durch ein geeignetes Tabellen- / Index- / Abfragedesign behoben werden und b) wird im Vergleich zum Overhead / geringfügig / kurzlebig sein. Verzögerungen mit der aktuellen Idee wiederholter Brute-Force-Indexsuchen.quelle
n
Schlüssel (wahrscheinlich 10 oder 20, um sie zu zwingen, nach niedrigeren, wünschenswerteren Werten zu suchen) ziehen. Schätzen Sie die Antwort hier wirklich, Sie schreiben die Gedanken schriftlich! :)KeyCol
Werten bereitstellen kann ... ja, das würde auch funktionieren :-) und offensichtlich die Notwendigkeit einer Änderung des Datenmodells beseitigen, ehKeyCol
Manager und die Notwendigkeit, mögliche PK-Verstöße zu codieren, wenn 2 (oder mehr) gleichzeitige Instanzen der App versuchen, denselbenKeyCol
Wert zu verwenden ... igitt ... definitiv einfacher mit einem einzelnen Middleware-Server oder einem db-zentrierte LösungBei dieser Frage gibt es einige Herausforderungen. Indizes in SQL Server können Folgendes mit jeweils nur wenigen logischen Lesevorgängen sehr effizient ausführen:
Sie können jedoch nicht verwendet werden, um die N-te Zeile in einem Index zu finden. Dazu müssen Sie Ihren eigenen Index rollen, der als Tabelle gespeichert ist, oder die ersten N Zeilen im Index scannen. Ihr C # -Code hängt stark von der Tatsache ab, dass Sie das N-te Element des Arrays effizient finden können, aber das können Sie hier nicht tun. Ich denke, dass der Algorithmus für T-SQL ohne eine Änderung des Datenmodells nicht verwendbar ist.
Die zweite Herausforderung betrifft die Einschränkungen der
BINARY
Datentypen. Soweit ich das beurteilen kann, können Sie Addition, Subtraktion oder Division nicht auf die übliche Weise durchführen. Sie können IhreBINARY(64)
in a konvertierenBIGINT
und es werden keine Konvertierungsfehler ausgegeben, aber das Verhalten ist nicht definiert :Darüber hinaus ist das Fehlen von Konvertierungsfehlern hier ein Problem. Sie können alles konvertieren, was größer als der größtmögliche
BIGINT
Wert ist, aber Sie erhalten die falschen Ergebnisse.Es stimmt, dass Sie derzeit Werte haben, die größer als 9223372036854775807 sind. Wenn Sie jedoch immer bei 1 beginnen und nach dem kleinsten Mindestwert suchen, können diese großen Werte nur relevant sein, wenn Ihre Tabelle mehr als 9223372036854775807 Zeilen enthält. Dies scheint unwahrscheinlich, da Ihre Tabelle zu diesem Zeitpunkt etwa 2000 Exabyte groß sein würde. Um Ihre Frage zu beantworten, gehe ich davon aus, dass die sehr großen Werte nicht durchsucht werden müssen. Ich werde auch eine Datentypkonvertierung durchführen, da diese unvermeidlich zu sein scheinen.
Für die Testdaten habe ich das Äquivalent von 50 Millionen aufeinanderfolgenden Ganzzahlen in eine Tabelle eingefügt, zusammen mit 50 Millionen weiteren Ganzzahlen mit einer einzelnen Wertelücke etwa alle 20 Werte. Ich habe auch einen einzelnen Wert eingefügt, der nicht richtig in ein signiertes passt
BIGINT
:Es dauerte einige Minuten, bis dieser Code auf meinem Computer ausgeführt wurde. Ich habe dafür gesorgt, dass die erste Tabellenhälfte keine Lücken aufweist, die einen schlechteren Fall für die Leistung darstellen. Der Code, mit dem ich das Problem gelöst habe, durchsucht den Index der Reihe nach, sodass er sehr schnell beendet wird, wenn die erste Lücke früh in der Tabelle liegt. Bevor wir dazu kommen, überprüfen wir, ob die Daten so sind, wie sie sein sollten:
Die Ergebnisse legen nahe, dass der maximale Wert, in den wir konvertieren,
BIGINT
102500672 beträgt:Es gibt 100 Millionen Zeilen mit Werten, die wie erwartet in BIGINT passen:
Ein Ansatz für dieses Problem besteht darin, den Index der Reihe nach zu scannen und zu beenden, sobald der Wert einer Zeile nicht mit dem erwarteten
ROW_NUMBER()
Wert übereinstimmt. Die gesamte Tabelle muss nicht gescannt werden, um die erste Zeile zu erhalten: nur die Zeilen bis zur ersten Lücke. Hier ist eine Möglichkeit, Code zu schreiben, der wahrscheinlich diesen Abfrageplan erhält:Aus Gründen, die nicht in diese Antwort passen, wird diese Abfrage häufig seriell von SQL Server ausgeführt, und SQL Server unterschätzt häufig die Anzahl der Zeilen, die gescannt werden müssen, bevor die erste Übereinstimmung gefunden wird. Auf meinem Computer durchsucht SQL Server 50000022 Zeilen aus dem Index, bevor die erste Übereinstimmung gefunden wird. Die Abfrage dauert 11 Sekunden. Beachten Sie, dass dies den ersten Wert nach der Lücke zurückgibt. Es ist nicht klar, welche Zeile Sie genau möchten, aber Sie sollten in der Lage sein, die Abfrage ohne große Probleme an Ihre Anforderungen anzupassen. So sieht der Plan aus:
Meine einzige andere Idee war, SQL Server dazu zu bringen, Parallelität für die Abfrage zu verwenden. Ich habe vier CPUs, also werde ich die Daten in vier Bereiche aufteilen und nach diesen Bereichen suchen. Jeder CPU wird ein Bereich zugewiesen. Um die Bereiche zu berechnen, habe ich einfach den Maximalwert ermittelt und angenommen, dass die Daten gleichmäßig verteilt sind. Wenn Sie klüger sein möchten, können Sie sich ein Stichprobenhistogramm für die Spaltenwerte ansehen und Ihre Bereiche auf diese Weise erstellen. Der folgende Code basiert auf vielen undokumentierten Tricks, die für die Produktion nicht sicher sind, einschließlich des Ablaufverfolgungsflags 8649 :
So sieht das parallele verschachtelte Schleifenmuster aus:
Insgesamt erledigt die Abfrage mehr Arbeit als zuvor, da mehr Zeilen in der Tabelle gescannt werden. Es läuft jetzt jedoch in 7 Sekunden auf meinem Desktop. Auf einem echten Server kann es möglicherweise besser parallelisiert werden. Hier ist ein Link zum aktuellen Plan .
Ich kann mir wirklich keinen guten Weg vorstellen, um dieses Problem zu lösen. Die Berechnung außerhalb von SQL durchzuführen oder das Datenmodell zu ändern, ist möglicherweise die beste Wahl.
quelle
Hier ist eine Antwort, die wahrscheinlich nicht für Sie funktioniert, aber ich werde sie trotzdem hinzufügen.
Obwohl BINARY (64) aufzählbar ist, gibt es eine schlechte Unterstützung, um den Nachfolger eines Elements zu bestimmen. Da BIGINT für Ihre Domain zu klein zu sein scheint, können Sie ein DECIMAL (38,0) verwenden, das der größte NUMBER-Typ in SQL Server zu sein scheint.
Die erste Lücke zu finden ist einfach, da wir die gesuchte Zahl konstruieren können:
Ein verschachtelter Loop-Join über dem pk-Index sollte ausreichen, um das erste verfügbare Element zu finden.
quelle