Frage

Wir verfügen über eine große Datenbank mit DB-seitiger Paginierung.Das geht schnell und gibt im Bruchteil einer Sekunde eine Seite mit 50 Zeilen aus Millionen von Datensätzen zurück.

Benutzer können ihre eigene Sortierung definieren und dabei grundsätzlich auswählen, nach welcher Spalte sortiert werden soll.Spalten sind dynamisch – einige enthalten numerische Werte, einige Datumsangaben und einige Text.

Während die meisten Texte wie erwartet sortiert werden, ist die Sortierung dumm.Nun ja, ich sage dumm, es macht für Computer Sinn, frustriert aber die Benutzer.

Das Sortieren nach einer Zeichenfolgen-Datensatz-ID ergibt beispielsweise Folgendes:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...und so weiter.

Ich möchte, dass dies die Anzahl berücksichtigt, also:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

Ich kann die Eingabe nicht kontrollieren (sonst würde ich nur mit führenden 000ern formatieren) und ich kann mich nicht auf ein einzelnes Format verlassen – einige sind Dinge wie „{Alpha-Code}-{Abteilungscode}-{Rec-ID}“.

Ich kenne einige Möglichkeiten, dies in C# zu tun, kann aber nicht alle Datensätze herunterziehen, um sie zu sortieren, da das zu langsam wäre.

Kennt jemand eine Möglichkeit, schnell eine natürliche Sortierung auf einem SQL-Server anzuwenden?


Wir verwenden:

ROW_NUMBER() over (order by {field name} asc)

Und dann blättern wir daran vorbei.

Wir können Auslöser hinzufügen, würden es aber nicht tun.Alle ihre Eingaben sind parametrisiert und dergleichen, aber ich kann das Format nicht ändern – wenn sie „rec2“ und „rec10“ eingeben, erwarten sie, dass sie einfach so und in natürlicher Reihenfolge zurückgegeben werden.


Wir verfügen über gültige Benutzereingaben, die für verschiedene Kunden unterschiedlichen Formaten folgen.

Man könnte rec1, rec2, rec3, ... gehenrec100, rec101

Während ein anderer sagen könnte:grp1rec1, grp1rec2, ...grp20rec300, grp20rec301

Wenn ich sage, dass wir die Eingabe nicht kontrollieren können, meine ich, dass wir Benutzer nicht zwingen können, diese Standards zu ändern – sie haben einen Wert wie grp1rec1 und ich kann ihn nicht in grp01rec001 umformatieren, da dies etwas ändern würde, das für Suchvorgänge und verwendet wird Anbindung an externe Systeme.

Diese Formate variieren stark, sind jedoch häufig Mischungen aus Buchstaben und Zahlen.

Diese in C# zu sortieren ist einfach – teilen Sie sie einfach auf { "grp", 20, "rec", 301 } und vergleichen Sie dann nacheinander die Sequenzwerte.

Obwohl es möglicherweise Millionen von Datensätzen gibt und die Daten ausgelagert werden, muss die Sortierung auf dem SQL-Server erfolgen.

SQL Server sortiert nach Wert, nicht nach Vergleich – in C# kann ich die Werte zum Vergleichen aufteilen, aber in SQL benötige ich eine Logik, die (sehr schnell) einen einzelnen Wert erhält, der konsistent sortiert.

@moebius – Ihre Antwort könnte funktionieren, aber es scheint ein hässlicher Kompromiss zu sein, für alle diese Textwerte einen Sortierschlüssel hinzuzufügen.

War es hilfreich?

Lösung

Die meisten SQL-basierten Lösungen, die ich gesehen habe, scheitern, wenn die Daten komplex genug werden (z. B.mehr als eine oder zwei Zahlen enthalten).Zunächst habe ich versucht, eine NaturalSort-Funktion in T-SQL zu implementieren, die meinen Anforderungen entsprach (unter anderem verarbeitet sie eine beliebige Anzahl von Zahlen innerhalb der Zeichenfolge), aber die Leistung war schlecht Weg zu langsam.

Letztendlich habe ich eine skalare CLR-Funktion in C# geschrieben, um eine natürliche Sortierung zu ermöglichen, und selbst mit nicht optimiertem Code ist die Leistung beim Aufruf von SQL Server unglaublich schnell.Es weist folgende Eigenschaften auf:

  • sortiert die ersten etwa 1.000 Zeichen korrekt (einfach im Code geändert oder in einen Parameter umgewandelt)
  • Sortiert Dezimalzahlen ordnungsgemäß, sodass 123,333 vor 123,45 kommt
  • Aufgrund des oben Gesagten werden Dinge wie IP-Adressen wahrscheinlich NICHT richtig sortiert.Wenn Sie ein anderes Verhalten wünschen, ändern Sie den Code
  • unterstützt das Sortieren einer Zeichenfolge mit einer beliebigen Anzahl von Zahlen darin
  • sortiert Zahlen mit einer Länge von bis zu 25 Ziffern korrekt (einfach im Code geändert oder in einen Parameter umgewandelt)

Der Code ist hier:

using System;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public class UDF
{
    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic=true)]
    public static SqlString Naturalize(string val)
    {
        if (String.IsNullOrEmpty(val))
            return val;

        while(val.Contains("  "))
            val = val.Replace("  ", " ");

        const int maxLength = 1000;
        const int padLength = 25;

        bool inNumber = false;
        bool isDecimal = false;
        int numStart = 0;
        int numLength = 0;
        int length = val.Length < maxLength ? val.Length : maxLength;

        //TODO: optimize this so that we exit for loop once sb.ToString() >= maxLength
        var sb = new StringBuilder();
        for (var i = 0; i < length; i++)
        {
            int charCode = (int)val[i];
            if (charCode >= 48 && charCode <= 57)
            {
                if (!inNumber)
                {
                    numStart = i;
                    numLength = 1;
                    inNumber = true;
                    continue;
                }
                numLength++;
                continue;
            }
            if (inNumber)
            {
                sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));
                inNumber = false;
            }
            isDecimal = (charCode == 46);
            sb.Append(val[i]);
        }
        if (inNumber)
            sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));

        var ret = sb.ToString();
        if (ret.Length > maxLength)
            return ret.Substring(0, maxLength);

        return ret;
    }

    static string PadNumber(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

Um dies zu registrieren, damit Sie es von SQL Server aus aufrufen können, führen Sie die folgenden Befehle im Query Analyzer aus:

CREATE ASSEMBLY SqlServerClr FROM 'SqlServerClr.dll' --put the full path to DLL here
go
CREATE FUNCTION Naturalize(@val as nvarchar(max)) RETURNS nvarchar(1000) 
EXTERNAL NAME SqlServerClr.UDF.Naturalize
go

Dann können Sie es wie folgt verwenden:

select *
from MyTable
order by dbo.Naturalize(MyTextField)

Notiz:Wenn Sie in SQL Server einen Fehler wie folgt erhalten: Die Ausführung von Benutzercode im .NET Framework ist deaktiviert.Aktivieren Sie die Konfigurationsoption „clr aktiviert“., folge den Anweisungen Hier um es zu aktivieren.Stellen Sie sicher, dass Sie die Auswirkungen auf die Sicherheit berücksichtigen, bevor Sie dies tun.Wenn Sie nicht der Datenbankadministrator sind, besprechen Sie dies unbedingt mit Ihrem Administrator, bevor Sie Änderungen an der Serverkonfiguration vornehmen.

Anmerkung 2:Dieser Code unterstützt die Internationalisierung nicht richtig (z. B. geht davon aus, dass das Dezimalzeichen „.“ ist, ist nicht auf Geschwindigkeit optimiert usw.).Verbesserungsvorschläge sind willkommen!

Bearbeiten: Die Funktion wurde umbenannt in Naturalisieren anstatt NaturalSort, da es keine eigentliche Sortierung durchführt.

Andere Tipps

order by LEN(value), value

Nicht perfekt, funktioniert aber in vielen Fällen gut.

Ich weiß, dass dies eine alte Frage ist, aber ich bin gerade erst darauf gestoßen und habe keine akzeptierte Antwort darauf.

Ich habe immer ähnliche Methoden wie diese verwendet:

SELECT [Column] FROM [Table]
ORDER BY RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))), 1000)

Die einzigen häufigen Fälle, in denen dies zu Problemen führt, sind, wenn Ihre Spalte nicht in VARCHAR(MAX) umgewandelt werden kann oder wenn LEN([Spalte]) > 1000 ist (aber Sie können diese 1000 bei Bedarf auch in etwas anderes ändern), aber Sie Sie können diese grobe Idee für das verwenden, was Sie brauchen.

Auch dies ist eine viel schlechtere Leistung als normales ORDER BY [Column], aber es liefert Ihnen das im OP geforderte Ergebnis.

Bearbeiten:Zur weiteren Verdeutlichung: Das oben Gesagte funktioniert nicht, wenn Sie Dezimalwerte haben, z. B. „have“. 1, 1.15 Und 1.5, (sie werden sortiert als {1, 1.5, 1.15}), da dies im OP nicht gefordert wird, aber das kann leicht erreicht werden durch:

SELECT [Column] FROM [Table]
ORDER BY REPLACE(RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))) + REPLICATE('0', 100 - CHARINDEX('.', REVERSE(LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX))))), 1)), 1000), '.', '0')

Ergebnis: {1, 1.15, 1.5}

Und immer noch alles vollständig in SQL.Dadurch werden IP-Adressen nicht sortiert, da Sie jetzt mit sehr spezifischen Zahlenkombinationen konfrontiert werden, im Gegensatz zu einfachem Text + Zahl.

RedFilters Antwort eignet sich hervorragend für Datensätze angemessener Größe, bei denen die Indizierung nicht entscheidend ist. Wenn Sie jedoch einen Index wünschen, sind mehrere Anpassungen erforderlich.

Markieren Sie zunächst die Funktion so, dass sie keinen Datenzugriff durchführt und deterministisch und präzise ist:

[SqlFunction(DataAccess = DataAccessKind.None,
                          SystemDataAccess = SystemDataAccessKind.None,
                          IsDeterministic = true, IsPrecise = true)]

Als nächstes hat MSSQL eine Beschränkung der Indexschlüsselgröße auf 900 Byte. Wenn der naturalisierte Wert also der einzige Wert im Index ist, darf er höchstens 450 Zeichen lang sein.Wenn der Index mehrere Spalten umfasst, muss der Rückgabewert noch kleiner sein.Zwei Änderungen:

CREATE FUNCTION Naturalize(@str AS nvarchar(max)) RETURNS nvarchar(450)
    EXTERNAL NAME ClrExtensions.Util.Naturalize

und im C#-Code:

const int maxLength = 450;

Schließlich müssen Sie Ihrer Tabelle eine berechnete Spalte hinzufügen, und diese muss beibehalten werden (da MSSQL dies nicht beweisen kann). Naturalize ist deterministisch und präzise), was bedeutet, dass der naturalisierte Wert tatsächlich in der Tabelle gespeichert wird, aber dennoch automatisch beibehalten wird:

ALTER TABLE YourTable ADD nameNaturalized AS dbo.Naturalize(name) PERSISTED

Sie können jetzt den Index erstellen!

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

Ich habe auch ein paar Änderungen am Code von RedFilter vorgenommen:Verwenden von Zeichen zur Verdeutlichung, Integrieren der Entfernung doppelter Leerzeichen in die Hauptschleife, Beenden, sobald das Ergebnis länger als das Limit ist, Festlegen der maximalen Länge ohne Teilzeichenfolge usw.Hier ist das Ergebnis:

using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public static class Util
{
    [SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)]
    public static SqlString Naturalize(string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        const int maxLength = 450;
        const int padLength = 15;

        bool isDecimal = false;
        bool wasSpace = false;
        int numStart = 0;
        int numLength = 0;

        var sb = new StringBuilder();
        for (var i = 0; i < str.Length; i++)
        {
            char c = str[i];
            if (c >= '0' && c <= '9')
            {
                if (numLength == 0)
                    numStart = i;
                numLength++;
            }
            else
            {
                if (numLength > 0)
                {
                    sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));
                    numLength = 0;
                }
                if (c != ' ' || !wasSpace)
                    sb.Append(c);
                isDecimal = c == '.';
                if (sb.Length > maxLength)
                    break;
            }
            wasSpace = c == ' ';
        }
        if (numLength > 0)
            sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));

        if (sb.Length > maxLength)
            sb.Length = maxLength;
        return sb.ToString();
    }

    private static string pad(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

Ich weiß, dass dies zu diesem Zeitpunkt etwas alt ist, aber auf meiner Suche nach einer besseren Lösung bin ich auf diese Frage gestoßen.Ich verwende derzeit eine Funktion zum Sortieren nach.Es funktioniert gut für meinen Zweck, Datensätze zu sortieren, die mit gemischten alphanumerischen Namen benannt sind („Element 1“, „Element 10“, „Element 2“ usw.).

CREATE FUNCTION [dbo].[fnMixSort]
(
    @ColValue NVARCHAR(255)
)
RETURNS NVARCHAR(1000)
AS

BEGIN
    DECLARE @p1 NVARCHAR(255),
        @p2 NVARCHAR(255),
        @p3 NVARCHAR(255),
        @p4 NVARCHAR(255),
        @Index TINYINT

    IF @ColValue LIKE '[a-z]%'
        SELECT  @Index = PATINDEX('%[0-9]%', @ColValue),
            @p1 = LEFT(CASE WHEN @Index = 0 THEN @ColValue ELSE LEFT(@ColValue, @Index - 1) END + REPLICATE(' ', 255), 255),
            @ColValue = CASE WHEN @Index = 0 THEN '' ELSE SUBSTRING(@ColValue, @Index, 255) END
    ELSE
        SELECT  @p1 = REPLICATE(' ', 255)

    SELECT  @Index = PATINDEX('%[^0-9]%', @ColValue)

    IF @Index = 0
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255),
            @ColValue = ''
    ELSE
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    SELECT  @Index = PATINDEX('%[0-9,a-z]%', @ColValue)

    IF @Index = 0
        SELECT  @p3 = REPLICATE(' ', 255)
    ELSE
        SELECT  @p3 = LEFT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    IF PATINDEX('%[^0-9]%', @ColValue) = 0
        SELECT  @p4 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255)
    ELSE
        SELECT  @p4 = LEFT(@ColValue + REPLICATE(' ', 255), 255)

    RETURN  @p1 + @p2 + @p3 + @p4

END

Dann ruf an

select item_name from my_table order by fnMixSort(item_name)

Es verdreifacht leicht die Verarbeitungszeit für einen einfachen Datenlesevorgang und ist daher möglicherweise nicht die perfekte Lösung.

Hier ist eine Lösung, die für SQL 2000 geschrieben wurde.Es kann wahrscheinlich für neuere SQL-Versionen verbessert werden.

/**
 * Returns a string formatted for natural sorting. This function is very useful when having to sort alpha-numeric strings.
 *
 * @author Alexandre Potvin Latreille (plalx)
 * @param {nvarchar(4000)} string The formatted string.
 * @param {int} numberLength The length each number should have (including padding). This should be the length of the longest number. Defaults to 10.
 * @param {char(50)} sameOrderChars A list of characters that should have the same order. Ex: '.-/'. Defaults to empty string.
 *
 * @return {nvarchar(4000)} A string for natural sorting.
 * Example of use: 
 * 
 *      SELECT Name FROM TableA ORDER BY Name
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1-1.       
 *  2.  A1-1.                       2.  A1.
 *  3.  R1             -->          3.  R1
 *  4.  R11                         4.  R11
 *  5.  R2                          5.  R2
 *
 *  
 *  As we can see, humans would expect A1., A1-1., R1, R2, R11 but that's not how SQL is sorting it.
 *  We can use this function to fix this.
 *
 *      SELECT Name FROM TableA ORDER BY dbo.udf_NaturalSortFormat(Name, default, '.-')
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1.     
 *  2.  A1-1.                       2.  A1-1.
 *  3.  R1              -->         3.  R1
 *  4.  R11                         4.  R2
 *  5.  R2                          5.  R11
 */
ALTER FUNCTION [dbo].[udf_NaturalSortFormat](
    @string nvarchar(4000),
    @numberLength int = 10,
    @sameOrderChars char(50) = ''
)
RETURNS varchar(4000)
AS
BEGIN
    DECLARE @sortString varchar(4000),
        @numStartIndex int,
        @numEndIndex int,
        @padLength int,
        @totalPadLength int,
        @i int,
        @sameOrderCharsLen int;

    SELECT 
        @totalPadLength = 0,
        @string = RTRIM(LTRIM(@string)),
        @sortString = @string,
        @numStartIndex = PATINDEX('%[0-9]%', @string),
        @numEndIndex = 0,
        @i = 1,
        @sameOrderCharsLen = LEN(@sameOrderChars);

    -- Replace all char that have the same order by a space.
    WHILE (@i <= @sameOrderCharsLen)
    BEGIN
        SET @sortString = REPLACE(@sortString, SUBSTRING(@sameOrderChars, @i, 1), ' ');
        SET @i = @i + 1;
    END

    -- Pad numbers with zeros.
    WHILE (@numStartIndex <> 0)
    BEGIN
        SET @numStartIndex = @numStartIndex + @numEndIndex;
        SET @numEndIndex = @numStartIndex;

        WHILE(PATINDEX('[0-9]', SUBSTRING(@string, @numEndIndex, 1)) = 1)
        BEGIN
            SET @numEndIndex = @numEndIndex + 1;
        END

        SET @numEndIndex = @numEndIndex - 1;

        SET @padLength = @numberLength - (@numEndIndex + 1 - @numStartIndex);

        IF @padLength < 0
        BEGIN
            SET @padLength = 0;
        END

        SET @sortString = STUFF(
            @sortString,
            @numStartIndex + @totalPadLength,
            0,
            REPLICATE('0', @padLength)
        );

        SET @totalPadLength = @totalPadLength + @padLength;
        SET @numStartIndex = PATINDEX('%[0-9]%', RIGHT(@string, LEN(@string) - @numEndIndex));
    END

    RETURN @sortString;
END

Hier ist eine andere Lösung, die mir gefällt:http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

Es ist nicht Microsoft SQL, aber da ich hier gelandet bin, als ich nach einer Lösung für Postgres suchte, dachte ich, dass es anderen helfen würde, dies hier hinzuzufügen.

Für den folgenden varchar Daten:

BR1
BR2
External Location
IR1
IR2
IR3
IR4
IR5
IR6
IR7
IR8
IR9
IR10
IR11
IR12
IR13
IR14
IR16
IR17
IR15
VCR

Das hat bei mir am besten funktioniert:

ORDER BY substring(fieldName, 1, 1), LEN(fieldName)

Wenn Sie Probleme haben, die Daten aus der Datenbank zu laden, um sie in C# zu sortieren, werden Sie sicher von jedem Ansatz, dies programmgesteuert in der Datenbank durchzuführen, enttäuscht sein.Wenn der Server sortieren soll, muss er jedes Mal die „wahrgenommene“ Reihenfolge berechnen, genau wie Sie es getan hätten.

Ich würde vorschlagen, dass Sie beim ersten Einfügen der Daten mithilfe einer C#-Methode eine zusätzliche Spalte hinzufügen, um die vorverarbeitete sortierbare Zeichenfolge zu speichern.Sie können beispielsweise versuchen, die Zahlen in Bereiche mit fester Breite umzuwandeln, sodass aus „xyz1“ „xyz00000001“ wird.Dann könnten Sie die normale SQL Server-Sortierung verwenden.

Auf die Gefahr hin, mein eigenes Horn zu betätigen, habe ich einen CodeProject-Artikel geschrieben, der das im CodingHorror-Artikel gestellte Problem umsetzt.Fühlen sich frei von meinem Code stehlen.

Ich habe gerade irgendwo einen Artikel zu einem solchen Thema gelesen.Der entscheidende Punkt ist:Sie benötigen nur den ganzzahligen Wert, um Daten zu sortieren, während die Zeichenfolge „rec“ zur Benutzeroberfläche gehört.Sie könnten die Informationen in zwei Felder aufteilen, beispielsweise Alpha und Num, nach Alpha und Num sortieren (separat) und dann eine Zeichenfolge anzeigen, die aus Alpha + Num besteht.Sie können eine berechnete Spalte zum Erstellen der Zeichenfolge oder eine Ansicht verwenden.Ich hoffe es hilft

Sie können den folgenden Code verwenden, um das Problem zu beheben:

Select *, 
    substring(Cote,1,len(Cote) - Len(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1)))alpha,
    CAST(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1) AS INT)intv 
FROM Documents 
   left outer join Sites ON Sites.IDSite = Documents.IDSite 
Order BY alpha, intv

Grüße, Rabihkahaleh@hotmail.com

Sortieren Sie einfach nach

ORDER BY 
cast (substring(name,(PATINDEX('%[0-9]%',name)),len(name))as int)

 ##

Ich verstehe es immer noch nicht (wahrscheinlich wegen meinem schlechten Englisch).

Du könntest es versuchen:

ROW_NUMBER() OVER (ORDER BY dbo.human_sort(field_name) ASC)

Aber es wird nicht für Millionen von Datensätzen funktionieren.

Aus diesem Grund habe ich vorgeschlagen, Trigger zu verwenden füllt separate Spalte mit menschlicher Wert.

Darüber hinaus:

  • Integrierte T-SQL-Funktionen sind sehr langsam und Microsoft schlägt vor, stattdessen .NET-Funktionen zu verwenden.
  • menschlicher Wert ist konstant, so dass es jeden Zeitpunkt, wenn die Abfrage ausgeführt wird, keinen Punkt berechnet.
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top