Postgresql sorgusunda birden fazla bitişik aralığın başlangıcını ve sonunu etkili bir şekilde seçin


19

1-288 aralığında bir ad ve tamsayı içeren bir tabloda yaklaşık bir milyar satır veri var. Belirli bir ad için , her int benzersizdir ve aralıktaki her olası tam sayı yoktur - bu nedenle boşluklar vardır.

Bu sorgu bir örnek olay oluşturur:

--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")

Her isim ve bitişik tamsayılar dizisi için bir satır içeren bir arama tablosu oluşturmak istiyorum. Bu satırların her biri şunları içerir:

name - ad sütunu
başlangıcının değeri - bitişik sıra
sonundaki ilk tam sayı - bitişik sıra
aralığındaki son değer - son - başlat + 1

Bu sorgu, yukarıdaki örnek için örnek çıktı oluşturur:

--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)

Çok fazla satırım olduğu için daha verimli olmak daha iyidir. Yani, sadece bir kez bu sorguyu çalıştırmak zorunda dedi, bu yüzden mutlak bir gereklilik değil.

Şimdiden teşekkürler!

Düzenle:

PL / pgSQL çözümlerinin açık olduğunu eklemeliyim (lütfen herhangi bir Fantezi Hileyi açıklayın - PL / pgSQL için hala yeniyim).


Ben tablo bir tür bellekte sığacak şekilde (belki "isim" N kova içine hashing veya adın ilk / son harfini alarak) yeterince küçük parçalar halinde işlemek için bir yol bulur. Tabloyu birkaç tablo taramanın, bir tür dökülmenin diske dökülmesine izin vermekten daha hızlı olması muhtemeldir. Bir kez ben vardı, ben pencereleme işlevlerini kullanarak gitmek istiyorum. Ayrıca, verilerdeki kalıplardan yararlanmayı unutmayın. Belki de "ad" ın çoğunun aslında 288 değeri vardır, bu durumda bu değerleri ana işlemden hariç tutabilirsiniz. Rastgele

harika - ve siteye hoş geldiniz. Sunulan çözümlerde şansınız oldu mu?
Jack Douglas

teşekkür ederim. Aslında bu soruyu gönderdikten kısa bir süre sonra projeleri değiştirdim (ve kısa bir süre sonra işleri değiştirdim), bu yüzden bu çözümleri test etme şansım olmadı. böyle bir durumda bir cevap seçme açısından ne yapmalıyım?
Güveç

Yanıtlar:


9

Kullanmaya ne dersiniz? with recursive

test görünümü:

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");

sorgu:

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;

sonuç:

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

Bunun milyar satır masanızda nasıl bir performans gösterdiğini bilmek isterim.


Performans bir sorunsa, work_mem ayarlarıyla oynamak performansı artırmaya yardımcı olabilir.
Frank Heikens

7

Pencereleme işlevleriyle yapabilirsiniz. Temel fikir, satırları geçerli satırın önüne ve arkasına çekmek için işlevleri leadve lagpencereleri kullanmaktır . O zaman dizinin başlangıcı veya sonu varsa hesaplayabiliriz:

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)

(Bir mantık kullandım, böylece mantığın aşağıda takip edilmesi daha kolay olacak.) Şimdi satırın başlangıç ​​mı yoksa bitiş mi olduğunu biliyoruz. Bunu sıraya daraltmalıyız:

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)

Bana doğru görünüyor :)


3

Başka bir pencere işlevi çözümü. Verimlilik hakkında hiçbir fikrim yok, sonunda yürütme planını ekledim (çok az satır olmasına rağmen, muhtemelen çok değerli değil). Oynamak istiyorsanız: SQL-Fiddle testi

Tablo ve veriler:

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)
  ;

Sorgu:

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
 ; 

Sorgu Planı

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'da, previousInt adlı bir sütun daha ekleyecektim:

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")

PreviousInt <int ve bir FK kısıtlaması (name, previousInt) (name, int) ve su geçirmez veri bütünlüğünü sağlamak için birkaç kısıtlama başvurmak emin olmak için bir CHECK kısıtlaması kullanırsınız. Bu, boşlukları seçmek önemsizdir:

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

Hızlandırmak için, sadece boşlukları içeren filtrelenmiş bir dizin oluşturabilirim. Bu, tüm boşluklarınızın önceden hesaplandığı anlamına gelir, bu nedenle seçimler çok hızlıdır ve kısıtlamalar önceden hesaplanmış verilerinizin bütünlüğünü sağlar. Bu tür çözümleri çok kullanıyorum, hepsi benim sistemimde.


1

Tabibitosan Yöntemi'ni arayabilirsiniz:

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/

Temelde:

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.

Bu performansı daha iyi düşünüyorum:

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

kaba bir plan:

  • Her ad için minimum değeri seçin (ada göre gruplandır)
  • Her isim için min2> min1 olan ve olmayan (alt sorgu: SEL min2-1) minimum 2'yi seçin.
  • Sel maks val1> min val1 burada maks val1 <min val2.

Daha fazla güncelleme yapılmayana kadar 2. tekrarlayın. Oradan karmaşıklaşıyor Gordian, max dakika ve min maks. Sanırım bir programlama dili seçerdim.

Not: Birkaç örnek değeri olan güzel bir örnek tablo iyi olur, bu herkes tarafından kullanılabilir, bu yüzden herkes test verilerini sıfırdan oluşturmaz.


0

Bu çözüm, pencereleme işlevleri ve OVER yan tümcesi kullanılarak nate c'nin yanıtından esinlenmiştir . İlginçtir ki, bu cevap dış referansları olan alt sorgulara geri döner. Satır konsolidasyonunu başka bir pencere fonksiyonu seviyesi kullanarak tamamlamak mümkündür. Çok hoş görünmeyebilir, ancak güçlü pencereleme işlevlerinin yerleşik mantığını kullandığından daha verimli olduğunu düşünüyorum.

Nate'in çözümünden, ilk satır kümesinin zaten 1) başlangıç ​​ve bitiş aralığı değerlerini seçmesi ve 2) arasındaki ekstra satırları ortadan kaldırmak için gerekli bayrakları ürettiğini fark ettim. Sorgu, yalnızca sütun takma adlarının nasıl kullanılabileceğini kısıtlayan pencere işlevlerinin sınırlamaları nedeniyle alt sorguları iki derinden iç içe geçirmiştir. Mantıksal olarak, sonuçları sadece bir iç içe alt sorgu ile üretebilirdim.

Birkaç not daha : SQLite3 için kod aşağıdadır. SQLite lehçesi postgresql'den türetilmiştir, bu yüzden çok benzerdir ve değişmeden bile çalışabilir. OVER deyimlerine çerçeveleme kısıtlaması ekledim, çünkü lag()ve lead()işlevlerinin sırasıyla önce ve sonra yalnızca tek satırlı bir pencereye ihtiyacı var (bu nedenle önceki tüm satırların varsayılan kümesini tutmaya gerek yoktu ). Ben de isimleri seçtim firstve lastkelime endayrıldığından beri .

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

Sonuçlar beklendiği gibi diğer cevaplar gibidir:

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.