정수 시퀀스에 주어진 하위 시퀀스가 ​​포함 된 행 찾기


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있습니다. 이러한 상황에서 숫자를 문자열로 저장하는 것이 가장 좋습니다 (이러한 일련의 수학 연산을 수행 할 필요가 없기 때문에). 그러나이 솔루션을 선택하면 위에서 언급 한 인타 레이 모듈 을 사용할 수 없습니다 .


오버 플로우 될 수있는 경우이를 저장하는 유형으로 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

답변:


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

큰 코드 블록을 최소화하고 확인 가능해야하기 때문에 응답에 큰 코드 블록을 붙여 넣는 것이 좋은 방법인지 확실하지 않습니다. 이 함수의 일반적인 배열 버전은 4 배 길고 번거 롭습니다. 나는 또한 그것을 테스트 한 numerictext및 개선 배열의 길이에 따라 20 ~ 50 시간에서였다.
Slonopotamus 4

그렇습니다. 그러나 답은 질문에 답해야합니다. :-). 여기서 제약 조건을 준수하는 답변이 흥미 롭습니다 (이 측면이 질문의 일부이기 때문에). 그럼에도 불구하고, 일반 버전을 제안 할 필요는 없습니다. 문자열이있는 버전 또는 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

@ mlpo : 세트 기반 솔루션을 추가했으며 성능 비교에 매우 관심이 있습니다 :-)
dnoeth

@ ypercube : 이것은 더 의미있는 결과를 반환하는 빠른 추가 일뿐입니다 :-) 좋아, 끔찍하다, 바꿀 것이다.
l
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.