Bei dieser Frage gibt es einige Herausforderungen. Indizes in SQL Server können Folgendes mit jeweils nur wenigen logischen Lesevorgängen sehr effizient ausführen:
- Überprüfen Sie, ob eine Zeile vorhanden ist
- Überprüfen Sie, ob keine Zeile vorhanden ist
- Finde die nächste Reihe, die irgendwann beginnt
- Finden Sie die vorherige Zeile ab einem bestimmten Punkt
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 Ihre BINARY(64)
in a konvertieren BIGINT
und es werden keine Konvertierungsfehler ausgegeben, aber das Verhalten ist nicht definiert :
Es wird nicht garantiert, dass die Konvertierungen zwischen einem Datentyp und den binären Datentypen zwischen den Versionen von SQL Server identisch sind.
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
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
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:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Die Ergebnisse legen nahe, dass der maximale Wert, in den wir konvertieren, BIGINT
102500672 beträgt:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Es gibt 100 Millionen Zeilen mit Werten, die wie erwartet in BIGINT passen:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
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:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
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 :
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 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.
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 ?