题
有没有一种优雅的方法可以在 MySQL 数据库中实现高性能、自然的排序?
例如,如果我有这个数据集:
- 最终幻想
- 最终幻想4
- 最终幻想10
- 最终幻想12
- 最终幻想12:普罗马西亚之链
- 最终幻想冒险
- 最终幻想起源
- 最终幻想战略版
任何其他 优雅的 解决方案不是将游戏名称拆分为各个组件
- 标题: :《最终幻想》
- 数字: "12"
- 字幕: :《普罗马西亚之链》
确保它们以正确的顺序出现?(10 在 4 之后,而不是在 2 之前)。
这样做是一件很痛苦的事情,因为时不时就会有另一个游戏打破了解析游戏标题的机制(例如《战锤 40,000》、《詹姆斯·邦德 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 等)执行排序。
让数据库为您进行排序几乎总是比将大型数据集读入您选择的编程语言并在那里进行排序要快,因此,如果您对此处的数据库模式有任何控制,那么请考虑添加如上所述,可以轻松排序字段,从长远来看,它将为您节省很多麻烦和维护工作。
有时会出现添加“自然排序”的请求 MySQL 错误 和 讨论论坛, ,许多解决方案都围绕着剥离数据的特定部分并将它们转换为 ORDER BY
查询的一部分,例如
SELECT * FROM table ORDER BY CAST(mid(name, 6, LENGTH(c) -5) AS unsigned)
这种解决方案几乎可以用于上面的《最终幻想》示例,但不是特别灵活,而且不太可能干净地扩展到包括“战锤 40,000”和“詹姆斯·邦德 007”在内的数据集。 。
我写这个函数是为了 SQL 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
因此,虽然我知道您已经找到了满意的答案,但我在这个问题上挣扎了一段时间,而且我们之前已经确定它无法在 SQL 中很好地完成,因此我们将不得不在 JSON 上使用 javascript大批。
以下是我仅使用 SQL 解决该问题的方法。希望这对其他人有帮助:
我有数据,例如:
Scene 1 Scene 1A Scene 1B Scene 2A Scene 3 ... Scene 101 Scene XXA1 Scene XXA2
我实际上并没有“投射”东西,尽管我认为这也可能有效。
我首先替换了数据中不变的部分,在本例中为“场景”,然后进行了 LPAD 来排列内容。这似乎可以很好地对字母字符串和编号字符串进行正确排序。
我的 ORDER BY
子句看起来像:
ORDER BY LPAD(REPLACE(`table`.`column`,'Scene ',''),10,'0')
显然,这对解决最初的问题没有帮助,因为原来的问题不太统一——但我想这可能适用于许多其他相关问题,所以把它放在那里。
在表中添加排序键(排名)。
ORDER BY rank
利用“发布日期”列。
ORDER BY release_date
从 SQL 中提取数据时,让您的对象进行排序,例如,如果提取到 Set 中,则将其设为 TreeSet,并使您的数据模型实现 Comparable 并在此处执行自然排序算法(如果您使用的是插入排序就足够了)一种没有集合的语言),因为在创建模型并将其插入到集合中时,您将逐一读取 SQL 中的行)
关于理查德·托特的最佳回应 https://stackoverflow.com/a/12257917/4052357
注意包含 2 字节(或更多)字符和数字的 UTF8 编码字符串,例如
12 南新宿
使用 MySQL 的 LENGTH()
在 udf_NaturalSortFormat
函数将返回字符串的字节长度并且不正确,而是使用 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
;;
附注我会将此添加为原始评论,但我还没有足够的声誉
订购:
0
1
2
10
23
101
205
1000
A
亚克
乙
卡萨萨萨
CSS
使用此查询:
SELECT column_name FROM table_name ORDER BY column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#\$\%\^\*\(\)\;\:\\,\?\/\~\`\|\_\-]' DESC, column_name + 0, column_name;
如果您不想重新发明轮子或对大量不起作用的代码感到头疼,只需使用 Drupal 自然排序 ...只需运行压缩的 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
我知道这个话题很古老,但我想我已经找到了一种方法来做到这一点:
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