ORDER BY y comparación de cadenas mixtas de letras y números


9

Necesitamos hacer algunos informes sobre valores que generalmente son cadenas mixtas de números y letras que deben clasificarse 'naturalmente'. Cosas como, por ejemplo, "P7B18" o "P12B3". @Las cadenas serán principalmente secuencias de letras y luego números alternados. Sin embargo, el número de estos segmentos y la longitud de cada uno podrían variar.

Nos gustaría que las porciones numéricas de estos se clasifiquen en orden numérico. Obviamente, si solo manejo esos valores de cadena directamente ORDER BY, entonces "P12B3" vendrá antes que "P7B18", ya que "P1" es anterior a "P7", pero me gustaría lo contrario, ya que "P7" precede naturalmente "P12".

También me gustaría poder hacer comparaciones de rango, por ejemplo, @bin < 'P13S6'o algo así. No tengo que manejar coma flotante o números negativos; estos serán estrictamente enteros no negativos con los que estamos tratando. Las longitudes de cadena y el número de segmentos podrían ser potencialmente arbitrarios, sin límites superiores fijos.

En nuestro caso, el entubado de cuerdas no es importante, aunque si hay una manera de hacer esto de manera ordenada, otros podrían encontrarlo útil. La parte más fea de todo esto es que me gustaría poder ordenar y filtrar los rangos en la WHEREcláusula.

Si estuviera haciendo esto en C #, sería una tarea bastante simple: realizar un análisis para separar el alfa de lo numérico, implementar IComparable y ya está. SQL Server, por supuesto, no parece ofrecer ninguna funcionalidad similar, al menos hasta donde yo sé.

¿Alguien sabe algún buen truco para que esto funcione? ¿Existe alguna capacidad poco publicitada para crear tipos de CLR personalizados que implementen IComparable y hagan que se comporte como se espera? Tampoco me opongo a Stupid XML Tricks (ver también: concatenación de listas), y también tengo funciones de contenedor de coincidencia / extracción / reemplazo de expresiones regulares CLR disponibles en el servidor.

EDITAR: Como un ejemplo un poco más detallado, me gustaría que los datos se comporten de esta manera.

SELECT bin FROM bins ORDER BY bin

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

es decir, divida las cadenas en fichas de todas las letras o todos los números, y ordénelas alfabéticamente o numéricamente respectivamente, siendo las fichas de la izquierda el término de clasificación más significativo. Como mencioné, pan comido en .NET si implementa IComparable, pero no sé cómo (o si) puede hacer ese tipo de cosas en SQL Server. Ciertamente no es algo con lo que me haya encontrado en más de 10 años trabajando con él.


Puede hacer esto con algún tipo de columna calculada indexada, convirtiendo la cadena en un número entero. Entonces P7B12podría convertirse P 07 B 12, entonces (a través de ASCII) 80 07 65 12, entonces80076512
Philᵀᴹ

Le sugiero que cree una columna calculada que rellena cada componente numérico en una gran longitud (es decir, 10 ceros). Dado que el formato es bastante arbitrario, necesitará una expresión en línea bastante grande, pero es factible. Luego, puede indexar / ordenar por / dónde en esa columna tanto como desee.
Nick.McDermaid

Por favor, vea el enlace que acabo de agregar a la parte superior de mi respuesta :)
Solomon Rutzky

1
@srutzky Niza, he votado a favor.
db2

Hola db2: debido a que Microsoft se mudó de Connect a UserVoice y no mantuvo exactamente el recuento de votos (lo pusieron en un comentario pero no están seguros de que lo vean), es posible que deba volver a votar por él: admite "clasificación natural" / DIGITSASNUMBERS como una opción de clasificación . ¡Gracias!
Solomon Rutzky

Respuestas:


8

¿Desea un medio sensato y eficiente de ordenar los números en cadenas como números reales? Considere votar por mi sugerencia de Microsoft Connect: soporte "clasificación natural" / DIGITSASNUMBERS como una opción de clasificación


No hay medios fáciles e integrados para hacerlo, pero aquí hay una posibilidad:

Normalice las cadenas formateándolas en segmentos de longitud fija:

  • Crea una columna de clasificación de tipo VARCHAR(50) COLLATE Latin1_General_100_BIN2. Es posible que deba ajustarse la longitud máxima de 50 en función del número máximo de segmentos y sus posibles longitudes máximas.
  • Si bien la normalización se podría hacer en la capa de la aplicación de manera más eficiente, manejar esto en la base de datos utilizando un UDF T-SQL permitiría colocar el UDF escalar en un AFTER [or FOR] INSERT, UPDATEdisparador de modo que tenga la garantía de establecer correctamente el valor para todos los registros, incluso aquellos llegando a través de consultas ad hoc, etc. Por supuesto, ese UDF escalar también se puede manejar a través de SQLCLR, pero necesitaría ser probado para determinar cuál era realmente más eficiente. ** **
  • El UDF (independientemente de estar en T-SQL o SQLCLR) debe:
    • Procese un número desconocido de segmentos leyendo cada carácter y deteniéndose cuando el tipo cambie de alfa a numérico o de numérico a alfa.
    • Por cada segmento, debe devolver un conjunto de cadenas de longitud fija al máximo de caracteres / dígitos posibles de cualquier segmento (o tal vez max + 1 o 2 para tener en cuenta el crecimiento futuro).
    • Los segmentos alfa deben estar justificados a la izquierda y rellenados a la derecha con espacios.
    • Los segmentos numéricos deben estar justificados a la derecha y rellenados a la izquierda con ceros.
    • Si los caracteres alfabéticos pueden aparecer en mayúsculas y minúsculas, pero el orden no debe distinguir entre mayúsculas y minúsculas, aplique la UPPER()función al resultado final de todos los segmentos (de modo que solo sea necesario hacerlo una vez y no por segmento). Esto permitirá una ordenación adecuada dada la clasificación binaria de la columna de ordenación.
  • Cree un AFTER INSERT, UPDATEdisparador en la tabla que llame a la UDF para establecer la columna de clasificación. Para mejorar el rendimiento, utilice la UPDATE()función para determinar si esta columna es el código incluso en la SETcláusula de la UPDATEdeclaración (sólo RETURNsi es falso), y luego unirse a los INSERTEDy DELETEDpseudo-mesas en la columna de código que sólo las filas de procesos que tienen los cambios en el valor de código . Asegúrese de especificar COLLATE Latin1_General_100_BIN2esa condición de UNIÓN para garantizar la precisión al determinar si hay un cambio.
  • Cree un índice en la nueva columna de clasificación.

Ejemplo:

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

En este enfoque, puede ordenar a través de:

ORDER BY tbl.SortColumn

Y puede hacer el filtrado de rango a través de:

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

o:

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

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

Tanto el ORDER BYy el WHEREfiltro debe utilizar la intercalación binaria definido para SortColumndebido a la compilación de precedencia.

Las comparaciones de igualdad aún se realizarían en la columna de valor original.


Otros pensamientos:

  • Use un SQLCLR UDT. Esto podría funcionar, aunque no está claro si presenta una ganancia neta en comparación con el enfoque descrito anteriormente.

    Sí, un UDT SQLCLR puede tener sus operadores de comparación anulados con algoritmos personalizados. Esto maneja situaciones en las que el valor se compara con otro valor que ya es el mismo tipo personalizado o uno que necesita convertirse implícitamente. Esto debería manejar el filtro de rango en una WHEREcondición.

    Con respecto a la clasificación del UDT como un tipo de columna normal (no una columna calculada), esto solo es posible si el UDT está "ordenado por bytes". Estar "ordenado por bytes" significa que la representación binaria del UDT (que se puede definir en el UDT) naturalmente se ordena en el orden apropiado. Suponiendo que la representación binaria se maneja de manera similar al enfoque descrito anteriormente para la columna VARCHAR (50) que tiene segmentos de longitud fija que están rellenados, eso calificaría. O, si no fuera fácil garantizar que la representación binaria se ordenaría naturalmente de la manera adecuada, podría exponer un método o propiedad del UDT que genere un valor que se ordenaría correctamente, y luego crear una PERSISTEDcolumna calculada en ese método o propiedad. El método debe ser determinista y marcado como IsDeterministic = true.

    Los beneficios de este enfoque son:

    • No es necesario un campo de "valor original".
    • No es necesario llamar a un UDF para insertar los datos o comparar valores. Suponiendo que el Parsemétodo de UDT toma el P7B18valor y lo convierte, entonces debería poder simplemente insertar los valores naturalmente como P7B18. Y con el método de conversión implícito establecido en el UDT, la condición WHERE también permitiría usar simplemente P7B18`.

    Las consecuencias de este enfoque son:

    • Simplemente seleccionando el campo devolverá la representación binaria, si utiliza el UDT ordenado por bytes como el tipo de datos de la columna. O si usa una PERSISTEDcolumna calculada en una propiedad o método del UDT, obtendrá la representación devuelta por la propiedad o el método. Si desea el P7B18valor original , debe llamar a un método o propiedad del UDT que está codificado para devolver esa representación. Como debe anular el ToStringmétodo de todos modos, es un buen candidato para proporcionarlo.
    • No está claro (al menos para mí en este momento, ya que no he probado esta parte) lo fácil / difícil que sería hacer cualquier cambio en la representación binaria. Cambiar la representación ordenada almacenada puede requerir soltar y volver a agregar el campo. Además, dejar caer el ensamblaje que contiene el UDT fallará si se usa de cualquier manera, por lo que debe asegurarse de que no haya nada más en el ensamblaje además de este UDT. Puede ALTER ASSEMBLYreemplazar la definición, pero hay algunas restricciones al respecto.

      Por otro lado, el VARCHAR()campo son datos que están desconectados del algoritmo, por lo que solo requeriría actualizar la columna. Y si hay decenas de millones de filas (o más), eso se puede hacer en un enfoque por lotes.

  • Implemente la biblioteca ICU que realmente permite hacer esta clasificación alfanumérica. Si bien es altamente funcional, la biblioteca solo viene en dos idiomas: C / C ++ y Java. Lo que significa que es posible que necesite hacer algunos ajustes para que funcione en Visual C ++, o existe la posibilidad de que el código Java se pueda convertir a MSIL usando IKVM . Hay uno o dos proyectos secundarios .NET vinculados en ese sitio que proporcionan una interfaz COM a la que se puede acceder en código administrado, pero creo que no se han actualizado en un tiempo y no los he probado. La mejor opción aquí sería manejar esto en la capa de la aplicación con el objetivo de generar claves de clasificación. Las claves de clasificación se guardarían en una nueva columna de clasificación.

    Este podría no ser el enfoque más práctico. Sin embargo, todavía es genial que exista tal habilidad. Proporcioné un recorrido más detallado de un ejemplo de esto en la siguiente respuesta:

    ¿Hay una clasificación para ordenar las siguientes cadenas en el siguiente orden 1,2,3,6,10,10A, 10B, 11?

    Pero el patrón que se trata en esa pregunta es un poco más simple. Para ver un ejemplo que muestra que el tipo de patrón que se trata en esta pregunta también funciona, vaya a la siguiente página:

    Demostración de colación de UCI

    En "Configuración", establezca la opción "numérica" ​​en "on" y todos los demás deben configurarse en "predeterminado". Luego, a la derecha del botón "ordenar", desmarque la opción para "puntos fuertes de diferencia" y marque la opción para "ordenar claves". Luego reemplace la lista de elementos en el área de texto "Entrada" con la siguiente lista:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23

    Haga clic en el botón "ordenar". El área de texto "Salida" debería mostrar lo siguiente:

    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 .

    Tenga en cuenta que las claves de clasificación están estructuradas en múltiples campos, separados por comas. Cada campo debe clasificarse de forma independiente, por lo que presenta otro pequeño problema para resolver si necesita implementar esto en SQL Server.


** Si hay alguna preocupación sobre el rendimiento con respecto al uso de funciones definidas por el usuario, tenga en cuenta que los enfoques propuestos hacen un uso mínimo de ellas. De hecho, la razón principal para almacenar el valor normalizado fue evitar llamar a un UDF por cada fila de cada consulta. En el enfoque principal, el UDF se usa para establecer el valor de SortColumn, y eso solo se hace sobre INSERTy a UPDATEtravés del Disparador. Seleccionar valores es mucho más común que insertar y actualizar, y algunos valores nunca se actualizan. Por cada SELECTconsulta que use el SortColumnfiltro de rango en la WHEREcláusula, el UDF solo se necesita una vez por cada uno de los valores range_start y range_end para obtener los valores normalizados; el UDF no se llama por fila.

Con respecto al UDT, el uso es en realidad el mismo que con el UDF escalar. Es decir, insertar y actualizar llamaría al método de normalización una vez por cada fila para establecer el valor. Entonces, el método de normalización se llamaría una vez por consulta por cada range_start y range_value en un filtro de rango, pero no por fila.

Un punto a favor de manejo de la normalización en su totalidad en un SQLCLR UDF es la que da no está haciendo ningún acceso a datos y es determinista, si se marca como IsDeterministic = true, entonces se puede participar en planes paralelos (que podría ayudar a los INSERTy UPDATElas operaciones) mientras que una T-SQL UDF evitará que se use un plan paralelo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.