Знайдіть “n” послідовних вільних номерів із таблиці


16

У мене є таблиця з такими номерами (статус або БЕЗКОШТОВНО, або ЗНАЧЕНО)

статус номера id_set         
-----------------------
1 000001 ВИЗНАЧЕНО
1 000002 БЕЗКОШТОВНО
1 000003 ВИЗНАЧЕНО
1 000004 БЕЗКОШТОВНО
1 000005 БЕЗКОШТОВНО
1 000006 ВИЗНАЧЕНО
1 000007 ВИЗНАЧЕНО
1 000008 БЕЗКОШТОВНО
1 000009 БЕЗКОШТОВНО
1 000010 БЕЗКОШТОВНО
1 000011 ВИЗНАЧЕНО
1 000012 ВИЗНАЧЕНО
1 000013 ВИЗНАЧЕНО
1 000014 БЕЗКОШТОВНО
1 000015 ВИЗНАЧЕНО

і мені потрібно знайти "n" послідовних номерів, тому для n = 3 запит повернеться

1 000008 БЕЗКОШТОВНО
1 000009 БЕЗКОШТОВНО
1 000010 БЕЗКОШТОВНО

Він повинен повертати лише першу можливу групу кожного id_set (насправді він би виконувався лише для id_set за запитом)

Я перевіряв функції WINDOW, спробував деякі запити на кшталт COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), але це все, що я отримав :) Я не міг подумати про логіку, як це зробити в Postgres.

Я думав над створенням віртуального стовпця за допомогою функцій WINDOW, що рахують попередні рядки для кожного числа, де status = 'FREE', а потім виберіть перше число, де кількість дорівнює моєму «n» номеру.

Або, можливо, номери груп за статусом, але лише від одного ПІДПРИЄМЛЕННЯ до іншого ЗНАЧЕННЯ та виберіть лише групи, що містять принаймні "n" числа

EDIT

Я знайшов цей запит (і трохи змінив його)

WITH q AS
(
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY id_set, status ORDER BY number) AS rnd,
         ROW_NUMBER() OVER (PARTITION BY id_set ORDER BY number) AS rn
  FROM numbers
)
SELECT id_set,
       MIN(number) AS first_number,
       MAX(number) AS last_number,
       status,
       COUNT(number) AS numbers_count
FROM q
GROUP BY id_set,
         rnd - rn,
         status
ORDER BY
     first_number

яка створює групи БЕЗКОШТОВНИХ / ВИЗНАЧЕНИХ номерів, але я хотів би, щоб усі номери були тільки з першої групи, яка відповідає умові

SQL Fiddle

Відповіді:


16

Це проблема . Якщо в одному id_setнаборі немає пропусків або дублікатів :

WITH partitioned AS (
  SELECT
    *,
    number - ROW_NUMBER() OVER (PARTITION BY id_set) AS grp
  FROM atable
  WHERE status = 'FREE'
),
counted AS (
  SELECT
    *,
    COUNT(*) OVER (PARTITION BY id_set, grp) AS cnt
  FROM partitioned
)
SELECT
  id_set,
  number
FROM counted
WHERE cnt >= 3
;

Ось демонстраційна посилання SQL Fiddle * для цього запиту: http://sqlfiddle.com/#!1/a2633/1 .

ОНОВЛЕННЯ

Щоб повернути лише один набір, ви можете додати ще один раунд рейтингу:

WITH partitioned AS (
  SELECT
    *,
    number - ROW_NUMBER() OVER (PARTITION BY id_set) AS grp
  FROM atable
  WHERE status = 'FREE'
),
counted AS (
  SELECT
    *,
    COUNT(*) OVER (PARTITION BY id_set, grp) AS cnt
  FROM partitioned
),
ranked AS (
  SELECT
    *,
    RANK() OVER (ORDER BY id_set, grp) AS rnk
  FROM counted
  WHERE cnt >= 3
)
SELECT
  id_set,
  number
FROM ranked
WHERE rnk = 1
;

Ось демонстрація і для цього: http://sqlfiddle.com/#!1/a2633/2 .

Якщо вам коли - небудь знадобиться , щоб зробити це один набір наid_set , змінити RANK()виклик , як це:

RANK() OVER (PARTITION BY id_set ORDER BY grp) AS rnk

Крім того, ви можете змусити запросити повернути найменший набір (тобто спершу спробуйте повернути перший набір рівно трьох послідовних чисел, якщо він існує, інакше чотири, п’ять тощо), як це:

RANK() OVER (ORDER BY cnt, id_set, grp) AS rnk

або так (один на id_set):

RANK() OVER (PARTITION BY id_set ORDER BY cnt, grp) AS rnk

* Демоски SQL Fiddle, пов'язані у цій відповіді, використовують екземпляр 9.1.8, оскільки 9.2.1 наразі не працює.


Дуже дякую, це виглядає приємно, але можливо змінити так, що повертається лише перша група номерів? Якщо я зміню його на cnt> = 2, то я отримаю 5 чисел (2 групи = 2 + 3 числа)
boobiq

@boobiq: Ви хочете одного id_setчи просто одного? Будь ласка, оновіть своє запитання, якщо це позначалося як його частина з самого початку. (Щоб інші могли побачити всі вимоги та запропонувати свої пропозиції чи оновити свої відповіді.)
Андрій М

Я відредагував своє запитання (після розшуку повернення), воно буде виконано лише для одного id_set, тому знайдена лише перша можлива група
boobiq

10

Простий і швидкий варіант:

SELECT min(number) AS first_number, count(*) AS ct_free
FROM (
    SELECT *, number - row_number() OVER (PARTITION BY id_set ORDER BY number) AS grp
    FROM   tbl
    WHERE  status = 'FREE'
    ) x
GROUP  BY grp
HAVING count(*) >= 3  -- minimum length of sequence only goes here
ORDER  BY grp
LIMIT  1;
  • Потрібна безперервна послідовність чисел у number(як це передбачено у запитанні).

  • Працює для будь-якої кількості можливих значень, statusкрім того 'FREE', навіть з NULL.

  • Головна особливість є віднімати row_number()з numberпісля усунення некваліфікаційних рядків. Послідовні номери закінчуються однаковими grp- а grpтакож гарантується у порядку зростання .

  • Тоді ви можете GROUP BY grpрахувати членів. Так як вам здається, що ви хочете першої зустрічі, ORDER BY grp LIMIT 1і ви отримуєте початкове положення і довжину послідовності (може бути> = n ).

Набір рядків

Щоб отримати фактичний набір чисел, не шукайте таблицю іншим разом. Набагато дешевше generate_series():

SELECT generate_series(first_number, first_number + ct_free - 1)
    -- generate_series(first_number, first_number + 3 - 1) -- only 3
FROM  (
   SELECT min(number) AS first_number, count(*) AS ct_free
   FROM  (
      SELECT *, number - row_number() OVER (PARTITION BY id_set ORDER BY number) AS grp
      FROM   tbl
      WHERE  status = 'FREE'
      ) x
   GROUP  BY grp
   HAVING count(*) >= 3
   ORDER  BY grp
   LIMIT  1
   ) y;

Якщо ви дійсно хочете рядок з провідними нулями , як ви показуєте в вашому прикладі значень, використовуйте to_char()з FMмодифікатором (режим заповнення):

SELECT to_char(generate_series(8, 11), 'FM000000')

SQL Fiddle з розширеним тестовим випадком та обома запитами.

Тісно пов'язана відповідь:


8

Це досить загальний спосіб зробити це.

Майте на увазі, це залежить від того, чи буде ваш numberстовпець послідовним. Якщо це не функція Window та / або тип рішення CTE, можливо, знадобиться:

SELECT 
    number
FROM
    mytable m
CROSS JOIN
   (SELECT 3 AS consec) x
WHERE 
    EXISTS
       (SELECT 1 
        FROM mytable
        WHERE number = m.number - x.consec + 1
        AND status = 'FREE')
    AND NOT EXISTS
       (SELECT 1 
        FROM mytable
        WHERE number BETWEEN m.number - x.consec + 1 AND m.number
        AND status = 'ASSIGNED')

Оголошення не працюватиме так у Postgres.
a_horse_with_no_name

@a_horse_with_no_name Будь ласка, виправте це тоді :)
JNK

Без віконних функцій, дуже приємно! Хоча я думаю, що так і має бути M.number-consec+1(наприклад, для 10 це повинно бути 10-3+1=8).
Андрій М

@AndriyM Ну це не "приємно", це крихко, оскільки він покладається на послідовні значення цього numberполя. Гарний дзвінок з математики, я його виправлю.
JNK

2
Я взяв на себе сміливість виправити синтаксис для Postgres. перше EXISTSможна було спростити. Оскільки нам потрібно лише переконатися, що існують будь-які n попередніх рядків, ми можемо скинути AND status = 'FREE'. І я хотів би змінити стан у 2 - EXISTSдо status <> 'FREE'твердеть його від доданих варіантів в майбутньому.
Erwin Brandstetter

5

Це поверне лише перший із 3-х номерів. При цьому не потрібно, щоб значення numberпослідовних були. Тестовано на SQL-Fiddle :

WITH cte3 AS
( SELECT
    *,
    COUNT(CASE WHEN status = 'FREE' THEN 1 END) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING)
      AS cnt
  FROM atable
)
SELECT
  id_set, number
FROM cte3
WHERE cnt = 3 ;

І це покаже всі цифри (де є 3 і більше послідовних 'FREE'позицій):

WITH cte3 AS
( SELECT
    *,
    COUNT(CASE WHEN status = 'FREE' THEN 1 END) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING)
      AS cnt
  FROM atable
)
, cte4 AS
( SELECT
    *, 
    MAX(cnt) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
      AS maxcnt
  FROM cte3
)
SELECT
  id_set, number
FROM cte4
WHERE maxcnt >= 3 ;

0
select r1.number from some_table r1, 
some_table r2,
some_table r3,
some_table r4 
where r3.number <= r2.number 
and r3.number >= r1.number 
and r3.status = 'FREE' 
and r2.number = r1.number + 4 
and r4.number <= r2.number 
and r4.number >= r1.number 
and r4.status = 'ASSIGNED'
group by r1.number, r2.number having count(r3.number) = 5 and count(r4.number) = 0 order by r1.number asc limit 1 ;

У цьому випадку 5 послідовних чисел - тому різниця повинна бути 4 або іншими словами count(r3.number) = nі r2.number = r1.number + n - 1.

З приєднанням:

select r1.number 
from some_table r1 join 
 some_table r2 on (r2.number = r1.number + :n -1) join
 some_table r3 on (r3.number <= r2.number and r3.number >= r1.number) join
 some_table r4 on (r4.number <= r2.number and r4.number >= r1.number)
where  
 r3.status = 'FREE' and
 r4.status = 'ASSIGNED'
group by r1.number, r2.number having count(r3.number) = :n and count(r4.number) = 0 order by r1.number asc limit 1 ;

Ви вважаєте, що чотиристоронній декартовий продукт - це ефективний спосіб зробити це?
JNK

Можна також написати це сучасним JOINсинтаксисом?
JNK

Ну, я не хотів покладатися на функції вікон і дав рішення, яке буде працювати на будь-якому sql-db.
Ununoctium

-1
CREATE TABLE #ConsecFreeNums
(
     id_set BIGINT
    ,number VARCHAR(10)
    ,status VARCHAR(10)
)

CREATE TABLE #ConsecFreeNumsResult
(
     Seq    INT
    ,id_set BIGINT
    ,number VARCHAR(10)
    ,status VARCHAR(10)
)

INSERT #ConsecFreeNums
SELECT 1, '000002', 'FREE' UNION
SELECT 1, '000003', 'ASSIGNED' UNION
SELECT 1, '000004', 'FREE' UNION
SELECT 1, '000005', 'FREE' UNION
SELECT 1, '000006', 'ASSIGNED' UNION
SELECT 1, '000007', 'ASSIGNED' UNION
SELECT 1, '000008', 'FREE' UNION
SELECT 1, '000009', 'FREE' UNION
SELECT 1, '000010', 'FREE' UNION
SELECT 1, '000011', 'ASSIGNED' UNION
SELECT 1, '000012', 'ASSIGNED' UNION
SELECT 1, '000013', 'ASSIGNED' UNION
SELECT 1, '000014', 'FREE' UNION
SELECT 1, '000015', 'ASSIGNED'

DECLARE @id_set AS BIGINT, @number VARCHAR(10), @status VARCHAR(10), @number_count INT, @number_count_check INT

DECLARE ConsecFreeNumsCursor CURSOR FAST_FORWARD FOR
SELECT
       id_set
      ,number
      ,status
 FROM
      #ConsecFreeNums
WHERE id_set = 1
ORDER BY number

OPEN ConsecFreeNumsCursor

FETCH NEXT FROM ConsecFreeNumsCursor INTO @id_set, @number, @status

SET @number_count_check = 3
SET @number_count = 0

WHILE @@FETCH_STATUS = 0
BEGIN
    IF @status = 'ASSIGNED'
    BEGIN
        IF @number_count = @number_count_check
        BEGIN
            SELECT 'Results'
            SELECT * FROM #ConsecFreeNumsResult ORDER BY number
            BREAK
        END
        SET @number_count = 0
        TRUNCATE TABLE #ConsecFreeNumsResult
    END
    ELSE
    BEGIN
        SET @number_count = @number_count + 1
        INSERT #ConsecFreeNumsResult SELECT @number_count, @id_set, @number, @status
    END
    FETCH NEXT FROM ConsecFreeNumsCursor INTO @id_set, @number, @status
END

CLOSE ConsecFreeNumsCursor
DEALLOCATE ConsecFreeNumsCursor

DROP TABLE #ConsecFreeNums
DROP TABLE #ConsecFreeNumsResult

Я використовую курсор для кращої продуктивності - якщо SELECT поверне велику кількість рядків
Ravi Ramaswamy

Я переформатував вашу відповідь, виділивши код і натиснувши { }кнопку редактора. Насолоджуйтесь!
jcolebrand

Ви також можете відредагувати свою відповідь і сказати, чому ви вважаєте, що курсор забезпечує кращу ефективність.
jcolebrand

Курсор - послідовний процес. Це майже як читання плоского файлу один за одним. В одній із ситуацій я замінив таблицю MEM TEMP одним єдиним курсором. Це зменшило час обробки з 26 годин до 6 годин. Мені довелося використовувати вкладений WHILE, щоб пройти цикл через набір результатів.
Раві Рамасвамі

Ви коли-небудь намагалися перевірити, чи є ваші припущення? Ви можете бути здивовані. За винятком кутових випадків, звичайний SQL є найшвидшим.
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.