34

Sivaram Chintalapudiによるこの質問のフォローアップとして、複数桁の数字と単語/文字の混合を含む文字列の「自然な」または「人間化された」ソートを行うことが PostgreSQL で実用的かどうかに興味があります。文字列内の単語と数字のパターン、および文字列内に複数桁の数字が複数存在する場合があります。

これが日常的に行われているのを私が見た唯一の場所は、Mac OS の Finder です。これは、数字と単語が混在するファイル名を自然にソートし、「20」を「3」の前ではなく「3」の後に配置します。

必要な照合順序は、各文字列を文字と数字の境界でブロックに分割し、各部分を並べ替えて、文字ブロックを通常の照合で、数字ブロックを照合目的の整数として扱うアルゴリズムによって生成されます。そう:

'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(..) コンストラクトまたはエントリ数が等しくないレコードを比較しないため、レコード比較アプローチは一般化されません。

この 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)。残念ながら、その構文は架空のものです。

更新: トピックに関するブログ投稿

4

7 に答える 7

19

テストデータに基づいて構築しますが、これは任意のデータで機能します。これは、文字列内の任意の数の要素で機能します。

データベースごとに 1 つの値textと 1 つの値で構成される複合型を登録します。integer私はそれを呼びますai

CREATE TYPE ai AS (a text, i int);

トリックは、ai列の各値からの配列を形成することです。

regexp_matches()(\D*)(\d*)このgオプションは、文字と数字の組み合わせごとに 1 つの行を返します。さらに、空の文字列が 2 つある無関係なぶら下がっている行が 1 つあり'{"",""}'、それをフィルタリングしたり抑制したりすると、コストが増えるだけです。コンポーネント内の空文字列 ( '') をに置き換えた後、これを配列に集約します (にキャストできないため)。0integer''integer

NULL値は最初に並べ替えます-またはそれらを特別なケースにする必要があります-またはSTRICT@Craigが提案するような関数でシバン全体を使用します。

Postgres 9.4 以降

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;

デシベル<>ここでフィドル

Postgres 9.1 (元の回答)

regexp_replace()動作がわずかに異なるPostgreSQL 9.1.5 でテスト済み。

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 では実行されませんが、私の db クラスターでは実行されます。JDBC はそれに対応していません。sqlfiddle は不平を言います:

メソッド org.postgresql.jdbc3.Jdbc3Array.getArrayImpl(long,int,Map) はまだ実装されていません。

これはその後修正されました: http://sqlfiddle.com/#!17/fad6e/1

于 2012-10-19T01:22:27.217 に答える
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)

2 つの正規表現がこれを行うための最も効率的な方法であると主張しているわけではありませんが、rr() は (固定長の場合) 不変であるため、インデックスを付けることができます。ああ - これは 9.1 です

もちろん、plperl を使用すると、置換を評価して一度にパディング/トリムすることができます。しかし、perl を使用すると、他のどのアプローチよりもオプション (TM) が常に 1 つだけ増えます :-)

于 2012-10-19T09:52:29.167 に答える
5

次の関数は、文字列を任意の長さの (word,number) ペアの配列に分割します。文字列が数字で始まる場合、最初のエントリには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);

この SQLFiddle に従って、期待される結果を生成します。数字で始まるすべての文字列で空の文字列をErwin の置換を採用0して、数字が最初にソートされるようにしました。を使用するよりもきれいですORDER BY left(data,1), regexp_split_numstring_depth_pairs(data)

関数はおそらく恐ろしく遅いですが、少なくとも式インデックスで使用できます。

それは楽しかった!

于 2012-10-19T01:08:06.973 に答える
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}

これにほぼ1時間取り組み、見ずに投稿しました.Erwinが同様の場所にたどり着いたようです. @Clodoaldo と同じ「データ型 text[] の配列型が見つかりませんでした」という問題に遭遇しました。ctid でグループ化することを考えるまで、クリーンアップの演習ですべての行を集約しないようにするのに多くの苦労がありました (これは本当に不正行為のように感じます-OP の例のように疑似テーブルでは機能しませんWITH dat AS ( VALUES ('AAA2fred'), ('AAA10bob') ) ...)。array_agg がセットを生成するサブセレクトを受け入れることができれば、より良いでしょう。

于 2012-10-19T02:08:53.633 に答える
2

私は正規表現の第一人者ではありませんが、ある程度は機能します。この答えを出すには十分です。

コンテンツ内で最大 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;

SQLフィドル

于 2012-10-19T00:14:52.987 に答える