web-dev-qa-db-ja.com

単語と数字の混合文字列の人間化または自然数の並べ替え

この質問 by Sivaram Chintalapudi のフォローアップ、PostgreSQLで 自然-または「人間化」-並べ替え を実行することが実用的かどうかに興味があります「複数桁の数字と単語/文字が混在する文字列の。文字列には単語と数字の固定パターンはなく、文字列には複数の複数桁の数字が含まれる場合があります。

これが日常的に行われているのを私が見た唯一の場所は、Mac OSのFinderで、数字と単語が混在するファイル名を自然に並べ替え、「3」の前ではなく「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(..)構造または異なる数のエントリのレコードを比較しないため、レコード比較アプローチは一般化されません。

サンプルデータが与えられた場合 この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
_

私は現在PostgreSQL9.1を使用していますが、9.2のみの提案で十分です。効率的な文字列分割方法を実現する方法と、説明した文字列と数値の交互照合で結果の分割データを比較する方法についてのアドバイスに興味があります。または、もちろん、文字列を分割する必要のない、まったく異なる、より優れたアプローチ。

PostgreSQLはコンパレータ関数をサポートしていないようです。そうでなければ、再帰コンパレータと_ORDER USING comparator_fn_やcomparator(text,text)関数のようなものを使用してこれをかなり簡単に行うことができます。残念ながら、その構文は架空のものです。

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

30
Craig Ringer

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

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

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

秘訣は、列の各値からaiの配列を作成することです。

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

NULL値は最初にソートされます-またはそれらを特殊なケースにする必要があります-または@Craigが提案するようなSTRICT関数でShebang全体を使用します。

Postgres9.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;
_

db <>フィドル ここ

Postgres 9.1(元の回答)

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では実行されませんが、私のdbクラスターでは実行されます。 JDBCはそれまでではありません。 sqlfiddleは文句を言います:

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

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

15

他の全員が配列などにアンラップしているように見えたため、この回答を遅く追加しました。過剰に見えた。

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を使用すると、他のどのアプローチよりも常に1つ多いオプション(TM)が得られます:-)

8
Richard Huxton

これと同じ問題に直面し、ソリューションを関数でラップして、簡単に再利用できるようにしたかったのです。 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;

postgres8.3.18および9.3.5で動作することがテストされています

  • 再帰はありません。再帰的なソリューションよりも高速である必要があります
  • Order by句だけで使用でき、主キーやctidを処理する必要はありません
  • すべての選択で機能します(PKやctidも必要ありません)
  • 他のいくつかのソリューションよりもシンプルで、拡張と保守が簡単なはずです
  • パフォーマンスを向上させるための機能インデックスでの使用に適しています
  • Postgresv8.3以降で動作します
  • 入力で無制限の数のテキスト/数字の変更を許可します
  • 1つの正規表現のみを使用し、複数の正規表現を持つバージョンよりも高速である必要があります
  • 20桁より長い番号は、最初の20桁で並べ替えられます

使用例は次のとおりです。

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   |
-----------------------------
7
TNelson

次の関数は、文字列を任意の長さの(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);
_

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

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

それは楽しかった!

5
Craig Ringer
_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がセットを生成する副選択を受け入れることができればもっと良いでしょう。

3
dbenhur

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

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

2
RichardTheKiwi