Pergunta

Temos um grande banco de dados sobre a qual estamos DB lado de paginação.Este é rápida, retornando uma página de 50 linhas de milhões de registros em uma pequena fração de segundo.

Os usuários podem definir suas próprias classificação, basicamente, a escolha de qual coluna para classificar por.Colunas são dinâmicas - alguns têm valores numéricos, datas e algum texto.

Enquanto a maioria dos classificar como esperado texto classifica em um mudo forma.Bem, eu digo-mudo, que faz sentido para computadores, mas frustra usuários.

Por exemplo, a classificação por uma seqüência de caracteres de id de registo dá algo como:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...e assim por diante.

Eu quero que isso tome conta do número, assim:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

Eu não posso controlar a entrada (caso contrário, eu tinha acabado de formatar na liderança 000s) e eu não posso confiar em um único formato - alguns são coisas como "{código alfa}-{dept código}-{rec id}".

Eu sei algumas maneiras de fazer isso em C#, mas não pode puxar para baixo de todos os registros para classificá-los, como seria lenta.

Alguém sabe uma maneira de aplicar rapidamente um natural de classificação no Sql server?


Estamos usando:

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

E então estamos de paginação por que.

Podemos adicionar activadores, embora nós não.Todos os seus entrada é parametrised e afins, mas eu não posso mudar o formato - se colocar em "rec2" e "rec10" eles esperam que ser devolvido, assim como que, e na ordem natural.


Temos de usuário válido de entrada que segue formatos diferentes para clientes diferentes.

Uma pessoa pode fazer rec1, rec2, rec3, ...rec100, rec101

Enquanto outro pode ir:grp1rec1, grp1rec2, ...grp20rec300, grp20rec301

Quando eu digo que não podemos controlar a entrada, quero dizer que não podemos forçar os usuários a mudar esses padrões - eles têm um valor como grp1rec1 e eu não posso reformatar como grp01rec001, como que seria mudar algo usados para pesquisas e a ligação a sistemas externos.

Estes formatos variam muito, mas muitas vezes são misturas de letras e números.

A classificação destes em C# é fácil - basta dividi-la em { "grp", 20, "rec", 301 } e depois comparar a sequência de valores, por sua vez.

No entanto, pode haver milhões de registros e dados é paginada, eu preciso do tipo para ser feito no SQL server.

O SQL server tipos por valor, e não de comparação - em C# que eu possa dividir os valores para comparar, mas no SQL eu preciso de um pouco de lógica que (muito rapidamente) é um valor único que, de forma consistente tipos.

@moebius - a sua resposta pode funcionar, mas ele se sente como um feio o compromisso de adicionar uma espécie-chave para todos esses valores de texto.

Foi útil?

Solução

Mais do SQL baseado em soluções de eu ter visto quebra quando os dados se tornam complexo o suficiente (por exemplo,mais do que um ou dois números).Inicialmente, tentei implementar um NaturalSort função em T-SQL que supriu minhas necessidades (entre outras coisas, lida com um número arbitrário de números na seqüência), mas o desempenho foi caminho muito lento.

Enfim, eu escrevi um escalar CLR função em C# para permitir um natural de classificação, e mesmo com unoptimized código do desempenho da chamada a partir do SQL Server é incrivelmente rápido.Ele tem as seguintes características:

  • serão classificados os primeiros 1000 caracteres ou menos corretamente (facilmente modificado no código ou feito em um parâmetro)
  • corretamente o tipo de casas decimais, de modo 123.333 vem antes 123.45
  • porque acima, provavelmente NÃO vai resolver as coisas, como endereços IP corretamente;se você desejar diferentes de comportamento, modifique o código
  • suporta a ordenação de uma seqüência de caracteres com um número arbitrário de números dentro dela
  • irá classificar corretamente os números até 25 dígitos (facilmente modificado no código ou feito em um parâmetro)

O código está aqui:

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 assim que você pode chamá-lo a partir do SQL Server, execute os comandos a seguir no Query Analyzer:

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

Em seguida, você pode usá-lo assim:

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

Nota:Se você obter um erro no SQL Server ao longo das linhas de Execução de código do usuário na .NET Framework é desativado.Activar "clr enabled" opção de configuração., siga as instruções aqui para activar-lo.Certifique-se de considerar as implicações de segurança antes de fazer isso.Se você não é o db admin, certifique-se que você discuta com o seu administrador antes de fazer quaisquer alterações na configuração do servidor.

Nota2:Este código não suporta corretamente internacionalização (por exemplo, assume o marcador decimal é ".", não é otimizado para velocidade, etc.Sugestões sobre como melhorá-lo são bem-vindos!

Editar: Renomeado a função Naturalizam em vez de NaturalSort, pois não faz qualquer classificação real.

Outras dicas

order by LEN(value), value

Não é perfeito, mas funciona bem em muitos casos.

Eu sei que essa é uma velha pergunta, mas eu só percebi isso e uma vez que não tenho uma aceitos resposta.

Eu sempre usado de maneira similar a este:

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

A única comuns vezes que isso tem questões é se a coluna não fundido para uma coluna VARCHAR(MAX), ou se LEN([Coluna]) > 1000 (mas você pode alterá que 1000 para outra coisa, se você quiser), mas você pode usar essa idéia para o que você precisa.

Também esta é muito pior desempenho do que o normal ORDER BY [Coluna], mas não dar o resultado, pediu para o OP.

Editar:Só para esclarecer, este acima não vai funcionar se você tiver valores decimais, tais como tendo 1, 1.15 e 1.5, que irá classificar como {1, 1.5, 1.15}) como isso não é o que se pede no OP, mas que pode facilmente ser feito 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}

E ainda inteiramente dentro do SQL.Isso não vai classificar endereços IP, porque agora você está se metendo muito específicos número de combinações de oposição ao texto simples + número.

RedFilter resposta é ótimo para conjuntos de dados com um tamanho razoável, onde a indexação não é crítica, no entanto, se você deseja um índice, vários ajustes são necessários.

Primeiro, marque a função de não fazer qualquer acesso a dados e a ser determinista e precisa:

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

Ao lado, MSSQL tem um 900 bytes limite no tamanho da chave de índice, por isso, se o naturalizado valor é o valor no índice, ele deve ser de, no máximo 450 caracteres.Se o índice inclui várias colunas, o valor de retorno deve ser ainda menor.Duas alterações:

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

e no código C#:

const int maxLength = 450;

Finalmente, você precisará adicionar uma coluna calculada para a sua mesa, e ele tem de ser persistente (porque MSSQL não é possível provar que Naturalize é determinista e preciso), o que significa que o naturalizado valor é armazenado na tabela, mas ainda é mantida automaticamente:

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

Agora você pode criar o índice!

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

Eu também já fez algumas alterações para RedFilter código:usando caracteres para maior clareza, a incorporação de duplicar o espaço de remoção para o loop principal, sair de uma vez o resultado é mais do que o limite, a definição de comprimento máximo, sem subseqüência de caracteres, etc.Aqui está o 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');
    }
}

Eu sei que isso é um pouco antigo, neste ponto, mas em minha busca por uma solução melhor, me deparei com essa pergunta.Atualmente estou usando uma função de ordem.Ele funciona muito bem para o meu propósito de classificar registros, que são nomeados com uma mistura de alfa numérico ('número 1', 'item 10', 'número 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

Em seguida, chamar

select item_name from my_table order by fnMixSort(item_name)

É facilmente triplicar o tempo de processamento para uma simples leitura de dados, por isso não pode ser a solução perfeita.

Aqui está uma solução escrito para o SQL 2000.Ele provavelmente pode ser melhorado para versões mais recentes do 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

Aqui está uma outra solução que eu gosto:http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

Não é Microsoft SQL, mas como eu acabei aqui quando eu estava procurando por uma solução para o Postgres, eu pensei adicionar este aqui poderia ajudar os outros.

Para os seguintes varchar dados:

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

Isso funcionou melhor para mim:

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

Se você está tendo problemas para carregar os dados do DB para classificar em C#, então eu tenho certeza que você vai se decepcionar com qualquer abordagem em fazê-lo por meio de programação no DB.Quando o servidor está indo para classificar, tem que calcular o "percebido" ordem justa, como seria necessário, a cada momento.

Eu sugiro que você adicione uma coluna para armazenar o processado exibe a seqüência de caracteres, usando alguns C# método, quando os dados é inserido pela primeira vez.Você pode tentar converter os números em largura fixa intervalos, por exemplo, para "xyz1" iria se transformar em "xyz00000001".Em seguida, você pode usar normal SQL Server classificação.

Com o risco de tooting meu próprio chifre, eu escrevi um CodeProject artigo implementação do problema como colocado no CodingHorror artigo.Sinta-se livre para roubar do meu código.

Acabei de ler um artigo em algum lugar sobre um tópico.O ponto-chave é:você precisa apenas o valor inteiro para classificar os dados, enquanto o 'rec' cadeia pertence à INTERFACE do usuário.Você pode dividir as informações em dois campos, dizer o alfa e o núm, classificar o alfa e o número (em separado) e, em seguida, mostrando uma seqüência de caracteres composta por alpha + num.Você pode usar uma coluna computada para compor a seqüência de caracteres, ou um modo de exibição.Espero que ajude

Você pode usar o código a seguir para resolver o 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

cumprimentos, rabihkahaleh@hotmail.com

Simplesmente você pode classificar por

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

 ##

Eu ainda não entendo (provavelmente por causa do meu pobre inglês).

Você pode tentar:

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

Mas ele não vai funcionar para milhões de registros.

Por isso que eu sugeri usar gatilho que preenche separado coluna com o valor humano.

Além disso:

  • construído-em T-SQL funções são realmente lenta e Microsoft sugerem a utilização .Funções de rede em vez disso.
  • o valor humano é constante, então não há nenhum ponto de cálculo de cada vez quando a consulta é executada.
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top