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)

行が非常に多いので、より効率的であることが望ましいです。ただし、このクエリを実行する必要があるのは1回だけなので、絶対的な要件ではありません。

前もって感謝します!

編集:

PL / pgSQLソリューションは大歓迎です(ファンシーなトリックについて説明してください-私はまだPL / pgSQLは初めてです)。


ソートがメモリに収まるように、「名前」をN個のバケットにハッシュするか、名前の最初/最後の文字を取得することで、十分に小さなチャンクでテーブルを処理する方法を見つけます。テーブルをいくつかのテーブルをスキャンすると、ソートをディスクに流出させるよりも高速になる可能性があります。それができたら、ウィンドウ関数を使用します。また、データのパターンを活用することを忘れないでください。おそらく、ほとんどの「名前」には実際に288個の値があり、その場合、これらの値をメインプロセスから除外できます。ランダムなとりとめの終わり:)

素晴らしい-そしてサイトへようこそ。提供されたソリューションに幸運はありましたか?
ジャックダグラス

ありがとうございました。私は実際にこの質問を投稿した直後にプロジェクトを変更し(その後すぐに仕事を変更しました)、これらのソリューションをテストする機会がありませんでした。そのような場合に答えを選択するという点で私は何をすべきですか?
シチュー

回答:


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の設定を使用すると、パフォーマンスが向上する場合があります。
フランクヘイケンズ

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という名前の列をもう1つ追加します。

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

あなたは、タビビトサン法を探すことができます:

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

大まかな計画:

  • 各名前の最小値を選択します(名前でグループ化)
  • 各名前の最小値2を選択します。min2> min1であり、存在しません(サブクエリ:SEL min2-1)。
  • Sel max val1> min val1ここで、max val1 <min val2。

更新が発生しなくなるまで、2から繰り返します。そこから、Gordianは複雑になります。Gordianは、maxのminとminのmaxをグループ化しています。プログラミング言語に行くと思います。

PS:いくつかのサンプル値を持つ素敵なサンプルテーブルは問題なく、誰でも使用できるので、誰もが最初からテストデータを作成するわけではありません。


0

このソリューションは、ウィンドウ関数とOVER句を使用したnate cの回答から着想を得ています。興味深いことに、その答えは外部参照を使用したサブクエリに戻ります。別のレベルのウィンドウ関数を使用して、行の統合を完了することができます。見た目はあまり美しくないかもしれませんが、強力なウィンドウ関数の組み込みロジックを利用しているため、より効率的であると思います。

私はnateのソリューションから、最初の行のセットはすでに1)開始および終了範囲値を選択し、2)間にある余分な行を削除するために必要なフラグを生成していることに気付きました。クエリには、列エイリアスの使用方法を制限するウィンドウ関数の制限のために、2つの深さのサブクエリがネストされています。論理的には、1つのネストされたサブクエリだけで結果を生成できました。

その他の注意事項:SQLite3のコードは次のとおりです。SQLiteの方言はpostgresqlから派生しているため、非常によく似ており、変更されずに動作することさえあります。OVER句にフレーミング制限を追加しました。これは、lag()およびlead()関数がそれぞれ前後の単一行ウィンドウのみを必要とするためです(したがって、前のすべての行のデフォルトセットを保持する必要はありませんでした)。私も名前firstを選んだ。lastというの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.