ค้นหาหมายเลขฟรี“ 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 นับจำนวนแถวก่อนหน้าสำหรับทุกหมายเลขที่สถานะ = 'ฟรี' จากนั้นเลือกหมายเลขแรกโดยที่การนับมีค่าเท่ากับจำนวน "n" ของฉัน

หรืออาจจัดกลุ่มหมายเลขตามสถานะ แต่จากกลุ่มหนึ่งไปยังอีกกลุ่มหนึ่งเท่านั้นและเลือกกลุ่มที่มีตัวเลขอย่างน้อย "n"

แก้ไข

ฉันพบข้อความค้นหานี้ (และเปลี่ยนมันเล็กน้อย)

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

ซึ่งสร้างกลุ่มของหมายเลขฟรี / ที่กำหนด แต่ฉันต้องการให้มีหมายเลขทั้งหมดจากกลุ่มแรกเท่านั้นที่ตรงตามเงื่อนไข

ซอ 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

UPDATE

หากต้องการส่งคืนเพียงชุดเดียวคุณสามารถเพิ่มการจัดอันดับได้อีกหนึ่งรอบ:

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หรือต่อหนึ่ง? โปรดอัปเดตคำถามของคุณหากนี่เป็นส่วนหนึ่งของคำถาม (เพื่อให้ผู้อื่นสามารถดูข้อกำหนดทั้งหมดและเสนอคำแนะนำหรืออัปเดตคำตอบของพวกเขาได้)
Andriy M

ฉันแก้ไขคำถามของฉัน (หลังจากที่ต้องการกลับมา) มันจะถูกดำเนินการเพียงหนึ่ง 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')

ซอ 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)
Andriy M

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

คุณคิดว่าผลิตภัณฑ์คาร์ทีเซียนแบบ 4 ทิศทางเป็นวิธีที่มีประสิทธิภาพในการทำสิ่งนี้หรือไม่?
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 ชั่วโมง ฉันต้องใช้ในขณะที่ neseted วนลูปผ่าน resultset
Ravi Ramaswamy

คุณเคยลองทดสอบสมมติฐานของคุณหรือไม่? คุณอาจประหลาดใจ ยกเว้นกรณีมุม SQL ธรรมดาจะเร็วที่สุด
Erwin Brandstetter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.