是否有一种优雅的方法可以在MySQL数据库中进行高效的自然排序?
例如,如果我有这个数据集:
任何其他优雅解决方案,而不是将游戏名称拆分为其组件
确保它们以正确的顺序出现? (4之后,而不是2之前)。
这样做是一个痛苦的**因为时不时有另一个游戏打破了解析游戏标题的机制(例如“Warhammer 40,000”,“James Bond 007”)
答案 0 :(得分:84)
这是一个快速解决方案:
SELECT alphanumeric,
integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
答案 1 :(得分:52)
刚刚发现:
SELECT names FROM your_table ORDER BY games + 0 ASC
当数字位于前面时,自然排序也适用于中间。
答案 2 :(得分:45)
与@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, ".")
答案 3 :(得分:22)
我认为这就是许多事情按发布日期排序的原因。
解决方案可能是在表中为“SortKey”创建另一列。这可能是标题的清理版本,符合您为便于排序或计数器而创建的模式。
答案 4 :(得分:15)
MySQL不允许这种“自然排序”,因此看起来最好的方法就是如上所述分割数据集(单独的id字段等) ,或者失败,根据非标题元素,数据库中的索引元素(日期,数据库中的插入ID等)执行排序。
让db为你做排序几乎总是比将大数据集读入你选择的编程语言并在那里排序更快,所以如果你在这里对db模式有任何控制,那么看看如上所述添加易于排序的字段,从长远来看,它将为您节省大量的麻烦和维护。
MySQL bugs和discussion forums上不时会出现添加“自然排序”的请求,许多解决方案围绕剥离数据的特定部分并将其投放到{{{} 1}}查询的一部分,例如
ORDER BY
这种解决方案几乎可以用于上面的“最终幻想”示例,但不是特别灵活,也不太可能完全扩展到数据集,包括“Warhammer 40,000”和“James Bond 007”I害怕。
答案 5 :(得分:15)
我刚才为 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
答案 6 :(得分:9)
所以,虽然我知道你已经找到了一个令人满意的答案,但我一直在努力解决这个问题,我们之前已经确定它在SQL中不能很好地完成,我们将不得不使用javascript在JSON数组上。
以下是我使用SQL解决它的方法。希望这对其他人有帮助:
我有以下数据:
Scene 1 Scene 1A Scene 1B Scene 2A Scene 3 ... Scene 101 Scene XXA1 Scene XXA2
我实际上并没有“投”一些东西,虽然我认为这也可能有用。
我首先替换了数据中不变的部分,在本例中为“Scene”,然后执行了一个LPAD来排序。这似乎允许alpha字符串正确排序以及编号的字符串。
我的ORDER BY
子句如下:
ORDER BY LPAD(REPLACE(`table`.`column`,'Scene ',''),10,'0')
显然这对原始问题没有帮助,但原始问题并不是那么统一 - 但我想这可能适用于许多其他相关问题,所以把它放在那里。
答案 7 :(得分:5)
在表格中添加排序键(排名)。 ORDER BY rank
使用“发布日期”列。 ORDER BY release_date
从SQL中提取数据时,让对象进行排序,例如,如果提取到Set中,使其成为TreeSet,并使您的数据模型实现Comparable并在此处制定自然排序算法(插入排序)如果您使用的是没有集合的语言就足够了)因为您在创建模型时将逐个从SQL中读取行并将其插入集合中。
答案 8 :(得分:5)
关于Richard Toth的最佳回应https://stackoverflow.com/a/12257917/4052357
注意包含2字节(或更多)字符和数字的UTF8编码字符串,例如
12 南新宿
在LENGTH()
函数中使用MySQL udf_NaturalSortFormat
将返回字符串的字节长度并且不正确,而是使用CHAR_LENGTH()
将返回正确的字符长度。
在我的情况下使用LENGTH()
导致查询永远不会完成并导致MySQL的100%CPU使用率
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。我会将此作为评论添加到原文中但我没有足够的声誉(还)
答案 9 :(得分:4)
另一个选择是从mysql中提取数据后在内存中进行排序。虽然从性能的角度来看它不是最佳选择,但如果你没有对大型列表进行排序,你应该没问题。
如果你看一下Jeff的帖子,你可以找到很多算法来解决你可能使用的语言。 Sorting for Humans : Natural Sort Order
答案 10 :(得分:4)
订购:
0
1
2
10个
23个
101个
205个
1000
一个
AAC
b
casdsadsa
CSS
使用此查询:
SELECT column_name FROM table_name ORDER BY column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#\$\%\^\*\(\)\;\:\\,\?\/\~\`\|\_\-]' DESC, column_name + 0, column_name;
答案 11 :(得分:4)
如果您不想重新发明轮子或者让大量代码无法解决问题,请使用Drupal Natural Sort ...只需运行压缩后的SQL(MySQL或Postgre),以及#39; s。在进行查询时,只需使用以下命令:
... ORDER BY natsort_canon(column_name, 'natural')
答案 12 :(得分:3)
为“排序键”添加一个字段,该字段将所有数字字符串零填充为固定长度,然后对该字段进行排序。
如果您可能有长数字串,另一种方法是在每个数字串前加上数字位数(固定宽度,零填充)。例如,如果连续不超过99位数,那么对于“Super Blast 10 Ultra”,排序键将是“Super Blast 0210 Ultra”。
答案 13 :(得分:3)
您还可以动态创建“排序列”:
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
这样,您无需在添加数据时以正确的顺序维护排序列。您还可以根据需要更改排序顺序。
答案 14 :(得分:3)
我尝试了几种解决方案但实际上非常简单:
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
...
*/
答案 15 :(得分:1)
如果你正在使用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能在未来的版本中实现自然排序,但feature request (#1588)自2003年开始开放,所以我不会屏住呼吸。
答案 16 :(得分:1)
我在这里(以及重复出现的问题)看到的许多其他答案基本上仅适用于非常特殊格式的数据,例如一个完全是数字的字符串,或者具有固定长度的字母前缀的字符串。在一般情况下,这是行不通的。
确实没有任何方法可以在MySQL中实现100%通用的nat排序,因为要做到这一点,您真正需要的是修改后的<比较>比较功能 ,当遇到数字时,可在字符串的字典排序排序和数字排序之间切换。这样的代码可以实现您希望用于识别和比较两个字符串中的数字部分的任何算法。不过,不幸的是,MySQL中的比较功能是其代码的内部功能,用户无法更改。
这留下了某种骇客,您尝试在其中为字符串创建 sort键,在其中重新格式化数字部分,以使标准词典分类实际上对它们进行排序您想要的方式。
对于不超过最大位数的纯整数,显而易见的解决方案是简单地将其左填充零,以使它们均为固定宽度。这是Drupal插件采用的方法,也是@plalx / @RichardToth的解决方案。 (@Christian有一个不同且复杂得多的解决方案,但我看不到任何优势)。
正如@tye指出的那样,您可以通过在每个数字前添加固定数字长度而不是简单地将其左填充来改进此功能。即使有本质上令人尴尬的hack的局限性,您仍然可以做很多改进。但是,似乎没有任何预先构建的解决方案!
例如,关于:
在@tye方法的基础上,我创建了一个相当紧凑的NatSortKey()存储函数,该函数将任意字符串转换为nat-sort键,并且可以处理上述所有情况,相当有效,并且保留了总排序顺序(没有两个不同的字符串具有比较相等的排序键)。第二个参数可用于限制每个字符串中处理的数字数量(例如,限制为前10个数字),这可用于确保输出适合给定长度。
注意:使用此第二个参数的给定值生成的排序键字符串应仅与使用该参数的 same 值生成的其他字符串进行排序,否则它们可能不会排序正确!
您可以直接在订购中使用它,例如
SELECT myString FROM myTable ORDER BY NatSortKey(myString,0); ### 0 means process all numbers - resulting sort key might be quite long for certain inputs
但是为了有效地对大型表进行排序,最好将排序键预先存储在另一列中(可能带有索引):
INSERT INTO myTable (myString,myStringNSK) VALUES (@theStringValue,NatSortKey(@theStringValue,10)), ...
...
SELECT myString FROM myTable ORDER BY myStringNSK;
[理想情况下,您可以通过以下方式将键列创建为计算所得的存储列来自动进行此操作:
CREATE TABLE myTable (
...
myString varchar(100),
myStringNSK varchar(150) AS (NatSortKey(myString,10)) STORED,
...
KEY (myStringNSK),
...);
但是目前 MySQL和MariaDB都不允许在计算列中存储函数,因此很遗憾,您还不能这样做。] < / p>
我的功能仅影响数字排序。如果您要进行其他排序规范化操作,例如删除所有标点符号,修剪两端的空白或使用单个空格替换多个空白序列,则可以扩展该功能,也可以在{之前或之后进行{1}}应用于您的数据。 (我建议为此目的使用NatSortKey()
。
在某种程度上,它也以英语为中心,我认为是“。”对于小数点,“,”表示千位分隔符,但是如果您想要反向,或者希望将其作为参数进行切换,应该足够容易地进行修改。
可以通过其他方式进一步改进;例如,它当前按绝对值对负数进行排序,因此-1位于-2之前,而不是相反。也没有办法为数字指定DESC排序顺序,同时保留文本的ASC字典顺序。这两个问题可以通过更多的工作来解决。如果有时间,我会更新代码。
还有很多其他细节需要注意-包括对您正在使用的字符集和排序规则的一些关键依赖性-但我已经将它们全部放入SQL的注释块中码。 在自己使用该功能之前,请仔细阅读!
所以,这是代码。如果您发现错误,或者有我未提及的改进,请在评论中让我知道!
REGEXP_REPLACE()
答案 17 :(得分:1)
@ 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
答案 18 :(得分:0)
如果标题的版本号为数字,这是一个简单的例子:
ORDER BY CAST(REGEXP_REPLACE(title, "[a-zA-Z]+", "") AS INT)';
否则,如果使用模式(此模式在版本之前使用#),则可以使用简单的SQL:
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)
如果需要,您可以使用其他模式。 例如,如果您有一部电影“我是#1”和“我是#1第2部分”,则可以包装该版本,例如“最终幻想{11}”
答案 19 :(得分:0)
还有natsort。它旨在成为drupal plugin的一部分,但它可以很好地独立工作。
答案 20 :(得分:-4)
我知道这个话题很古老,但我想我找到了一种方法:
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
报废,它对下面的集合进行了错误的排序(这是无用的lol):
最终幻想1 最终幻想2 最终幻想5 最终幻想7 最终幻想7:降临儿童 最终幻想12 最终幻想112 FF1 FF2