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 WHERE
clá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.
P7B12
podría convertirseP 07 B 12
, entonces (a través de ASCII)80 07 65 12
, entonces80076512
Respuestas:
¿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:
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.AFTER [or FOR] INSERT, UPDATE
disparador 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. ** **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.AFTER INSERT, UPDATE
disparador en la tabla que llame a la UDF para establecer la columna de clasificación. Para mejorar el rendimiento, utilice laUPDATE()
función para determinar si esta columna es el código incluso en laSET
cláusula de laUPDATE
declaración (sóloRETURN
si es falso), y luego unirse a losINSERTED
yDELETED
pseudo-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 especificarCOLLATE Latin1_General_100_BIN2
esa condición de UNIÓN para garantizar la precisión al determinar si hay un cambio.Ejemplo:
En este enfoque, puede ordenar a través de:
Y puede hacer el filtrado de rango a través de:
o:
Tanto el
ORDER BY
y elWHERE
filtro debe utilizar la intercalación binaria definido paraSortColumn
debido 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
WHERE
condició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
PERSISTED
columna calculada en ese método o propiedad. El método debe ser determinista y marcado comoIsDeterministic = true
.Los beneficios de este enfoque son:
Parse
método de UDT toma elP7B18
valor y lo convierte, entonces debería poder simplemente insertar los valores naturalmente comoP7B18
. 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:
PERSISTED
columna calculada en una propiedad o método del UDT, obtendrá la representación devuelta por la propiedad o el método. Si desea elP7B18
valor original , debe llamar a un método o propiedad del UDT que está codificado para devolver esa representación. Como debe anular elToString
mé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 ASSEMBLY
reemplazar 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:
Haga clic en el botón "ordenar". El área de texto "Salida" debería mostrar lo siguiente:
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 sobreINSERT
y aUPDATE
través del Disparador. Seleccionar valores es mucho más común que insertar y actualizar, y algunos valores nunca se actualizan. Por cadaSELECT
consulta que use elSortColumn
filtro de rango en laWHERE
clá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 losINSERT
yUPDATE
las operaciones) mientras que una T-SQL UDF evitará que se use un plan paralelo.fuente