Temukan “n” nomor gratis berturut-turut dari tabel

16

Saya memiliki beberapa tabel dengan angka seperti ini (statusnya GRATIS atau DITANDATANGANI)

status nomor id_set         
-----------------------
1 000001 DITANDATANGANI
1 000002 GRATIS
1 000003 DITANDATANGANI
1 000004 GRATIS
1 000005 GRATIS
1 000006 DITANDATANGANI
1 000007 DITANDATANGANI
1 000008 GRATIS
1 000009 GRATIS
1 000010 GRATIS
1 000011 DITANDATANGANI
1 000012 DITANDATANGANI
1 000013 DITANDATANGANI
1 000014 GRATIS
1 000015 DITANDATANGANI

dan saya perlu menemukan angka berurutan "n", jadi untuk n = 3, kueri akan kembali

1 000008 GRATIS
1 000009 GRATIS
1 000010 GRATIS

Itu harus mengembalikan hanya kelompok pertama yang mungkin dari setiap id_set (pada kenyataannya, itu akan dieksekusi hanya untuk id_set per permintaan)

Saya sedang memeriksa fungsi WINDOW, mencoba beberapa pertanyaan seperti COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), tapi hanya itu yang saya dapat :) Saya tidak bisa memikirkan logika, bagaimana melakukannya di Postgres.

Saya sedang berpikir tentang membuat kolom virtual menggunakan fungsi WINDOW menghitung baris sebelumnya untuk setiap nomor di mana status = 'GRATIS', lalu pilih angka pertama, di mana jumlah sama dengan nomor "n" saya.

Atau mungkin mengelompokkan nomor berdasarkan status, tetapi hanya dari satu yang DITANDATANGANI ke yang lain DITANDATANGANI dan pilih hanya grup yang mengandung setidaknya "n" angka

EDIT

Saya menemukan pertanyaan ini (dan mengubahnya sedikit)

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

yang menghasilkan grup angka GRATIS / TANDA TANGAN, tetapi saya ingin memiliki semua angka dari hanya grup pertama yang memenuhi persyaratan

SQL Fiddle

boobiq
sumber

Jawaban:

16

Ini adalah masalah . Dengan asumsi tidak ada celah atau duplikat di id_setset yang sama :

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
;

Berikut ini tautan demo * SQL Fiddle untuk kueri ini: http://sqlfiddle.com/#!1/a2633/1 .

MEMPERBARUI

Untuk mengembalikan hanya satu set, Anda dapat menambahkan satu ronde lagi peringkat:

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
;

Berikut ini demo untuk yang ini juga: http://sqlfiddle.com/#!1/a2633/2 .

Jika Anda perlu membuatnya satu set perid_set , ubah RANK()panggilan seperti ini:

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

Selain itu, Anda dapat membuat kueri mengembalikan set pencocokan terkecil (yaitu mencoba pertama untuk mengembalikan set pertama tepat tiga angka berturut-turut jika ada, jika tidak empat, lima dll), seperti ini:

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

atau seperti ini (satu per id_set):

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

* Demo SQL Fiddle yang ditautkan dalam jawaban ini menggunakan instance 9.1.8 karena 9.2.1 yang tampaknya tidak berfungsi saat ini.

Andriy M
sumber
Terima kasih banyak, ini terlihat bagus, tetapi dimungkinkan untuk mengubahnya sehingga hanya grup nomor pertama yang dikembalikan? Jika saya ubah ke cnt> = 2, maka saya mendapat 5 angka (2 grup = 2 + 3 angka)
boobiq
@boobiq: Apakah Anda ingin satu per id_setatau hanya satu? Harap perbarui pertanyaan Anda jika ini dimaksudkan sebagai bagian dari awal. (Agar orang lain dapat melihat persyaratan lengkap dan menawarkan saran mereka atau memperbarui jawaban mereka.)
Andriy M
Saya mengedit pertanyaan saya (setelah ingin kembali), itu akan dieksekusi hanya untuk satu id_set, jadi hanya grup pertama yang mungkin ditemukan
boobiq
10

Varian sederhana dan cepat :

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;
  • Membutuhkan urutan angka tanpa celah di number(seperti yang disediakan dalam pertanyaan).

  • Bekerja untuk sejumlah nilai yang mungkin di statussamping 'FREE', bahkan dengan NULL.

  • Fitur utama adalah untuk mengurangi row_number()dari numbersetelah menghilangkan non-kualifikasi baris. Nomor yang berurutan berakhir dengan angka yang sama grp- dan grpjuga dijamin dalam urutan menaik .

  • Kemudian Anda dapat GROUP BY grpdan menghitung anggota. Karena Anda tampaknya menginginkan kejadian pertama , ORDER BY grp LIMIT 1dan Anda mendapatkan posisi awal dan panjang urutan (bisa> = n ).

Set baris

Untuk mendapatkan satu set angka yang sebenarnya, jangan mencari di tabel lain waktu. Jauh lebih murah dengan 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;

Jika Anda benar-benar menginginkan string dengan nol di depan seperti yang Anda tampilkan di nilai contoh Anda, gunakan to_char()dengan FMpengubah (mode isi):

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

SQL Fiddle dengan test case yang diperluas dan kedua query.

Jawaban yang terkait erat:

Erwin Brandstetter
sumber
8

Ini adalah cara yang cukup umum untuk melakukan ini.

Ingatlah bahwa itu tergantung pada numberkolom Anda berturut-turut. Jika itu bukan fungsi Window dan / atau tipe-solusi CTE mungkin akan diperlukan:

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')
JNK
sumber
Deklarasi tidak akan berfungsi seperti itu di Postgres.
a_horse_with_no_name
@a_horse_with_no_name Silahkan memperbaikinya kalau begitu :)
JNK
Tidak ada fungsi jendela, sangat bagus! Meskipun saya pikir seharusnya M.number-consec+1(misalnya untuk 10 itu harus 10-3+1=8).
Andriy M
@AndriyM Yah itu tidak "bagus" itu rapuh karena bergantung pada nilai berurutan dari numberbidang itu. Panggilan bagus pada matematika saya akan memperbaikinya.
JNK
2
Saya mengambil kebebasan untuk memperbaiki sintaks untuk Postgres. yang pertama EXISTSbisa disederhanakan. Karena kita hanya perlu memastikan setiap n baris sebelumnya ada, kita bisa drop AND status = 'FREE'. Dan saya akan mengubah kondisi di 2 EXISTSuntuk status <> 'FREE'mengeras itu terhadap pilihan ditambahkan di masa depan.
Erwin Brandstetter
5

Ini akan mengembalikan hanya yang pertama dari 3 angka. Itu tidak mengharuskan nilai-nilai numberberturut-turut. Diuji di 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 ;

Dan ini akan menampilkan semua angka (di mana ada 3 'FREE'posisi berturut-turut atau lebih ):

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 ;
ypercubeᵀᴹ
sumber
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 ;

Dalam hal ini 5 angka berurutan - oleh karena itu perbedaannya harus 4 atau dengan kata lain count(r3.number) = ndan r2.number = r1.number + n - 1.

Dengan bergabung:

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 ;
Ununoctium
sumber
Anda pikir produk kartesius 4 arah adalah cara yang efisien untuk melakukan ini?
JNK
Atau dapatkah Anda menulisnya dengan JOINsintaksis modern ?
JNK
Yah saya tidak ingin bergantung pada fungsi jendela dan memberikan solusi yang akan bekerja pada 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
Ravi Ramaswamy
sumber
Saya menggunakan kursor untuk kinerja yang lebih baik - jika SELECT mengembalikan banyak baris
Ravi Ramaswamy
Saya memformat ulang jawaban Anda dengan menyorot kode dan menekan { }tombol pada editor. Nikmati!
jcolebrand
Anda mungkin juga ingin mengedit jawaban Anda dan memberi tahu mengapa menurut Anda kursor memberikan kinerja yang lebih baik.
jcolebrand
Kursor adalah proses berurutan. Ini hampir seperti membaca file flat satu catatan pada suatu waktu. Dalam salah satu situasi, saya mengganti tabel MEM TEMP dengan satu kursor tunggal. Ini mengurangi waktu pemrosesan dari 26 jam menjadi 6 jam. Saya harus menggunakan WHILE yang telah dihapus untuk mengulang melalui resultset.
Ravi Ramaswamy
Pernahkah Anda mencoba menguji asumsi Anda? Anda mungkin terkejut. Kecuali untuk kasus sudut, SQL sederhana adalah yang tercepat.
Erwin Brandstetter