我在这里(以及重复出现的问题)中看到的许多其他答案基本上仅适用于格式非常特殊的数据,例如,字符串完全是数字,或具有固定长度的字母前缀。在一般情况下,这是行不通的。
的确,在MySQL中实际上没有任何方法可以实现100%通用的nat-sort,因为要做到这一点,您真正需要的是一个经过修改的比较函数,当遇到时,该函数可以在字符串的字典排序排序和数字排序之间切换一个号码。这样的代码可以实现您希望用于识别和比较两个字符串中的数字部分的任何算法。但是,不幸的是,MySQL中的比较功能是其代码的内部功能,用户无法更改。
这留下了某种骇客,您尝试在其中为字符串创建一个排序键,在该键中重新格式化数字部分,以便标准词典编目排序实际上按照您想要的方式对其进行排序。
对于最大位数不超过0的纯整数,显而易见的解决方案是简单地用零左键填充它们,以使它们均为固定宽度。这是Drupal插件采用的方法,也是@plalx / @RichardToth的解决方案。(@Christian有一个不同且复杂得多的解决方案,但我看不到任何优势)。
正如@tye指出的那样,您可以通过在每个数字前添加固定数字长度而不是简单地将其左填充来改进此功能。尽管有本质上笨拙的hack的局限性,但是您还有很多可以改进的地方。但是,似乎没有任何预先构建的解决方案!
例如,关于:
- 加号和减号?+10 vs 10 vs -10
- 小数点?8.2、8.5、1.006,.75
- 前导零?020、030、00000922
- 千位分隔符?“ 1,001达姆斯”和“ 1001达姆斯”
- 版本号?MariaDB v10.3.18和MariaDB v10.3.3
- 数字很长?103,768,276,592,092,364,859,236,487,687,870,234,598.55
扩展@tye的方法,我创建了一个相当紧凑的NatSortKey()存储函数,该函数将任意字符串转换为nat-sort键,并且可以处理上述所有情况,效率相当高,并且可以保留总排序-顺序(没有两个不同的字符串具有比较相等的排序键)。第二个参数可用于限制每个字符串中处理的数字数量(例如,限制为前10个数字),这可用于确保输出适合给定长度。
注意:使用此第二个参数的给定值生成的排序键字符串只能与使用相同参数值生成的其他字符串进行排序,否则它们可能无法正确排序!
您可以直接在订购中使用它,例如
SELECT myString FROM myTable ORDER BY NatSortKey(myString,0);
但是为了有效地对大型表进行排序,最好将排序键预先存储在另一列中(可能带有索引):
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都不允许在计算列中存储函数,因此很遗憾,您还不能这样做。]
我的功能仅影响数字排序。如果要执行其他排序规范化操作,例如删除所有标点符号,修剪两端的空白或将多个空白序列替换为单个空格,则可以扩展该函数,也可以在此函数之前或之后NatSortKey()
进行应用于您的数据。(我建议REGEXP_REPLACE()
为此目的使用)。
我认为“。”在某种程度上也是以英语为中心的。代表小数点,“,”代表千位分隔符,但是如果您想取反或希望将其作为参数进行切换,应该足够容易地进行修改。
也许可以通过其他方式进一步改进;例如,它当前按绝对值对负数进行排序,因此-1位于-2之前,而不是相反。也没有办法为数字指定DESC排序顺序,同时保留文本的ASC字典顺序。这两个问题可以通过更多的工作来解决。如果有时间,我将更新代码。
还有很多其他细节需要注意-包括对所使用的字符集和排序规则的一些关键依赖性-但我已经将它们全部放入SQL代码的注释块中。在自己使用该功能之前,请仔细阅读该内容!
所以,这是代码。如果您发现错误,或者有我未提及的改进,请在评论中让我知道!
delimiter $$
CREATE DEFINER=CURRENT_USER FUNCTION NatSortKey (s varchar(100), n int) RETURNS varchar(350) DETERMINISTIC
BEGIN
DECLARE x,y varchar(100);
DECLARE r varchar(350) DEFAULT '';
DECLARE suf varchar(101);
DECLARE i,j,k int UNSIGNED;
IF n<=0 THEN SET n := -1; END IF;
LOOP
SET i := REGEXP_INSTR(s,'\\d');
IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF;
SET n := n-1, suf := ' ';
IF i>1 THEN
IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF;
IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF;
SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i);
END IF;
SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+'));
SET s := SUBSTRING(s,LENGTH(x)+1);
SET i := INSTR(x,'.');
IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF;
SET i := LENGTH(x);
SET x := REPLACE(x,',','');
SET j := LENGTH(x);
SET x := TRIM(LEADING '0' FROM x);
SET k := LENGTH(x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0'));
SET i := LOCATE('.',y,2);
IF i=0 THEN
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf);
ELSE
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
WHILE LENGTH(y)>0 AND n!=0 DO
IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF;
SET j := LENGTH(x);
SET x := TRIM(LEADING '0' FROM x);
SET k := LENGTH(x);
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0'));
SET n := n-1;
END WHILE;
SET r := CONCAT(r,y,suf);
END IF;
END LOOP;
END
$$
delimiter ;