web-dev-qa-db-ja.com

再帰クエリのより簡単な代替手段を探す

実際のクエリはより複雑ですが、私が直面している問題はこれに抽出することができます:

単調に増加する整数の行セットをフィルタリングするクエリ。これにより、最終結果セットで-、row(n + 1).value> = row(n)になります。値+ 5

私が解決する必要がある実際の問題については、行セット数は1000です。

明確にするいくつかの例:

  • 行が1、2、3、4、5の場合:クエリは次を返す必要があります:1
  • 行が1,5,7,10,11,12,13の場合:クエリは次を返す必要があります:1,7,12
  • 行が6,8,11,16,20,23の場合:クエリは次を返す必要があります:6,11,16,23
  • 行が次の場合:6,8,12,16,20,23:クエリは次を返す必要があります:6,12,20

次のクエリで必要な結果を得ることができましたが、複雑すぎるようです。異なる "..with t(k).."のコメントを外して試してください。

同じ結果を得るための簡素化または代替アプローチを探しています。

with recursive r(n, pri) as (
    with t(k) as (values (1),(2),(3),(4),(5))   -- the data we want to filter
    -- with t(k) as (values (1),(5),(7),(10),(11),(12),(13))
    -- with t(k) as (values (6),(8),(11),(16),(20),(23))
    -- with t(k) as (values (6),(8),(12),(16),(20),(23))
    select min(k), 1::bigint from t             -- bootstrap for recursive processing. 1 here represents rank().
    UNION
    select k, (rank() over(order by k)) rr      -- rank() is required just to filter out the rows we dont want from the final result set, and no other reason
    from r, t 
    where t.k >= r.n+5 and r.pri = 1            -- capture the rows we want, AND unfortunately a bunch of rows we dont want 
)
select n from r where pri = 1; 
7
sr33

これは大変でした!これがsimplerかどうかはわかりませんが、少なくともウィンドウ関数を使用せず、フィルターで除外する必要のある行を生成しません。

with recursive r(k, n) as (
    with t(k) as (values (1),(2),(3),(4),(5))   -- the data we want to filter
    -- with t(k) as (values (1),(5),(7),(10),(11),(12),(13))
    -- with t(k) as (values (6),(8),(11),(16),(20),(23))
    -- with t(k) as (values (6),(8),(12),(16),(20),(23))
         ,t2(k,n) AS (select k, (select min(k) from t tt where k >= t.k+5) from t) -- precalculate what's next
    select * from (select * from t2 limit 1) x   -- limit 1 directly fails in a union!
    UNION ALL
    select t2.* from r, t2 where t2.k = r.n      -- on each iteration, keep only the value that matches the previous precalculated next one
)
select k from r

テスト

この代替案は、非常に小さなセットでは効率が悪いように見えますが、パフォーマンスはほぼ直線的ですが、オリジナルは指数関数的に遅くなります。

drop table if exists t;
create temp table t(k) AS
with recursive r(n) as (
  select floor(random()*10)::int + 1
  UNION ALL
  select n + floor(random()*10)::int + 1
  from r
  where n < 100000)        -- change to increase or reduce set
select * from r;           -- surprisingly fast! Go PG!
create index on t(k);

with recursive r(n, pri) as (
    select min(k), 1::bigint from t
    UNION
    select k, (rank() over(order by k)) rr
    from r, t 
    where t.k >= r.n+5 and r.pri = 1
)
select count(*) from r where pri = 1; -- I aborted it after waiting for a minute

with recursive r(k, n) as (
    with t2(k,n) AS (select k, (select min(k) from t tt where k >= t.k+5) from t)
    select * from (select * from t2 limit 1) x
    UNION ALL
    select t2.* from r, t2 where t2.k = r.n
)
select count(*) from r -- 26" in my server

通常、前の行のdataにアクセスする必要がある場合は、ウィンドウ化された集計関数を利用できますが、ここでは現在の行の計算は前のresult、行ではありません。この種の質問については、許容できるパフォーマンスのセットベースのソリューションを見つけることはできませんでした。

しかし、行ごとのロジックを使用して書くのは簡単です。一時テーブルでROW_NUMBERを具体化したい場合、次の行を見つけるのはn+1だけです。

CREATE TABLE temp AS
 ( SELECT k, 
      ROW_NUMBER() OVER (ORDER BY k) AS rn 
   FROM t
 )
;
-- I don't know enough about PostgreSQL, but this is probably needed for performance
create unique index on temp(rn)
;

WITH RECURSIVE cte(k,rn, k1) AS 
 (
   SELECT k, rn, k AS k1
   FROM tmp 
   WHERE rn = 1  -- start with the minimum value
   UNION ALL
   SELECT tmp.k, tmp.rn,      
      CASE 
        WHEN tmp.k >= cte.k1 + 5 
        THEN tmp.k   -- use the new value
        ELSE cte.k1  -- keep the old value
      END 
   FROM tmp JOIN cte
     ON tmp.rn = cte.rn+1 -- next row
)
SELECT count(k) 
FROM cte
WHERE k = k1

これは非常に高速に実行され、@ ZiggyCrueltyfreeZeitgeisterの fiddle をハイジャックしました。

実際、これは古いスタイルのカーソルロジック処理であり、実際にはカーソルの方が高速な場合があります(一時テーブルとROW_NUMBERと再帰は必要ありません)。私のPostgreSQLの知識は非常に限られていて、PGランドで何が好まれるのかわかりません:)

2
dnoeth