Precisamos fazer alguns relatórios sobre valores que geralmente são strings mistas de números e letras que precisam ser classificados 'naturalmente'. Coisas como, por exemplo, "P7B18" ou "P12B3". @As strings serão principalmente sequências de letras e depois números alternados. O número desses segmentos e o comprimento de cada um podem variar, no entanto.
Gostaríamos que as partes numéricas deles fossem classificadas em ordem numérica. Obviamente, se eu manusear esses valores de string diretamente com ORDER BY
, então "P12B3" virá antes de "P7B18", já que "P1" é anterior a "P7", mas eu gostaria do contrário, já que "P7" precede naturalmente "P12".
Eu também gostaria de poder fazer comparações de intervalo, 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. Os comprimentos das strings e o número de segmentos podem ser arbitrários, sem limites superiores fixos.
Em nosso caso, o uso de maiúsculas e minúsculas não é importante, embora, se houver uma maneira de fazer isso com reconhecimento de agrupamento, outras pessoas possam achar isso útil. A parte mais feia de tudo isso é que eu gostaria de poder fazer a ordenação e a filtragem de intervalo na WHERE
cláusula.
Se eu estivesse fazendo isso em C#, seria uma tarefa bastante simples: faça algumas análises para separar o alfa do numérico, implemente IComparable e basicamente pronto. O SQL Server, é claro, não parece oferecer nenhuma funcionalidade semelhante, pelo menos até onde eu sei.
Alguém conhece algum bom truque para fazer isso funcionar? Existe alguma capacidade pouco divulgada de criar tipos CLR personalizados que implementam IComparable e se comportam conforme o esperado? Também não me oponho a truques de XML estúpidos (consulte também: concatenação de lista) e também tenho funções de wrapper de correspondência/extração/substituição de CLR regex disponíveis no servidor.
EDIT: Como um exemplo um pouco mais detalhado, gostaria que os dados se comportassem assim.
SELECT bin FROM bins ORDER BY bin
bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0
isto é, 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, é fácil no .NET se você implementar IComparable, mas não sei como (ou se) você pode fazer esse tipo de coisa no SQL Server. Certamente não é algo que eu já encontrei em 10 ou mais anos trabalhando com isso.
Quer um meio sensato e eficiente 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á meios integrados e fáceis de fazer isso, mas aqui está uma possibilidade:
Normalize as strings reformatando-as em segmentos de comprimento fixo:
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 seus potenciais comprimentos máximos.AFTER [or FOR] INSERT, UPDATE
Trigger de modo que você tenha a garantia de definir corretamente o valor para todos os registros, mesmo aqueles entrando por meio de consultas ad hoc etc. É claro que esse UDF escalar também pode ser manipulado por SQLCLR, mas precisaria ser testado para determinar qual deles era realmente mais eficiente. **UPPER()
função ao resultado final de todos os segmentos (para que isso precise ser feito apenas uma vez e não por segmento). Isso permitirá uma classificação adequada, dada a ordenação binária da coluna de classificação.AFTER INSERT, UPDATE
Trigger na tabela que chama o UDF para definir a coluna de classificação. Para melhorar o desempenho, use aUPDATE()
função para determinar se esta coluna de código está mesmo naSET
cláusula daUPDATE
instrução (simplesmenteRETURN
se for falsa) e, em seguida, junte as pseudotabelas e na coluna de código para processar apenas as linhas que tiverem alterações no valor doINSERTED
códigoDELETED
. Certifique-se de especificarCOLLATE Latin1_General_100_BIN2
essa condição JOIN para garantir a precisão ao determinar se há uma alteração.Exemplo:
Nesta abordagem, você pode classificar por meio de:
E você pode fazer filtragem de intervalo via:
ou:
Tanto o
ORDER BY
quanto oWHERE
filtro devem usar a ordenação binária definida paraSortColumn
devido à precedência de ordenação.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 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 personalizados. Isso lida com situações em que o valor está sendo comparado a outro valor que já é do mesmo tipo personalizado ou a um que precisa ser convertido implicitamente. Isso deve manipular o filtro de intervalo em uma
WHERE
condição.Com relação à classificação do UDT como um tipo de coluna regular (não uma coluna computada), isso só é possível se o UDT for "ordenado por bytes". Ser "ordenado por bytes" significa que a representação binária do UDT (que pode ser definido no UDT) é classificada naturalmente na ordem apropriada. Supondo que a representação binária seja tratada de forma semelhante à abordagem descrita acima para a coluna VARCHAR(50) que possui segmentos de comprimento fixo que são preenchidos, isso se qualificaria. Ou, se não fosse fácil garantir que a representação binária seria naturalmente ordenada da maneira adequada, você poderia expor um método ou propriedade do UDT que gera um valor que seria ordenado corretamente e, em seguida, criar uma
PERSISTED
coluna computada nessa método ou propriedade. O método precisa ser determinístico e marcado comoIsDeterministic = true
.Os benefícios dessa abordagem são:
Parse
método do UDT receba oP7B18
valor e o converta, você poderá simplesmente inserir os valores naturalmente comoP7B18
. E com o método de conversão implícito definido no UDT, a condição WHERE também permitiria usar simplesmente P7B18`.As consequências desta abordagem são:
PERSISTED
coluna computada em uma propriedade ou método do UDT, você obterá a representação retornada pela propriedade ou método. Se você deseja oP7B18
valor original, precisa chamar um método ou propriedade do UDT que é codificado para retornar essa representação. Como você precisa substituir oToString
método de qualquer maneira, esse é um bom candidato para fornecer isso.Não está claro (pelo menos para mim agora, já que não testei esta parte) como seria fácil/difícil fazer alterações na representação binária. A alteração da representação armazenada e classificável pode exigir a eliminação e a adição novamente do campo. Além disso, descartar o Assembly contendo o UDT falharia se usado de qualquer maneira, portanto, você deve se certificar de que não haja mais nada no Assembly além deste UDT. Você pode
ALTER ASSEMBLY
substituir a definição, mas há algumas restrições quanto a isso.Por outro lado, o
VARCHAR()
campo são dados desconectados do algoritmo, portanto, exigiria apenas a atualização da coluna. E se houver dezenas de milhões de linhas (ou mais), isso poderá ser feito em uma abordagem em lote.Implemente a biblioteca ICU que realmente permite fazer essa classificação alfanumérica. Embora altamente funcional, a biblioteca vem em apenas duas linguagens: C/C++ e Java. O que significa que você pode precisar fazer alguns ajustes para fazê-lo funcionar no Visual C++, ou há a chance de que o código Java possa ser convertido em MSIL usando IKVM . Há um ou dois projetos paralelos .NET vinculados nesse site que fornecem uma interface COM que pode ser acessada em código gerenciado, mas acredito que eles não sejam atualizados há algum tempo e não os testei. A melhor aposta aqui seria lidar com isso na camada do aplicativo 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 tal habilidade exista. Eu forneci um passo a passo mais detalhado de um exemplo disso na seguinte resposta:
Existe um agrupamento para classificar as seguintes strings 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 mostrando que o tipo de padrão tratado nesta questão também funciona, vá para a página a seguir:
Demonstração de agrupamento de UTI
Em "Configurações", defina a opção "numérica" como "ativada" e todas as outras devem ser definidas como "padrão". Em seguida, à direita do botão "sort", desmarque a opção "diff strengths" e marque a opção "sort keys". Em seguida, substitua a lista de itens na área de texto "Input" pela seguinte lista:
Clique no botão "classificar". A área de texto "Saída" deve exibir o seguinte:
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, de modo que apresenta outro pequeno problema para resolver se precisar implementar isso 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 uso mínimo delas. Na verdade, o principal motivo para armazenar o valor normalizado era evitar chamar uma UDF para cada linha de cada consulta. Na abordagem primária, o UDF é usado para definir o valor de
SortColumn
, e isso só é feito porINSERT
meioUPDATE
do Trigger. Selecionar valores é muito mais comum do que inserir e atualizar, e alguns valores nunca são atualizados. Para cadaSELECT
consulta que usa oSortColumn
filtro for a range naWHERE
cláusula, a UDF é necessária apenas uma vez para cada um dos valores range_start e range_end para obter os valores normalizados; a UDF não é chamada por linha.Com relação ao UDT, o uso é realmente o mesmo do 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 para 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 ele não está fazendo nenhum acesso a dados e é determinístico, se estiver marcado como
IsDeterministic = true
, poderá participar de planos paralelos (o que pode ajudar as operaçõesINSERT
eUPDATE
), enquanto um O T-SQL UDF impedirá que um plano paralelo seja usado.