通过this question跟进Sivaram Chintalapudi,我感兴趣的是PostgreSQL中是否可以执行包含多位数字和单词混合的字符串natural - or "humanized" - sorting字母。字符串中没有固定的单词和数字模式,字符串中可能有多个多位数字。
我经常看到的这个例子就是在Mac OS的Finder中,它自然地对包含混合数字和单词的文件名进行排序,在“3”之后放置“20”,而不是在它之前。
所需的归类顺序将由算法生成,该算法将每个字符串拆分为字母数字边界的块,然后对每个部分进行排序,将正常整理的字母块和数字块作为整数处理整数。所以:
'AAA2fred'
将成为('AAA',2,'fred')
,'AAA10bob'
将成为('AAA',10,'bob')
。然后可以根据需要对它们进行分类:
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)
与通常的字符串排序规则相比:
regress=# WITH dat AS ( VALUES ('AAA2fred'), ('AAA10bob') )
regress-# SELECT dat FROM dat ORDER BY dat;
dat
------------
(AAA10bob)
(AAA2fred)
(2 rows)
然而,记录比较方法没有概括,因为Pg不会比较ROW(..)构造或不等数量条目的记录。
给定样本数据in this SQLFiddle,默认的en_AU.UTF-8排序规则产生排序:
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的建议会没问题。我对如何实现有效的字符串分割方法的建议感兴趣,以及如何在所描述的交替的字符串然后数字校对中比较得到的分割数据。或者,当然,对于不需要拆分字符串的完全不同且更好的方法。
PostgreSQL似乎不支持比较器函数,否则使用递归比较器和ORDER USING comparator_fn
以及comparator(text,text)
函数可以很容易地完成。唉,这种语法是虚构的。
答案 0 :(得分:14)
以您的测试数据为基础,但这适用于任意数据。这适用于字符串中的任意数量的元素。
为每个数据库注册一个由text
和一个integer
值组成的复合类型。我称之为ai
:
CREATE TYPE ai AS (a text, i int);
诀窍是从列中的每个值形成一个ai
数组。
regexp_matches()
的 (\D*)(\d*)
和g
选项为每个字母和数字组合返回一行。加上一个不相关的悬空行和两个空字符串'{"",""}'
过滤或抑制它只会增加成本。在将''
组件中的0
替换为空integer
之后,将此聚合为数组(因为''
无法转换为integer
)。< / p>
NULL
值首先排序 - 或者您需要特殊情况 - 或者使用STRICT
函数中的整个shebang,例如@Craig建议。
SELECT data
FROM alnum
ORDER BY ARRAY(SELECT ROW(x[1], CASE x[2] WHEN '' THEN '0' ELSE x[2] END)::ai
FROM regexp_matches(data, '(\D*)(\d*)', 'g') x)
, data;
db&lt;&gt;小提琴here
使用PostgreSQL 9.1.5进行测试,其中regexp_replace()
的行为略有不同。
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
添加regexp_replace (left(data, 1), '[1-9]', '0')
作为第一个ORDER BY
项,以处理前导数字和空字符串。
如果可能出现 {}()"',
之类的特殊字符,则必须相应地转义它们。
@Craig建议使用ROW
表达式来处理这个问题。
<击> 顺便说一句,这不会在sqlfiddle中执行,但它在我的数据库集群中执行。 JDBC不能胜任它。 sqlfiddle抱怨:
<击>
方法org.postgresql.jdbc3.Jdbc3Array.getArrayImpl(long,int,Map)是
尚未实现。
击>
答案 1 :(得分:8)
迟到添加这个答案,因为看起来其他人都在展开数组或其他类似的东西。看似过度。
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)
我没有声称两个正则表达式是最有效的方法,但rr()是不可变的(对于固定长度),所以你可以索引它。哦 - 这是9.1
当然,使用plperl你可以一次性评估更换垫子/修剪它。但是使用perl你总是得到比其他方法更多的选项(TM): - )
答案 2 :(得分:7)
我遇到了同样的问题,我想将解决方案包装在一个函数中,以便我可以轻松地重复使用它。我创建了以下函数来实现Postgres中的“人性化”排序顺序。
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;
测试用于Postgres 8.3.18和9.3.5
以下是一个示例用法:
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 |
-----------------------------
答案 3 :(得分:5)
以下函数将字符串拆分为任意长度的(字,数字)对数组。如果字符串以数字开头,那么第一个条目将有一个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的复合类型排序发挥作用:
SELECT data FROM alnum ORDER BY regexp_split_numstring_depth_pairs(data);
并根据this SQLFiddle生成预期结果。我已经采用Erwin的0
替换所有以数字开头的字符串中的空字符串,以便数字先排序;它比使用ORDER BY left(data,1), regexp_split_numstring_depth_pairs(data)
更清洁。
虽然该函数可能非常慢,但它至少可以用在表达式索引中。
这很有趣!
答案 4 :(得分:3)
create table dat(val text)
insert into dat ( VALUES ('BBB0adam'), ('AAA10fred'), ('AAA2fred'), ('AAA2bob') );
select
array_agg( case when z.x[1] ~ E'\\d' then lpad(z.x[1],10,'0') else z.x[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}
对此进行了近一个小时的研究并且没有看过 - 我看到Erwin来到了一个类似的地方。进入相同的#34;找不到数据类型text []&#34;的数组类型像@Clodoaldo一样麻烦。在我想到由ctid进行分组之前,让清理练习没有聚合所有的行很麻烦(这真的像作弊一样 - 并且不像在OP示例中那样在psuedo表上工作{{1 }})。如果array_agg可以接受产生集合的子选择,那就更好了。
答案 5 :(得分:2)
我不是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;
答案 6 :(得分:0)
以下解决方案结合了其他答案中提出的各种想法,以及classic solution 中的一些想法:
create function natsort(s text) returns text immutable language sql as $$
select string_agg(r[1] || E'\x01' || lpad(r[2], 20, '0'), '')
from regexp_matches(s, '(\D*)(\d*)', 'g') r;
$$;
这个函数的设计目标是简单和纯字符串操作(没有自定义类型和数组),所以它可以很容易地用作一个嵌入式解决方案,并且很容易被索引。
注意:如果您希望数字超过 20
位,则必须将函数中的硬编码最大长度 20
替换为合适的更大的长度。请注意,这将直接影响结果字符串的长度,因此不要将该值设置为大于所需的值。