Pregunta

Tenemos una gran base de datos en la que hemos DB lado de la paginación.Esta es rápida, lo que devuelve una página de 50 filas de millones de registros en una pequeña fracción de un segundo.

Los usuarios pueden definir su propia especie, básicamente elegir qué columna para ordenar por.Las columnas son dinámicos - algunos tienen valores numéricos, fechas y texto.

Mientras que la mayoría de la especie, como se esperaba de texto tipo en un mudo manera.Bueno, yo digo tonto, tiene sentido para los equipos, pero frustra a los usuarios.

Por ejemplo, la clasificación por una cadena de id de registro que le da algo así como:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...y así sucesivamente.

Yo quiero esto para tener en cuenta el número, así:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

Yo no puedo controlar la entrada (de lo contrario me acababa de formato en los principales 000s) y yo no puedo confiar en un solo formato, algunas son cosas como "{código alfa}-{código dept}-{rec id}".

Conozco a un par de maneras de hacer esto en C#, pero no se puede tirar abajo todos los registros ordenar, como que sería lento.

¿Alguien sabe una manera de aplicar rápidamente un natural de la especie en Sql server?


Estamos usando:

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

Y entonces estamos de paginación por que.

Podemos añadir desencadenantes, aunque nosotros no.Todos sus aportes son parametrizada que y el como, pero no puedo cambiar el formato - si se ponen en "rec2" y "rec10" que esperan a ser devueltos al igual que, y en el orden natural.


Hemos de usuario válido de entrada que sigue a diferentes formatos para diferentes clientes.

Uno podría ir rec1, rec2, rec3, ...rec100, rec101

Mientras que otro podría ir así:grp1rec1, grp1rec2, ...grp20rec300, grp20rec301

Cuando yo digo que no podemos controlar la entrada me refiero a que no podemos forzar a los usuarios a cambiar a estas normas, tienen un valor como grp1rec1 y yo no puedo volver a formatear como grp01rec001, ya que eso sería cambiar algo que se utiliza para las búsquedas y enlaces a sistemas externos.

Estos formatos varían mucho, pero a menudo son mezclas de letras y números.

La clasificación de estos en C# es fácil - sólo se rompen en { "grp", 20, "rec", 301 } y, a continuación, compare los valores de secuencia de turno.

Sin embargo, hay millones de registros y los datos se avisa, tengo la necesidad de ordenar a realizarse en el servidor SQL server.

SQL server tipo por valor, no por comparación en C# puedo dividir los valores a comparar, pero en SQL necesito un poco de lógica (muy rápidamente) obtiene un valor único que consistentemente tipo.

@moebius - su respuesta podría funcionar, pero no se siente como un feo compromiso para agregar una especie de clave para todos estos valores de texto.

¿Fue útil?

Solución

La mayoría de SQL-base de las soluciones que he visto romperse cuando los datos llegan a ser lo suficientemente complejo (por ejemplo,más de uno o dos números en el mismo).Al principio me trató de la implementación de un NaturalSort de la función en T-SQL que cumplió con mis requisitos (entre otras cosas, maneja un número arbitrario de números dentro de la cadena), pero el rendimiento fue camino demasiado lento.

Finalmente, escribí un escalar función CLR en C# para permitir natural de la especie, e incluso con unoptimized código el rendimiento de llamadas desde SQL Server es cegadoramente rápido.Tiene las siguientes características:

  • se ordenará a los primeros 1.000 caracteres o tan correctamente (modificar fácilmente en el código o en un parámetro)
  • correctamente tipo decimales, 123.333 viene antes de 123.45
  • porque los de arriba, NO va a arreglar las cosas como direcciones IP correctamente;si usted desea un comportamiento diferente, modificar el código
  • admite la clasificación de una cadena con un número arbitrario de números dentro de ella
  • clasifique correctamente los números hasta el 25 dígitos (modificar fácilmente en el código o en un parámetro)

El código está aquí:

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

Para registrar este modo que se le puede llamar de SQL Server, ejecute los siguientes comandos en el Analizador de Consultas:

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

A continuación, se puede utilizar como así:

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

Nota:Si obtiene un error en SQL Server a lo largo de las líneas de Ejecución de código de usuario en el .NET Framework es la movilidad.Habilitar "clr habilitado" opción de configuración., siga las instrucciones aquí para activarlo.Asegúrese de considerar las implicaciones de seguridad antes de hacerlo.Si usted no es el db admin, asegúrese de que usted hable con su administrador antes de hacer cualquier cambio a la configuración del servidor.

Note2:Este código no funciona correctamente apoyo a la internacionalización (por ejemplo, asume que el marcador decimal es ".", no está optimizado para la velocidad, etc.Sugerencias sobre cómo mejorar son bienvenidos!

Editar: Cambiado el nombre de la función a Naturalizar en lugar de NaturalSort, ya que no hace ningún tipo de ordenación.

Otros consejos

order by LEN(value), value

No es perfecto, pero funciona bien en muchos de los casos.

Sé que esto es una vieja pregunta, pero me encontré con él y ya que no tengo aceptado la respuesta.

Yo siempre he utilizado una manera parecida a esta:

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

El único común veces que este tiene problemas en caso de que su columna no convierte a un tipo de datos VARCHAR(MAX), o si LEN([Columna]) > 1000 (pero usted puede cambiar eso 1000 a algo más si quieres), pero usted puede utilizar esta idea aproximada de lo que usted necesita.

También esto es mucho peor rendimiento que el ORDEN normal POR [Columna], pero no dará el resultado que se pidió en la OP.

Editar:Solo para aclarar, esta el de arriba no funcionará si usted tiene los valores decimales como tener 1, 1.15 y 1.5, (se clasifique como {1, 1.5, 1.15}) como que no es lo que pide el OP, pero que se puede hacer fácilmente por:

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

Resultado: {1, 1.15, 1.5}

Y todavía todas totalmente dentro de SQL.Esto no volverá a ordenar las direcciones IP porque usted está ahora en el número específico de combinaciones, como contraposición a la simple texto + número.

RedFilter la respuesta es ideal para un tamaño razonable de los conjuntos de datos donde la indexación no es crítica, sin embargo, si desea un índice, varios ajustes son necesarios.

En primer lugar, marque la función como de no hacer ningún tipo de acceso a datos y de ser determinista y precisa:

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

Siguiente, MSSQL tiene un 900 bytes límite en el tamaño de clave de índice, de modo que si el naturalizado valor es el único valor en el índice, se debe en la mayoría de 450 caracteres de largo.Si el índice incluye varias columnas, el valor de retorno debe ser aún menor.Dos cambios:

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

y en el código de C#:

const int maxLength = 450;

Finalmente, usted tendrá que añadir una columna calculada a su mesa, y se debe ser persistente (porque MSSQL no puede demostrar que Naturalize es determinista y precisa), lo que significa que el naturalizado valor se almacena en la tabla, pero aún se mantiene de forma automática:

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

Ahora puede crear el índice.

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

También he hecho un par de cambios para RedFilter del código:el uso de caracteres para mayor claridad, la incorporación de duplicar el espacio de eliminación en el bucle principal, salir de una vez el resultado es mayor que el límite de ajuste de la longitud máxima sin subcadena etc.Aquí está el resultado:

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

Sé que esto es un poco viejo en este punto, pero en mi búsqueda de una solución mejor, me encontré con esta pregunta.Actualmente estoy usando una función para ordenar por.Funciona muy bien para mi propósito de ordenar los registros que se denomina con una mezcla de alfa numérico ('elemento 1', 'artículo 10', 'artículo 2", etc)

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

Luego de la llamada

select item_name from my_table order by fnMixSort(item_name)

Fácilmente se triplica el tiempo de procesamiento de una simple lectura de datos, por lo que no puede ser la solución perfecta.

He aquí una solución preparada para el SQL 2000.Esto probablemente puede ser mejorado para las nuevas versiones de SQL.

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

Aquí hay otra solución que me gusta:http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

No se trata de Microsoft SQL, pero desde que acabé aquí cuando yo estaba buscando una solución para Postgres, pensé que la adición de este aquí ayudará a los demás.

Para el siguiente varchar datos:

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

Esto funcionó mejor para mí:

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

Si usted está teniendo problemas para cargar los datos de la DB para ordenar en C#, entonces estoy seguro de que usted será decepcionado con cualquier enfoque de hacerlo mediante programación en el DB.Cuando el servidor se va a ordenar, tiene que calcular la "aparente" orden justo como usted tendría -- cada vez.

Me gustaría sugerir que agregar una columna adicional para almacenar los procesados que se pueden ordenar de la cadena, utilizando algún método de C#, cuando los datos que se inserta.Usted podría tratar de convertir los valores numéricos en ancho fijo rangos, por ejemplo, por lo que "xyz1" se convertiría en "xyz00000001".Entonces usted podría utilizar SQL Server normales de clasificación.

En el riesgo de tooting mi propio cuerno, escribí un CodeProject artículo implementando el problema tal como se plantea en el CodingHorror artículo.Siéntase libre de robar de mi código.

Acabo de leer un artículo en alguna parte acerca de este tema.El punto clave es:sólo se necesita el valor entero para ordenar los datos, mientras que el botón 'rec' de la cadena pertenece a la interfaz de usuario.Se podría dividir la información en dos campos, dicen alfa y num, ordenar por alfa y num (por separado) y, a continuación, que muestra una cadena compuesta por alfa + num.Usted podría utilizar una columna calculada para componer la cadena, o una vista.Espero que ayude

Usted puede utilizar el código siguiente para resolver el 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

saludos, rabihkahaleh@hotmail.com

Simplemente ordenar por

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

 ##

Todavía no entiendo (probablemente a causa de mi pobre inglés).

Puedes intentar:

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

Pero no funciona para millones de registros.

Que por qué me sugiere el uso de gatillo que llena independiente columna con valor humano.

Además:

  • construido-en T-SQL funciones son realmente lento y Microsoft sugieren el uso de .NET funciones en su lugar.
  • valor humano es constante por lo que no hay punto de cálculo cada vez cuando se ejecuta la consulta.
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top