ORDER BY und Vergleich gemischter Buchstaben- und Zahlenfolgen

9

Wir müssen einige Berichte über Werte erstellen, bei denen es sich normalerweise um gemischte Zeichenfolgen aus Zahlen und Buchstaben handelt, die "natürlich" sortiert werden müssen. Dinge wie zB "P7B18" oder "P12B3". @Die Zeichenfolgen bestehen hauptsächlich aus Buchstabenfolgen und abwechselnden Zahlen. Die Anzahl dieser Segmente und die Länge der einzelnen Segmente können jedoch variieren.

Wir möchten, dass die numerischen Teile davon in numerischer Reihenfolge sortiert werden. Wenn ich diese Zeichenfolgenwerte nur direkt mit ORDER BYbehandle, wird "P12B3" natürlich vor "P7B18" stehen, da "P1" früher als "P7" ist, aber ich möchte das Gegenteil, da "P7" natürlich vorausgeht "P12".

Ich möchte auch in der Lage sein, Bereichsvergleiche durchzuführen, z @bin < 'P13S6'. B. oder solche. Ich muss nicht mit Gleitkomma- oder negativen Zahlen umgehen. Dies sind ausschließlich nicht negative Ganzzahlen, mit denen wir es zu tun haben. Die Zeichenfolgenlängen und die Anzahl der Segmente können möglicherweise beliebig sein, ohne feste Obergrenzen.

In unserem Fall ist das String-Gehäuse nicht wichtig. Wenn es jedoch eine Möglichkeit gibt, dies kollationsbewusst zu tun, finden andere dies möglicherweise nützlich. Das Hässlichste daran ist, dass ich in der WHEREKlausel sowohl die Reihenfolge als auch die Bereichsfilterung durchführen kann .

Wenn ich dies in C # tun würde, wäre es eine ziemlich einfache Aufgabe: Führen Sie eine Analyse durch, um das Alpha von der Zahl zu trennen, implementieren Sie IComparable, und Sie sind im Grunde genommen fertig. SQL Server scheint natürlich keine ähnliche Funktionalität zu bieten, zumindest soweit mir bekannt ist.

Kennt jemand gute Tricks, um diese Arbeit zu machen? Gibt es eine wenig publizierte Möglichkeit, benutzerdefinierte CLR-Typen zu erstellen, die IComparable implementieren und sich wie erwartet verhalten? Ich bin auch nicht gegen dumme XML-Tricks (siehe auch: Listenverkettung), und ich habe auch CLR-Regex-Matching- / Extraktions- / Ersatz-Wrapper-Funktionen auf dem Server verfügbar.

BEARBEITEN: Als etwas detaillierteres Beispiel möchte ich, dass sich die Daten so verhalten.

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0

Teilen Sie die Zeichenfolgen in Token aller Buchstaben oder Zahlen auf und sortieren Sie sie entweder alphabetisch oder numerisch, wobei die Token ganz links der wichtigste Sortierbegriff sind. Wie ich bereits erwähnt habe, ein Kinderspiel in .NET, wenn Sie IComparable implementieren, aber ich weiß nicht, wie (oder ob) Sie so etwas in SQL Server tun können. Es ist sicherlich nicht etwas, auf das ich in 10 oder so Jahren der Arbeit jemals gestoßen bin.

db2
quelle
Sie können dies mit einer Art indizierter berechneter Spalte tun und die Zeichenfolge in eine Ganzzahl umwandeln. So P7B12könnte es P 07 B 12dann werden (via ASCII) 80 07 65 12, so80076512
Philᵀᴹ
Ich schlage vor, Sie erstellen eine berechnete Spalte, die jede numerische Komponente auf eine große Länge (dh 10 Nullen) auffüllt. Da das Format ziemlich willkürlich ist, benötigen Sie einen ziemlich großen Inline-Ausdruck, der jedoch machbar ist. Dann können Sie in dieser Spalte so viel indizieren / sortieren, wie Sie möchten.
Nick.McDermaid
Bitte sehen Sie den Link, den ich gerade oben in meiner Antwort hinzugefügt habe :)
Solomon Rutzky
1
@srutzky Schön, ich habe dafür gestimmt.
DB2
Hey db2: Da Microsoft von Connect zu UserVoice wechselt und die Stimmenzahl nicht genau beibehält (sie haben sie in einen Kommentar eingefügt, aber nicht sicher, ob sie das sehen), müssen Sie möglicherweise erneut abstimmen: Unterstützen Sie "natürliche Sortierung" / DIGITSASNUMBERS als Sortieroption . Vielen Dank!
Solomon Rutzky

Antworten:

8

Möchten Sie ein vernünftiges und effizientes Mittel zum Sortieren von Zahlen in Zeichenfolgen als tatsächliche Zahlen? Stimmen Sie für meinen Microsoft Connect-Vorschlag ab: Unterstützen Sie "natürliche Sortierung" / DIGITSASNUMBERS als Sortieroption


Es gibt keine einfachen, eingebauten Mittel, um dies zu tun, aber hier ist eine Möglichkeit:

Normalisieren Sie die Zeichenfolgen, indem Sie sie in Segmente fester Länge umformatieren:

  • Erstellen Sie eine Sortierspalte vom Typ VARCHAR(50) COLLATE Latin1_General_100_BIN2. Die maximale Länge von 50 muss möglicherweise basierend auf der maximalen Anzahl von Segmenten und ihren möglichen maximalen Längen angepasst werden.
  • Während die Normalisierung in der App-Ebene effizienter durchgeführt werden könnte, würde die Handhabung in der Datenbank mithilfe einer T-SQL-UDF die Platzierung der skalaren UDF in einem AFTER [or FOR] INSERT, UPDATETrigger ermöglichen, sodass Sie garantiert den Wert für alle Datensätze, auch für diese, richtig einstellen können Eingehen über Ad-hoc-Abfragen usw. Natürlich kann diese skalare UDF auch über SQLCLR verarbeitet werden, aber es müsste getestet werden, um festzustellen, welche tatsächlich effizienter ist. ** **.
  • Die UDF (unabhängig davon, ob sie sich in T-SQL oder SQLCLR befindet) sollte:
    • Verarbeiten Sie eine unbekannte Anzahl von Segmenten, indem Sie jedes Zeichen lesen und anhalten, wenn der Typ von Alpha zu Numerisch oder Numerisch zu Alpha wechselt.
    • Für jedes Segment sollte eine Zeichenfolge mit fester Länge zurückgegeben werden, die auf die maximal möglichen Zeichen / Ziffern eines Segments festgelegt ist (oder möglicherweise maximal + 1 oder 2, um zukünftiges Wachstum zu berücksichtigen).
    • Alpha-Segmente sollten linksbündig und rechts mit Leerzeichen aufgefüllt sein.
    • Numerische Segmente sollten rechtsbündig und mit Nullen links aufgefüllt sein.
    • Wenn Alpha-Zeichen in Groß- und Kleinschreibung eingegeben werden können, die Reihenfolge jedoch nicht zwischen Groß- und Kleinschreibung unterscheidet, wenden Sie die UPPER()Funktion auf das Endergebnis aller Segmente an (sodass sie nur einmal und nicht pro Segment ausgeführt werden muss). Dies ermöglicht eine ordnungsgemäße Sortierung angesichts der binären Sortierung der Sortierspalte.
  • Erstellen Sie einen AFTER INSERT, UPDATETrigger für die Tabelle, der die UDF aufruft, um die Sortierspalte festzulegen. Um die Leistung zu verbessern, verwenden Sie die UPDATE()Funktion, um festzustellen, ob diese Codespalte gerade in der SETKlausel der UPDATEAnweisung enthalten ist (einfach RETURNwenn falsch), und verknüpfen Sie dann die INSERTEDund die DELETEDPseudotabellen in der Codespalte, um nur Zeilen zu verarbeiten, deren Codewert geändert wurde . Stellen Sie sicher, COLLATE Latin1_General_100_BIN2dass Sie diese JOIN-Bedingung angeben , um sicherzustellen, dass genau festgestellt wird, ob eine Änderung vorliegt.
  • Erstellen Sie einen Index für die neue Sortierspalte.

Beispiel:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"

Bei diesem Ansatz können Sie sortieren über:

ORDER BY tbl.SortColumn

Und Sie können die Bereichsfilterung durchführen über:

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')

oder:

DECLARE @RangeStart VARCHAR(50),
        @RangeEnd VARCHAR(50);
SELECT @RangeStart = dbo.MyUDF('P7B18'),
       @RangeEnd = dbo.MyUDF('P12B3');

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

Sowohl der ORDER BYals auch der WHEREFilter sollten die binäre Kollatierung verwenden, die SortColumnaufgrund der Kollatierungspräzedenz definiert ist .

Gleichheitsvergleiche würden weiterhin für die ursprüngliche Wertespalte durchgeführt.


Andere Gedanken:

  • Verwenden Sie eine SQLCLR-UDT. Dies könnte funktionieren, obwohl es unklar ist, ob es im Vergleich zu dem oben beschriebenen Ansatz einen Nettogewinn darstellt.

    Ja, bei einem SQLCLR-UDT können die Vergleichsoperatoren mit benutzerdefinierten Algorithmen überschrieben werden. Dies behandelt Situationen, in denen der Wert entweder mit einem anderen Wert verglichen wird, der bereits denselben benutzerdefinierten Typ hat, oder mit einem Wert, der implizit konvertiert werden muss. Dies sollte den Bereichsfilter in einem WHEREZustand behandeln.

    In Bezug auf das Sortieren des UDT als regulären Spaltentyp (keine berechnete Spalte) ist dies nur möglich, wenn das UDT "Byte geordnet" ist. "Byte geordnet" bedeutet, dass die binäre Darstellung des UDT (die im UDT definiert werden kann) natürlich in der entsprechenden Reihenfolge sortiert wird. Unter der Annahme, dass die binäre Darstellung ähnlich wie der oben für die Spalte VARCHAR (50) beschriebene Ansatz behandelt wird, bei dem Segmente mit fester Länge aufgefüllt sind, würde dies qualifizieren. Wenn es nicht einfach wäre, sicherzustellen, dass die binäre Darstellung natürlich in der richtigen Reihenfolge angeordnet ist, können Sie eine Methode oder Eigenschaft des UDT verfügbar machen, die einen Wert ausgibt, der ordnungsgemäß geordnet ist, und dann eine PERSISTEDberechnete Spalte darauf erstellen Methode oder Eigenschaft. Die Methode muss deterministisch sein und als gekennzeichnet sein IsDeterministic = true.

    Vorteile dieses Ansatzes sind:

    • Kein Feld "Originalwert" erforderlich.
    • Sie müssen keine UDF aufrufen, um die Daten einzufügen oder Werte zu vergleichen. Angenommen, die ParseMethode des UDT nimmt den P7B18Wert auf und konvertiert ihn, dann sollten Sie in der Lage sein, die Werte einfach auf natürliche Weise als einzufügen P7B18. Und mit der im UDT festgelegten impliziten Konvertierungsmethode würde die WHERE-Bedingung auch die Verwendung von einfach P7B18` ermöglichen.

    Konsequenzen dieses Ansatzes sind:

    • Durch einfaches Auswählen des Felds wird die binäre Darstellung zurückgegeben, wenn der nach Byte geordnete UDT als Spaltendatentyp verwendet wird. Wenn Sie eine PERSISTEDberechnete Spalte für eine Eigenschaft oder Methode des UDT verwenden, wird die von der Eigenschaft oder Methode zurückgegebene Darstellung angezeigt. Wenn Sie den ursprünglichen P7B18Wert möchten , müssen Sie eine Methode oder Eigenschaft des UDT aufrufen, die codiert ist, um diese Darstellung zurückzugeben. Da Sie die ToStringMethode ohnehin überschreiben müssen, ist dies ein guter Kandidat dafür.
    • Es ist unklar (zumindest für mich im Moment, da ich diesen Teil nicht getestet habe), wie einfach / schwierig es wäre, Änderungen an der binären Darstellung vorzunehmen. Das Ändern der gespeicherten, sortierbaren Darstellung erfordert möglicherweise das Löschen und erneute Hinzufügen des Felds. Außerdem würde das Löschen der Assembly, die den UDT enthält, fehlschlagen, wenn sie auf eine der beiden Arten verwendet wird. Sie sollten also sicherstellen, dass sich in der Assembly außer diesem UDT nichts anderes befindet. Sie können ALTER ASSEMBLYdie Definition ersetzen, es gibt jedoch einige Einschränkungen.

      Auf der anderen Seite handelt es sich bei dem VARCHAR()Feld um Daten, die vom Algorithmus getrennt sind, sodass nur die Spalte aktualisiert werden muss. Und wenn es mehrere zehn Millionen Zeilen (oder mehr) gibt, kann dies in einem Stapelansatz erfolgen.

  • Implementieren Sie die ICU- Bibliothek, die diese alphanumerische Sortierung tatsächlich ermöglicht. Die Bibliothek ist zwar hochfunktional, aber nur in zwei Sprachen verfügbar: C / C ++ und Java. Dies bedeutet, dass Sie möglicherweise einige Änderungen vornehmen müssen, damit es in Visual C ++ funktioniert, oder dass die Möglichkeit besteht, dass der Java-Code mithilfe von IKVM in MSIL konvertiert werden kann . Auf dieser Site sind ein oder zwei .NET-seitige Projekte verlinkt, die eine COM-Schnittstelle bieten, auf die in verwaltetem Code zugegriffen werden kann. Ich glaube jedoch, dass sie seit einiger Zeit nicht mehr aktualisiert wurden und ich sie nicht ausprobiert habe. Am besten ist es, dies in der App-Ebene mit dem Ziel zu behandeln, Sortierschlüssel zu generieren. Die Sortierschlüssel würden dann in einer neuen Sortierspalte gespeichert.

    Dies ist möglicherweise nicht der praktischste Ansatz. Es ist jedoch immer noch sehr cool, dass eine solche Fähigkeit existiert. In der folgenden Antwort habe ich ein detaillierteres Beispiel dafür gegeben:

    Gibt es eine Sortierung, um die folgenden Zeichenfolgen in der folgenden Reihenfolge zu sortieren: 1,2,3,6,10,10A, 10B, 11?

    Das in dieser Frage behandelte Muster ist jedoch etwas einfacher. Ein Beispiel, das zeigt, dass die Art des Musters, mit dem sich diese Frage befasst, auch funktioniert, finden Sie auf der folgenden Seite:

    ICU Collation Demo

    Setzen Sie unter "Einstellungen" die Option "numerisch" auf "ein" und alle anderen sollten auf "Standard" gesetzt sein. Deaktivieren Sie rechts neben der Schaltfläche "Sortieren" die Option für "Diff-Stärken" und aktivieren Sie die Option für "Sortierschlüssel". Ersetzen Sie dann die Liste der Elemente im Textbereich "Eingabe" durch die folgende Liste:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23

    Klicken Sie auf die Schaltfläche "Sortieren". Der Textbereich "Ausgabe" sollte Folgendes anzeigen:

    as456456hgjg6786867
        29 4D 0F 7A EA C8 37 35 3B 35 0F 84 17 A7 0F 93 90 , 0D , , 0D .
    P7B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P007B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P7BA19
        47 0F 09 2B 29 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19
        47 0F 09 2B 2B 0F 15 , 09 , FD F2 , DC C5 DC 06 .
    P7BB19
        47 0F 09 2B 2B 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19z23
        47 0F 09 2B 2B 0F 15 5B 0F 19 , 0B , FD F4 , DC C5 DC 08 .
    P7Bb20
        47 0F 09 2B 2B 0F 16 , 09 , FD F2 , DC C5 DC 06 .
    P12B3
        47 0F 0E 2B 0F 05 , 08 , FD F1 , DC C5 DC 05 .
    P12B22
        47 0F 0E 2B 0F 18 , 08 , FD F1 , DC C5 DC 05 .

    Bitte beachten Sie, dass die Sortierschlüssel in mehreren Feldern strukturiert sind, die durch Kommas getrennt sind. Jedes Feld muss unabhängig sortiert werden, damit ein weiteres kleines Problem gelöst werden kann, wenn dies in SQL Server implementiert werden muss.


** Wenn Bedenken hinsichtlich der Leistung hinsichtlich der Verwendung von benutzerdefinierten Funktionen bestehen, beachten Sie bitte, dass die vorgeschlagenen Ansätze diese nur minimal nutzen. Tatsächlich bestand der Hauptgrund für das Speichern des normalisierten Werts darin, zu vermeiden, dass für jede Zeile jeder Abfrage eine UDF aufgerufen wird. Im primären Ansatz wird die UDF verwendet, um den Wert von festzulegen SortColumn, und dies erfolgt nur über INSERTund UPDATEüber den Trigger. Das Auswählen von Werten ist weitaus häufiger als das Einfügen und Aktualisieren, und einige Werte werden nie aktualisiert. Für jede SELECTAbfrage, die den SortColumnFilter für einen Bereich in der WHEREKlausel verwendet, wird die UDF nur einmal pro Wert für range_start und range_end benötigt, um die normalisierten Werte zu erhalten. Die UDF wird nicht pro Zeile aufgerufen.

In Bezug auf die UDT ist die Verwendung tatsächlich dieselbe wie bei der skalaren UDF. Das heißt, das Einfügen und Aktualisieren würde die Normalisierungsmethode einmal pro Zeile aufrufen, um den Wert festzulegen. Dann würde die Normalisierungsmethode einmal pro Abfrage pro Bereichsstart und Bereichswert in einem Bereichsfilter aufgerufen, jedoch nicht pro Zeile.

Ein Punkt für die vollständige Behandlung der Normalisierung in einer SQLCLR-UDF ist, dass sie, da sie keinen Datenzugriff ausführt und deterministisch ist, wenn sie als markiert IsDeterministic = trueist, an parallelen Plänen teilnehmen kann (was den INSERTund UPDATEOperationen helfen könnte ), während a T-SQL UDF verhindert, dass ein paralleler Plan verwendet wird.

Solomon Rutzky
quelle