如何在MySQL中对字母数字数据进行排序?

3
首先,我想指出我已经尝试了几乎所有的方法。在过去的8个小时里,我一直在尝试让我的列表有序,并且我已经应用了这里找到的数十种解决方案。
这里是带有示例数据的SQL Fiddle。我发现一个页面可以按正确顺序对我的列表进行排序。
1
2
2.B3
5
9
10 A-1
10 A-3
10 B-4
10 B-5
11
12
B3-43
B3-44
B3 - 48
B3 - 49
Basztowa 3
Basztowa 4
Basztowa 5
Basztowa 7
Basztowa 9
D.1
D.2
D.10
D.11
D.12
Kabaty ul. Pod lipą 4

但是我无法使用MySQL复现这个问题。

如果可能的话,我想避免使用PHP来解决我的问题,因为据我所知,DBMS已经针对此类操作进行了优化。如有任何帮助,我将不胜感激。

@更新

感谢@Jakumi,我创建了两个函数来帮助我解决问题。

你需要创建一个列以存储排序友好格式的值(zeropadded_name),在更新和插入时创建触发器,以使zeropadded_name填充name更改时,就可以按zeropadded_name排序并享受它!

辅助函数

  1. regex_replace - 它的任务是通过删除所有非字母数字字符来帮助我们清理值。
  2. lpad_numbers - 填充字符串中的每个数字。它有点丑陋,因为我不太了解MySQL函数,但嘿,它很有效,而且速度很快。

例子:

SELECT lpad_numbers(regex_replace('[^a-zA-Z0-9]', ' ', 'B3 - A-5'));
#B0003A0005

DROP FUNCTION IF EXISTS regex_replace;
CREATE FUNCTION `regex_replace`(
  pattern     VARCHAR(1000)
              CHARSET utf8
              COLLATE utf8_polish_ci,
  replacement VARCHAR(1000)
              CHARSET utf8
              COLLATE utf8_polish_ci,
  original    VARCHAR(1000)
              CHARSET utf8
              COLLATE utf8_polish_ci
) RETURNS varchar(1000) CHARSET utf8
    DETERMINISTIC
BEGIN
    DECLARE temp VARCHAR(1000)
    CHARSET utf8
    COLLATE utf8_polish_ci;
    DECLARE ch VARCHAR(1)
    CHARSET utf8
    COLLATE utf8_polish_ci;
    DECLARE i INT;
    SET i = 1;
    SET temp = '';
    IF original REGEXP pattern
    THEN
      loop_label: LOOP
        IF i > CHAR_LENGTH(original)
        THEN
          LEAVE loop_label;
        END IF;
        SET ch = SUBSTRING(original, i, 1);
        IF NOT ch REGEXP pattern
        THEN
          SET temp = CONCAT(temp, ch);
        ELSE
          SET temp = CONCAT(temp, replacement);
        END IF;
        SET i = i + 1;
      END LOOP;
    ELSE
      SET temp = original;
    END IF;
    RETURN temp;
  END;

DROP FUNCTION IF EXISTS lpad_numbers;
CREATE FUNCTION `lpad_numbers`(str VARCHAR(256)) RETURNS varchar(256) CHARSET utf8 COLLATE utf8_polish_ci
BEGIN
    DECLARE i, len SMALLINT DEFAULT 1;
    DECLARE ret VARCHAR(256) DEFAULT '';
    DECLARE num VARCHAR(256) DEFAULT '';
    DECLARE c CHAR(1);

    IF str IS NULL
    THEN
      RETURN "";
    END IF;

    SET len = CHAR_LENGTH(str);
    REPEAT
      BEGIN
        SET c = MID(str, i, 1);
        IF c BETWEEN '0' AND '9'
        THEN
          SET num = c;
          SET i = i + 1;
          REPEAT
            BEGIN
              SET c = MID(str, i, 1);
              SET num = CONCAT(num, c);
              SET i = i + 1;
            END;
          UNTIL c NOT BETWEEN '0' AND '9' END REPEAT;
          SET ret = CONCAT(ret, LPAD(num, 4, '0'));
        ELSE
          SET ret = CONCAT(ret, c);
          SET i = i + 1;
        END IF;
      END;
    UNTIL i > len END REPEAT;
    RETURN ret;
  END;

你想按字母顺序从数据库中获取数据? - Mark Twigg
好吧,我们可以称之为字母顺序。按名称的常规顺序不适用,因为2在11之后,等等。 - Kamil Latosinski
数据共享的常见模式是什么?有点/连字符/空格等。 - 1000111
1
可能是MySQL 'Order By' - sorting alphanumeric correctly的重复问题。 - Jester
1
@KamilLatosinski 嗯,当它们被替换为空格时,仍然存在空格与非空格的问题,而且在字典/字母顺序中,10仍然小于2。我真的认为你应该生成另一列,其中包含可以正确排序的“经过消毒”的名称版本。 - Jakumi
显示剩余8条评论
2个回答

2

根据基础结构进行拆分

技术上,mysql的排序机制是正确的,但您的字符串格式不正确。您的数据的基础结构类似于以下内容(为方便示例保留了Original列):

alpha1   num1 alpha2 num2 ...   Original      
            1                   1             
            2                   2             
            2      B    3       2.B3          
            5                   5             
            9                   9             
           10      A    1       10 A-1        
           10      A    3       10 A-3        
           10      B    4       10 B-4        
           10      B    5       10 B-5        
           11                   11            
           12                   12            
B           3          43       B3-43         
B           3          44       B3-44         
B           3          48       B3 - 48       
B           3          49       B3 - 49       
Basztowa    3                   Basztowa 3    
Basztowa    4                   Basztowa 4    
Basztowa    5                   Basztowa 5    
Basztowa    7                   Basztowa 7    
Basztowa    9                   Basztowa 9    
D           1                   D.1           
D           2                   D.2           
D          10                   D.10          
D          11                   D.11          
D          12                   D.12          

如果您现在使用ORDER BY alpha1,num1,alpha2,num2进行排序,它们将按您想要的方式进行排序。但是,已经“格式化”的版本(Original列)很难排序,因为应按字母顺序排序和应按数字顺序排序的部分混合在一起。
零填充
有一个较少的替代方案,只需要一个额外的列,其中您假设没有任何数字超过例如10000,现在您可以用零填充版本替换每个数字(而不是数字!),因此10 A-1将变为0010A0001(显然是0010A0001),但我不认为这可以在ORDER BY语句中即时完成。
但是对于此示例,零填充版本(假设:每个数字<10000):
Original      Zeropadded 
1             0001       
2             0002       
2.B3          0002B0003  
5             0005       
9             0009       
10 A-1        0010A0001  
10 A-3        0010A0003  
10 B-4        0010B0004  
10 B-5        0010B0005  
11            0011       
12            0012       
B3-43         B00030043  
B3-44         B00030043  
B3 - 48       B00030048  
B3 - 49       B00030049  
Basztowa 3    Baztowa0003
Basztowa 4    Baztowa0004
Basztowa 5    Baztowa0005
Basztowa 7    Baztowa0007
Basztowa 9    Baztowa0009
D.1           D0001      
D.2           D0002      
D.10          D0010      
D.11          D0011      
D.12          D0012      

这句话可以用ORDER BY zeropadded来实现按需排序。
最终,您可能需要在php中进行排序或创建更多的列以通过重新格式化/清理/拆分输入来帮助您进行排序。 更新

零填充解释(简化版)

零填充的主要思想是数字的自然格式与计算机中的格式不同。在计算机中,数字 2 实际上是 数字序列 0..0002(因此包括前导零),类似于10(0..0010)。当计算机比较数字时,它将从左到右查找不同的数字。
0...0002
0...0010
======!.    (the ! marks the point where the first digit is different)

然后程序将确定哪个数字较大或较小。在这种情况下,0 < 1,因此2 < 10。(当然,计算机使用的是二进制,但这并不改变概念)。
现在,字符串在技术上是一系列字符。字符串比较的工作方式略有不同。当两个字符串进行比较时,它们不会被(左边)填充,所以每个字符串的第一个字符确实是第一个字符,而不是一个填充(例如空格)。因此,在技术上,字符串“A10”是字符序列“A”,“1”和“0”。由于使用了字符串比较,因此它比“A2”“小”,因为字符串比较不将数字视为数字,而只被视为字符(即数字字符)。
A10
A2
=!     (the ! marks the point where the first character is different)

由于字符 1 小于 2,所以字符串 A10A2 要小。为了解决这个问题,我们需要将字符串中的数字格式强制转换成数字比较时的格式,即通过在数字前面填充零使它们长度相等,根据它们的 数位价值 对齐数字:

A0010
A0002
===!.  (the ! marks the point where the first character is different)

现在,它实际上是您在数字比较中期望的相同比较。但是,您必须做出一些关于数字的最大长度的假设,以便您可以适当地选择填充。如果没有这个假设,您将会遇到问题。
唯一(逻辑上)剩下的问题是:当比较的字符串中有一个字母字符而另一个字符串有一个数字时,填充会发生什么变化?答案是:什么也不会发生。我们不会将数字转换为字母,数字小于字母,因此在这种情况下,一切都保持不变。
零填充的效果是:通过根据其值对齐数字字符,我们调整了字符串中的“数字”比较,使其类似于实际数字比较。

我已将您的解决方案调整到我的需求。由于我的真实房地产名称既是建筑符号又是名称,如果我使用DBMS来完成这项工作,那对我来说将是一场噩梦,因此我创建了一个PHP脚本,将该列填充为所需的名称,并且从此以后,在每次更新时,我都会计算新的零填充名称。非常好用!您能简要解释一下为什么这样做可以吗?或者提供一些关于这个主题的阅读资料吗? - Kamil Latosinski
1
@KamilLatosinski 我添加了一个有些冗长的解释 ;o) - Jakumi

-1
SELECT name FROM realestate ORDER BY name ASC;

这应该可以按字母数字数据对您的列表进行排序...我看不出有什么问题。

编辑:好的,我仍然不知道我是否真正理解了这个问题的目标(是为了比赛吗?),但我可以提交这个“扭曲”的查询(希望我永远不会在我的职业生涯中使用):

SELECT name FROM realestate
ORDER BY IF(SUBSTRING(name, 1, 2) REGEXP '[A-Z]', 100000, CAST(name AS UNSIGNED)) ASC,
SUBSTRING(name, 1, 2) ASC,
CAST(SUBSTRING(name FROM LOCATE('.', name)+1) AS UNSIGNED) ASC,
REPLACE(name, ' ', '') ASC;

也许有人能找到更简单的方法,因为我承认我的答案有点复杂。但是,Kamil和Jakumi的解决方案要复杂得多。

在编程中,"2"之前的数字是"10"。 - Mark Baker
这并没有提供他在问题中给出的结果。 - Jester
按字母顺序,10在2之前,因为1在2之前。您必须将该列转换为INT或DECIMAL,以便使2在10之前。 - FragBis
我已经编辑了我的答案,并添加了一个查询(第二个查询),它完全按照他在问题中给出的顺序,并且只使用了一个mysql查询来对结果进行排序。我认为Jakumi给出的答案要复杂得多。但是,随你便。 - FragBis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接