排序时如何将字符串中的数字视为数字(“A3”排序在“A10”之前,而不是之后)

use*_*516 6 postgresql collation sorting natural-sort

对于所有这些查询:

SELECT label FROM personal.storage_disks ORDER BY label ASC;
SELECT label FROM personal.storage_disks ORDER BY label COLLATE "C" ASC;
SELECT label FROM personal.storage_disks ORDER BY label COLLATE "POSIX" ASC;
SELECT label FROM personal.storage_disks ORDER BY label COLLATE "default" ASC;
Run Code Online (Sandbox Code Playgroud)

输出总是:DISK 1, DISK 10, DISK 2, DISK 3, [...]
但是,我想要并期望:DISK 1, DISK 2, DISK 3, [...] DISK 10

我现在没有根据SELECT * FROM pg_collation;......尝试的排序规则,除非我应该使用具有神秘名称的许多非常奇怪的排序规则之一。(我什至尝试了一堆结果相同的方法。)

请注意,我已经阅读了现有的看似相关的 SE 问题以及许多关于 的文章SORT BY,但它们没有帮助,也没有为我清除任何内容。

我正在使用 PostgreSQL 12.4

Sol*_*zky 14

字符串进行排序自然会将“15”放在“2”之前,因为“15”中的第一个数字是“1”,它排在“2”之前。可以通过几种方式在“15”之前对存储在字符串类型中的“2”进行排序。最有效的方法是让排序规则本身在内部处理它。这个选项并不广为人知,甚至在大多数地方都不可用,但任何实现ICU(Unicode 国际组件)** 的系统都有可能允许这种类型的排序(只要它允许自定义排序选项),这通常被称为“自然”排序。

处理自然排序通常是通过将字符串切割成纯字母和数字片段,然后分别对它们进行排序来以编程方式完成的。在许多情况下,这是一个必要的邪恶,但幸运的是,PostgreSQL(至少从版本 10 开始)在内部确实允许这样做。您需要创建一个自定义排序规则(这个甚至直接来自他们的文档):

CREATE COLLATION numeric (provider = icu, locale = 'en-u-kn-true');
Run Code Online (Sandbox Code Playgroud)

并在ORDER BY. 这是它的一个工作示例:

https://dbfiddle.uk/?rdbms=postgres_11&fiddle=58763b51a8ccb2360cf387d8c2b91d51

笔记

由于通常没有很好地理解排序规则,并且自定义它们的能力更新(至少对于数据库而言)甚至更深奥,我建议在实施此解决方案时执行以下操作:

  1. 用“custom_”前缀新排序规则的名称以提高人们的意识,这确实是一个自定义排序规则,可能具有不明显的行为并且可能不存在于其他系统上(因此可能需要添加到系统或应用程序设置过程中)
  2. 在使用此自定义排序规则的每个查询之后添加注释,指出它是自定义排序规则,并包含指向官方文档的链接:
    https : //www.postgresql.org/docs/12/collat​​ion.html#id -1.6.10.4.5.7.5
    您甚至可以提到该-kn-true部件启用“数字”排序。

奖金

为了更全面地演示“数字”排序规则选项的工作原理,我在前面的示例中添加了一些数据以显示:

  • 字符串中的多个/单独的数字组被单独处理
  • 不同的非数字字符按预期处理
  • 前导 0 不影响结果

额外的数据是:

DISK 2A
DISK 2B
DISK 2B 33
DISK 2B 4
FILE 62
FILE 7
DIRECTORY 1000000
DIRECTORY 57
DIRECTORY 9999
DIRECTORY 57000
DIRECTORY 057
DIRECTORY 0057
DIRECTORY 52
Run Code Online (Sandbox Code Playgroud)

这是更新的示例:

https://dbfiddle.uk/?rdbms=postgres_11&fiddle=20416b0dd731b2cc28b6fdee8ef70ec7


**公平地说,鉴于任何排序规则或系统都可以实现相同的算法,ICU / Unicode 不是进行此类排序的“必需”。但是,它内置在ICU中,并且越来越多的系统正在集成ICU。

  • 虽然这解决了 OP 的问题,但值得注意的是,这种方式与 [Deep Magic](https://en.wikipedia.org/wiki/Magic_(programming)#Variants) 领域的排序规则接壤。请注意,如果你这样做,你应该留下解释为什么 - 更重要的是,如何 - 你这样做的文档,这样下一个开发人员(甚至你自己)就不会浪费时间试图了解正在发生的事情。记得善待未来的你。 (3认同)
  • @T.Sar 你好。我绝对同意:避免使用 Deep Magic / Black Magic / Magic Numbers / 没有发表像样的评论 / 以及大多数使系统_不必要地_难以维护的事情。然而,我不相信这种方法甚至接近这样的定义,除了排序规则之外,一般还没有被很好地理解。但是,鉴于这种排序问题的解决方案只是配置进行排序的组件,我看不出它是如何被视为模糊/隐藏的。不过,也许包含文档 URL 并在排序规则名称前加上“custom_”的注释可能会有所帮助? (3认同)

bob*_*lux 2

磁盘 1、磁盘 10、磁盘 2、磁盘 3

这不是您想要的,但这是排序文本时的正确顺序。数字也是字母,它们按字母顺序排序。引号的意思是“这是一个文本文字”,“10”<“2”确实如此。

我想要并期待:DISK 1、DISK 2、DISK 3、[...] DISK 10

这很常见。我认为 Windows 资源管理器可以做到这一点。

如果您想按字母顺序对它们进行排序,那么一个快速的解决方案是像 ISO 日期一样进行操作:“Disk 01”确实位于“Disk 10”之前,因为按字母顺序“0”<“1”...

如果您不想在标签中添加前导零,那么您需要创建一个函数来动态添加它们。你可以使用 plpgsql 来做到这一点,或者拼凑一个正则表达式:

select regexp_replace(
    regexp_replace(column1, '(\d+)', '000000\1')
    , '0*(\d{4})', '\1' ) r 
FROM (VALUES ('Drive 1'),('Drive 10'),('Drive 2'),('Drive 100000') )v 
ORDER BY r;
      r
--------------
 Drive 0001
 Drive 0002
 Drive 0010
 Drive 100000
Run Code Online (Sandbox Code Playgroud)

这会向所有数字添加固定数量的前导零,然后将其修剪回来,使总位数为 4。如果需要,您可以添加更多数字。最后一行表明,当原始数字包含太多位数时,它不再起作用,但希望您不会有超过一个大的驱动器来排序。正则表达式可能需要一些修改。

如果这是 python,则可以将文本字符串转换为元组或数组,如 ['Drive',10],其中整数是实际的整数,然后可以正确排序。但是你不能在 postgres 数组中混合数据类型,因此会出现混乱。

如果您想使用它,请将上面的 regexp_replace() 作为您的 ORDER BY 参数,无需将其放入选择列表中,这只是为了示例并显示其结果。