混合字数string的人性化或自然数字sorting

在Sivaram Chintalapudi的 这个问题 之后 ,我感兴趣的是在PostgreSQL中对于包含多位数字和单词/字母混合的string进行自然的或者“人性化的”sorting “是否实用,没有固定的string中的单词和数字模式,并且string中可能有多个多位数字。

我经常看到这个事情的唯一地方就是在Mac OS的Finder中,它自然地对包含混合数字和单词的文件名进行sorting,在“3”之后放置“20”,而不是在之前。

所需的对照顺序将由一个algorithm产生,该algorithm将每个string在字母 – 数字边界处分成块,然后对每个部分进行sorting,将正常sorting和数字块的字母块视为整数来整理。 所以:

'AAA2fred'会变成('AAA',2,'fred')'AAA10bob'变成('AAA',10,'bob') 。 然后可以根据需要对这些进行sorting:

 regress=# WITH dat AS ( VALUES ('AAA',2,'fred'), ('AAA',10,'bob') ) regress-# SELECT dat FROM dat ORDER BY dat; dat -------------- (AAA,2,fred) (AAA,10,bob) (2 rows) 

与通常的string整理顺序相比:

 regress=# WITH dat AS ( VALUES ('AAA2fred'), ('AAA10bob') ) regress-# SELECT dat FROM dat ORDER BY dat; dat ------------ (AAA10bob) (AAA2fred) (2 rows) 

但是,logging比较的方法并没有概括,因为Pg不会比较ROW(..)结构或不等数目条目的logging。

给定此SQLFiddle中的示例数据,默认的en_AU.UTF -8归类生成sorting:

 1A, 10A, 2A, AAA10B, AAA11B, AAA1BB, AAA20B, AAA21B, X10C10, X10C2, X1C1, X1C10, X1C3, X1C30, X1C4, X2C1 

但我想要:

 1A, 2A, 10A, AAA1BB, AAA10B, AAA11B, AAA20B, AAA21B, X1C1, X1C3, X1C4, X1C10, X1C30, X2C1, X10C10, X10C2 

目前我正在使用PostgreSQL 9.1,但只有9.2的build议可以。 我对如何实现高效的string拆分方法以及如何比较所描述的交替string – 数字sorting规则中的结果拆分数据的build议感兴趣。 或者,当然,完全不同的,更好的方法,不需要拆分string。

PostgreSQL似乎不支持比较函数,否则这可以通过recursion比较器来完成,比如ORDER USING comparator_fncomparator(text,text)函数comparator(text,text) ORDER USING comparator_fn 。 唉,那句话是虚构的。

更新: 关于主题的博文 。

build立在您的testing数据上,但是可以处理任意数据:

 CREATE TYPE ai AS (a text, i int); -- Could also be a table or even a temp table SELECT data FROM ( SELECT ctid, data, regexp_matches(data, '(\D*)(\d*)', 'g') AS x FROM alnum ) x GROUP BY ctid, data -- ctid as stand-in for a missing pk ORDER BY regexp_replace (left(data, 1), '[0-9]', '0') , array_agg(ROW(x[1], CASE x[2] WHEN '' THEN '0' ELSE x[2] END)::ai) , data -- for special case of trailing 0 

用PostgreSQL 9.1.5testing。

  • 诀窍是形成一个由textinteger列组成的复合types的aiai数组。 这与不同数量的元素一起工作。

  • 带有模式(\D*)(\d*) regexp_matches(), g选项为每个字母和数字的组合返回一行,最后加上一行。 用前面的数字,我们在字母部分的开始处得到一个空的元素。

  • regexp_replace (left(data, 1), '[0-9]', '0')为第一个ORDER BY项目,以处理前导数字和空string。

  • 将空stringreplace为integer部分的0

– 如果像{}()"',这样的特殊字符可以发生,那么你必须相应地转义这些字符。

  • @ Craigbuild议使用ROWexpression式来处理这个问题。

  • 如果NULL可能发生,你将不得不特殊情况 – 在@Craig提出的STRICT函数中使用整个shebang。

顺便说一句,这不会在sqlfiddle执行,但它在我的数据库集群。 JDBC不能胜任。 sqlfiddle抱怨:

方法org.postgresql.jdbc3.Jdbc3Array.getArrayImpl(long,int,Map)尚未实现。

因为它看起来像其他人都打开数组或其他类似的东西,所以迟到了。 看似过度。

 CREATE FUNCTION rr(text,int) RETURNS text AS $$ SELECT regexp_replace( regexp_replace($1, '[0-9]+', repeat('0',$2) || '\&', 'g'), '[0-9]*([0-9]{' || $2 || '})', '\1', 'g' ) $$ LANGUAGE sql; SELECT t,rr(t,9) FROM mixed ORDER BY t; t | rr --------------+----------------------------- AAA02free | AAA000000002free AAA10bob | AAA000000010bob AAA2bbb03boo | AAA000000002bbb000000003boo AAA2bbb3baa | AAA000000002bbb000000003baa AAA2fred | AAA000000002fred (5 rows) (reverse-i-search)`OD': SELECT crypt('richpass','$2$08$aJ9ko0uKa^C1krIbdValZ.dUH8D0R0dj8mqte0Xw2FjImP5B86ugC'); richardh=> richardh=> SELECT t,rr(t,9) FROM mixed ORDER BY rr(t,9); t | rr --------------+----------------------------- AAA2bbb3baa | AAA000000002bbb000000003baa AAA2bbb03boo | AAA000000002bbb000000003boo AAA2fred | AAA000000002fred AAA02free | AAA000000002free AAA10bob | AAA000000010bob (5 rows) 

我没有声称两个正则expression式是最有效的方法来做到这一点,但rr()是不可变的(固定长度),所以你可以索引它。 哦 – 这是9.1

当然,用plperl你可以评估replace垫/修剪一次。 但是,与Perl一样,你总是得到一个更多的select(TM)比任何其他的方法:-)

以下函数将一个string拆分为任意长度的(字,数字)对的数组。 如果string以数字开头,则第一个条目将有一个NULL字。

 CREATE TYPE alnumpair AS (wordpart text,numpart integer); CREATE OR REPLACE FUNCTION regexp_split_numstring_depth_pairs(instr text) RETURNS alnumpair[] AS $$ WITH x(match) AS (SELECT regexp_matches($1, '(\D*)(\d+)(.*)')) SELECT ARRAY[(CASE WHEN match[1] = '' THEN '0' ELSE match[1] END, match[2])::alnumpair] || (CASE WHEN match[3] = '' THEN ARRAY[]::alnumpair[] ELSE regexp_split_numstring_depth_pairs(match[3]) END) FROM x;$$ LANGUAGE 'sql' IMMUTABLE; 

允许PostgreSQL的复合typessorting发挥作用:

 SELECT data FROM alnum ORDER BY regexp_split_numstring_depth_pairs(data); 

并根据此SQLFiddle生成预期的结果。 我已经采用了Erwin的0代替所有string中的空string,以数字开始sorting。 它比使用ORDER BY left(data,1), regexp_split_numstring_depth_pairs(data)更清洁。

虽然这个函数可能非常慢,但至less可以用在expression式索引中。

蛮好玩的!

我面临同样的问题,我想将解决scheme包装在一个函数中,以便我可以轻松地重新使用它。 我创build了以下函数来实现Postgres中的“人类风格”sorting顺序。

 CREATE OR REPLACE FUNCTION human_sort(text) RETURNS text[] AS $BODY$ /* Split the input text into contiguous chunks where no numbers appear, and contiguous chunks of only numbers. For the numbers, add leading zeros to 20 digits, so we can use one text array, but sort the numbers as if they were big integers. For example, human_sort('Run 12 Miles') gives {'Run ', '00000000000000000012', ' Miles'} */ select array_agg( case when a.match_array[1]::text is not null then a.match_array[1]::text else lpad(a.match_array[2]::text, 20::int, '0'::text)::text end::text) from ( select regexp_matches( case when $1 = '' then null else $1 end, E'(\\D+)|(\\d+)', 'g' ) AS match_array ) AS a $BODY$ LANGUAGE sql IMMUTABLE; 

经过testing可以在Postgres 8.3.18和9.3.5上运行

  • 没有recursion,应该比recursion解决scheme更快
  • 只能按顺序使用子句,不必处理主键或ctid
  • 适用于任何select(甚至不需要PK或ctid)
  • 比其他解决scheme更简单,应该更容易扩展和维护
  • 适用于function指标以提高性能
  • 适用于Postgres v8.3或更高版本
  • 在input中允许不限数量的文本/数字变化
  • 使用一个正则expression式,应该比具有多个正则expression式的版本更快
  • 长于20位的数字按其前20位数字sorting

这是一个示例用法:

 select * from (values ('Books 1', 9), ('Book 20 Chapter 1', 8), ('Book 3 Suffix 1', 7), ('Book 3 Chapter 20', 6), ('Book 3 Chapter 2', 5), ('Book 3 Chapter 1', 4), ('Book 1 Chapter 20', 3), ('Book 1 Chapter 3', 2), ('Book 1 Chapter 1', 1), ('', 0), (null::text, 0) ) as a(name, sort) order by human_sort(a.name) ----------------------------- |name | sort | ----------------------------- | | 0 | | | 0 | |Book 1 Chapter 1 | 1 | |Book 1 Chapter 3 | 2 | |Book 1 Chapter 20 | 3 | |Book 3 Chapter 1 | 4 | |Book 3 Chapter 2 | 5 | |Book 3 Chapter 20 | 6 | |Book 3 Suffix 1 | 7 | |Book 20 Chapter 1 | 8 | |Books 1 | 9 | ----------------------------- 
 create table dat(val text) insert into dat ( VALUES ('BBB0adam'), ('AAA10fred'), ('AAA2fred'), ('AAA2bob') ); select array_agg( case when zx[1] ~ E'\\d' then lpad(zx[1],10,'0') else zx[1] end ) alnum_key from ( SELECT ctid, regexp_matches(dat.val, E'(\\D+|\\d+)','g') as x from dat ) z group by z.ctid order by alnum_key; alnum_key ----------------------- {AAA,0000000002,bob} {AAA,0000000002,fred} {AAA,0000000010,fred} {BBB,0000000000,adam} 

在这上面工作了将近一个小时,并且没有看上去 – 我看到欧文到达了一个类似的地方。 跑到同一个“无法find数据types的数组types文本[]”麻烦作为@Clodoaldo。 有很多麻烦得到清理练习,以不join所有的行,直到我想到由ctid分组(这感觉就像作弊真的 – 而且不能在OP示例WITH dat AS ( VALUES ('AAA2fred'), ('AAA10bob') ) ... as WITH dat AS ( VALUES ('AAA2fred'), ('AAA10bob') ) ... )。 如果array_agg可以接受一个set生成的子查询,那将会更好。

我不是RegEx大师,但我可以在一定程度上工作。 足以产生这个答案。

它将处理内容中最多2个数字值。 我不认为OSX比这更进一步,如果它甚至处理2。

 WITH parted AS ( select data, substring(data from '([A-Za-z]+).*') part1, substring('a'||data from '[A-Za-z]+([0-9]+).*') part2, substring('a'||data from '[A-Za-z]+[0-9]+([A-Za-z]+).*') part3, substring('a'||data from '[A-Za-z]+[0-9]+[A-Za-z]+([0-9]+).*') part4 from alnum ) select data from parted order by part1, cast(part2 as int), part3, cast(part4 as int), data; 

SQLFiddle