Domanda

Disponiamo di un database di grandi dimensioni sul quale abbiamo l'impaginazione lato DB.Questo è veloce e restituisce una pagina di 50 righe da milioni di record in una piccola frazione di secondo.

Gli utenti possono definire il proprio ordinamento, scegliendo sostanzialmente in base a quale colonna ordinare.Le colonne sono dinamiche: alcune hanno valori numerici, altre date e parte del testo.

Mentre la maggior parte ordina come previsto, il testo viene ordinato in modo stupido.Beh, dico stupido, ha senso per i computer, ma frustra gli utenti.

Ad esempio, l'ordinamento in base all'ID di un record di stringa restituisce qualcosa come:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...e così via.

Voglio che questo tenga conto del numero, quindi:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

Non posso controllare l'input (altrimenti formatterei semplicemente con 000 iniziali) e non posso fare affidamento su un unico formato: alcuni sono cose come "{alpha code}-{dept code}-{rec id}".

Conosco alcuni modi per farlo in C#, ma non riesco a estrarre tutti i record per ordinarli, poiché sarebbe troppo lento.

Qualcuno conosce un modo per applicare rapidamente un ordinamento naturale nel server SQL?


Stiamo utilizzando:

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

E poi stiamo sfogliando quello.

Possiamo aggiungere trigger, anche se non lo faremmo.Tutto il loro input è parametrizzato e simili, ma non posso cambiare il formato: se inseriscono "rec2" e "rec10" si aspettano che vengano restituiti proprio così e in ordine naturale.


Disponiamo di input utente validi che seguono formati diversi per client diversi.

Si potrebbe andare rec1, rec2, rec3, ...rec100, rec101

Mentre un altro potrebbe dire:grp1rec1, grp1rec2, ...grp20rec300, grp20rec301

Quando dico che non possiamo controllare l'input intendo che non possiamo forzare gli utenti a modificare questi standard: hanno un valore come grp1rec1 e non posso riformattarlo come grp01rec001, poiché ciò cambierebbe qualcosa utilizzato per le ricerche e collegamento a sistemi esterni.

Questi formati variano molto, ma spesso sono una combinazione di lettere e numeri.

Ordinarli in C# è semplice: basta suddividerli in { "grp", 20, "rec", 301 } e quindi confrontare a turno i valori della sequenza.

Tuttavia potrebbero esserci milioni di record e i dati sono impaginati, è necessario che l'ordinamento venga eseguito sul server SQL.

Il server SQL ordina per valore, non per confronto: in C# posso dividere i valori per confrontarli, ma in SQL ho bisogno di una logica che (molto rapidamente) ottenga un singolo valore che ordini in modo coerente.

@moebius: la tua risposta potrebbe funzionare, ma sembra un brutto compromesso aggiungere una chiave di ordinamento per tutti questi valori di testo.

È stato utile?

Soluzione

La maggior parte delle soluzioni basate su SQL che ho visto si interrompono quando i dati diventano sufficientemente complessi (ad es.più di uno o due numeri al suo interno).Inizialmente ho provato a implementare una funzione NaturalSort in T-SQL che soddisfacesse i miei requisiti (tra le altre cose, gestisce un numero arbitrario di numeri all'interno della stringa), ma le prestazioni erano modo troppo lento.

Alla fine, ho scritto una funzione CLR scalare in C# per consentire un ordinamento naturale e, anche con codice non ottimizzato, le prestazioni chiamandola da SQL Server sono incredibilmente veloci.Ha le seguenti caratteristiche:

  • ordinerà i primi 1.000 caratteri circa correttamente (facilmente modificabili nel codice o trasformati in un parametro)
  • ordina correttamente i decimali, quindi 123.333 viene prima di 123.45
  • a causa di quanto sopra, probabilmente NON ordinerà correttamente cose come gli indirizzi IP;se si desidera un comportamento diverso modificare il codice
  • supporta l'ordinamento di una stringa con un numero arbitrario di numeri al suo interno
  • ordinerà correttamente i numeri lunghi fino a 25 cifre (facilmente modificabili nel codice o trasformati in un parametro)

Il codice è qui:

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');
    }
}

Per registrarlo in modo da poterlo richiamare da SQL Server, eseguire i seguenti comandi in Query Analyser:

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

Quindi, puoi usarlo in questo modo:

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

Nota:Se ricevi un errore in SQL Server sulla falsariga di L'esecuzione del codice utente in .NET Framework è disabilitata.Abilita l'opzione di configurazione "clr abilitato"., seguire le istruzioni Qui per abilitarlo.Assicurati di considerare le implicazioni sulla sicurezza prima di farlo.Se non sei l'amministratore del database, assicurati di discuterne con il tuo amministratore prima di apportare qualsiasi modifica alla configurazione del server.

Nota 2:Questo codice non supporta correttamente l'internazionalizzazione (ad esempio, presuppone che il contrassegno decimale sia ".", non è ottimizzato per la velocità, ecc.Suggerimenti per migliorarlo sono benvenuti!

Modificare: Rinominata la funzione in Naturalizzare invece di Ordinamento naturale, poiché non esegue alcun ordinamento effettivo.

Altri suggerimenti

order by LEN(value), value

Non perfetto, ma funziona bene in molti casi.

So che questa è una vecchia domanda, ma l'ho appena trovata e poiché non ha una risposta accettata.

Ho sempre utilizzato metodi simili a questo:

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

Gli unici casi comuni in cui ciò presenta problemi è se la colonna non verrà trasmessa a VARCHAR(MAX) o se LEN([Colonna]) > 1000 (ma puoi modificare quel 1000 in qualcos'altro se lo desideri), ma tu puoi usare questa idea approssimativa per quello che ti serve.

Anche questa è una prestazione molto peggiore rispetto al normale ORDER BY [Column], ma ti dà il risultato richiesto nell'OP.

Modificare:Giusto per chiarire ulteriormente, quanto sopra non funzionerà se si hanno valori decimali come avere 1, 1.15 E 1.5, (verranno ordinati come {1, 1.5, 1.15}) in quanto non è ciò che viene richiesto nel PO, ma ciò può essere facilmente fatto:

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')

Risultato: {1, 1.15, 1.5}

E ancora tutto interamente all'interno di SQL.Questo non ordinerà gli indirizzi IP perché ora stai entrando in combinazioni di numeri molto specifiche invece del semplice testo + numero.

La risposta di RedFilter è ottimo per set di dati di dimensioni ragionevoli in cui l'indicizzazione non è fondamentale, tuttavia se si desidera un indice sono necessarie diverse modifiche.

Innanzitutto, contrassegna la funzione in modo che non esegua alcun accesso ai dati e sia deterministica e precisa:

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

Successivamente, MSSQL ha un limite di 900 byte sulla dimensione della chiave dell'indice, quindi se il valore naturalizzato è l'unico valore nell'indice, deve essere lungo al massimo 450 caratteri.Se l'indice include più colonne, il valore restituito deve essere ancora più piccolo.Due modifiche:

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

e nel codice C#:

const int maxLength = 450;

Infine, dovrai aggiungere una colonna calcolata alla tua tabella e deve essere persistente (perché MSSQL non può dimostrarlo Naturalize è deterministico e preciso), il che significa che il valore naturalizzato viene effettivamente memorizzato nella tabella ma viene comunque mantenuto automaticamente:

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

Ora puoi creare l'indice!

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

Ho anche apportato un paio di modifiche al codice di RedFilter:utilizzando i caratteri per chiarezza, incorporando la rimozione dello spazio duplicato nel ciclo principale, uscendo una volta che il risultato è più lungo del limite, impostando la lunghezza massima senza sottostringa ecc.Ecco il risultato:

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');
    }
}

So che è un po' vecchio a questo punto, ma nella mia ricerca di una soluzione migliore, mi sono imbattuto in questa domanda.Attualmente sto utilizzando una funzione per ordinare in base a.Funziona bene per il mio scopo di ordinare i record denominati con caratteri alfanumerici misti ("elemento 1", "elemento 10", "elemento 2", ecc.)

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

Allora chiama

select item_name from my_table order by fnMixSort(item_name)

Triplica facilmente il tempo di elaborazione per una semplice lettura dei dati, quindi potrebbe non essere la soluzione perfetta.

Ecco una soluzione scritta per SQL 2000.Probabilmente può essere migliorato per le versioni SQL più recenti.

/**
 * 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

Ecco un'altra soluzione che mi piace:http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

Non è Microsoft SQL, ma dal momento che sono finito qui mentre cercavo una soluzione per Postgres, ho pensato che aggiungerlo qui avrebbe aiutato gli altri.

Per i seguenti varchar dati:

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

Questo ha funzionato meglio per me:

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

Se hai problemi a caricare i dati dal DB per ordinarli in C#, sono sicuro che rimarrai deluso da qualsiasi approccio nel farlo a livello di codice nel DB.Quando il server sta per ordinare, deve calcolare l'ordine "percepito" proprio come faresti tu, ogni volta.

Suggerirei di aggiungere una colonna aggiuntiva per archiviare la stringa ordinabile preelaborata, utilizzando un metodo C#, quando i dati vengono inseriti per la prima volta.Potresti provare a convertire i numeri in intervalli di larghezza fissa, ad esempio, in modo che "xyz1" diventi "xyz00000001".Quindi potresti utilizzare il normale ordinamento di SQL Server.

A rischio di esagerare, ho scritto un articolo di CodeProject implementando il problema come posto nell'articolo di CodingHorror.Sentiti libero di rubare dal mio codice.

Ho appena letto un articolo da qualche parte su un argomento del genere.Il punto chiave è:hai solo bisogno del valore intero per ordinare i dati, mentre la stringa "rec" appartiene all'interfaccia utente.Potresti dividere le informazioni in due campi, ad esempio alfa e num, ordinarle per alfa e num (separatamente) e quindi mostrare una stringa composta da alfa + num.Potresti utilizzare una colonna calcolata per comporre la stringa o una vista.Spero che sia d'aiuto

È possibile utilizzare il seguente codice per risolvere il problema:

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

Saluti, rabihkahaleh@hotmail.com

Basta ordinare per

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

 ##

Ancora non capisco (probabilmente a causa del mio scarso inglese).

Potresti provare:

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

Ma non funzionerà per milioni di record.

Ecco perché ho suggerito di utilizzare trigger which riempie separato colonna con valore umano.

Inoltre:

  • built-in funzioni T-SQL sono davvero lento e Microsoft suggeriscono di utilizzare .NET funziona invece.
  • valore umano è costante, quindi non ha senso calcolarlo ogni volta in cui esegue la query.
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top