2016-07-14 14 views
1

タイムスタンプの範囲とユーザーIDを含むPostgreSQL(9.4)の表があり、重複する範囲(同じユーザーID)を1つのレコードに集約する必要があります。PostgreSQLの時間範囲が重複しています

これを達成するために複雑なCTEを試しましたが、私たちの(40,000+行)実テーブルにはいくつかの問題があり、問題を複雑にしています。私はおそらく再帰的なCTEが必要であるという結論に達しましたが、私はそれを書いている運がありませんでした。

テストテーブルを作成し、データを入力するコードです。これは私たちのテーブルのレイアウトではありませんが、例のためには十分に近いです。私は、個々のレコードは、以前、使用してウィンドウと重なっているかどうかを判断するためにこれを使用することができ

select * from test order by fk_user_id, sessionrange 

:私は、彼らが開始された時刻によってソートされたセッションを取得するには、この操作を行うことができることを発見した

CREATE TABLE public.test 
(
    id serial, 
    sessionrange tstzrange, 
    fk_user_id integer 
); 

insert into test (sessionrange, fk_user_id) 
values 
('[2016-01-14 11:57:01-05,2016-01-14 12:06:59-05]', 1) 
,('[2016-01-14 12:06:53-05,2016-01-14 12:17:28-05]', 1) 
,('[2016-01-14 12:17:24-05,2016-01-14 12:21:56-05]', 1) 
,('[2016-01-14 18:18:00-05,2016-01-14 18:42:09-05]', 2) 
,('[2016-01-14 18:18:08-05,2016-01-14 18:18:15-05]', 1) 
,('[2016-01-14 18:38:12-05,2016-01-14 18:48:20-05]', 1) 
,('[2016-01-14 18:18:16-05,2016-01-14 18:18:26-05]', 1) 
,('[2016-01-14 18:18:24-05,2016-01-14 18:18:31-05]', 1) 
,('[2016-01-14 18:18:12-05,2016-01-14 18:18:20-05]', 3) 
,('[2016-01-14 19:32:12-05,2016-01-14 23:18:20-05]', 3) 
,('[2016-01-14 18:18:16-05,2016-01-14 18:18:26-05]', 4) 
,('[2016-01-14 18:18:24-05,2016-01-14 18:18:31-05]', 2); 

機能:

SELECT *, sessionrange && lag(sessionrange) OVER (PARTITION BY fk_user_id ORDER BY sessionrange) 
FROM test 
ORDER BY fk_user_id, sessionrange 

しかし、これはただ一つ前のレコードは、現在の1(レコードid = 6を参照)と重なるかどうかを検出します。私は、パーティションの始めまでずっと検出する必要があります。

その後、最も早いセッションの開始と終了する最後のセッションの終了を見つけるために、一緒に重なるレコードをグループ化する必要があります。

私は見落としていることをこれを行う方法があると確信しています。これらの重複するレコードをどのように崩壊させることができますか?

答えて

1

配列の要素として重複範囲をマージするのは比較的簡単です。簡単にするために次の関数はset of tstzrangeを返す:

create or replace function merge_ranges(tstzrange[]) 
returns setof tstzrange language plpgsql as $$ 
declare 
    t tstzrange; 
    r tstzrange; 
begin 
    foreach t in array $1 loop 
     if r && t then r:= r + t; 
     else 
      if r notnull then return next r; 
      end if; 
      r:= t; 
     end if; 
    end loop; 
    if r notnull then return next r; 
    end if; 
end $$; 

ただユーザの範囲を集約機能を使用:

select fk_user_id, merge_ranges(array_agg(sessionrange)) 
from test 
group by 1 
order by 1, 2 

fk_user_id |     merge_ranges      
------------+----------------------------------------------------- 
      1 | ["2016-01-14 17:57:01+01","2016-01-14 18:21:56+01"] 
      1 | ["2016-01-15 00:18:08+01","2016-01-15 00:18:15+01"] 
      1 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:31+01"] 
      1 | ["2016-01-15 00:38:12+01","2016-01-15 00:48:20+01"] 
      2 | ["2016-01-15 00:18:00+01","2016-01-15 00:42:09+01"] 
      3 | ["2016-01-15 00:18:12+01","2016-01-15 00:18:20+01"] 
      3 | ["2016-01-15 01:32:12+01","2016-01-15 05:18:20+01"] 
      4 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:26+01"] 
(8 rows)  

あるいは、アルゴリズムは、一つの機能ループにテーブル全体に適用することができます。私は確かではないが、大規模なデータセットの場合、この方法は高速にすべきである。

create or replace function merge_ranges_in_test() 
returns setof test language plpgsql as $$ 
declare 
    curr test; 
    prev test; 
begin 
    for curr in 
     select * 
     from test 
     order by fk_user_id, sessionrange 
    loop 
     if prev notnull and prev.fk_user_id <> curr.fk_user_id then 
      return next prev; 
      prev:= null; 
     end if; 
     if prev.sessionrange && curr.sessionrange then 
      prev.sessionrange:= prev.sessionrange + curr.sessionrange; 
     else 
      if prev notnull then 
       return next prev; 
      end if; 
      prev:= curr; 
     end if; 
    end loop; 
    return next prev; 
end $$; 

結果:

select * 
from merge_ranges_in_test(); 

id |     sessionrange      | fk_user_id 
----+-----------------------------------------------------+------------ 
    1 | ["2016-01-14 17:57:01+01","2016-01-14 18:21:56+01"] |   1 
    5 | ["2016-01-15 00:18:08+01","2016-01-15 00:18:15+01"] |   1 
    7 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:31+01"] |   1 
    6 | ["2016-01-15 00:38:12+01","2016-01-15 00:48:20+01"] |   1 
    4 | ["2016-01-15 00:18:00+01","2016-01-15 00:42:09+01"] |   2 
    9 | ["2016-01-15 00:18:12+01","2016-01-15 00:18:20+01"] |   3 
10 | ["2016-01-15 01:32:12+01","2016-01-15 05:18:20+01"] |   3 
11 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:26+01"] |   4 
(8 rows) 

問題は非常に興味深いです。私は再帰的な解決策を見つけようとしましたが、手続き的な試みが最も自然で効率的であるようです。


私は最終的に再帰的解を見つけました。クエリは、重複行を削除し、その圧縮された同等の挿入:

with recursive cte (user_id, ids, range) as (
    select t1.fk_user_id, array[t1.id, t2.id], t1.sessionrange + t2.sessionrange 
    from test t1 
    join test t2 
     on t1.fk_user_id = t2.fk_user_id 
     and t1.id < t2.id 
     and t1.sessionrange && t2.sessionrange 
union all 
    select user_id, ids || t.id, range + sessionrange 
    from cte 
    join test t 
     on user_id = t.fk_user_id 
     and ids[cardinality(ids)] < t.id 
     and range && t.sessionrange 
    ), 
list as (
    select distinct on(id) id, range, user_id 
    from cte, unnest(ids) id 
    order by id, upper(range)- lower(range) desc 
    ), 
deleted as (
    delete from test 
    where id in (select id from list) 
    ) 
insert into test 
select distinct on (range) id, range, user_id 
from list 
order by range, id; 

結果:

select * 
from test 
order by 3, 2; 

id |     sessionrange      | fk_user_id 
----+-----------------------------------------------------+------------ 
    1 | ["2016-01-14 17:57:01+01","2016-01-14 18:21:56+01"] |   1 
    5 | ["2016-01-15 00:18:08+01","2016-01-15 00:18:15+01"] |   1 
    7 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:31+01"] |   1 
    6 | ["2016-01-15 00:38:12+01","2016-01-15 00:48:20+01"] |   1 
    4 | ["2016-01-15 00:18:00+01","2016-01-15 00:42:09+01"] |   2 
    9 | ["2016-01-15 00:18:12+01","2016-01-15 00:18:20+01"] |   3 
10 | ["2016-01-15 01:32:12+01","2016-01-15 05:18:20+01"] |   3 
11 | ["2016-01-15 00:18:16+01","2016-01-15 00:18:26+01"] |   4 
(8 rows) 
+0

それが収まるように全く適合させる必要はありませんでしたので、私は、最初のソリューションと一緒に行くことになったが私の実際のスキーマ。これは本当に簡単に作業して正しいと思われます。私は確かにいくつかの追加のテストをする必要がありますが、私はあなたの答えを受け入れるために今日後でここに戻ってくると思います。ありがとうございました! –

+0

いくつかのテストを行いましたが、これは私が望むようにすべてを組み合わせたように見えます。ありがとう! –

+1

あなたの質問は私の挑戦であることが判明しました。立てられない私は機能なしでそれをすることはできません;) – klin

関連する問題