MySQLのナチュラルソート
-
03-07-2019 - |
質問
MySQLデータベースでパフォーマンスの高い自然なソートを行うエレガントな方法はありますか?
たとえば、このデータセットがある場合:
- ファイナルファンタジー
- ファイナルファンタジー4
- ファイナルファンタジー10
- ファイナルファンタジー12
- ファイナルファンタジー12:プロマシアの鎖
- ファイナルファンタジーアドベンチャー
- ファイナルファンタジーの起源
- ファイナルファンタジータクティクス
ゲームの名前をコンポーネントに分割する以外のエレガントのソリューション
- タイトル:"ファイナルファンタジー"
- 数字:" 12"
- 字幕:" Chains of Promathia"
正しい順序で出てくることを確認するには? (2の前ではなく4の後の10)。
これはa **の痛みです。ゲームタイトルを解析するメカニズムを壊す別のゲームが時々あるためです(例:「Warhammer 40,000」、「James Bond 007」)
解決
だから、多くのことがリリース日でソートされているのだと思う。
解決策として、「SortKey」用にテーブルに別の列を作成することができます。これは、簡単に並べ替えたりカウンターを作成したりするために作成したパターンに適合する、サニタイズされたバージョンのタイトルである可能性があります。
他のヒント
簡単な解決策は次のとおりです。
SELECT alphanumeric,
integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
これを見つけました:
SELECT names FROM your_table ORDER BY games + 0 ASC
数字が先頭にあるときに自然な並べ替えを行います。中間にも機能する可能性があります。
@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, ".")
MySQLではこのような「自然な並べ替え」が許可されていないため、目的のものを取得する最善の方法は、上記のようにデータセットを分割することです(個別のidフィールドなど)、または失敗した場合、タイトル以外の要素、データベース内のインデックス付き要素(日付、データベース内の挿入されたIDなど)に基づいてソートを実行します。
dbにソートを行わせると、ほとんどの場合、選択したプログラミング言語に大きなデータセットを読み込んでそこでソートするよりも速くなるため、ここでdbスキーマを制御する場合は、上記のように簡単にソートできるフィールドを追加すると、長い目で見れば手間とメンテナンスを大幅に節約できます。
「自然な並べ替え」を追加するリクエスト MySQLバグおよびディスカッションフォーラム、およびデータの特定の部分を削除してキャストすることを中心とした多くのソリューションそれらは、クエリの ORDER BY
部分に使用します。たとえば、
SELECT * FROM table ORDER BY CAST(mid(name, 6, LENGTH(c) -5) AS unsigned)
この種のソリューションは、上記のファイナルファンタジーの例で動作するように作られていますが、特に柔軟性がなく、「Warhammer 40,000」などのデータセットに完全に拡張される可能性は低いです。および「James Bond 007」怖いです。
MSSQL 2000 用にこの関数を記述しました:
/**
* 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
だから、あなたが満足のいく答えを見つけたことは知っていますが、私はしばらくこの問題に苦労していました。 JSON配列。
これは、SQLを使用して解決した方法です。これが他の人にも役立つことを願っています:
次のようなデータがありました:
Scene 1 Scene 1A Scene 1B Scene 2A Scene 3 ... Scene 101 Scene XXA1 Scene XXA2
実際には「キャスト」しませんでした。物事は私もそれが働いたかもしれないと思うが。
最初にデータで変更されていない部分、この場合は&quot; Scene&quot;を交換し、次にLPADを行って物事を並べました。これにより、アルファ文字列と番号付き文字列を適切に並べ替えることができます。
私の ORDER BY
句は次のようになります:
ORDER BY LPAD(REPLACE(`table`.`column`,'Scene ',''),10,'0')
明らかにこれは、それほど均一ではなかった元の問題には役立ちませんが、他の多くの関連する問題でおそらく機能すると思われるため、そこに配置します。
-
テーブルにソートキー(ランク)を追加します。
ORDER BYランク
-
「リリース日」を活用するカラム。
ORDER BY release_date
-
SQLからデータを抽出するとき、オブジェクトにソートを実行させます。たとえば、Setに抽出する場合、TreeSetにし、データモデルにComparableを実装させ、ここで自然なソートアルゴリズムを実行させます(挿入ソートコレクションを持たない言語を使用している場合は十分です)モデルを作成してコレクションに挿入するときにSQLから行を1つずつ読み取るため)
リチャード・トスからの最高の回答について 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
;;
p.s。元のコメントとしてこれを追加しますが、評判はまだありません(まだ)
注文するには:
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;
車輪を再発明したくない、または機能しない多くのコードで頭痛がしたくない場合は、単に Drupal Natural Sort ...圧縮されたSQL(MySQLまたはPostgre)を実行するだけです。クエリを作成するときは、次を使用して注文します。
... ORDER BY natsort_canon(column_name, 'natural')
別のオプションは、mysqlからデータをプルした後、メモリ内でソートを行うことです。パフォーマンスの観点からは最適なオプションではありませんが、巨大なリストを並べ替えるのでなければ大丈夫です。
Jeffの投稿を見ると、使用している言語に対応したアルゴリズムがたくさん見つかります。 人間向けのソート:自然なソート順
「ソートキー」のフィールドを追加しますすべての数字の文字列が固定長になるまでゼロ詰めされ、代わりにそのフィールドでソートされます。
長い数字列がある場合、別の方法として、各数字列に数字の数(固定幅、ゼロ詰め)を追加します。たとえば、連続して99桁を超えない場合、「Super Blast 10 Ultra」の場合、ソートキーは「Super Blast 0210 Ultra」となります。
「並べ替え列」を動的に作成することもできます。 :
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
この方法では、データを追加するときにソート列を正しい順序に維持する必要はありません。必要に応じてソート順を変更することもできます。
いくつかの解決策を試しましたが、実際には非常に簡単です:
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
...
*/
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が将来のバージョンで自然なソートを実装することを願っていますが、機能リクエスト(# 1588)は2003年以来営業しているため、息を止めません。
@ 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
natsort もあります。 drupalプラグインの一部となることを意図していますが、スタンドアロンで正常に動作します。
このトピックは古代ですが、これを行う方法を見つけたと思います:
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
それをスクラップ、それは次のセットを間違ってソートしました(それは役に立たないです):
ファイナルファンタジー1 ファイナルファンタジー2 ファイナルファンタジー5 ファイナルファンタジー7 ファイナルファンタジー7:アドベントチルドレン ファイナルファンタジー12 ファイナルファンタジー112 FF1 FF2