web-dev-qa-db-ja.com

MySQLの自然な並べ替え

MySQLデータベースでパフォーマンスの高い自然なソートを行うエレガントな方法はありますか?

たとえば、このデータセットがある場合:

  • ファイナルファンタジー
  • ファイナルファンタジー4
  • ファイナルファンタジー10
  • ファイナルファンタジー12
  • ファイナルファンタジー12:プロマシアの鎖
  • ファイナルファンタジーアドベンチャー
  • ファイナルファンタジーの起源
  • ファイナルファンタジータクティクス

ゲームの名前をコンポーネントに分割する以外のエレガントソリューション

  • タイトル:「ファイナルファンタジー」
  • 番号: "12"
  • 字幕:「プロマシアの鎖」

彼らが正しい順序で出てくることを確認するには? (2の前ではなく、4の後の10)。

ゲームタイトルを解析するメカニズムを壊す別のゲームが時々あるので、そうすることはa **の痛みです(例:「Warhammer 40,000」、「James Bond 007」)

73
BlaM

これが多くのことがリリース日でソートされている理由だと思います。

解決策は、「SortKey」用にテーブルに別の列を作成することです。これは、簡単な並べ替えまたはカウンター用に作成したパターンに適合する、タイトルのサニタイズバージョンである可能性があります。

22
Michael Haren

簡単な解決策を次に示します。

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

ちょうどこれを見つけました:

SELECT names FROM your_table ORDER BY games + 0 ASC

数字が先頭にあるときに自然な並べ替えを行いますが、中間にも機能する場合があります。

53
markletp

@plalxによって投稿されたものと同じ関数ですが、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
;;

使用法:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")
47
Richard Toth

MySQLはこの種の「自然な並べ替え」を許可していないため、目的のものを取得する最良の方法は、上記で説明したようにデータセットを分割することです(個別のidフィールドなど)。つまり、タイトル以外の要素、データベース内のインデックス付き要素(日付、データベース内に挿入されたIDなど)に基づいてソートを実行します。

データベースにソートを行わせることは、ほとんどの場合、選択したプログラミング言語に大きなデータセットを読み込んでそこでソートするよりも速くなるので、ここでdbスキーマを制御する場合は、追加を見てください上記のようにフィールドを簡単に並べ替えることで、長い目で見れば手間とメンテナンスを大幅に節約できます。

「自然な並べ替え」を追加するリクエストは、 MySQLバグ および ディスカッションフォーラム で時々発生し、多くのソリューションはデータの特定の部分を削除してキャストすることを中心に展開します。 ORDER BYクエリの一部、例えば.

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

この種のソリューションは、上記のファイナルファンタジーの例で動作するように作られていますが、特に柔軟性がなく、「Warhammer 40,000」や「James Bond 007」などのデータセットに完全に拡張される可能性は低いと思います。

15
ConroyP

MSSQL 20の前にこの関数を書きました:

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

あなたが満足のいく答えを見つけたことは知っていますが、私はしばらくこの問題に苦労していました、そして私たちは以前はSQLで合理的にうまくできないと判断し、JSONでjavascriptを使用する必要がありましたアレイ。

以下は、SQLを使用して解決した方法です。うまくいけば、これは他の人に役立つ:

次のようなデータがありました。

シーン1 
シーン1A 
シーン1B 
シーン2A 
シーン3 
 ... 
シーン101 
シーンXXA1 
シーンXXA2 

私は実際に物を「キャスト」しませんでしたが、それもうまくいったかもしれません。

最初にデータで変更されていない部分(この場合は「Scene」)を交換し、次にLPADを実行して整列させました。これにより、アルファ文字列と番号付き文字列を適切に並べ替えることができます。

僕の ORDER BY句は次のようになります。

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

明らかに、これはそれほど均一ではなかった元の問題には役立ちません-しかし、これはおそらく他の多くの関連する問題でうまくいくと思いますので、そこに置いてください。

9
FilmJ
  1. テーブルにソートキー(ランク)を追加します。 ORDER BY rank

  2. [リリース日]列を活用します。 ORDER BY release_date

  3. SQLからデータを抽出するとき、たとえば、Setに抽出する場合はオブジェクトをソートさせ、TreeSetを作成し、データモデルにComparableを実装させ、ここで自然なソートアルゴリズムを実行させます(使用する場合は挿入ソートで十分です)コレクションのない言語)モデルを作成してコレクションに挿入するときにSQLから行を1つずつ読み取るため)

5
JeeBee

リチャード・トスからの最高の回答について https://stackoverflow.com/a/12257917/4052357

2バイト(またはそれ以上)の文字と数字を含むUTF8エンコード文字列に注意してください。

_12 南新宿
_

_udf_NaturalSortFormat_関数でMySQLのLENGTH()を使用すると、文字列のバイト長が返され、正しくなくなります。代わりに、正しい文字長を返すCHAR_LENGTH()を使用します。

私の場合、LENGTH()を使用すると、クエリが完了せず、MySQLのCPU使用率が100%になります。

_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
;;
_

追伸元のコメントとしてこれを追加しますが、評判はまだありません(まだ)

5
Luke Hoggett

別のオプションは、mysqlからデータをプルした後、メモリ内でソートを行うことです。パフォーマンスの観点からは最適なオプションではありませんが、巨大なリストを並べ替えるのでなければ大丈夫です。

Jeffの投稿を見ると、使用している言語に対応したアルゴリズムがたくさん見つかります。 人間の並べ替え:自然なソート順

4
Bob

注文する:
0
1
2
10
23
101
205
1000
a
aac
b
casdsadsa
css

このクエリを使用:

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

車輪を再発明したくない、または機能しない多くのコードで頭痛がしたくない場合は、単に Drupal Natural Sort ...を使用します。圧縮されたSQL(MySQLまたはPostgre)を実行します。以上です。クエリを作成するときは、次を使用して注文するだけです。

... ORDER BY natsort_Canon(column_name, 'natural')
4
Neto Queiroz

動的な方法で「ソート列」を作成することもできます。

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

そのようにして、ソートするグループを作成できます。

私のクエリでは、すべての前に「-」、次に数字、次にテキストが必要でした。次のような結果になる可能性があります:

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

そうすれば、データを追加するときにソート列を正しい順序に維持する必要がありません。必要に応じてソート順を変更することもできます。

3
antoine

すべての数字の文字列が固定長になるまでゼロ詰めされた「ソートキー」のフィールドを追加し、代わりにそのフィールドでソートします。

長い数字列がある場合は、別の方法として、各数字列に数字の数(固定幅、ゼロ詰め)を追加します。たとえば、99桁を超えない場合、「Super Blast 10 Ultra」の場合、ソートキーは「Super Blast 0210 Ultra」になります。

3
tye

私はいくつかの解決策を試しましたが、実際には非常に簡単です:

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

@ plaix/Richard Toth/Luke Hoggettの最適な応答の簡易非UDFバージョンは、フィールドの最初の整数に対してのみ機能します。

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
1
bonger

PHPを使用している場合、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]; 
}

MySQLが将来のバージョンで自然な並べ替えを実装することを願っていますが、2003年以来 機能要求(#1588) が公開されているため、息を止めません。

1
Bob Fanger

natsort もあります。 drupalプラグイン の一部となることを目的としていますが、スタンドアロンで正常に動作します。

0
Peter V. Mørch