PostgreSQL 쿼리에서 여러 연속 범위의 시작과 끝을 효율적으로 선택


19

1-288 범위의 이름과 정수를 가진 테이블에 약 10 억 행의 데이터가 있습니다. 주어진 name 에 대해, 모든 int 는 고유하며, 범위 내의 모든 가능한 정수가 존재하지는 않습니다.

이 쿼리는 예제 사례를 생성합니다.

--what I have:
SELECT *
FROM ( VALUES ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3)
     ) AS baz ("name", "int")

각 이름과 연속 정수 시퀀스에 대한 행이있는 조회 테이블을 생성하고 싶습니다. 이러한 각 행에는 다음이 포함됩니다.

이름 - 값의 이름
스타트 - 인접한 시퀀스에서 첫 번째 정수
단부 - 인접한 시퀀스에서 최종 값
범위 - 엔드 - 시작 + 1

이 쿼리는 위 예제에 대한 예제 출력을 생성합니다.

--what I need:
SELECT * 
FROM ( VALUES ('foo', 2, 4, 3),
              ('foo', 10, 11, 2),
              ('foo', 13, 13, 1),
              ('bar', 1, 3, 3)
     ) AS contiguous_ranges ("name", "start", "end", span)

행이 너무 많으므로 더 효율적입니다. 즉,이 쿼리를 한 번만 실행하면되므로 절대적인 요구 사항은 아닙니다.

미리 감사드립니다!

편집하다:

PL / pgSQL 솔루션을 환영한다고 덧붙여 야합니다 (팬시 트릭을 설명해주세요. 저는 여전히 PL / pgSQL을 처음 사용합니다).


정렬이 메모리에 맞도록 작은 덩어리로 테이블을 처리하는 방법을 찾을 수 있습니다 ( "이름"을 N 버킷으로 해시하거나 이름의 첫 번째 / 마지막 문자를 사용하여). 정렬을 디스크에 유출시키는 것보다 여러 테이블을 스캔하는 것이 더 빠를 수 있습니다. 일단 그런 후에 창 기능을 사용합니다. 또한 데이터의 패턴을 활용하는 것을 잊지 마십시오. 어쩌면 대부분의 "이름"은 실제로 288 개의 값을가집니다.이 경우 기본 프로세스에서 해당 값을 제외 할 수 있습니다. 랜덤

위대하고 사이트에 오신 것을 환영합니다. 제공된 솔루션으로 운이 있었습니까?
Jack Douglas

고맙습니다. 실제로이 질문을 게시 한 직후 프로젝트를 변경 한 다음 곧 작업을 변경 했으므로 이러한 솔루션을 테스트 할 기회가 없었습니다. 이 경우 답변 선택과 관련하여 어떻게해야합니까?
스튜

답변:


9

사용은 어떻습니까 with recursive

테스트 뷰 :

create view v as 
select *
from ( values ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3)
     ) as baz ("name", "int");

질문:

with recursive t("name", "int") as ( select "name", "int", 1 as span from v
                                     union all
                                     select "name", v."int", t.span+1 as span
                                     from v join t using ("name")
                                     where v."int"=t."int"+1 )
select "name", "start", "start"+span-1 as "end", span
from( select "name", ("int"-span+1) as "start", max(span) as span
      from ( select "name", "int", max(span) as span 
             from t
             group by "name", "int" ) z
      group by "name", ("int"-span+1) ) z;

결과:

 name | start | end | span
------+-------+-----+------
 foo  |     2 |   4 |    3
 foo  |    13 |  13 |    1
 bar  |     1 |   3 |    3
 foo  |    10 |  11 |    2
(4 rows)

나는 그것이 10 억 행 테이블에서 어떻게 수행되는지 알고 싶습니다.


성능이 문제인 경우 work_mem 설정을 사용하면 성능을 향상시키는 데 도움이 될 수 있습니다.
Frank Heikens 2016 년

7

창 기능으로 할 수 있습니다. 기본 개념은 사용하는 것입니다 leadlag기능을 윈도하기에 앞서 현재 행 뒤에 행을 끌어. 그런 다음 시퀀스의 시작 또는 끝이 있는지 계산할 수 있습니다.

create temp view temp_view as
    select
        n,
        val,
        (lead <> val + 1 or lead is null) as islast,
        (lag <> val - 1 or lag is null) as isfirst,
        (lead <> val + 1 or lead is null) and (lag <> val - 1 or lag is null) as orphan
    from
    (
        select
            n,
            lead(val, 1) over( partition by n order by n, val),
            lag(val, 1) over(partition by n order by n, val ),
            val
        from test
        order by n, val
    ) as t
;  
select * from temp_view;
 n  | val | islast | isfirst | orphan 
-----+-----+--------+---------+--------
 bar |   1 | f      | t       | f
 bar |   2 | f      | f       | f
 bar |   3 | t      | f       | f
 bar |  24 | t      | t       | t
 bar |  42 | t      | t       | t
 foo |   2 | f      | t       | f
 foo |   3 | f      | f       | f
 foo |   4 | t      | f       | f
 foo |  10 | f      | t       | f
 foo |  11 | t      | f       | f
 foo |  13 | t      | t       | t
(11 rows)

(보기를 사용하여 논리를 쉽게 따라갈 수있게되었습니다.) 이제 행이 시작인지 끝인지를 알 수 있습니다. 우리는 그것을 행으로 축소해야합니다 :

select
    n as "name",
    first,
    coalesce (last, first) as last,
    coalesce (last - first + 1, 1) as span
from
(
    select
    n,
    val as first,
    -- this will not be excellent perf. since were calling the view
    -- for each row sequence found. Changing view into temp table 
    -- will probably help with lots of values.
    (
        select min(val)
        from temp_view as last
        where islast = true
        -- need this since isfirst=true, islast=true on an orphan sequence
        and last.orphan = false
        and first.val < last.val
        and first.n = last.n
    ) as last
    from
        (select * from temp_view where isfirst = true) as first
) as t
;

 name | first | last | span 
------+-------+------+------
 bar  |     1 |    3 |    3
 bar  |    24 |   24 |    1
 bar  |    42 |   42 |    1
 foo  |     2 |    4 |    3
 foo  |    10 |   11 |    2
 foo  |    13 |   13 |    1
(6 rows)

나에게 맞는 것 같습니다 :)


3

또 다른 창 기능 솔루션. 효율성에 대해 전혀 몰라, 마지막에 실행 계획을 추가했습니다 (행이 적더라도 가치가 높지 않습니다). 당신이 놀러 가고 싶다면 : SQL-Fiddle 테스트

테이블과 데이터 :

CREATE TABLE baz
( name VARCHAR(10) NOT NULL
, i INT  NOT NULL
, UNIQUE  (name, i)
) ;

INSERT INTO baz
  VALUES 
    ('foo', 2),
    ('foo', 3),
    ('foo', 4),
    ('foo', 10),
    ('foo', 11),
    ('foo', 13),
    ('bar', 1),
    ('bar', 2),
    ('bar', 3)
  ;

질문:

SELECT a.name     AS name
     , a.i        AS start
     , b.i        AS "end"
     , b.i-a.i+1  AS span
FROM
      ( SELECT name, i
             , ROW_NUMBER() OVER (PARTITION BY name ORDER BY i) AS rn
        FROM baz AS a
        WHERE NOT EXISTS
              ( SELECT * 
                FROM baz AS prev
                WHERE prev.name = a.name
                  AND prev.i = a.i - 1
              ) 
      ) AS a
    JOIN
      ( SELECT name, i 
             , ROW_NUMBER() OVER (PARTITION BY name ORDER BY i) AS rn
        FROM baz AS a
        WHERE NOT EXISTS
              ( SELECT * 
                FROM baz AS next
                WHERE next.name = a.name
                  AND next.i = a.i + 1
              )
      ) AS b
    ON  b.name = a.name
    AND b.rn  = a.rn
 ; 

쿼리 계획

Merge Join (cost=442.74..558.76 rows=18 width=46)
Merge Cond: ((a.name)::text = (a.name)::text)
Join Filter: ((row_number() OVER (?)) = (row_number() OVER (?)))
-> WindowAgg (cost=221.37..238.33 rows=848 width=42)
-> Sort (cost=221.37..223.49 rows=848 width=42)
Sort Key: a.name, a.i
-> Merge Anti Join (cost=157.21..180.13 rows=848 width=42)
Merge Cond: (((a.name)::text = (prev.name)::text) AND (((a.i - 1)) = prev.i))
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: a.name, ((a.i - 1))
-> Seq Scan on baz a (cost=0.00..21.30 rows=1130 width=42)
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: prev.name, prev.i
-> Seq Scan on baz prev (cost=0.00..21.30 rows=1130 width=42)
-> Materialize (cost=221.37..248.93 rows=848 width=50)
-> WindowAgg (cost=221.37..238.33 rows=848 width=42)
-> Sort (cost=221.37..223.49 rows=848 width=42)
Sort Key: a.name, a.i
-> Merge Anti Join (cost=157.21..180.13 rows=848 width=42)
Merge Cond: (((a.name)::text = (next.name)::text) AND (((a.i + 1)) = next.i))
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: a.name, ((a.i + 1))
-> Seq Scan on baz a (cost=0.00..21.30 rows=1130 width=42)
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: next.name, next.i
-> Seq Scan on baz next (cost=0.00..21.30 rows=1130 width=42)

3

SQL Server에서 previousInt라는 열을 하나 더 추가합니다.

SELECT *
FROM ( VALUES ('foo', 2, NULL),
              ('foo', 3, 2),
              ('foo', 4, 3),
              ('foo', 10, 4),
              ('foo', 11, 10),
              ('foo', 13, 11),
              ('bar', 1, NULL),
              ('bar', 2, 1),
              ('bar', 3, 2)
     ) AS baz ("name", "int", "previousInt")

CHECK 제약 조건을 사용하여 previousInt <int 및 FK 제약 조건 (name, previousInt)이 (name, int)를 참조하고 수밀 한 데이터 무결성을 보장하는 몇 가지 제약 조건이 있는지 확인합니다. 갭을 선택하는 것은 쉽지 않습니다.

SELECT NAME, PreviousInt, Int from YourTable WHERE PreviousInt < Int - 1;

속도를 높이기 위해 간격 만 포함하는 필터링 된 인덱스를 만들 수 있습니다. 즉, 모든 격차가 사전 계산되므로 선택이 매우 빠르며 제약 조건이 사전 계산 된 데이터의 무결성을 보장합니다. 나는 그러한 솔루션을 많이 사용하고 있으며, 그들은 내 시스템 전체에 있습니다.


1

Tabibitosan Method를 찾을 수 있습니다.

https://community.oracle.com/docs/DOC-915680
http://rwijk.blogspot.com/2014/01/tabibitosan.html
https://www.xaprb.com/blog/2006/03/22/find-contiguous-ranges-with-sql/

원래:

SQL> create table mytable (nr)
  2  as
  3  select 1 from dual union all
  4  select 2 from dual union all
  5  select 3 from dual union all
  6  select 6 from dual union all
  7  select 7 from dual union all
  8  select 11 from dual union all
  9  select 18 from dual union all
 10  select 19 from dual union all
 11  select 20 from dual union all
 12  select 21 from dual union all
 13  select 22 from dual union all
 14  select 25 from dual
 15  /

 Table created.

 SQL> with tabibitosan as
 2  ( select nr
 3         , nr - row_number() over (order by nr) grp
 4      from mytable
 5  )
 6  select min(nr)
 7       , max(nr)
 8    from tabibitosan
 9   group by grp
10   order by grp
11  /

   MIN(NR)    MAX(NR)
---------- ----------
         1          3
         6          7
        11         11
        18         22
        25         25

5 rows selected.

이 성능이 더 좋다고 생각합니다.

SQL> r
  1  select min(nr) as range_start
  2    ,max(nr) as range_end
  3  from (-- our previous query
  4    select nr
  5      ,rownum
  6      ,nr - rownum grp
  7    from  (select nr
  8       from   mytable
  9       order by 1
 10      )
 11   )
 12  group by grp
 13* order by 1

RANGE_START  RANGE_END
----------- ----------
      1      3
      6      7
     11     11
     18     22
     25     25

0

대략적인 계획 :

  • 각 이름의 최소값을 선택하십시오 (이름별로 그룹화)
  • min2> min1이며 존재하지 않는 각 이름에 대해 minimum2를 선택하십시오 (하위 조회 : SEL min2-1).
  • Sel max val1> min val1 여기서 max val1 <min val2입니다.

더 이상 업데이트가 발생하지 않을 때까지 2부터 반복하십시오. 거기서부터 Gordian은 최대 분과 최대 분을 그룹화하여 복잡해집니다. 프로그래밍 언어로 갈 것 같아요.

추신 : 몇 가지 샘플 값이있는 멋진 샘플 테이블은 괜찮을 것입니다. 모든 사람이 사용할 수 있으므로 모든 사람이 테스트 데이터를 처음부터 만들지는 않습니다.


0

이 솔루션은 nate c 에서 영감을 얻었습니다. 윈도우 함수와 OVER 절을 사용하는 의 답변 . 흥미롭게도 그 대답은 외부 참조가있는 하위 쿼리로 되돌아갑니다. 다른 수준의 윈도우 기능을 사용하여 행 통합을 완료 할 수 있습니다. 너무 예쁘지는 않지만 강력한 윈도우 기능의 내장 로직을 사용하기 때문에 더 효율적이라고 생각합니다.

nate의 솔루션에서 초기 행 세트가 이미 1) 시작 및 종료 범위 값을 선택하고 2) 사이에 여분의 행을 제거하기 위해 필요한 플래그를 생성했습니다. 쿼리는 열 별칭 사용 방법을 제한하는 윈도우 기능의 제한으로 인해 중첩 된 하위 쿼리를 두 가지 깊이 만 가지고 있습니다. 논리적으로 하나의 중첩 된 하위 쿼리로 결과를 생성 할 수있었습니다.

기타 참고 사항 : 다음은 SQLite3의 코드입니다. SQLite 방언은 postgresql에서 파생되었으므로 매우 유사하며 변경되지 않을 수도 있습니다. OVER 절에 프레이밍 제한을 추가했습니다. lag()lead()함수는 각각 앞뒤에 단일 행 창만 필요하므로 모든 이전 행 의 기본 세트를 유지할 필요가 없었습니다 . 또한 이름에 대한 거부 firstlast단어가 있기 때문에 end예약되어 있습니다.

create temp view test as 
with cte(name, int) AS (
select * from ( values ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3) ))
select * from cte;


SELECT name,
       int AS first, 
       endpoint AS last,
       (endpoint - int + 1) AS span
FROM ( SELECT name, 
             int, 
             CASE WHEN prev <> 1 AND next <> -1 -- orphan
                  THEN int
                WHEN next = -1 -- start of range
                  THEN lead(int) OVER (PARTITION BY name 
                                       ORDER BY int 
                                       ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
                ELSE null END
             AS endpoint
        FROM ( SELECT name, 
                   int,
                   coalesce(int - lag(int) OVER (PARTITION BY name 
                                                 ORDER BY int 
                                                 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 
                            0) AS prev,
                   coalesce(int - lead(int) OVER (PARTITION BY name 
                                                  ORDER BY int 
                                                  ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING),
                            0) AS next
              FROM test
            ) AS mark_boundaries
        WHERE NOT (prev = 1 AND next = -1) -- discard values within range
      ) as raw_ranges
WHERE endpoint IS NOT null
ORDER BY name, first

결과는 다른 답변과 동일합니다.

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.