整数シーケンスが特定のサブシーケンスを含む行を検索します


9

問題

注:PostgreSQLのシーケンスメカニズムではなく、数学的なシーケンスを参照しています

整数のシーケンスを表すテーブルがあります。定義は次のとおりです。

CREATE TABLE sequences
(
  id serial NOT NULL,
  title character varying(255) NOT NULL,
  date date NOT NULL,
  sequence integer[] NOT NULL,
  CONSTRAINT "PRIM_KEY_SEQUENCES" PRIMARY KEY (id)
);

私の目標は、指定されたサブシーケンスを使用して行を見つけることです。つまり、sequenceフィールドが指定されたサブシーケンスを含むシーケンスである行(私の場合、シーケンスは順序付けされています)。

テーブルに次のデータが含まれているとします。

+----+-------+------------+-------------------------------+
| id | title |    date    |           sequence            |
+----+-------+------------+-------------------------------+
|  1 | BG703 | 2004-12-24 | {1,3,17,25,377,424,242,1234}  |
|  2 | BG256 | 2005-05-11 | {5,7,12,742,225,547,2142,223} |
|  3 | BD404 | 2004-10-13 | {3,4,12,5698,526}             |
|  4 | BK956 | 2004-08-17 | {12,4,3,17,25,377,456,25}     |
+----+-------+------------+-------------------------------+

したがって、指定されたサブシーケンスが{12, 742, 225, 547}である場合、行2を検索します。

同様に、指定されたサブシーケンスがである場合、{3, 17, 25, 377}行1と行4を検索します。

最後に、指定されたサブシーケンスがの場合、{12, 4, 3, 25, 377}返される行はありません。

調査

まず、配列データ型でシーケンスを表すことが賢明であるかどうかは完全にはわかりません。これは状況に適しているように見えますが、取り扱いが複雑になるのではないでしょうか。おそらく、別のテーブルとの関係のモデルを使用して、シーケンスを異なる方法で表す方が良いでしょう。

同様に、unnest配列関数を使用してシーケンスを拡張し、検索条件を追加することを考えています。それにもかかわらず、シーケンス内の項の数が可変であるため、その方法がわかりません。

intarrayモジュールのsubarray関数を使用してシーケンスをサブシーケンスにカットすることも可能ですが、検索にどのように役立つかわかりません。

制約

現時点で私のモデルがまだ開発中である場合でも、テーブルは50,000から300,000行までの多くのシーケンスで構成されることが意図されています。だから私は強力なパフォーマンスの制約があります。

私の例では、比較的小さな整数を使用しました。実際には、これらの整数は、オーバーフローするまではるかに大きくなる可能性がありbigintます。そのような状況では、数値を文字列として保存するのが最善だと思います(これらの一連の数学演算を実行する必要がないため)。ただし、このソリューションを選択すると、前述のintarrayモジュールを使用できなくなります。


それらがオーバーフローbigintする可能性がある場合はnumeric、それらを格納するタイプとして使用する必要があります。ただし、速度はかなり遅くなり、場所が大きくなります。
クレイグリンガー、

@CraigRingerなぜnumeric文字列ではなく(textたとえば)使用するのですか?シーケンスで数学演算を実行する必要はありません。
mlpo 2015

2
なぜならそれはよりコンパクトであり、多くの点で高速でありtext、偽の非数値データを格納することを妨げます。場合によっては、I / O のみを実行している場合は、I / O処理を減らすためにテキストが必要になることがあります。
クレイグリンガー

@CraigRinger確かに、型はより一貫しています。パフォーマンスについては、検索方法を見つけたときにテストします。
mlpo 2015

2
@CraigRinger順序が関係ない場合は機能する可能性があります。しかし、ここではシーケンスが順序付けられています。例:SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];この演算子は注文を考慮しないため、trueを返します。
mlpo 2015

回答:


3

dnoethの回答のパフォーマンスを大幅に改善する必要がある場合は、ネイティブC関数を使用して適切な演算子を作成することを検討してください。

以下はint4配列の例です。(一般的な配列バリアント対応するSQLスクリプト)。

Datum
_int_sequence_contained(PG_FUNCTION_ARGS)
{
    return DirectFunctionCall2(_int_contains_sequence,
                               PG_GETARG_DATUM(1),
                               PG_GETARG_DATUM(0));
}

Datum
_int_contains_sequence(PG_FUNCTION_ARGS)
{
    ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
    ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
    int         na, nb;
    int32      *pa, *pb;
    int         i, j;

    na = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
    nb = ArrayGetNItems(ARR_NDIM(b), ARR_DIMS(b));
    pa = (int32 *) ARR_DATA_PTR(a);
    pb = (int32 *) ARR_DATA_PTR(b);

    /* The naive searching algorithm. Replace it with a better one if your arrays are quite large. */
    for (i = 0; i <= na - nb; ++i)
    {
        for (j = 0; j < nb; ++j)
            if (pa[i + j] != pb[j])
                break;

        if (j == nb)
            PG_RETURN_BOOL(true);
    }

    PG_RETURN_BOOL(false);
}
CREATE FUNCTION _int_contains_sequence(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION _int_sequence_contained(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE OPERATOR @@> (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_contains_sequence,
  COMMUTATOR = '<@@',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

CREATE OPERATOR <@@ (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_sequence_contained,
  COMMUTATOR = '@@>',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

これで、このように行をフィルタリングできます。

SELECT * FROM sequences WHERE sequence @@> '{12, 742, 225, 547}'

このソリューションがどれだけ高速かを確認するために、少し実験を行いました。

CREATE TEMPORARY TABLE sequences AS
SELECT array_agg((random() * 10)::int4) AS sequence, g1 AS id
FROM generate_series(1, 100000) g1
  CROSS JOIN generate_series(1, 30) g2
GROUP BY g1;
EXPLAIN ANALYZE SELECT * FROM sequences
WHERE        translate(cast(sequence as text), '{}',',,')
 LIKE '%' || translate(cast('{1,2,3,4}'as text), '{}',',,') || '%'

"Seq Scan on sequences  (cost=0.00..7869.42 rows=28 width=36) (actual time=2.487..334.318 rows=251 loops=1)"
"  Filter: (translate((sequence)::text, '{}'::text, ',,'::text) ~~ '%,1,2,3,4,%'::text)"
"  Rows Removed by Filter: 99749"
"Planning time: 0.104 ms"
"Execution time: 334.365 ms"
EXPLAIN ANALYZE SELECT * FROM sequences WHERE sequence @@> '{1,2,3,4}'

"Seq Scan on sequences  (cost=0.00..5752.01 rows=282 width=36) (actual time=0.178..20.792 rows=251 loops=1)"
"  Filter: (sequence @@> '{1,2,3,4}'::integer[])"
"  Rows Removed by Filter: 99749"
"Planning time: 0.091 ms"
"Execution time: 20.859 ms"

したがって、約16倍高速です。十分でない場合は、GINまたはGiSTインデックスのサポートを追加できますが、これははるかに困難な作業になります。


興味深いように聞こえますがnumeric、データがオーバーフローする可能性があるため、文字列またはタイプを使用してデータを表現していますbigint。質問の制約に一致するように回答を編集することをお勧めします。とにかく、ここで投稿する比較パフォーマンスを行います。
mlpo 2015

大きなコードブロックを最小限に抑えて検証できるはずなので、回答に大きなコードブロックを貼り付けるのが良い方法かどうかはわかりません。この関数の一般的な配列バージョンは4倍長く、かなり面倒です。私もでそれをテストしているnumerictext、改善は、配列の長さに応じて20〜50倍の範囲でした。
スロノポタマス2015

はい。ただし、回答が質問に回答する必要があります:-)。ここで、制約に準拠した回答は興味深いと思われます(この側面は質問の一部であるため)。それにもかかわらず、一般的なバージョンを提案する必要はないかもしれません。文字列またはnumeric
mlpo 2015

とにかく、汎用配列のバージョンは、どの可変長データ型でもほぼ同じであるため追加しました。ただし、パフォーマンスに本当に懸念がある場合は、などの固定サイズのデータ​​型を使用する必要がありますbigint
Slonopotamus

私はそうしたいと思います。問題は、シーケンスの一部がをはるかに超えてオーバーフローするbigintため、選択の余地がないようです。しかし、アイデアがあれば、私は興味があります:)。
mlpo 2015

1

配列を文字列にキャストし、中括弧をコンマで置き換えると、サブシーケンスを簡単に見つけることができます。

translate(cast(sequence as varchar(10000)), '{}',',,')

{1,3,17,25,377,424,242,1234} -> ',1,3,17,25,377,424,242,1234,'

検索する配列についても同じようにして、先頭と末尾を追加します%

'%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

{3, 17, 25, 377} -> '%,3,17,25,377,%'

今あなたはそれを使ってそれを比較しますLIKE

WHERE        translate(cast(sequence      as varchar(10000)), '{}',',,')
 LIKE '%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

編集:

フィドルは再び働いています。

配列が値ごとに1行に正規化されている場合は、セットベースのロジックを適用できます。

CREATE TABLE sequences
( id int NOT NULL,
  n int not null,
  val numeric not null
);

insert into sequences values(  1, 1,1     );
insert into sequences values(  1, 2,3     );
insert into sequences values(  1, 3,17    );
insert into sequences values(  1, 4,25    );
insert into sequences values(  1, 5,377   );
insert into sequences values(  1, 6,424   );
insert into sequences values(  1, 7,242   );
insert into sequences values(  1, 8,1234  );
insert into sequences values(  2, 1,5     );
insert into sequences values(  2, 2,7     );
insert into sequences values(  2, 3,12    );
insert into sequences values(  2, 4,742   );
insert into sequences values(  2, 5,225   );
insert into sequences values(  2, 6,547   );
insert into sequences values(  2, 7,2142  );
insert into sequences values(  2, 8,223   );
insert into sequences values(  3, 1,3     );
insert into sequences values(  3, 2,4     );
insert into sequences values(  3, 3,12    );
insert into sequences values(  3, 4,5698  );
insert into sequences values(  3, 5,526   );          
insert into sequences values(  4, 1,12    );
insert into sequences values(  4, 2,4     );
insert into sequences values(  4, 3,3     );
insert into sequences values(  4, 4,17    );
insert into sequences values(  4, 5,25    );
insert into sequences values(  4, 6,377   );
insert into sequences values(  4, 7,456   );
insert into sequences values(  4, 8,25    );
insert into sequences values(  5, 1,12    );
insert into sequences values(  5, 2,4     );
insert into sequences values(  5, 3,3     );
insert into sequences values(  5, 4,17    );
insert into sequences values(  5, 5,17    );
insert into sequences values(  5, 6,25    );
insert into sequences values(  5, 7,377   );
insert into sequences values(  5, 8,456   );
insert into sequences values(  5, 9,25    );

n連続していて、重複やギャップがない必要があります。今度は一般的な値に参加して、シーケンスがシーケンシャルであるという事実を利用します:-)

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select seq.id, 
   -- this will return the same result if the values from both tables are in the same order
   -- it's a meaningless dummy, but the same meaningless value for sequential rows 
   seq.n - s.n as dummy,
   seq.val,
   seq.n,
   s.n 
from sequences as seq join searched as s
on seq.val = s.val
order by seq.id, dummy, seq.n;

最後に、同じダミーの行数を数え、それが正しい数かどうかを確認します。

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select distinct seq.id
from sequences as seq join searched as s
on seq.val = s.val
group by 
   seq.id,
   seq.n - s.n
having count(*) = (select count(*) from searched)
;

sequence(val、id、n)のインデックスを試してください。


後でこの解決策も検討しました。しかし、かなり厄介な問題がいくつかあります。まず、この解決策は非常に非効率的だと思います。検索パターンを作成する前に、各行の各配列をキャストする必要があります。キャストを回避するために、シーケンスをTEXTフィールドに格納することを検討することは可能です(varchar私の考えでは悪い考えです。シーケンスは数として長くなる可能性があるため、サイズはかなり予測不可能です)。ただし、パフォーマンスを向上させるためにインデックスを使用することはまだ不可能です(さらに、文字列フィールドの使用は必ずしも賢明ではないようです。上記の@CraigRingerのコメントを参照してください)。
mlpo 2015

@mlpo:あなたのパフォーマンスの期待は何ですか?インデックスを使用できるようにするには、シーケンスを値ごとに1行に正規化し、関係部を適用して、最後に順序が正しいかどうかを確認する必要があります。あなたの例で25は、2回存在しますがid=4、これは実際に可能ですか?検索されたシーケンスの平均/最大の一致はいくつ存在しますか?
dnoeth 2015

シーケンスには、同じ番号が数回含まれる場合があります。たとえば、{1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}かなり可能です。一致の数に関しては、使用される部分列は通常結果の数を制限すると考えられています。ただし、いくつかのシーケンスは非常によく似ており、より多くの結果を得るためにサブシーケンスを短くすることが興味深い場合があります。ほとんどの場合の一致数は0から100の間であると推定します。常に、サブシーケンスが短い場合や非常に一般的な場合に、多くのシーケンスと一致する可能性があります。
mlpo

@mlpo:セットベースのソリューションを追加しましたが、パフォーマンスの比較に非常に興味があります:-)
dnoeth

@ypercube:これは、より意味のある結果:-) [OK]を返すためだけのクイック追加し、それは恐ろしいですが、私はit.lを変更します
dnoeth
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.