Pergunta

Existe uma maneira elegante de ter performance de classificação, natural em um banco de dados MySQL?

Por exemplo, se eu tenho esse conjunto de dados:

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

Qualquer outra elegante solução de dividir os nomes dos jogos em seus componentes

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

para se certificar de que eles saem na ordem certa? (10 ap 4, não antes de 2).

Fazer isso é uma dor no a ** porque a cada momento e, em seguida, há um outro jogo que quebra esse mecanismo de analisar o título do jogo (por exemplo, "Warhammer 40,000", "James Bond 007")

Foi útil?

Solução

Eu acho que é por isso que um monte de coisas são classificadas segundo a data de lançamento.

Uma solução poderia ser a criação de uma outra coluna na tabela para o "SortKey". Esta poderia ser uma versão higienizada do título que está em conformidade com um padrão que você criar a fácil classificação ou um contador.

Outras dicas

Aqui está uma solução rápida:

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

Apenas achei isso:

SELECT names FROM your_table ORDER BY games + 0 ASC

Será uma espécie natural, quando os números estão na frente, o trabalho poder para meio também.

A mesma função postado por @plalx, ??mas reescrito para 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 não permite esse tipo de "ordenação natural", então parece que a melhor maneira de conseguir o que você está depois é dividir definir seus dados como você descreveu acima (campo id separado, etc) ou, caso contrário, execute uma base tipo em um elemento não-título, elemento indexado em sua db (data, id inserido no db, etc).

Ter a db fazer a triagem para você é quase sempre vai ser mais rápido do que a leitura de grandes conjuntos de dados em sua linguagem de programação de escolha e classificando-o lá, por isso, se você tem qualquer controle em todo o esquema db aqui, então olhar para a adição de campos facilmente classificadas como descrito acima, que vai lhe poupar um monte de problemas e manutenção no longo prazo.

Os pedidos para adicionar um "tipo natural" surgem de tempos em tempos nos MySQL erros e discussão fóruns , e muitas soluções giram em torno eliminasse partes específicas de seus dados e lançando-os para a parte ORDER BY da consulta, por exemplo,

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

Este tipo de solução pode praticamente ser feito para trabalhar em seu exemplo Final Fantasy acima, mas não é particularmente flexível e improvável para estender limpa para um conjunto de dados, incluindo, por exemplo, "Warhammer 40,000" e "James Bond 007" Eu 'estou com medo.

Eu escrevi essa função para MSSQL de 2000 há um tempo atrás:

/**
 * 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

Assim, enquanto eu sei que você encontrou uma resposta satisfatória, eu estava lutando com este problema por algum tempo, e tínhamos anteriormente determinado que não poderia ser feito razoavelmente bem em SQL e íamos ter que usar javascript em uma matriz JSON.

Aqui está como eu resolvido apenas usando SQL. Esperemos que este é útil para os outros:

Eu tive de dados, tais como:

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

Na verdade, eu não coisas "elenco" embora eu suponho que também pode ter trabalhado.

A primeira vez que substituiu as partes que eram imutáveis ??nos dados, neste caso "Scene", e depois fez um LPAD a linha de coisas. Este parece permitir muito bem para as cordas alfa para classificar corretamente, bem como os que foram contados.

As minhas cláusula ORDER BY parece com:

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

Obviamente, isso não ajuda com o problema original, que não era tão uniforme -. Mas eu imagino que isso provavelmente funcionaria para muitos outros problemas relacionados, então colocá-lo lá fora

  1. Adicionar um Sort Key (classificação) em sua tabela. ORDER BY rank

  2. Utilize a coluna "Data de Lançamento". ORDER BY release_date

  3. Ao extrair os dados do SQL, faça o seu objecto fazer a triagem, por exemplo, se extrair em um Set, torná-lo um TreeSet, e fazer o seu modelo de dados implementar Comparable e promulgar o algoritmo de ordenação natural aqui (tipo de inserção será suficiente se você estiver usando uma linguagem sem cobranças) como você vai estar lendo as linhas de SQL, um por um, como você criar o seu modelo e inseri-lo na coleção)

No que diz respeito a melhor resposta de Richard Toth https://stackoverflow.com/a/12257917/4052357

Watch para fora para UTF8 codificado seqüências que contêm 2byte (ou mais) caracteres e números v.g..

12 南新宿

Usando LENGTH() do MySQL em função udf_NaturalSortFormat retornará o comprimento do byte da corda e estar incorrecta, em vez usar CHAR_LENGTH() que irá retornar o comprimento de caracteres correto.

No meu caso usando LENGTH() causada consultas para nunca mais completo e resultado em 100% da 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. Eu teria adicionou-a como um comentário ao original, mas eu não tenho reputação suficiente (ainda)

Para ordem:
0
1 | 2
10
23
101
205
1000
um
aac
b
casdsadsa
css

Use esta consulta:

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

Se você não quer reinventar a roda ou ter uma dor de cabeça com grande quantidade de código que não funciona, basta usar Drupal Natural Sort ... Basta executar o SQL que vem zipado (MySQL ou Postgre), e é isso. Ao fazer uma consulta, simplesmente pedir usando:

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

Outra opção é fazer a triagem na memória depois de puxar os dados do mysql. Enquanto isso não vai ser a melhor opção do ponto de vista do desempenho, se você não está classificando listas enormes você deve ser fino.

Se você der uma olhada post de Jeff, você pode encontrar muitos dos algoritmos para o que nunca idioma que você pode estar trabalhando com. classificação para seres humanos: Natural Ordenação

Adicionar um campo para "espécie chave" que tem todas as cadeias de dígitos com zeros à esquerda para um comprimento fixo e, em seguida, tipo nesse campo em seu lugar.

Se você pode ter longas seqüências de dígitos, outro método é para preceder o número de dígitos (de largura fixa, com zeros à esquerda) para cada seqüência de dígitos. Por exemplo, se você não terá mais de 99 dígitos em uma linha, em seguida, para "Super Explosão 10 Ultra" a chave de classificação seria "Super Explosão 0210 Ultra".

Você também pode criar de uma forma dinâmica a "coluna de classificação":

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

Dessa forma, você pode criar grupos para classificar.

Na minha consulta, eu queria que o '-' na frente de tudo, então os números, então o texto. Que poderia resultar em algo como:

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

Dessa forma, você não tem que manter a coluna de classificação na ordem correta como você adicionar dados. Você também pode mudar a sua ordem de classificação dependendo do que você precisa.

Eu tentei várias soluções, mas a verdade é muito simples:

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

Se você estiver usando PHP você pode fazer o tipo natural em 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 MySQL irá implementar ordenação natural em uma versão futura, mas o href="http://bugs.mysql.com/bug.php?id=1588" rel="nofollow"> pedido de recurso está aberto desde 2003, então eu não iria segurar minha respiração.

A simplificado versão não UDF da melhor resposta de @ plaix / Richard Toth / Luke Hoggett, que funciona apenas para o primeiro inteiro no campo, é

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

Também há natsort . Destina-se a ser uma parte de um drupal plug-in , mas ele funciona bem independente.

Eu sei que este tema é antigo, mas eu acho que eu encontrei uma maneira de fazer isso:

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

Scrap isso, ele classificados o seguinte conjunto incorretamente (É lol inútil):

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

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top