Pregunta

¿Existe una manera elegante de tener una clasificación natural y con rendimiento en una base de datos MySQL?

Por ejemplo, si tengo este conjunto de datos:

  • Final Fantasy
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Chains of Promathia
  • Final Fantasy Adventure
  • Final Fantasy Origins
  • Final Fantasy Tactics

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

  • Título : " Final Fantasy "
  • Número : " 12 "
  • Subtitle : " Chains of Promathia "

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

Hacerlo es un dolor en la a ** 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")

¿Fue útil?

Solución

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 el " SortKey " ;. Esta podría ser una versión desinfectada del título que se ajuste a un patrón que creas para una clasificación fácil o un contador.

Otros consejos

Aquí hay una solución rápida:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric

Acabo de encontrar esto:

SELECT names FROM your_table ORDER BY games + 0 ASC

Hace una clasificación natural cuando los números están en la parte delantera, también podría funcionar en el medio.

La 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, ".")

MySQL no permite este tipo de " clasificación natural " ;, por lo que parece que la mejor manera de obtener lo que está buscando es dividir sus datos como se describe anteriormente (campo de identificación separado, etc.) ), o en su defecto, realice una ordenación basada en un elemento sin título, elemento indexado en su base de datos (fecha, identificador insertado en la base de datos, etc.).

Hacer que la db haga la clasificación por usted casi siempre será más rápido que leer los grandes conjuntos de datos en el lenguaje de programación que elija y ordenarlos allí, así que si tiene algún control sobre el esquema de db aquí, entonces Observe cómo agregar campos fáciles de clasificar como se describe anteriormente, le ahorrará muchos problemas y mantenimiento a largo plazo.

Solicita agregar un " ordenación natural " sube de vez en cuando en los errores de MySQL y foros de discusión , y muchas soluciones giran en torno a eliminar partes específicas de sus datos y su lanzamiento ellos para la parte ORDER BY de la consulta, por ejemplo,

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

Este tipo de solución podría casi funcionar para su ejemplo de Final Fantasy, pero no es particularmente flexible y es poco probable que se extienda limpiamente a un conjunto de datos que incluya, por ejemplo, "Warhammer 40,000" y " James Bond 007 " Tengo miedo.

He escrito 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

Entonces, aunque sé que ha encontrado una respuesta satisfactoria, estuve luchando con este problema por un tiempo, y previamente habíamos determinado que no podía hacerse razonablemente bien en SQL y que tendríamos que usar javascript en una matriz JSON.

Aquí es cómo lo resolví simplemente usando SQL. Esperemos que esto sea útil para otros:

Tenía datos como:

Scene 1
Scene 1A
Scene 1B
Scene 2A
Scene 3
...
Scene 101
Scene XXA1
Scene XXA2

En realidad no lo hice " emitido " Aunque supongo que eso también podría haber funcionado.

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

Mi cláusula ORDER BY 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 me imagino que esto probablemente funcionaría para muchos otros problemas relacionados, así que expóngalo.

  1. Agregue una clave de clasificación (clasificación) en su tabla. ORDENAR POR rango

  2. Utilice la " Fecha de publicación " columna. 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 clasificación natural aquí (clasificación por inserción será suficiente si está utilizando un idioma 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)

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

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

12 南新宿

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

En mi caso, el uso de LENGTH () hizo que las consultas nunca se completaran y resultaran en un 100% de uso de CPU 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
;;

p.s. Habría agregado esto como un comentario al original pero no tengo suficiente reputación (todavía)

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

Utilice esta consulta:

SELECT 
    column_name 
FROM 
    table_name 
ORDER BY
    column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#\$\%\^\*\(\)\;\:\\,\?\/\~\`\|\_\-]' DESC, 
    column_name + 0, 
    column_name;

Si no desea reinventar la rueda o tiene un dolor de cabeza con una gran cantidad de código que no funciona, simplemente use Drupal Natural Sort ... Simplemente ejecute el SQL que viene comprimido (MySQL o Postgre), y eso es todo. Al hacer una consulta, simplemente ordene usando:

... ORDER BY natsort_canon(column_name, 'natural')

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á clasificando 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 de humanos: orden natural

Agregar un campo para " clave de ordenación " que tiene todas las cadenas de dígitos rellenadas con cero a una longitud fija y, a continuación, ordena en ese campo.

Si puede tener cadenas largas de dígitos, otro método es anteponer la cantidad de dígitos (ancho fijo, rellenado con cero) 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 " ;.

También puede crear de forma dinámica la " columna de ordenació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 manera, puedes 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 tiene que mantener 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.

He intentado 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
...
*/

Si está utilizando PHP, puede hacer el ordenamiento 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 una clasificación natural en una versión futura, pero la solicitud de función (# 1588) está abierto desde 2003, así que no contendría la respiración.

Una versión simplificada no udf de la mejor respuesta de @ plaix / Richard Toth / Luke Hoggett, que funciona solo para el primer 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

También hay natsort . Está pensado para ser parte de un plugin de drupal , pero funciona bien de forma independiente.

Sé que este tema es antiguo pero creo que he encontrado una manera de hacer esto:

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

Deshazte de eso, ordenó incorrectamente el siguiente conjunto (es inútil lol):

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top