手记

MySQL里 alphanumeric 自然排序竟然这么麻烦!三十年了还解决不了? 😤

我们的QA团队简直是怪物,你知道的!

不管我在下次公司聚会上怎么用请客喝酒贿赂他们,让他们停止找我代码里的问题,他们总是提出一些不合理的要求和所谓的“预期行为”……真恶心,呕!

别相信我吧?

他们最近要求将列表按照字母和数字的顺序排列。

你能相信吗?我告诉你,怪物啊!

我的意思是,毕竟一般用户不会期望清单是自动排序的。

    测试 1
    测试 2
    测试 12

全屏模式 退出全屏

...为什么就不能按照MySQL那种方式来排序呢

以下为测试代码:
    测试用例 1
    测试用例 12
    测试用例 2

全屏,退出全屏

有道理吧?是吧?

随便怎么说,我们不要纠结谁对谁错(顺便说一句,他们是对的),我觉得最好试着解决一下。

我说……这有啥难的?(暗示)

简短的说

我知道你可能很忙的,简单来说就是用JS、PHP或其他语言来处理。不要用MySQL来处理。

真的,直接返回你的数据集,然后在你用的是 Node 时使用 Intl.Collator,或者在 PHP 中,你可以使用 natsort()

你知道,那样做也还好。问题解决了,我就可以继续我的日常生活。质量保证那边会满意,列表也整理好了,一切就都好了。

然而,如果你和我一样,你的CTO在审查你的PR时说:

请查看图片。

我心领神会!

真的,他真是太对了。

我们为什么还要在代码里手动分类?MySQL应该能为我们搞定这些。

接着发生的是超过4小时的捣鼓和骂娘,试图让MySQL屈从于我。

没完全做到,但差不多成功了。

在这个特定情况下,我最终采用的方法在用户输入方面还算过得去,并且比我发现的其他方案都好很多,但也有它的局限性。

但现在我们说得太超前了,我想展示一下在 MySQL 中试着解决这个问题是怎样的吧!

测试数据

让我们首先来谈谈我用的测试数据,这样你就能检查我是否遗漏了什么。

测试数据

测试资料

    INSERT INTO test_data (data_value) VALUES
    ('2 test'),
    ('12 test'),
    ('1 test'),
    ('20 test'),
    ('10 test'),
    ('1test'),
    ('2test'),
    ('test 1'),
    ('my data 2020-01-01'),
    ('my data 2020-02-01'),
    ('01-02-24 data'),
    ('12 test'),
    ('4a test'),
    ('my 2020-01-01 data 2020-01-01'),
    ('my 2020-02-01 data 2020-01-01'),
    ('my 2020-02-01 data 202-01-01'),
    ('my 2020-02-01 data 20-01-01'),
    ('my 2020-02-01 data 1-01-01'),
    ('my 2020-02-01 data 2-01-01'),
    ('my 2020-02-01 data 12-01-01'),
    ('my 2020-02-01 data 01-01-01'),
    ('my 2020-01-01 data 2020-02-01'),
    ('my 2020-01-01 data 2021-01-01'),
    ('my 2020-01-01 data 2120-01-01'),
    ('my 2120-01-01 data 2020-01-01'),
    ('4b test'),
    ('my test'),
    ('my 12 magic test'),
    ('my magic 12 test'),
    ('cheese and test 12'),
    ('42-a-1'),
    ('40-a-1'),
    ('40a'),
    ('FoClSy4727'),
    ('Pthw068bf'),
    ('6bfS'),
    ('HOFAp_Yx7920'),
    ('25hWTX'),
    ('dnjLlW1'),
    ('RHrIt72402eaLr'),
    ('cIhb42WFNQ'),
    ('9244uVCpGa'),
    ('yDKrkCp7960'),
    ('GeGIrPM-H86'),
    ('wrOae537LGCT'),
    ('WffSPaBA318'),
    ('kQ33596c'),
    ('3uEKHmHePf'),
    ('796h-eYWy'),
    ('833HufIZAS'),
    ('utjtV03Xns'),
    ('dlCSh87811'),
    ('13IUkOxEVl'),
    ('VHCok55901XYVk'),
    ('2RnSVwq'),
    ('AwtwQdn09'),
    ('gvSV6z'),
    ('uxWLO039hb'),
    ('vTg946');

全屏模式 退出全屏

一些字符串中夹杂着各种位置的数字,这在我看来还算有点道理。

这可能不是完美的数据集,但对我来说已经足够了!

所以我有了些数据,每个开发者都会做的事我也做了,向谷歌大神求助。

在 MySQL 中查找自然排序法的方法

看来这第一步挺有道理的,是吧?

MySQL 已经存在了 30 年了,字母数字排序可能是很多人会经常遇到的需求,所以应该已经有现成的方法了吧?

简而言之……这太令人筋疲力尽了。每篇文章、Stack Overflow帖子等等都说的都是没用的废话,要么不管用,要么只适用于特定数据格式。Stack Overflow帖子等。

我想要一个通用的,这样在任何地方都可以按字母数字顺序排列。

让我带你经历我经历过的痛苦,这样你就不用再经历同样的痛苦了,也能判断我的回答是否更好……或者我只是让你的痛苦更多了。

我们来试试别人提过的常见排序方式:

测试一:‘+0’方法

    SELECT 

* 
    FROM 
        test_data 
    ORDER BY 
        `data_value`+0 ASC
    -- 这将从test_data表中选择所有数据,并按data_value的数值升序排列。

点击全屏 点击退出

目的是让它尝试让MySQL使用数值顺序,而不是按字典顺序。

如果每一行都有数字单词,这会工作得很好(这样的话,数字会按顺序排列,然后是单词)。

然而,由于我们的数据杂乱无章,它只是输出了一堆无意义的内容。

第1次测试结果

id data_value
44 GeGIrPM-H86
25 2020-01-01 的数据
27 我的测试
28 我的12魔法测试
29 我的魔法12测试
34 FoClSy4727

测试1的完整结果如下

id data_value
44 GeGIrPM-H86
25 我 2020-01-01 数据 2020-01-01
27 测试
28 带有魔法数字12的测试
29 带有数字12魔法的测试
34 FoClSy4727
35 Pthw068bf
37 HOFAp_Yx7920
39 dnjLlW1
40 RHrIt72402eaLr
41 cIhb42WFNQ
43 yDKrkCp7960
30 奶酪和12号测试
45 wrOae537LGCT
46 WffSPaBA318
47 kQ33596c
51 utjtV03Xns
52 dlCSh87811
54 VHCok55901XYVk
56 AwtwQdn09
57 gvSV6z
58 uxWLO039hb
59 vTg946
17 我 2020-02-01 数据 20-01-01
8 测试 1
9 我 2020-01-01 数据
10 我 2020-02-01 数据
14 我 2020-01-01 数据 2020-01-01
15 我 2020-02-01 数据 2020-01-01
16 我 2020-02-01 数据 202-01-01
18 我 2020-02-01 数据 1-01-01
19 我 2020-02-01 数据 2-01-01
21 我 2020-02-01 数据 01-01-01
23 我 2020-01-01 数据 2021-01-01
22 我 2020-01-01 数据 2020-02-01
20 我 2020-02-01 数据 12-01-01
24 我 2020-01-01 数据 2120-01-01
6 1test
11 01-02-24 数据
3 1 测试
55 2RnSVwq
1 2 测试
7 2test
48 3uEKHmHePf
13 4a 测试
26 4b 测试
36 6bfS
5 10 测试
2 12 测试
12 12 测试
53 13IUkOxEVl
4 20 测试
38 25hWTX
33 40a
32 40-a-1
31 42-a-1
49 796h-eYWy
50 833HufIZAS
42 9244uVCpGa

好的,这没成功(其实我也没指望它会成功)。

我们试试别的东西吧:

测试 2: '长度' 小技巧

    SELECT 
        字符和数字, 整型
    FROM 
        排序测试表
    ORDER BY
        LENGTH(字符和数字), 字符和数字

点击全屏 点击退出全屏

这也是一个挺有趣的尝试。

显然,“12”比“1”要长,所以在那里应该没问题。

它也适用于您使用标准的数据格式,比如我之前提到的“test 1, test 12, test 2”这样的格式。

不过,对于我们的数据来说,它给出更多的无意义信息:

第二次测试的结果是

id data_value
33 40a
36 6bfS
6 1test
7 2test
3 1 test
1 2 test
38 25hWTX
32 40-a-1
31 42-a-1
57 gvSV6z
8 test 1
59 vTg946
5 10 test

测试 2 的全部结果

id data_value
33 40a
36 6bfS
6 1test
7 2test
3 1 test
1 2 test
38 25hWTX
32 40-a-1
31 42-a-1
57 gvSV6z
8 test 1
59 vTg946
5 10 test
2 12 test
12 12 test
4 20 test
55 2RnSVwq
13 4a test
26 4b test
39 dnjLlW1
27 my test
47 kQ33596c
49 796h-eYWy
56 AwtwQdn09
35 Pthw068bf
53 13IUkOxEVl
48 3uEKHmHePf
50 833HufIZAS
42 9244uVCpGa
41 cIhb42WFNQ
52 dlCSh87811
34 FoClSy4727
51 utjtV03Xns
58 uxWLO039hb
44 GeGIrPM-H86
46 WffSPaBA318
43 yDKrkCp7960
37 HOFAp_Yx7920
45 wrOae537LGCT
11 01-02-24 数据
40 RHrIt72402eaLr
54 VHCok55901XYVk
28 我的12个魔法测试
29 我的魔法测试12
30 奶酪和测试12
9 我的数据2020-01-01
10 我的数据2020-02-01
18 我的2020-02-01 数据 1-01-01
19 我的2020-02-01 数据 2-01-01
21 我的2020-02-01 数据 01-01-01
20 我的2020-02-01 数据 12-01-01
17 我的2020-02-01 数据 20-01-01
16 我的2020-02-01 数据 202-01-01
14 我的2020-01-01 数据 2020-01-01
22 我的2020-01-01 数据 2020-02-01
23 我的2020-01-01 数据 2021-01-01
24 我的2020-01-01 数据 2120-01-01
15 我的2020-02-01 数据 2020-01-01
25 我的2120-01-01 数据 2020-01-01

而且这也说得通,它只是按长度排序。

按字符串长度排序,'a1' 会排在 '1potato' 前面。

测试3:豪连肯

看看这个美女:

看看这件漂亮的东西:

(注:如果上下文明确是指物品,建议使用“看看这个漂亮的物品”以使句子更加自然。)

SELECT 

* 
FROM 
    test_data 
ORDER BY 
    REPLACE(
        REPLACE(
            REPLACE(
                REPLACE(
                    REPLACE(
                        REPLACE(
                            REPLACE(
                                REPLACE(
                                    REPLACE(
                                        REPLACE(
                                            data_value, 
                                            '0', 'Ā'), 
                                            '1', 'ā'), 
                                            '2', 'Ă'), 
                                            '3', 'ă'), 
                                            '4', 'Ą'), 
                                            '5', 'ą'), 
                                            '6', 'Ć'), 
                                            '7', 'ć'), 
                                            '8', 'Ĉ'), 
                                            '9', 'ĉ') 
    COLLATE utf8_bin; -- 对data_value进行替换并按utf8_bin排序规则排序

进入全屏,退出全屏

这梗是真的火!

但是尽管如此,这个方法基本上有效(虽然和我原本想的不同)。

这种方法很巧妙,因为它将每个数值替换为一个根据它们在Unicode中的二进制表示排序的字符。

测试3的结果如下

id data_value
56 AwtwQdn09
34 FoClSy4727
44 GeGIrPM-H86
37 HOFAp_Yx7920
35 Pthw068bf
40 RHrIt72402eaLr
54 VHCok55901XYVk
46 WffSPaBA318
41 cIhb42WFNQ
30 奶酪和测试12

测试3的完整结果

id data_value
56 AwtwQdn09
34 FoClSy4727
44 GeGIrPM-H86
37 HOFAp_Yx7920
35 Pthw068bf
40 RHrIt72402eaLr
54 VHCok55901XYVk
46 WffSPaBA318
41 cIhb42WFNQ
30 奶酪测试 12
52 dlCSh87811
39 dnjLlW1
57 gvSV6z
47 kQ33596c
9 2020年01月01日的数据
10 2020年02月01日的数据
29 魔法测试 12
27 测试
28 魔法测试 12
14 2020年01月01日的数据
22 2020年01月01日的数据
23 2020年01月01日的数据
24 2020年01月01日的数据
21 2020年02月01日的数据
18 2020年02月01日的数据
20 2020年02月01日的数据
19 2020年02月01日的数据
17 2020年02月01日的数据
16 2020年02月01日的数据
15 2020年02月01日的数据
25 2120年01月01日的数据
8 测试一
51 utjtV03Xns
58 uxWLO039hb
59 vTg946
45 wrOae537LGCT
43 yDKrkCp7960
11 01月02日24日的数据
3 一测试
6 一test
5 十测试
2 十二测试
12 十二测试
53 13IUkOxEVl
1 二测试
55 2RnSVwq
7 二test
4 二十测试
38 25hWTX
48 3uEKHmHePf
13 四a测试
26 四b测试
32 40-a-1
33 40a
31 42-a-1
36 6bfS
49 796h-eYWy
50 833HufIZAS
42 9244uVCpGa

你知道吗,如果不是因为一件事,我会用这个的。但是这意味着我们不能再接受如 'Ā' 等输入了。

不幸的是,我们的软件产品将被国际使用,所以我们不能做出那样的选择。

此外,如果你决定使用这个,你的数据库中的这一列也应使用 COLLATE 'utf8_bin',否则可能会出错。

聊天时间:GPT

当谷歌搜索没用的时候,总能帮上忙的是ChatGPT,是吧?

我不会展示所有的结果,但这么说吧,以下只是它没成功的几个!

第 4 测试:GPT 的幻想

    SELECT
        data_value,
        -- 将数字替换成 'a',将字母替换成 'b',其他字符则保持原样
        (
            SELECT GROUP_CONCAT(
                CASE
                    WHEN c REGEXP '[0-9]' THEN 'a' -- 将数字替换成 'a'
                    WHEN c REGEXP '[a-zA-Z]' THEN 'b' -- 将字母替换成 'b'
                    ELSE c -- 其他字符则保持原样
                END
                ORDER BY seq
            ) SEPARATOR ''
            FROM (
                SELECT SUBSTRING(data_value, seq, 1) AS c, seq
                FROM (
                    SELECT data_value, seq
                    FROM test_data
                    JOIN (
                        SELECT n AS seq
                        FROM (SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10) numbers
                        WHERE n <= 255 -- 最多处理 255 个字符的字符串
                    ) seq_table
                    ON seq <= CHAR_LENGTH(data_value)
                ) char_table
            ) char_map
        ) AS sort_key
    FROM test_data
    ORDER BY sort_key;

全屏,退出全屏

它没运行起来,我也没太搞清楚原因,但它看起来还是挺酷的吧!

进行比较,生成类似'aabaaa, aabba, abbaa'这样的字符串,它似乎想要将所有数字字符替换为"a",将所有字母字符替换为"b"。

如果它没有问题,我可能会花更多时间来修复它。

11ab1 变成 aabba
111ab1 变成 aaaba

所以按照 MySQL 的排序方式,111ab1 本来就会排在 11ab1 之前,因为 'aaa' 比 'aab' 更靠前。

不错的想法,但是可惜不太对路。

第五次测试:GPT 差一点就成功了!

这让我挺惊讶的。

我之前在别的地方见过这种情况(正如我们所知,GPT只是在重复它之前见过的内容,并不是真正的有创造力),但它的解释更合理!

    SELECT 

* 
    FROM 
      test_data
    ORDER BY 
      CAST(data_value AS UNSIGNED), data_value ASC;

全屏模式 退出全屏

啊哈,差点儿。我还以为这就是答案呢

不过,在像 "Test 1, Test 12, Test 2" 这样的测试中失败了。不过,如果数字在前面,它还是适合用来排序。

轮到我了

我试了好几次还是没能得到我想要的,但看了几项技巧后,我现在离答案已经近多了。

挠了挠头,哭了一会儿,我想我找到了解决办法!

这个勉强凑合...

这是我想到的一个点子:

    CREATE TABLE test_data (
        id INT AUTO_INCREMENT PRIMARY KEY, 
        data_value VARCHAR(255) NOT NULL, 
        transformed_column VARCHAR(255) AS (
            CASE
                WHEN REGEXP_SUBSTR(data_value, '[0-9]+') IS NOT NULL THEN
                    REGEXP_REPLACE(
                        data_value,
                        '[0-9]+',
                        LPAD(REGEXP_SUBSTR(data_value, '[0-9]+'), 8, '0')
                    )
                ELSE data_value -- 或默认值如 '无数字'
            END
        ) STORED
    );

进入全屏模式 退出全屏模式

我们就可以这样查一下。

    SELECT 
      *
    FROM 
      test_data
    ORDER BY 
      transformed_column, data_value;

全屏(点击进入/点击退出)

这几乎得到了我们预料中的结果。

测试 6 结果

数据值 转换后的列
1 test 00000001 test
01-02-24 00000001-00000001-00000001 数据
1test 00000001test
2 test 00000002 test
2RnSVwq 00000002RnSVwq
2test 00000002test
3uEKHmHePf 00000003uEKHmHePf
4a test 00000004a test

完整结果 - 测试6

数据值 转换后的列
1 test 00000001 test
01-02-24 日期 00000001-00000001-00000001 日期
1test 00000001test
2 test 00000002 test
2RnSVwq 00000002RnSVwq
2test 00000002test
3uEKHmHePf 00000003uEKHmHePf
4a test 00000004a test
4b test 00000004b test
6bfS 00000006bfS
10 test 00000010 test
12 test 00000012 test
12 test 00000012 test
13IUkOxEVl 00000013IUkOxEVl
20 test 00000020 test
25hWTX 00000025hWTX
40-a-1 00000040-a-00000040
40a 00000040a
42-a-1 00000042-a-00000042
796h-eYWy 00000796h-eYWy
833HufIZAS 00000833HufIZAS
9244uVCpGa 00009244uVCpGa
AB-34Y67846 AB-34Y67846
AwtwQdn09 AwtwQdn09
奶酪和测试 12 奶酪和测试 12
cIhb42WFNQ cIhb00000042WFNQ
dlCSh87811 dlCSh00087811
dnjLlW1 dnjLlW00000001
E5-RMT893Y9 E00000005-RMT00000005Y00000005
EV-489RY3DA EV-00000489RY00000489DA
FoClSy4727 FoClSy00004727
GeGIrPM-H86 GeGIrPM-H00000086
gvSV6z gvSV00000006z
HOFAp_Yx7920 HOFAp_Yx00007920
kQ33596c kQ00033596c
我的 12 个魔法测试 我的 12 个魔法测试
我的 2020-01-01 数据 2020-01-01 我的 2020-01-01 数据 2020-01-01
我的 2020-01-01 数据 2020-02-01 我的 2020-01-01 数据 2020-02-01
我的 2020-01-01 数据 2021-01-01 我的 2020-01-01 数据 2021-01-01
我的 2020-01-01 数据 2120-01-01 我的 2020-01-01 数据 2120-01-01
我的 2020-02-01 数据 01-01-01 我的 2020-02-01 数据 01-01-01
我的 2020-02-01 数据 1-01-01 我的 2020-02-01 数据 1-01-01
我的 2020-02-01 数据 12-01-01 我的 2020-02-01 数据 12-01-01
我的 2020-02-01 数据 2-01-01 我的 2020-02-01 数据 2-01-01
我的 2020-02-01 数据 20-01-01 我的 2020-02-01 数据 20-01-01
我的 2020-02-01 数据 202-01-01 我的 2020-02-01 数据 202-01-01
我的 2020-02-01 数据 2020-01-01 我的 2020-02-01 数据 2020-01-01
我的 2120-01-01 数据 2020-01-01 我的 2120-01-01 数据 2020-01-01
我的数据 2020-01-01 我的数据 2020-01-01
我的数据 2020-02-01 我的数据 2020-02-01
我的魔法 12 测试 我的魔法 12 测试
我的测试 我的测试
Pthw068bf Pthw00000068bf
RHrIt72402eaLr RHrIt00072402eaLr
测试 1 测试 1
utjtV03Xns utjtV00000003Xns
uxWLO039hb uxWLO00000039hb
VHCok55901XYVk VHCok00055901XYVk
vTg946 vTg00000946
WffSPaBA318 WffSPaBA00000318
wrOae537LGCT wrOae00000537LGCT
yDKrkCp7960 yDKrkCp00007960
数据值 转换后的列
2020-02-01 数据 1-01-01 2020-02-01 数据 2020-02-01-01-01
2020-02-01 数据 12-01-01 2020-02-01 数据 2020-02-01-12-01
2020-02-01 数据 2-01-01 2020-02-01 数据 2020-02-01-02-01

就像你看到的那样,不过我觉得这暂时能符合我们的需求。

这是根据我看到的一个LPAD示例。然而,它存在同样的问题,即它只能处理以数字开头的字符串。

于是我把那个原则应用到了所有的数列。

这里也用了一个生成列,这样读取速度快(如果我们不太关心读取性能,或者更关心写入性能,我们很快就可以把它加到查询里)。

主要的问题是这个方法通过“强行处理”来绕过MySQL将“11”视为小于“2”这一现象,因为MySQL是逐字符比较(这是简化了的解释)。通过将所有数字统一为8位数来避免此问题。

它还将 transformed_column 中的所有数字替换为它找到的第一个数字。据我了解,除此之外,我没找到其他方法可以避免这种行为模式,除此之外,只能通过嵌套大约 20 个语句或使用 coalesce 等功能来避免这种问题。

结论部分

我不太确定,MySQL 是否应该提供像其他数据库那样的字母数字排序功能?

虽然我们希望事情能够发生,我觉得就这篇文章来说,我还没找到解决这个难题的方法。

但话说回来,我觉得第六次测试的答案比我在外面找到的其他信息更接近正确答案,说不定哪天对谁来说有用呢?

显然,如果你期望的数字长度超过8位数,你可能需要调整LPAD参数,因为这是一一个限制。

(注:此处“一一个限制”中的“一”多余,应去掉。)

显然,如果你期望的数字长度超过8位数,你可能需要调整LPAD参数,因为这是 一个 限制。

此外,它也不是完美的,所以除了帮用户轻松找信息,它做不了其他的事。

我认为现在最好的选择可能是把这个问题抛向社区,看看是否有比我聪明的人能把它正确解决!(我可能需要更深入地了解MySQL中的排序知识,才能真正解决这个问题!)

如果有人能解决我遇到的问题,即如果能将所有数字都补足到8位,那么这样效果会更好!(所以“我的数据2020-02-01 2-01-01”会变成“我的数据00002020-00000002-00000001 00000002-00000001-00000001”)。

或许有一个我遗漏的一句话解决方案?(如果有,我保证我只会稍微哭一会儿!)

总之,这是我最好的了,但还是希望能看到更好的东西!

最后终于这里有一个 DB Fiddle(一个在线数据库工具)可以试试

如果你认为你能找到改进的地方,可以看看这里,这里是一个基于生成列版本的DB Fiddle。

https://www.db-fiddle.com/f/o2ohcGVAgHZQg4teg1s9jW/1034

感谢您的阅读,很快见!

0人推荐
随时随地看视频
慕课网APP