Ефективно виберіть початок і кінець декількох суміжних діапазонів у запиті Postgresql


19

Я отримав близько мільярда рядків даних у таблиці з іменем та цілим числом в діапазоні 1-288. Для даного імені кожен 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)

Оскільки у мене так багато рядків, ефективніше - краще. При цьому я повинен запустити цей запит лише один раз, тому це не є абсолютною вимогою.

Спасибі заздалегідь!

Редагувати:

Слід додати, що рішення 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)

Мені було б цікаво дізнатися, як це працює на вашій таблиці мільярдів рядків.


Якщо продуктивність є проблемою, гра з налаштуваннями для work_mem може допомогти підвищити продуктивність.
Френк Хайкенс

7

Це можна зробити за допомогою віконних функцій. Основна ідея полягає у використанні функцій вікон leadта lagвікон для виведення рядків вперед та позаду поточного ряду. Тоді ми можемо обчислити, чи є у нас початок або кінець послідовності:

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:

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, щоб переконатися, що попереднійInt <int, та обмеження FK (ім'я, попереднійInt) посилаються на (ім'я, 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. до тих пір, поки не відбудеться більше оновлення. Звідси стає складним, гордійським, з групуванням на максимум хвилин і хв. Макс. Я думаю, я б пішов на мову програмування.

PS: Гарна вибіркова таблиця з кількома значеннями вибірки була б чудовою, яка може бути використана всіма, тому не кожен створює свої тестові дані з нуля.


0

Це рішення надихається з відповіді nate c за допомогою віконних функцій та пункту OVER. Цікаво, що ця відповідь повертається до підзапитів із зовнішніми посиланнями. Завершити консолідацію рядків можна за допомогою іншого рівня віконних функцій. Це може виглядати не надто красиво, але я вважаю, що це більш ефективно, оскільки він використовує вбудовану логіку потужних віконних функцій.

З рішення Nate я зрозумів, що початковий набір рядків уже передбачив необхідні прапорці до 1) вибору значень початкового та кінцевого діапазонів І 2) для усунення зайвих рядків між ними. Запит вклав два глибокі підзапити лише через обмеження віконних функцій, які обмежують використання псевдонімів стовпців. Логічно я міг би отримати результати лише одним вкладеним підзапитом.

Ще кілька зауважень : Далі йде код для 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.