Ich werde eine Antwort posten, um loszulegen. Mein erster Gedanke war, dass es möglich sein sollte, die sortierungserhaltende Natur eines verschachtelten Schleifen-Joins zusammen mit einigen Hilfstabellen zu nutzen, die für jeden Buchstaben eine Zeile haben. Der knifflige Teil sollte in einer solchen Schleife ablaufen, dass die Ergebnisse nach Länge sortiert und Duplikate vermieden wurden. Wenn Sie beispielsweise einem CTE beitreten, der alle 26 Großbuchstaben zusammen mit '' enthält, können Sie am Ende generieren, 'A' + '' + 'A'
und '' + 'A' + 'A'
das ist natürlich dieselbe Zeichenfolge.
Die erste Entscheidung war, wo die Helferdaten gespeichert werden sollten. Ich habe versucht, eine temporäre Tabelle zu verwenden, was sich jedoch überraschend negativ auf die Leistung auswirkte, obwohl die Daten auf eine einzelne Seite passten. Die Temp-Tabelle enthielt die folgenden Daten:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
Im Vergleich zur Verwendung eines CTE dauerte die Abfrage bei einer gruppierten Tabelle 3-mal länger und bei einem Heap 4-mal länger. Ich glaube nicht, dass das Problem darin besteht, dass sich die Daten auf der Festplatte befinden. Es sollte als einzelne Seite in den Speicher eingelesen und für den gesamten Plan im Speicher verarbeitet werden. Möglicherweise kann SQL Server mit Daten eines Constant Scan-Operators effizienter arbeiten als mit Daten, die in typischen Rowstore-Seiten gespeichert sind.
Interessanterweise wählt SQL Server, die geordneten Ergebnisse aus einer einseitigen Tempdb-Tabelle mit geordneten Daten in einen Tabellenspool zu packen:
SQL Server legt häufig Ergebnisse für die innere Tabelle eines Cross-Joins in einem Tabellenspool ab, auch wenn dies unsinnig erscheint. Ich denke, dass der Optimierer ein wenig Arbeit in diesem Bereich benötigt. Ich habe die Abfrage mit ausgeführt NO_PERFORMANCE_SPOOL
, um den Leistungseinbruch zu vermeiden.
Ein Problem bei der Verwendung eines CTE zum Speichern der Hilfsdaten besteht darin, dass die Daten nicht garantiert bestellt werden können. Ich kann mir nicht vorstellen, warum der Optimierer es nicht bestellt hat, und in all meinen Tests wurden die Daten in der Reihenfolge verarbeitet, in der ich den CTE geschrieben habe:
Es ist jedoch am besten, kein Risiko einzugehen, insbesondere wenn es eine Möglichkeit gibt, dies ohne großen Leistungsaufwand zu tun. Es ist möglich, die Daten in einer abgeleiteten Tabelle zu ordnen, indem ein überflüssiger TOP
Operator hinzugefügt wird . Beispielsweise:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
Dieser Zusatz zur Abfrage sollte gewährleisten, dass die Ergebnisse in der richtigen Reihenfolge zurückgegeben werden. Ich habe erwartet, dass alle Sorten einen großen negativen Einfluss auf die Performance haben. Das Abfrageoptimierungsprogramm hat dies auch auf der Grundlage der geschätzten Kosten erwartet:
Sehr überraschend konnte ich keinen statistisch signifikanten Unterschied in der CPU-Zeit oder Laufzeit mit oder ohne explizite Bestellung feststellen. Wenn überhaupt, schien die Abfrage mit ORDER BY
! Schneller zu laufen. Ich habe keine Erklärung für dieses Verhalten.
Der knifflige Teil des Problems bestand darin, herauszufinden, wie leere Zeichen an den richtigen Stellen eingefügt werden können. Wie bereits erwähnt, CROSS JOIN
würde ein einfacher Vorgang zu doppelten Daten führen. Wir wissen, dass die 100000000. Zeichenfolge eine Länge von sechs Zeichen hat, weil:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
aber
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Deshalb müssen wir uns dem Brief CTE nur sechsmal anschließen. Angenommen, wir treten sechs Mal dem CTE bei, nehmen einen Buchstaben von jedem CTE und verknüpfen sie alle miteinander. Angenommen, der Buchstabe ganz links ist nicht leer. Wenn einer der nachfolgenden Buchstaben leer ist, bedeutet dies, dass die Zeichenfolge kürzer als sechs Zeichen ist, sodass es sich um ein Duplikat handelt. Daher können wir Duplikate vermeiden, indem wir das erste nicht leere Zeichen suchen und alle Zeichen danach auch nicht leer lassen. Ich entschied mich, dies zu verfolgen, indem ich FLAG
einer der CTEs eine Spalte zuwies und der WHERE
Klausel ein Häkchen hinzufügte . Dies sollte nach Betrachtung der Abfrage klarer sein. Die letzte Abfrage lautet wie folgt:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
Die CTEs sind wie oben beschrieben. ALL_CHAR
wird fünfmal verbunden, da es eine Zeile für ein Leerzeichen enthält. Das letzte Zeichen in der Zeichenfolge sollte niemals leer sein, damit ein separater CTE dafür definiert wird FIRST_CHAR
. Die zusätzliche Flag-Spalte in ALL_CHAR
wird verwendet, um Duplikate wie oben beschrieben zu verhindern. Möglicherweise gibt es eine effizientere Methode, um diese Prüfung durchzuführen, aber es gibt definitiv ineffizientere Methoden, um sie durchzuführen. Ein Versuch von mir mit LEN()
und POWER()
machte die Abfrage sechsmal langsamer als die aktuelle Version.
Die MAXDOP 1
und FORCE ORDER
-Hinweise sind wichtig, um sicherzustellen, dass die Reihenfolge in der Abfrage erhalten bleibt. Ein mit Anmerkungen versehener geschätzter Plan kann hilfreich sein, um festzustellen, warum die Verknüpfungen in der aktuellen Reihenfolge vorliegen:
Abfragepläne werden häufig von rechts nach links gelesen, aber Zeilenanforderungen erfolgen von links nach rechts. Im Idealfall fordert SQL Server vom d1
Konstantenscan-Operator genau 100 Millionen Zeilen an . Wenn Sie sich von links nach rechts bewegen, erwarte ich, dass von jedem Operator weniger Zeilen angefordert werden. Wir können dies in der sehen tatsächlichen Ausführungsplan entnehmen . Darüber hinaus ist unten ein Screenshot von SQL Sentry Plan Explorer:
Wir haben genau 100 Millionen Zeilen von d1, was eine gute Sache ist. Beachten Sie, dass das Verhältnis der Zeilen zwischen d2 und d3 fast genau 27: 1 beträgt (165336 * 27 = 4464072). Dies ist sinnvoll, wenn Sie sich überlegen, wie die Kreuzverknüpfung funktioniert. Das Verhältnis der Zeilen zwischen d1 und d2 beträgt 22,4, was eine Verschwendung darstellt. Ich glaube, die zusätzlichen Zeilen stammen von Duplikaten (aufgrund der Leerzeichen in der Mitte der Zeichenfolgen), die nicht hinter den Join-Operator für verschachtelte Schleifen gelangen, der die Filterung ausführt.
Der LOOP JOIN
Hinweis ist technisch unnötig, da aCROSS JOIN
nur als Loop-Join in SQL Server implementiert werden kann. Das NO_PERFORMANCE_SPOOL
ist das unnötige Tabelle Spooling zu verhindern. Wenn der Spool-Hinweis weggelassen wurde, dauerte die Abfrage auf meinem Computer 3-mal länger.
Die endgültige Abfrage hat eine CPU-Zeit von ca. 17 Sekunden und eine Gesamtzeit von 18 Sekunden. Das war, als die Abfrage über SSMS ausgeführt und die Ergebnismenge verworfen wurde. Ich bin sehr daran interessiert, andere Methoden zur Erzeugung der Daten zu sehen.