Chọn hiệu quả bắt đầu và kết thúc nhiều phạm vi liền kề trong truy vấn Postgresql


19

Tôi đã có khoảng một tỷ hàng dữ liệu trong một bảng có tên và số nguyên trong phạm vi 1-288. Đối với một tên cụ thể , mọi int là duy nhất và không phải mọi số nguyên có thể có trong phạm vi đều có mặt - vì vậy có những khoảng trống.

Truy vấn này tạo ra một trường hợp ví dụ:

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

Tôi muốn tạo một bảng tra cứu với một hàng cho mỗi tên và chuỗi các số nguyên liền kề. Mỗi hàng như vậy sẽ chứa:

tên - giá trị của cột tên
bắt đầu - số nguyên đầu tiên trong
kết thúc chuỗi liên tục - giá trị cuối cùng trong
khoảng trình tự liền kề - kết thúc - bắt đầu + 1

Truy vấn này tạo đầu ra ví dụ cho ví dụ trên:

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

Bởi vì tôi có rất nhiều hàng, hiệu quả hơn là tốt hơn. Điều đó nói rằng, tôi chỉ phải chạy truy vấn này một lần, vì vậy nó không phải là một yêu cầu tuyệt đối.

Cảm ơn trước!

Chỉnh sửa:

Tôi nên thêm rằng các giải pháp PL / pgQuery đều được chào đón (vui lòng giải thích mọi Thủ thuật Fancy - Tôi vẫn chưa quen với PL / pgQuery).


Tôi sẽ tìm cách xử lý bảng thành từng phần nhỏ (có thể bằng cách băm "tên" vào N xô, hoặc lấy chữ cái đầu tiên / cuối cùng của tên), sao cho sắp xếp phù hợp với bộ nhớ. Có khả năng quét bảng một số bảng sẽ nhanh hơn so với việc để một loại sắp xếp vào đĩa. Khi tôi đã có nó, tôi sẽ sử dụng các chức năng cửa sổ. Ngoài ra, đừng quên khai thác các mẫu trong dữ liệu. Có thể hầu hết "tên" thực sự có số lượng 288 giá trị, trong trường hợp đó bạn có thể loại trừ các giá trị đó khỏi quy trình chính. Kết thúc lan man ngẫu nhiên :)

tuyệt vời - và chào mừng đến với trang web. Bạn đã có may mắn với các giải pháp được cung cấp?
Jack Douglas

cảm ơn bạn. Tôi thực sự đã thay đổi các dự án ngay sau khi đăng câu hỏi này (và ngay sau đó, tôi đã thay đổi công việc), vì vậy tôi chưa bao giờ có cơ hội thử nghiệm các giải pháp này. Tôi nên làm gì trong việc lựa chọn một câu trả lời trong trường hợp như vậy?
Hầm

Câu trả lời:


9

Cách sử dụng with recursive

xem thử nghiệ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");

truy vấn:

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;

kết quả:

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

Tôi muốn biết làm thế nào điều đó thực hiện trên bảng hàng tỷ của bạn.


Nếu hiệu suất là một vấn đề, chơi với các cài đặt cho work_mem có thể giúp cải thiện hiệu suất.
Frank Heikens

7

Bạn có thể làm điều đó với các chức năng cửa sổ. Ý tưởng cơ bản là sử dụng leadvà các lagchức năng cửa sổ để kéo các hàng phía trước và phía sau hàng hiện tại. Sau đó, chúng ta có thể tính toán nếu chúng ta có bắt đầu hoặc kết thúc chuỗi:

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)

(Tôi đã sử dụng một khung nhìn để logic sẽ dễ theo dõi hơn bên dưới.) Vì vậy, bây giờ chúng ta biết nếu hàng là bắt đầu hay kết thúc. Chúng ta phải thu gọn nó thành hàng:

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)

Có vẻ đúng với tôi :)


3

Một giải pháp chức năng cửa sổ. Không có ý tưởng về hiệu quả, tôi đã thêm kế hoạch thực hiện vào cuối (mặc dù với rất ít hàng, nó có thể không có nhiều giá trị). Nếu bạn muốn chơi xung quanh: SQL-Fiddle test

Bảng và dữ liệu:

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

Truy vấn:

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
 ; 

Kế hoạch truy vấn

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

Trên SQL Server, tôi sẽ thêm một cột có tên trướcInt:

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

Tôi sẽ sử dụng một ràng buộc CHECK để đảm bảo rằng trước đó <int và ràng buộc FK (tên, trướcInt) tham chiếu đến (tên, int) và một vài ràng buộc nữa để đảm bảo tính toàn vẹn dữ liệu kín nước. Thế là xong, chọn khoảng trống là chuyện nhỏ:

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

Để tăng tốc, tôi có thể tạo một chỉ mục được lọc chỉ bao gồm các khoảng trống. Điều này có nghĩa là tất cả các khoảng trống của bạn đều được tính toán trước, vì vậy các lựa chọn rất nhanh và các ràng buộc đảm bảo tính toàn vẹn của dữ liệu được tính toán trước của bạn. Tôi đang sử dụng các giải pháp như vậy rất nhiều, tất cả chúng đều trên hệ thống của tôi.


1

Bạn có thể tìm Phương pháp Tabibitosan:

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/

Về cơ bản:

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.

Tôi nghĩ hiệu suất này tốt hơn:

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

một kế hoạch sơ bộ:

  • Chọn mức tối thiểu cho mỗi tên, (nhóm theo tên)
  • Chọn tối thiểu2 cho mỗi tên, trong đó min2> min1 và không tồn tại (truy vấn con: SEL min2-1).
  • Sel max val1> min val1 trong đó max val1 <min val2.

Lặp lại từ 2. cho đến khi không có thêm cập nhật xảy ra. Từ đó trở nên phức tạp, Gordian, với việc nhóm tối đa phút và phút tối đa. Tôi đoán tôi sẽ đi cho một ngôn ngữ lập trình.

PS: Một bảng mẫu đẹp với một vài giá trị mẫu sẽ ổn, có thể được sử dụng bởi mọi người, vì vậy không phải ai cũng tạo ra testdata của mình từ đầu.


0

Giải pháp này được lấy cảm hứng từ câu trả lời của nate c bằng cách sử dụng các hàm windowing và mệnh đề OVER. Thật thú vị, câu trả lời đó trở lại các truy vấn con với các tham chiếu bên ngoài. Có thể hoàn thành hợp nhất hàng bằng cách sử dụng một cấp độ khác của các chức năng cửa sổ. Nó có thể trông không đẹp lắm, nhưng tôi cho rằng nó hiệu quả hơn vì nó sử dụng logic tích hợp của các chức năng cửa sổ mạnh mẽ.

Tôi nhận ra từ giải pháp của nate rằng tập hợp các hàng ban đầu đã đưa ra các cờ cần thiết thành 1) chọn các giá trị phạm vi bắt đầu và kết thúc VÀ 2) để loại bỏ các hàng thừa ở giữa. Truy vấn đã lồng hai truy vấn con sâu chỉ do giới hạn của các hàm cửa sổ, điều này hạn chế cách sử dụng bí danh cột. Theo logic, tôi có thể tạo ra kết quả chỉ với một truy vấn con lồng nhau.

Một vài lưu ý khác : Dưới đây là mã cho SQLite3. Phương ngữ SQLite có nguồn gốc từ postgresql, vì vậy nó rất giống nhau và thậm chí có thể hoạt động không thay đổi. Tôi đã thêm hạn chế đóng khung cho các mệnh đề QUÁ, vì lag()lead() hàm hàm chỉ cần một cửa sổ một hàng, trước và sau tương ứng (vì vậy không cần phải giữ tập hợp mặc định của tất cả các hàng trước). Tôi cũng đã chọn tên firstlastvì từ endnày được dành riêng.

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

Kết quả cũng giống như các câu trả lời khác, như người ta mong đợi:

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.