查找整数序列包含给定子序列的行


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},我想找到第二行。

同样,如果给定的子序列是{3, 17, 25, 377},我想找到第1行和第4行。

最后,如果给定的子序列为{12, 4, 3, 25, 377},则不会返回任何行。

调查

首先,我不能完全确定用数组数据类型表示序列是明智的。尽管这似乎适合这种情况;我担心这会使处理更加复杂。也许最好使用与另一个表的关系模型来不同地表示序列。

同样,我考虑使用unnest数组函数扩展序列,然后添加搜索条件。但是,序列中的术语数是可变的,我看不出该怎么做。

我知道也可以使用intarray模块的subarray功能在子序列中剪切序列,但是我看不出它对我的搜索有何好处。

约束条件

即使目前仍在开发我的模型,该表也打算由许多序列组成,这些序列介于50,000至300,000行之间。所以我有很强的性能约束。

在我的示例中,我使用了相对较小的整数。实际上,这些整数有可能变得更大,直到溢出为止bigint。在这种情况下,我认为最好的方法是将数字存储为字符串(因为没有必要执行这些数学运算序列)。但是,选择此解决方案将导致无法使用上述intarray模块。


如果它们可能溢出bigint,则应将其numeric用作存储它们的类型。它慢很多,但占用更多空间。
Craig Ringer

@CraigRinger为什么不使用numeric而不是字符串(text例如)?我不需要对序列执行数学运算。
mlpo

2
因为它比更加紧凑,并且在许多方面更快text,并且阻止了您存储虚假的非数字数据。取决于,如果您执行I / O,则可能希望文本减少I / O处理。
Craig Ringer

@CraigRinger实际上,类型更加一致。关于性能,我将测试何时找到搜索方法。
mlpo

2
@CraigRinger如果顺序无关紧要,它可能会起作用。但是在这里,序列是有序的。示例:SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];将返回true,因为此运算符未考虑订单。
mlpo

Answers:


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

我不确定将大型代码块粘贴到答案中是否是一个好习惯,因为它们应该是最小且可验证的。此函数的通用数组版本长四倍且非常麻烦。我还与测试了它numerictext和改善从20至50倍取决于阵列的长度范围内。
Slonopotamus

是的,但是答案必须回答问题:-)。在我看来,符合约束条件的答案很有趣(因为这是问题的一部分)。但是,可能没有必要提出通用版本。只是带有字符串或的版本numeric
mlpo

无论如何,我添加了通用数组的版本,因为对于任何可变长度的数据类型它几乎都是相同的。但是,如果您确实关心性能,则应该坚持使用固定大小的数据类型,例如bigint
Slonopotamus

我很乐意那样做。问题是我的某些序列超出了范围bigint,所以看来我别无选择。但是,如果您有想法,我很感兴趣:)。
mlpo

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)), '{}',',,') || '%'

编辑:

小提琴又在工作。

如果将数组标准化为每个值一行,则可以应用基于集合的逻辑:

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

@mlpo:您的表现期望是什么?为了能够使用索引,您必须将序列归一化为每个值一行,应用关系除法,最后检查顺序是否正确。在您的示例中25,两次存在id=4,这实际上可能吗?搜索序列的平均值/最大值中有多少个匹配项?
dnoeth

一个序列可能包含相同数字的几倍。例如{1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}很有可能。关于匹配的数目,通常认为所使用的子序列限制了结果的数目。但是,某些序列非常相似,有时使用较短的子序列以获得更多结果可能会很有趣。我估计大多数情况下的匹配数在0到100之间。当子序列很短或很常见时,总有可能偶尔会与许多序列匹配。
mlpo 2015年

@mlpo:我添加了一个基于集合的解决方案,并且我会对某些性能比较非常感兴趣:-)
dnoeth

@ypercube:这只是一个快速添加,可以返回更有意义的结果:-)好的,这太可怕了,我将其更改。l–
dnoeth
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.