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


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 배 길고 번거 롭습니다. 나는 또한 그것을 테스트 한 numeric과 text및 개선 배열의 길이에 따라 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.