Orden natural en MySQL

81

¿Existe una forma elegante de tener una ordenación natural y eficaz en una base de datos MySQL?

Por ejemplo, si tengo este conjunto de datos:

  • Fantasía Final
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Cadenas de Promathia
  • Final Fantasy Adventure
  • Orígenes de Final Fantasy
  • Tácticas de Final Fantasy

Cualquier otra solución elegante que dividir los nombres de los juegos en sus componentes

  • Título : "Final Fantasy"
  • Número : "12"
  • Subtítulo : "Cadenas de Promathia"

para asegurarse de que salgan en el orden correcto? (10 después de 4, no antes de 2).

Hacerlo es un fastidio porque de vez en cuando hay otro juego que rompe ese mecanismo de análisis del título del juego (por ejemplo, "Warhammer 40,000", "James Bond 007")

Culpa
fuente
28
Chains of Promathia está relacionado con 11.
Flame
Relacionado: stackoverflow.com/questions/48600059/…
Paul Spiegel

Respuestas:

20

Creo que esta es la razón por la que muchas cosas están ordenadas por fecha de lanzamiento.

Una solución podría ser crear otra columna en su tabla para "SortKey". Esta podría ser una versión desinfectada del título que se ajuste a un patrón que cree para una clasificación fácil o un contador.

Michael Haren
fuente
Acabo de escribir una clase exactamente para eso stackoverflow.com/a/47522040/935122
Christian
2
Este es definitivamente el enfoque correcto, ¡pero no es una respuesta en sí mismo!
Doin
90

Aquí hay una solución rápida:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
tipo de ranura
fuente
49
Eso es bueno si todo es "Final Fantasy", pero pone "Goofy" por delante de la suite FF.
fortboise
4
Esta solución no funciona todo el tiempo. A veces se rompe. Preferiría usar este: stackoverflow.com/a/12257917/384864
Borut Tomazin
6
Acumulando kludge sobre kludge: SELECT alphanumeric, integer FROM sorting_test ORDER BY SOUNDEX(alphanumeric), LENGTH(alphanumeric), alphanumeric. Si esto funciona, es porque SOUNDEX descarta convenientemente los números, asegurando así que, por ejemplo, apple1venga antes z1.
fuera del 1 de
gran solución, gracias, aunque tuve que cambiar alphanmuric, length(alphanumeric)para evitar "Goofy" antes de "Final Fantasy"
Asped
1
La sugerencia @ offby1 solo funciona si el texto está 100% escrito en inglés, ya que SOUNDEX()está diseñado para funcionar correctamente solo con palabras en inglés.
Raymond Nijland
56

Acabo de encontrar esto:

SELECT names FROM your_table ORDER BY games + 0 ASC

Hace una ordenación natural cuando los números están al frente, también podría funcionar para el medio.


fuente
2
No lo he probado, pero lo dudo seriamente. La razón por la que funciona con el número al frente es porque gamesse usa como en un contexto numérico y, por lo tanto, se convierte en un número antes de la comparación. Si está en el medio, siempre se convertirá a 0 y la clasificación se volverá pseudoaleatoria.
manixrock
1
Este no es un tipo natural. Más bien, eche un vistazo a esta solución de trabajo: stackoverflow.com/a/12257917/384864
Borut Tomazin
@fedir Esto también funcionó bien para mí. Ni siquiera estoy completamente seguro de por qué funciona esto. ¿Alguna posibilidad de un markletp de explicación?
BizNuge
Acabo de hacer una investigación rápida de esto y lo entiendo. ¡Ni siquiera me di cuenta de que MySQL haría este tipo de conversión simplemente usando un operador matemático en una cadena! Lo bueno es que simplemente devuelve zer0 en el caso de que no haya un número entero al principio de la cadena para "lanzar". ¡Gracias por esto! ---> SELECCIONAR DIRECCIÓN, (DIRECCIÓN * 1) como _cast DESDE las instalaciones DONDE EL CÓDIGO POSTAL COMO 'NE1%' ORDEN POR DIRECCIÓN * 1 ASC, LÍMITE DE DIRECCIÓN 100000;
BizNuge
1
En realidad, esto no funciona cuando los números están en el medio, como "Final Fantasy 100" o "Final Fantasy 2". "Final Fantasy 100" se mostrará primero. Sin embargo, funciona cuando el número entero es primero "100 Final Fantasy"
dwenaus
52

Misma función publicada por @plalx, ​​pero reescrita en MySQL:

DROP FUNCTION IF EXISTS `udf_FirstNumberPos`;
DELIMITER ;;
CREATE FUNCTION `udf_FirstNumberPos` (`instring` varchar(4000)) 
RETURNS int
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE position int;
    DECLARE tmp_position int;
    SET position = 5000;
    SET tmp_position = LOCATE('0', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF; 
    SET tmp_position = LOCATE('1', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('2', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('3', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('4', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('5', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('6', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('7', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('8', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('9', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;

    IF (position = 5000) THEN RETURN 0; END IF;
    RETURN position;
END
;;

DROP FUNCTION IF EXISTS `udf_NaturalSortFormat`;
DELIMITER ;;
CREATE FUNCTION `udf_NaturalSortFormat` (`instring` varchar(4000), `numberLength` int, `sameOrderChars` char(50)) 
RETURNS varchar(4000)
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE sortString varchar(4000);
    DECLARE numStartIndex int;
    DECLARE numEndIndex int;
    DECLARE padLength int;
    DECLARE totalPadLength int;
    DECLARE i int;
    DECLARE sameOrderCharsLen int;

    SET totalPadLength = 0;
    SET instring = TRIM(instring);
    SET sortString = instring;
    SET numStartIndex = udf_FirstNumberPos(instring);
    SET numEndIndex = 0;
    SET i = 1;
    SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars);

    WHILE (i <= sameOrderCharsLen) DO
        SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' ');
        SET i = i + 1;
    END WHILE;

    WHILE (numStartIndex <> 0) DO
        SET numStartIndex = numStartIndex + numEndIndex;
        SET numEndIndex = numStartIndex;

        WHILE (udf_FirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO
            SET numEndIndex = numEndIndex + 1;
        END WHILE;

        SET numEndIndex = numEndIndex - 1;

        SET padLength = numberLength - (numEndIndex + 1 - numStartIndex);

        IF padLength < 0 THEN
            SET padLength = 0;
        END IF;

        SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength));

        SET totalPadLength = totalPadLength + padLength;
        SET numStartIndex = udf_FirstNumberPos(RIGHT(instring, CHAR_LENGTH(instring) - numEndIndex));
    END WHILE;

    RETURN sortString;
END
;;

Uso:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")
Richard Toth
fuente
5
Ésta es la única solución que realmente funciona. También probé el código de drupals, pero a veces falla. ¡Gracias hombre!
Borut Tomazin
¿Alguien usa esto en mesas realmente grandes con más de 10 millones?
Mark Steudel
3
@MarkSteudel Usamos una función similar a esta (aunque no esta exacta) para la ordenación natural en varias tablas, la más grande de las cuales es ~ 5 millones de filas. Sin embargo, no lo llamamos directamente en nuestras consultas, sino que lo usamos para establecer el valor de una nat_namecolumna. Usamos un disparador para ejecutar la función cada vez que se actualiza una fila. Este enfoque le brinda una clasificación natural sin costo de rendimiento real a expensas de una columna adicional.
Jacob
esto funciona, ordena los números antes que las letras, y se puede implementar en Drupal usando hook_views_query_alter, usando algo similar a estoif ($query->orderby[0]["field"] === "node_field_data.title") { $orderBySql = " udf_NaturalSortFormat(node_field_data.title, 10, '.') "; $query->orderby = []; $query->addOrderBy(NULL, $orderBySql, $query->orderby[0]["direction"], 'title_natural'); array_unshift($query->orderby, end($query->orderby)); }
realgt
16

Escribí esta función para MSSQL 2000 hace un tiempo:

/**
 * Returns a string formatted for natural sorting. This function is very useful when having to sort alpha-numeric strings.
 *
 * @author Alexandre Potvin Latreille (plalx)
 * @param {nvarchar(4000)} string The formatted string.
 * @param {int} numberLength The length each number should have (including padding). This should be the length of the longest number. Defaults to 10.
 * @param {char(50)} sameOrderChars A list of characters that should have the same order. Ex: '.-/'. Defaults to empty string.
 *
 * @return {nvarchar(4000)} A string for natural sorting.
 * Example of use: 
 * 
 *      SELECT Name FROM TableA ORDER BY Name
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                    ID  Name
 *  1.  A1.                 1.  A1-1.       
 *  2.  A1-1.                   2.  A1.
 *  3.  R1      -->         3.  R1
 *  4.  R11                 4.  R11
 *  5.  R2                  5.  R2
 *
 *  
 *  As we can see, humans would expect A1., A1-1., R1, R2, R11 but that's not how SQL is sorting it.
 *  We can use this function to fix this.
 *
 *      SELECT Name FROM TableA ORDER BY dbo.udf_NaturalSortFormat(Name, default, '.-')
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                    ID  Name
 *  1.  A1.                 1.  A1.     
 *  2.  A1-1.                   2.  A1-1.
 *  3.  R1      -->         3.  R1
 *  4.  R11                 4.  R2
 *  5.  R2                  5.  R11
 */
CREATE FUNCTION dbo.udf_NaturalSortFormat(
    @string nvarchar(4000),
    @numberLength int = 10,
    @sameOrderChars char(50) = ''
)
RETURNS varchar(4000)
AS
BEGIN
    DECLARE @sortString varchar(4000),
        @numStartIndex int,
        @numEndIndex int,
        @padLength int,
        @totalPadLength int,
        @i int,
        @sameOrderCharsLen int;

    SELECT 
        @totalPadLength = 0,
        @string = RTRIM(LTRIM(@string)),
        @sortString = @string,
        @numStartIndex = PATINDEX('%[0-9]%', @string),
        @numEndIndex = 0,
        @i = 1,
        @sameOrderCharsLen = LEN(@sameOrderChars);

    -- Replace all char that has to have the same order by a space.
    WHILE (@i <= @sameOrderCharsLen)
    BEGIN
        SET @sortString = REPLACE(@sortString, SUBSTRING(@sameOrderChars, @i, 1), ' ');
        SET @i = @i + 1;
    END

    -- Pad numbers with zeros.
    WHILE (@numStartIndex <> 0)
    BEGIN
        SET @numStartIndex = @numStartIndex + @numEndIndex;
        SET @numEndIndex = @numStartIndex;

        WHILE(PATINDEX('[0-9]', SUBSTRING(@string, @numEndIndex, 1)) = 1)
        BEGIN
            SET @numEndIndex = @numEndIndex + 1;
        END

        SET @numEndIndex = @numEndIndex - 1;

        SET @padLength = @numberLength - (@numEndIndex + 1 - @numStartIndex);

        IF @padLength < 0
        BEGIN
            SET @padLength = 0;
        END

        SET @sortString = STUFF(
            @sortString,
            @numStartIndex + @totalPadLength,
            0,
            REPLICATE('0', @padLength)
        );

        SET @totalPadLength = @totalPadLength + @padLength;
        SET @numStartIndex = PATINDEX('%[0-9]%', RIGHT(@string, LEN(@string) - @numEndIndex));
    END

    RETURN @sortString;
END

GO
plalx
fuente
@MarkSteudel Tendría que intentarlo y probarlo usted mismo. En el peor de los casos, siempre puede almacenar en caché los valores formateados. Eso es probablemente lo que haría con tablas grandes porque también podría indexar el campo.
plalx
15

MySQL no permite este tipo de "clasificación natural", por lo que parece que la mejor manera de obtener lo que busca es dividir su configuración de datos como se describió anteriormente (campo de identificación separado, etc.), o fallando que, realice una clasificación basada en un elemento sin título, elemento indexado en su base de datos (fecha, identificación insertada en la base de datos, etc.).

Hacer que la base de datos haga la clasificación por usted casi siempre será más rápido que leer grandes conjuntos de datos en el lenguaje de programación de su elección y ordenarlos allí, por lo que si tiene algún control sobre el esquema de la base de datos aquí, entonces busque agregar campos fácilmente ordenados como se describe anteriormente, le ahorrará muchos problemas y mantenimiento a largo plazo.

Las solicitudes para agregar un "orden natural" surgen de vez en cuando en los foros de discusión y errores de MySQL , y muchas soluciones giran en torno a eliminar partes específicas de sus datos y convertirlos en la parte de la consulta, por ejemploORDER BY

SELECT * FROM table ORDER BY CAST(mid(name, 6, LENGTH(c) -5) AS unsigned) 

Este tipo de solución podría funcionar en tu ejemplo de Final Fantasy anterior, pero no es particularmente flexible y es poco probable que se extienda claramente a un conjunto de datos que incluya, por ejemplo, "Warhammer 40,000" y "James Bond 007". Me temo .

ConroyP
fuente
9

Entonces, aunque sé que ha encontrado una respuesta satisfactoria, estuve luchando con este problema por un tiempo, y previamente habíamos determinado que no se podía hacer razonablemente bien en SQL y que íbamos a tener que usar javascript en un JSON. formación.

Así es como lo resolví simplemente usando SQL. Con suerte, esto es útil para otros:

Tenía datos como:

Escena 1
Escena 1a
Escena 1B
Escena 2a
Escena 3
...
Escena 101
Escena XXA1
Escena XXA2

De hecho, no "lancé" cosas, aunque supongo que eso también puede haber funcionado.

Primero reemplacé las partes que no cambiaban en los datos, en este caso "Escena", y luego hice un LPAD para alinear las cosas. Esto parece permitir bastante bien que las cadenas alfa se clasifiquen correctamente, así como las numeradas.

Mi ORDER BYcláusula se parece a:

ORDER BY LPAD(REPLACE(`table`.`column`,'Scene ',''),10,'0')

Obviamente, esto no ayuda con el problema original que no era tan uniforme, pero imagino que probablemente funcionaría para muchos otros problemas relacionados, así que ponerlo ahí.

FilmJ
fuente
La LPAD()pista fue muy útil. Tengo palabras y números para ordenar, con los LPADque podría ordenar los números de forma natural. Y al usar CONCATignoro los no números. Mi consulta se ve así (el alias es la columna a ordenar): IF(CONCAT("",alias*1)=alias, LPAD(alias,5,"0"), alias) ASC;👍
Kai Noack
6
  1. Agregue una clave de clasificación (clasificación) en su tabla. ORDER BY rank

  2. Utilice la columna "Fecha de lanzamiento". ORDER BY release_date

  3. Al extraer los datos de SQL, haga que su objeto haga la clasificación, por ejemplo, si extrae en un Conjunto, conviértalo en un TreeSet y haga que su modelo de datos implemente Comparable y promulgue el algoritmo de ordenación natural aquí (la ordenación por inserción será suficiente si está utilizando un lenguaje sin colecciones) ya que leerá las filas de SQL una por una a medida que crea su modelo y lo inserta en la colección)

JeeBee
fuente
5

Respecto a la mejor respuesta de Richard Toth https://stackoverflow.com/a/12257917/4052357

Tenga cuidado con las cadenas codificadas en UTF8 que contienen números y caracteres de 2 bytes (o más), por ejemplo

12 南新宿

El uso de la función LENGTH()in de MySQL udf_NaturalSortFormatdevolverá la longitud en bytes de la cadena y será incorrecta, en su lugar, use CHAR_LENGTH()que devolverá la longitud correcta del carácter.

En mi caso, el uso de LENGTH()consultas causadas nunca se completó y resultó en un uso de CPU del 100% para MySQL

DROP FUNCTION IF EXISTS `udf_NaturalSortFormat`;
DELIMITER ;;
CREATE FUNCTION `udf_NaturalSortFormat` (`instring` varchar(4000), `numberLength` int, `sameOrderChars` char(50)) 
RETURNS varchar(4000)
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE sortString varchar(4000);
    DECLARE numStartIndex int;
    DECLARE numEndIndex int;
    DECLARE padLength int;
    DECLARE totalPadLength int;
    DECLARE i int;
    DECLARE sameOrderCharsLen int;

    SET totalPadLength = 0;
    SET instring = TRIM(instring);
    SET sortString = instring;
    SET numStartIndex = udf_FirstNumberPos(instring);
    SET numEndIndex = 0;
    SET i = 1;
    SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars);

    WHILE (i <= sameOrderCharsLen) DO
        SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' ');
        SET i = i + 1;
    END WHILE;

    WHILE (numStartIndex <> 0) DO
        SET numStartIndex = numStartIndex + numEndIndex;
        SET numEndIndex = numStartIndex;

        WHILE (udf_FirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO
            SET numEndIndex = numEndIndex + 1;
        END WHILE;

        SET numEndIndex = numEndIndex - 1;

        SET padLength = numberLength - (numEndIndex + 1 - numStartIndex);

        IF padLength < 0 THEN
            SET padLength = 0;
        END IF;

        SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength));

        SET totalPadLength = totalPadLength + padLength;
        SET numStartIndex = udf_FirstNumberPos(RIGHT(instring, CHAR_LENGTH(instring) - numEndIndex));
    END WHILE;

    RETURN sortString;
END
;;

ps habría agregado esto como un comentario al original pero no tengo suficiente reputación (todavía)

Luke Hoggett
fuente
4

Agregue un campo para "clave de clasificación" que tenga todas las cadenas de dígitos rellenos con ceros a una longitud fija y luego ordene en ese campo.

Si puede tener cadenas largas de dígitos, otro método es anteponer el número de dígitos (ancho fijo, relleno de ceros) a cada cadena de dígitos. Por ejemplo, si no tiene más de 99 dígitos seguidos, entonces para "Super Blast 10 Ultra" la clave de clasificación sería "Super Blast 0210 Ultra".

tye
fuente
4

Para orden:
0
1
2
10
23
101
205
1000
un
aac
b
casdsadsa
css

Utilice esta consulta:

SELECCIONE 
    column_name 
DESDE 
    nombre de la tabla 
PEDIR POR
    nombre_columna REGEXP '^ \ d * [^ \ da-z & \. \' \ - \ "\! \ @ \ # \ $ \% \ ^ \ * \ (\) \; \: \\, \? \ / \ ~ \ `\ | \ _ \ -] 'DESC, 
    column_name + 0, 
    column_name;
Guma
fuente
Por desgracia, este se descompone si se suman los valores de, como a1, a2, a11, etc ...
random_user_name
4

Si no quieres reinventar la rueda o tienes dolor de cabeza con mucho código que no funciona, simplemente usa Drupal Natural Sort ... Simplemente ejecuta el SQL que viene comprimido (MySQL o Postgre), y listo. Al hacer una consulta, simplemente ordene usando:

... ORDER BY natsort_canon(column_name, 'natural')
Neto Queiroz
fuente
Gracias por esto, he estado probando todo tipo de soluciones (ja, ja, ¿ves lo que hice allí?) Pero ninguna de ellas funcionó realmente para todos los datos que tenía. La función de drupal funcionó a las mil maravillas. Gracias por publicar.
Ben Hitchcock
esto funciona pero ordena los números al final (AZ luego 0-9)
realgt
4

Otra opción es hacer la clasificación en la memoria después de extraer los datos de mysql. Si bien no será la mejor opción desde el punto de vista del rendimiento, si no está ordenando listas enormes, debería estar bien.

Si echas un vistazo a la publicación de Jeff, puedes encontrar muchos algoritmos para cualquier idioma con el que estés trabajando. Clasificación para humanos: orden de clasificación natural

Beto
fuente
2

También puede crear de forma dinámica la "columna de clasificación":

SELECT name, (name = '-') boolDash, (name = '0') boolZero, (name+0 > 0) boolNum 
FROM table 
ORDER BY boolDash DESC, boolZero DESC, boolNum DESC, (name+0), name

De esa forma, puede crear grupos para ordenar.

En mi consulta, quería el '-' delante de todo, luego los números, luego el texto. Lo que podría resultar en algo como:

-
0    
1
2
3
4
5
10
13
19
99
102
Chair
Dog
Table
Windows

De esa manera, no es necesario que mantenga la columna de clasificación en el orden correcto a medida que agrega datos. También puede cambiar su orden de clasificación según lo que necesite.

Antonio
fuente
No sé qué tan eficaz sería esto. Lo estoy usando todo el tiempo sin ningún inconveniente. Aunque mi base de datos no es grande.
antoine
1

Si está usando PHP, puede hacer la ordenación natural en php.

$keys = array();
$values = array();
foreach ($results as $index => $row) {
   $key = $row['name'].'__'.$index; // Add the index to create an unique key.
   $keys[] = $key;
   $values[$key] = $row; 
}
natsort($keys);
$sortedValues = array(); 
foreach($keys as $index) {
  $sortedValues[] = $values[$index]; 
}

Espero que MySQL implemente la ordenación natural en una versión futura, pero la solicitud de función (# 1588) está abierta desde 2003, así que no aguantaría la respiración.

Bob Fanger
fuente
En teoría, eso es posible, pero primero tendría que leer todos los registros de la base de datos en mi servidor web.
BlaM
Alternativamente, considere: usort($mydata, function ($item1, $item2) { return strnatcmp($item1['key'], $item2['key']); });(Tengo una matriz asociativa y ordeno por clave). Ref: stackoverflow.com/q/12426825/1066234
Kai Noack
1

Una versión simplificada no udf de la mejor respuesta de @ plaix / Richard Toth / Luke Hoggett, que funciona solo para el primer número entero en el campo, es

SELECT name,
LEAST(
    IFNULL(NULLIF(LOCATE('0', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('1', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('2', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('3', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('4', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('5', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('6', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('7', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('8', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('9', name), 0), ~0)
) AS first_int
FROM table
ORDER BY IF(first_int = ~0, name, CONCAT(
    SUBSTR(name, 1, first_int - 1),
    LPAD(CAST(SUBSTR(name, first_int) AS UNSIGNED), LENGTH(~0), '0'),
    SUBSTR(name, first_int + LENGTH(CAST(SUBSTR(name, first_int) AS UNSIGNED)))
)) ASC
bonger
fuente
1

He probado varias soluciones pero en realidad es muy simple:

SELECT test_column FROM test_table ORDER BY LENGTH(test_column) DESC, test_column DESC

/* 
Result 
--------
value_1
value_2
value_3
value_4
value_5
value_6
value_7
value_8
value_9
value_10
value_11
value_12
value_13
value_14
value_15
...
*/
Tarik
fuente
1
Funciona muy bien para ordenar números en formato 23-4244. Gracias :)
Pyton
1
solo funciona con estos datos de prueba porque las cadenas antes del número son todas iguales. Intente mantener un valor z_99allí y se colocará en la parte superior, pero zvendrá después v.
Samuel Neff
@SamuelNeff, consulte SQL: ORDER BY LENGTH (test_column) DESC, test_column DESC, así que sí, porque primero ordenará por longitud de la columna. Esto funciona bien ordenando un grupo de prefijos de tabla que, de lo contrario, no podría ordenar solo con "test_column DESC"
Tarik
1

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);  ### 0 means process all numbers - resulting sort key might be quite long for certain inputs

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
/****
  Converts numbers in the input string s into a format such that sorting results in a nat-sort.
  Numbers of up to 359 digits (before the decimal point, if one is present) are supported.  Sort results are undefined if the input string contains numbers longer than this.
  For n>0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves).
  Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n.
  Numbers may contain ',' as a thousands separator, and '.' as a decimal point.  To reverse these (as appropriate for some European locales), the code would require modification.
  Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign.
  Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001).
  Numbers with leading zeros sort after the same number with no (or fewer) leading zeros.
  Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character.
  Numbers with thousand separators sort after the same number without them.
  Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly.
  (When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers).
  Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted.
  The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11)
  Note that The first number component in an entity like this is also permitted to contain thousand separators.

  To achieve this, numbers within the input string are prefixed and suffixed according to the following format:
  - The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros.  If there is a decimal point, this length only includes the integer part of the number.
  - A 3-character suffix is appended after the number (after the decimals if present).
    - The first character is a space, or a '+' sign if the number was preceded by '+'.  Any preceding '+' sign is also removed from the front of the number.
    - This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25)
    - The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise)
  - For version number sequences, each component number has the prefix in front of it, and the separating dots are removed.
    Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence.

  e.g. here is how some simple sample strings get converted:
  'Foo055' --> 'Foo0255 02'
  'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade'
  'The $1,000,000 prize' --> 'The $071000000 01 prize'
  '+99.74 degrees' --> '0299.74+00 degrees'
  'I have 0 apples' --> 'I have 00 02 apples'
  '.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08'
  'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004'

  The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal.
  The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix.
  A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789.
  However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length.

  The function definition does not specify the charset or collation to be used for string-type parameters or variables:  The default database charset & collation at the time the function is defined will be used.
  This is to make the function code more portable.  However, there are some important restrictions:

  - Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work.
    This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia).
    To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use.

  - For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters.
    This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4).
    If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH()

  Length of the output:

  Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert;
  This parameter is provided as a means to limit the maximum output length (to input length + 5*n).
  If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n.
  Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2))
  So for the current input length of 100, the maximum output length is 350.
  If changing the input length, the output length must be modified according to the above formula.  The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate.
****/
  DECLARE x,y varchar(100);            # need to be same length as input s
  DECLARE r varchar(350) DEFAULT '';   # return value:  needs to be same length as return type
  DECLARE suf varchar(101);   # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
  DECLARE i,j,k int UNSIGNED;
  IF n<=0 THEN SET n := -1; END IF;   # n<=0 means "process all numbers"
  LOOP
    SET i := REGEXP_INSTR(s,'\\d');   # find position of next digit
    IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF;   # no more numbers to process -> we're done
    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;   # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control"
      IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF;   # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together
      SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i);   # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s)
    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++)*+'));   # capture the number + following decimals (including multiple consecutive '.<digits>' sequences)
    SET s := SUBSTRING(s,LENGTH(x)+1);   # NOTE: LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8)
    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;   # move any following decimals into y
    SET i := LENGTH(x);
    SET x := REPLACE(x,',','');
    SET j := LENGTH(x);
    SET x := TRIM(LEADING '0' FROM x);   # strip leading zeros
    SET k := LENGTH(x);
    SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0'));   # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise)  Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters
    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);   # k = count of digits in number, bounded to be <= '9Z' base-36
    ELSE   # encode a version number (like 3.12.707, etc)
      SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);   # k = count of digits in number, bounded to be <= '9Z' base-36
      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);   # strip leading zeros
        SET k := LENGTH(x);
        SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);   # k = count of digits in number, bounded to be <= '9Z' base-36
        SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0'));   # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits
        SET n := n-1;
      END WHILE;
      SET r := CONCAT(r,y,suf);
    END IF;
  END LOOP;
END
$$
delimiter ;
Haciendo
fuente
Soy un principiante en MySQL y probé esto. Recibí este error: "# 1305 - FUNCIÓN mydatabase.REGEXP_INSTR no existe". ¿Alguna idea?
John T
Para cualquier otro novato. No tenía MySQL 8.0 instalado. Es necesario para REGEXP_INSTR (y otras cosas de REGEXP).
John T
Se corrigió un error grave en NatSortKey: había un carácter de expresión regular incorrecto. Si ha utilizado esta función usted mismo, actualice su código.
Hasta
0

Aquí hay uno simple si los títulos solo tienen la versión como un número:

ORDER BY CAST(REGEXP_REPLACE(title, "[a-zA-Z]+", "") AS INT)';

De lo contrario, puede usar SQL simple si usa un patrón (este patrón usa un # antes de la versión):

create table titles(title);

insert into titles (title) values 
('Final Fantasy'),
('Final Fantasy #03'),
('Final Fantasy #11'),
('Final Fantasy #10'),
('Final Fantasy #2'),
('Bond 007 ##2'),
('Final Fantasy #01'),
('Bond 007'),
('Final Fantasy #11}');

select REGEXP_REPLACE(title, "#([0-9]+)", "\\1") as title from titles
ORDER BY REGEXP_REPLACE(title, "#[0-9]+", ""),
CAST(REGEXP_REPLACE(title, ".*#([0-9]+).*", "\\1") AS INT);     
+-------------------+
| title             |
+-------------------+
| Bond 007          |
| Bond 007 #2       |
| Final Fantasy     |
| Final Fantasy 01  |
| Final Fantasy 2   |
| Final Fantasy 03  |
| Final Fantasy 10  |
| Final Fantasy 11  |
| Final Fantasy 11} |
+-------------------+
8 rows in set, 2 warnings (0.001 sec)

Puede utilizar otros patrones si es necesario. Por ejemplo, si tienes una película "Soy el n. ° 1" y "Soy el n. ° 1 parte 2", tal vez envuelvas la versión, por ejemplo, "Final Fantasy {11}"

Frank Forte
fuente
-4

Sé que este tema es antiguo, pero creo que encontré una manera de hacerlo:

SELECT * FROM `table` ORDER BY 
CONCAT(
  GREATEST(
    LOCATE('1', name),
    LOCATE('2', name),
    LOCATE('3', name),
    LOCATE('4', name),
    LOCATE('5', name),
    LOCATE('6', name),
    LOCATE('7', name),
    LOCATE('8', name),
    LOCATE('9', name)
   ),
   name
) ASC

Deseche eso, ordenó el siguiente conjunto incorrectamente (es inútil jajaja):

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7: Advent Children Final Fantasy 12 Final Fantasy 112 FF1 FF2

usuario1467716
fuente
3
¿Por qué no eliminas esta respuesta? obtendrás una insignia por esto
m47730