ORDER BY e comparação de seqüências mistas de letras e números

9

Precisamos fazer alguns relatórios sobre valores que geralmente são sequências mistas de números e letras que precisam ser classificadas 'naturalmente'. Coisas como, por exemplo, "P7B18" ou "P12B3". @ As strings serão principalmente seqüências de letras e números alternados. O número desses segmentos e o comprimento de cada um podem variar.

Gostaríamos que as partes numéricas delas fossem classificadas em ordem numérica. Obviamente, se eu apenas manipular esses valores de string diretamente com ORDER BY, "P12B3" virá antes de "P7B18", pois "P1" é anterior a "P7", mas eu gostaria do contrário, pois "P7" precede naturalmente "P12".

Eu também gostaria de poder fazer comparações de intervalos, por exemplo, @bin < 'P13S6'ou algo assim. Não preciso lidar com ponto flutuante ou números negativos; estes serão estritamente números inteiros não negativos com os quais estamos lidando. O comprimento da string e o número de segmentos podem ser arbitrários, sem limites superiores fixos.

No nosso caso, o invólucro de cordas não é importante, embora, se houver uma maneira de fazer isso de maneira compatível com agrupamentos, outros possam achar isso útil. A parte mais feia de tudo isso é que eu gostaria de poder fazer pedidos e filtragem de intervalo na WHEREcláusula.

Se eu estivesse fazendo isso em C #, seria uma tarefa bem simples: faça uma análise para separar o alfa do numérico, implemente IComparable e pronto. O SQL Server, é claro, não parece oferecer nenhuma funcionalidade semelhante, pelo menos até onde eu saiba.

Alguém conhece algum bom truque para fazer isso funcionar? Existe alguma capacidade pouco divulgada de criar tipos personalizados de CLR que implementam IComparable e têm esse comportamento conforme o esperado? Também não sou contra o Stupid XML Tricks (veja também: concatenação de listas) e também tenho funções de wrapper de correspondência / extração / substituição de regex CLR disponíveis no servidor.

Edição: Como um exemplo um pouco mais detalhado, eu gostaria que os dados se comportassem algo assim.

SELECT bin FROM bins ORDER BY bin

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

ou seja, divida as strings em tokens de todas as letras ou todos os números e classifique-os alfabeticamente ou numericamente, respectivamente, com os tokens mais à esquerda sendo o termo de classificação mais significativo. Como mencionei, pedaço de bolo no .NET se você implementar o IComparable, mas não sei como (ou se) você pode fazer esse tipo de coisa no SQL Server. Certamente não é algo que me deparei em 10 anos ou mais trabalhando com ele.

db2
fonte
Você poderia fazer isso com algum tipo de coluna computada indexada, transformando a string em um número inteiro. Assim, P7B12poderia tornar-se P 07 B 12, em seguida, (via ASCII) 80 07 65 12, então80076512
Philᵀᴹ
Sugiro que você crie uma coluna calculada que preencha cada componente numérico com um comprimento maior (ou seja, 10 zeros). Como o formato é bastante arbitrário, você precisará de uma expressão inline bastante grande, mas é factível. Então você pode indexar / ordenar por / onde, nessa coluna, quantas vezes quiser.
Nick.McDermaid
Por favor, consulte o link eu adicionei para o topo da minha resposta :)
Solomon Rutzky
11
@srutzky Nice, votei a favor.
DB2
Ei, db2: devido ao fato da Microsoft migrar do Connect para o UserVoice e não manter exatamente a contagem de votos (eles o colocam em um comentário, mas não sabem ao certo), você pode precisar votar novamente: Support "natural sorting" / DIGITSASNUMBERS como uma opção de agrupamento . Obrigado!
Solomon Rutzky

Respostas:

8

Deseja um meio eficiente e sensível de classificar números em strings como números reais? Considere votar na minha sugestão do Microsoft Connect: Suporte "classificação natural" / DIGITSASNUMBERS como uma opção de agrupamento


Não há um meio fácil e interno de fazer isso, mas aqui está uma possibilidade:

Normalize as strings reformatando-as em segmentos de comprimento fixo:

  • Crie uma coluna de classificação do tipo VARCHAR(50) COLLATE Latin1_General_100_BIN2. O comprimento máximo de 50 pode precisar ser ajustado com base no número máximo de segmentos e em seus possíveis comprimentos máximos.
  • Embora a normalização possa ser feita na camada de aplicativo de maneira mais eficiente, o manuseio no banco de dados usando um T-SQL UDF permitiria colocar o UDF escalar em um AFTER [or FOR] INSERT, UPDATEgatilho, para garantir a configuração correta do valor de todos os registros, mesmo aqueles vindo através de consultas ad hoc etc. É claro que esse UDF escalar também pode ser tratado via SQLCLR, mas precisaria ser testado para determinar qual era realmente mais eficiente. **
  • O UDF (independentemente de estar em T-SQL ou SQLCLR) deve:
    • Processe um número desconhecido de segmentos lendo cada caractere e parando quando o tipo alternar de alfa para numérico ou numérico para alfa.
    • Por cada segmento, ele deve retornar uma cadeia de comprimento fixo definida para o máximo possível de caracteres / dígitos de qualquer segmento (ou talvez max + 1 ou 2 para explicar o crescimento futuro).
    • Os segmentos alfa devem ser justificados à esquerda e preenchidos à direita com espaços.
    • Os segmentos numéricos devem ser justificados à direita e preenchidos à esquerda com zeros.
    • Se os caracteres alfa puderem aparecer em maiúsculas e minúsculas, mas a ordem precisar diferenciar maiúsculas de minúsculas, aplique a UPPER()função ao resultado final de todos os segmentos (para que seja necessário apenas uma vez e não por segmento). Isso permitirá uma classificação adequada, considerando o agrupamento binário da coluna de classificação.
  • Crie um AFTER INSERT, UPDATEgatilho na tabela que chama o UDF para definir a coluna de classificação. Para melhorar o desempenho, use a UPDATE()função para determinar se esta coluna código é mesmo na SETcláusula da UPDATEdeclaração (simplesmente RETURNse falso), e depois juntar os INSERTEDe DELETEDpseudo-tabelas na coluna código para apenas as linhas de processos que têm alterações no valor de código . Certifique-se de especificar COLLATE Latin1_General_100_BIN2a condição JOIN para garantir a precisão na determinação de uma alteração.
  • Crie um índice na nova coluna de classificação.

Exemplo:

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

Nesta abordagem, você pode classificar via:

ORDER BY tbl.SortColumn

E você pode fazer a filtragem de alcance via:

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

ou:

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

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

O filtro ORDER BYe o WHEREfiltro devem usar o agrupamento binário definido SortColumndevido à Precedência do agrupamento .

As comparações de igualdade ainda seriam feitas na coluna de valor original.


Outros pensamentos:

  • Use um SQLCLR UDT. Isso pode funcionar, embora não esteja claro se ele apresenta um ganho líquido em comparação com a abordagem descrita acima.

    Sim, um SQLCLR UDT pode ter seus operadores de comparação substituídos por algoritmos customizados. Isso lida com situações em que o valor está sendo comparado a outro valor que já é do mesmo tipo personalizado ou que precisa ser convertido implicitamente. Isso deve lidar com o filtro de faixa em uma WHEREcondição.

    Com relação à classificação da UDT como um tipo de coluna regular (não uma coluna calculada), isso só é possível se a UDT for "ordenada por byte". Ser "ordenado por byte" significa que a representação binária da UDT (que pode ser definida na UDT) é classificada naturalmente na ordem apropriada. Supondo que a representação binária seja tratada de maneira semelhante à abordagem descrita acima para a coluna VARCHAR (50) que possui segmentos de comprimento fixo preenchidos, que se qualificariam. Ou, se não fosse fácil garantir que a representação binária fosse ordenada naturalmente da maneira correta, você poderia expor um método ou propriedade da UDT que produza um valor que seria ordenado corretamente e, em seguida, criar uma PERSISTEDcoluna computada nessa método ou propriedade. O método precisa ser determinístico e marcado como IsDeterministic = true.

    Os benefícios dessa abordagem são:

    • Não há necessidade de um campo "valor original".
    • Não é necessário chamar um UDF para inserir os dados ou comparar valores. Supondo que o Parsemétodo da UDT capte o P7B18valor e o converta, você poderá simplesmente inserir os valores naturalmente como P7B18. E com o método implícito de conversão definido na UDT, a condição WHERE também permitiria usar simplesmente P7B18`.

    As conseqüências dessa abordagem são:

    • Simplesmente selecionar o campo retornará a representação binária, se você estiver usando o UDT ordenado por bytes como o tipo de dados da coluna. Ou, se estiver usando uma PERSISTEDcoluna computada em uma propriedade ou método da UDT, você obterá a representação retornada pela propriedade ou método. Se você quiser o P7B18valor original , precisará chamar um método ou propriedade da UDT codificada para retornar essa representação. Como você precisa substituir o ToStringmétodo de qualquer maneira, esse é um bom candidato para fornecer isso.
    • Não está claro (pelo menos para mim, agora que não testei esta parte) o quão fácil / difícil seria fazer alterações na representação binária. Alterar a representação classificada e armazenada pode exigir a remoção e a inclusão novamente do campo. Além disso, a eliminação do Assembly que contém a UDT falharia se usada de qualquer maneira, portanto, você deve garantir que não haja mais nada no Assembly além deste UDT. Você pode ALTER ASSEMBLYsubstituir a definição, mas existem algumas restrições nisso.

      Por outro lado, o VARCHAR()campo são dados desconectados do algoritmo, portanto, seria necessário apenas atualizar a coluna. E se houver dezenas de milhões de linhas (ou mais), isso poderá ser feito em uma abordagem em lotes.

  • Implemente a biblioteca ICU que realmente permite fazer essa classificação alfanumérica. Embora altamente funcional, a biblioteca vem apenas em duas linguagens: C / C ++ e Java. O que significa que você pode precisar fazer alguns ajustes para que funcione no Visual C ++, ou existe a chance de que o código Java possa ser convertido para MSIL usando o IKVM . Existem um ou dois projetos paralelos do .NET vinculados nesse site que fornecem uma interface COM que pode ser acessada no código gerenciado, mas acredito que eles não foram atualizados há algum tempo e ainda não os tentei. A melhor aposta aqui seria lidar com isso na camada de aplicativos com o objetivo de gerar chaves de classificação. As chaves de classificação seriam salvas em uma nova coluna de classificação.

    Esta pode não ser a abordagem mais prática. No entanto, ainda é muito legal que essa capacidade exista. Forneci uma explicação mais detalhada de um exemplo disso na seguinte resposta:

    Existe um agrupamento para classificar as seguintes cadeias na seguinte ordem 1,2,3,6,10,10A, 10B, 11?

    Mas o padrão tratado nessa questão é um pouco mais simples. Para um exemplo que mostra que o tipo de padrão tratado nesta pergunta também funciona, vá para a seguinte página:

    Demonstração de agrupamento na UTI

    Em "Configurações", defina a opção "numérico" como "ativado" e todos os outros devem ser definidos como "padrão". Em seguida, à direita do botão "classificar", desmarque a opção "Pontos fortes" e marque a opção "Chaves de classificação". Em seguida, substitua a lista de itens na área de texto "Entrada" pela seguinte lista:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23
    

    Clique no botão "classificar". A área de texto "Saída" deve exibir o seguinte:

    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 .

    Observe que as chaves de classificação são estruturadas em vários campos, separados por vírgulas. Cada campo precisa ser classificado de forma independente, para que apresente outro pequeno problema a ser resolvido se for necessário implementá-lo no SQL Server.


** Se houver alguma preocupação com o desempenho em relação ao uso de funções definidas pelo usuário, observe que as abordagens propostas fazem um uso mínimo delas. De fato, o principal motivo para armazenar o valor normalizado foi evitar chamar um UDF por cada linha de cada consulta. Na abordagem primária, o UDF é usado para definir o valor de SortColumn, e isso é feito somente no INSERTe UPDATEatravés do gatilho. Selecionar valores é muito mais comum do que inserir e atualizar, e algum valor nunca é atualizado. Por cada SELECTconsulta que usa o SortColumnfiltro para um intervalo na WHEREcláusula, o UDF é necessário apenas uma vez por cada um dos valores range_start e range_end para obter os valores normalizados; o UDF não é chamado por linha.

Com relação à UDT, o uso é realmente o mesmo que com a UDF escalar. Ou seja, inserir e atualizar chamaria o método de normalização uma vez por cada linha para definir o valor. Em seguida, o método de normalização seria chamado uma vez por consulta por cada range_start e range_value em um filtro de intervalo, mas não por linha.

Um ponto a favor de lidar com a normalização inteiramente em um SQLCLR UDF é que, dado que não está fazendo qualquer acesso a dados e é determinista, se ele é marcado como IsDeterministic = true, então ele pode participar em planos paralelos (que pode ajudar o INSERTe UPDATEoperações), enquanto um O T-SQL UDF impedirá que um plano paralelo seja usado.

Solomon Rutzky
fonte