Muchas otras respuestas que veo aquí (y en las preguntas duplicadas) básicamente solo funcionan para datos formateados muy específicamente, por ejemplo, una cadena que es completamente un número, o para la cual hay un prefijo alfabético de longitud fija. Esto no va a funcionar en el caso general.
Es cierto que realmente no hay forma de implementar una ordenación nativa 100% general en MySQL, porque para hacerlo lo que realmente necesita es una función de comparación modificada , que cambia entre la ordenación lexicográfica de las cadenas y la ordenación numérica si / cuando encuentra un número. Dicho código podría implementar cualquier algoritmo que desee para reconocer y comparar las porciones numéricas dentro de dos cadenas. Sin embargo, desafortunadamente, la función de comparación en MySQL es interna a su código y el usuario no puede cambiarla.
Esto deja un truco de algún tipo, en el que intenta crear una clave de clasificación para su cadena en la que las partes numéricas se vuelven a formatear para que la clasificación lexicográfica estándar realmente las clasifique de la manera que desee .
Para enteros simples hasta un número máximo de dígitos, la solución obvia es simplemente rellenarlos con ceros a la izquierda para que todos tengan un ancho fijo. Este es el enfoque adoptado por el complemento de Drupal y las soluciones de @plalx / @RichardToth. (@Christian tiene una solución diferente y mucho más compleja, pero no ofrece ventajas que yo pueda ver).
Como señala @tye, puede mejorar esto agregando una longitud de dígito fijo a cada número, en lugar de simplemente rellenarlo a la izquierda. Sin embargo, hay mucho, mucho más en lo que puede mejorar, incluso dadas las limitaciones de lo que es esencialmente un truco incómodo. Sin embargo, ¡no parece haber ninguna solución prediseñada por ahí!
Por ejemplo, ¿qué pasa con:
- ¿Signos más y menos? +10 frente a 10 frente a -10
- Decimales 8.2, 8.5, 1.006, .75
- ¿Ceros a la izquierda? 020, 030, 00000922
- ¿Mil separadores? "1001 dálmatas" frente a "1001 dálmatas"
- ¿Números de versión? MariaDB v10.3.18 frente a MariaDB v10.3.3
- ¿Números muy largos? 103,768,276,592,092,364,859,236,487,687,870,234,598.55
Ampliando el método de @ tye, he creado una función almacenada NatSortKey () bastante compacta que convertirá una cadena arbitraria en una clave nat-sort, y que maneja todos los casos anteriores, es razonablemente eficiente y conserva una clasificación total. orden (no hay dos cadenas diferentes que tengan claves de clasificación que se comparen iguales). Se puede usar un segundo parámetro para limitar el número de números procesados en cada cadena (por ejemplo, a los primeros 10 números, por ejemplo), que se puede usar para garantizar que la salida se ajuste a una longitud determinada.
NOTA: La cadena de clave de clasificación generada con un valor dado de este segundo parámetro solo debe clasificarse frente a otras cadenas generadas con el mismo valor para el parámetro, de lo contrario, es posible que no se clasifiquen correctamente.
Puede usarlo directamente en el pedido, por ejemplo
SELECT myString FROM myTable ORDER BY NatSortKey(myString,0);
Pero para una clasificación eficiente de tablas grandes, es mejor almacenar previamente la clave de clasificación en otra columna (posiblemente con un índice en ella):
INSERT INTO myTable (myString,myStringNSK) VALUES (@theStringValue,NatSortKey(@theStringValue,10)), ...
...
SELECT myString FROM myTable ORDER BY myStringNSK;
[Idealmente, haría que esto suceda automáticamente creando la columna clave como una columna almacenada calculada, usando algo como:
CREATE TABLE myTable (
...
myString varchar(100),
myStringNSK varchar(150) AS (NatSortKey(myString,10)) STORED,
...
KEY (myStringNSK),
...);
Pero por ahora ni MySQL ni MariaDB permiten funciones almacenadas en columnas calculadas , por lo que, lamentablemente , todavía no puede hacer esto .]
Mi función solo afecta a la clasificación de números . Si usted quiere hacer otras cosas tipo-de normalización, tales como la eliminación de todos los puntuacion, o recortar los espacios en blanco de cada extremo, o la sustitución de secuencias múltiples espacios en blanco con espacios individuales, se puede extender bien la función, o podría ser hecho antes o después NatSortKey()
es aplicado a sus datos. (Recomiendo usarlo REGEXP_REPLACE()
para este propósito).
También es algo anglocéntrico en el sentido de que supongo '.' para un punto decimal y ',' para el separador de miles, pero debería ser bastante fácil de modificar si desea lo contrario, o si desea que se pueda cambiar como parámetro.
Podría ser susceptible de mejora adicional de otras formas; por ejemplo, actualmente clasifica los números negativos por valor absoluto, por lo que -1 viene antes de -2, en lugar de al revés. Tampoco hay forma de especificar un orden de clasificación DESC para los números mientras se conserva la clasificación lexicográfica ASC para el texto. Ambos problemas se pueden solucionar con un poco más de trabajo; Actualizaré el código si / cuando tenga la hora.
Hay muchos otros detalles a tener en cuenta, incluidas algunas dependencias críticas en el chaset y la intercalación que está utilizando , pero los he puesto todos en un bloque de comentarios dentro del código SQL. ¡Lea esto detenidamente antes de utilizar la función usted mismo!
Entonces, aquí está el código. Si encuentra un error o tiene una mejora que no he mencionado, ¡hágamelo saber en los comentarios!
delimiter $$
CREATE DEFINER=CURRENT_USER FUNCTION NatSortKey (s varchar(100), n int) RETURNS varchar(350) DETERMINISTIC
BEGIN
DECLARE x,y varchar(100);
DECLARE r varchar(350) DEFAULT '';
DECLARE suf varchar(101);
DECLARE i,j,k int UNSIGNED;
IF n<=0 THEN SET n := -1; END IF;
LOOP
SET i := REGEXP_INSTR(s,'\\d');
IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF;
SET n := n-1, suf := ' ';
IF i>1 THEN
IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF;
IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF;
SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i);
END IF;
SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+'));
SET s := SUBSTRING(s,LENGTH(x)+1);
SET i := INSTR(x,'.');
IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF;
SET i := LENGTH(x);
SET x := REPLACE(x,',','');
SET j := LENGTH(x);
SET x := TRIM(LEADING '0' FROM x);
SET k := LENGTH(x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0'));
SET i := LOCATE('.',y,2);
IF i=0 THEN
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf);
ELSE
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
WHILE LENGTH(y)>0 AND n!=0 DO
IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF;
SET j := LENGTH(x);
SET x := TRIM(LEADING '0' FROM x);
SET k := LENGTH(x);
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0'));
SET n := n-1;
END WHILE;
SET r := CONCAT(r,y,suf);
END IF;
END LOOP;
END
$$
delimiter ;