Tablodan art arda "n" boş sayılarını bulun


17

Böyle sayılarla bazı tablo var (durum ÜCRETSİZ veya Atandı)

id_set sayı durumu         
-----------------------
1 000001 Atandı
1 000002 ÜCRETSİZ
1 000003 Atandı
1 000004 ÜCRETSİZ
1 000005 ÜCRETSİZ
1 000006 Atandı
1 000007 Atandı
1 000008 ÜCRETSİZ
1 000009 ÜCRETSİZ
1 000010 ÜCRETSİZ
1 000011 Atandı
1 000012 Atandı
1 000013 Atandı
1 000014 ÜCRETSİZ
1 000015 Atandı

ve "n" ardışık sayıları bulmam gerekiyor, bu yüzden n = 3 için sorgu dönecekti

1 000008 ÜCRETSİZ
1 000009 ÜCRETSİZ
1 000010 ÜCRETSİZ

Her id_set için yalnızca ilk olası grubu döndürmelidir (aslında, sorgu başına sadece id_set için yürütülür)

PENCERE işlevlerini kontrol ediyordum, bazı sorguları denedim COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), ama hepsi bu kadar :) :) Mantık, Postgres'de bunu nasıl yapacağımı düşünemedim.

Status = 'FREE' her sayı için önceki satırları sayma WINDOW işlevlerini kullanarak sanal sütun oluşturma, sonra sayım benim "n" numarasına eşit nerede ilk sayı seçin düşünüyordum.

Veya sayıları duruma göre gruplayabilirsiniz, ancak yalnızca bir tanesi Atandı diğerine Atandı ve yalnızca en az "n" sayı içeren grupları seçin

DÜZENLE

Bu sorguyu buldum (ve biraz değiştirdim)

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

FREE / ASSIGNED sayı grupları oluşturur, ancak koşulu karşılayan ilk gruptaki tüm sayılara sahip olmak istiyorum

SQL Keman

Yanıtlar:


17

Bu sorunudur. Aynı id_setsette boşluk veya kopya olmadığı varsayılırsa :

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
;

İşte bu sorgu için bir SQL Fiddle demo * bağlantısı: http://sqlfiddle.com/#!1/a2633/1 .

GÜNCELLEME

Yalnızca bir set döndürmek için bir sıralama turu daha ekleyebilirsiniz:

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
;

İşte bunun için bir demo: http://sqlfiddle.com/#!1/a2633/2 .

Hiç bir set yapmak gerekirse başınaid_set değiştirmek RANK()böyle çağrıyı:

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

Ayrıca, sorgunun en küçük eşleşen kümeyi döndürmesini sağlayabilirsiniz (örneğin, ilk önce tam olarak üç ardışık sayının ilk kümesini döndürmeye çalışın, aksi takdirde dört, beş vb.):

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

veya bunun gibi (her biri için bir tane id_set):

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

* Bu cevapta bağlanan SQL Fiddle demoları, 9.2.1 sürümü şu anda çalışmıyor gibi göründüğü için 9.1.8 örneğini kullanmaktadır.


Çok teşekkür ederim, bu güzel görünüyor, ancak değiştirmek için sadece ilk sayı grubu döndürülüyor mu? Eğer cnt> = 2 olarak değiştirirsem, 5 sayı alırım (2 grup = 2 + 3 sayı)
boobiq

@boobiq: Kişi başına id_setbir tane mi yoksa sadece bir tane mi istiyorsun ? Bu, en başından itibaren parçasıysa, sorunuzu güncelleyin. (Böylece diğerleri tüm gereksinimleri görebilir ve önerilerini sunabilir veya cevaplarını güncelleyebilir.)
Andriy M

Sorumu (istediğim dönüşten sonra) düzenledim, sadece bir id_set için yürütülecek, bu yüzden sadece ilk olası grup bulundu
boobiq

10

Basit ve hızlı bir varyant:

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( Soruda belirtildiği gibi) içindeki aralıksız bir sayı dizisi gerektirir .

  • Olası değerlerin herhangi bir sayı için çalışır statusyanında 'FREE'bile, NULL.

  • Önemli özellik çıkarmak için row_number()gelen numbersivil eleme satırları ortadan kaldırarak sonra. Ardışık sayılar aynı şekilde sonuçlanır grpve grpayrıca artan sırada olmaları garanti edilir .

  • Sonra GROUP BY grpüyeleri sayabilir ve sayabilirsiniz. İlk oluşumu istediğiniz gibi göründüğünden ORDER BY grp LIMIT 1ve dizinin başlangıç ​​pozisyonunu ve uzunluğunu alırsınız (> = n olabilir ).

Satır kümesi

Gerçek bir sayı kümesi elde etmek için tabloya başka bir kez bakmayın. İle çok daha ucuz 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;

Örnek değerlerinizde sizin gibi sıfırların bulunduğu bir dize görüntülemek istiyorsanız to_char(), FM(dolgu modu) değiştiricisiyle kullanın:

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

Genişletilmiş test durumu ve her iki sorgu ile SQL Fiddle .

Yakından ilgili cevap:


8

Bu, bunu yapmanın oldukça genel bir yoludur.

Unutmayın ki numbersütununuzun ardışık olmasına bağlıdır . Bir Pencere işlevi değilse ve / veya CTE tip çözümüne ihtiyaç duyulursa:

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

Deklare Postgres'de böyle çalışmaz.
a_horse_with_no_name

@a_horse_with_no_name Lütfen bunu düzeltmekten çekinmeyin :)
JNK

Pencere fonksiyonu yok, çok güzel! Her ne kadar olması gerektiğini düşünüyorum M.number-consec+1(örneğin 10 için olması gerekir 10-3+1=8).
Andriy M

@AndriyM Bu numberalanın sıralı değerlerine dayandığından kırılgan "hoş" değil . Matematiğe iyi bir çağrı düzeltirim.
JNK

2
Postgres sözdizimini düzeltme özgürlüğünü aldım. birincisi EXISTSbasitleştirilebilir. Biz sadece emin olmak gerekir beri herhangi n önceki satır var, biz bırakın AND status = 'FREE'. Ve 2. koşulu değiştirecek EXISTSiçin status <> 'FREE'gelecekte ilave seçenek karşı sertleşmeye.
Erwin Brandstetter

5

Bu, 3 sayının yalnızca ilkini döndürür. Değerlerinin numberardışık olmasını gerektirmez . SQL-Fiddle'da test edildi :

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 ;

Ve bu tüm sayıları gösterecektir (arka arkaya 3 veya daha fazla 'FREE'pozisyon varsa):

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 ;

Bu durumda 5 ardışık sayı - bu nedenle fark 4 veya başka bir deyişle olmalıdır count(r3.number) = n ve olmalıdır r2.number = r1.number + n - 1.

Birleşimlerle:

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 yönlü kartezyen bir ürünün bunu yapmanın etkili bir yolu olduğunu mu düşünüyorsunuz?
JNK

Alternatif olarak modern JOINsözdizimi ile yazabilir misiniz ?
JNK

Peki pencere fonksiyonlarına güvenmek istemedim ve herhangi bir sql-db üzerinde çalışacak bir çözüm verdi.
Ununoktiyum

-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

Daha iyi performans için imleci kullanıyorum - SELECT çok sayıda satır döndürürse
Ravi Ramaswamy

Kodu vurgulayıp { }editördeki düğmeye basarak cevabınızı yeniden biçimlendirdim . Zevk almak!
jcolebrand

Cevabınızı düzenlemek ve imlecin neden daha iyi performans sağladığını düşündüğünüzü de söylemek isteyebilirsiniz.
jcolebrand

İmleç sıralı bir işlemdir. Neredeyse her seferinde tek bir kayıt okumak gibi. Durumlardan birinde, MEM TEMP tablosunu tek bir imleçle değiştirdim. Bu işlem süresini 26 saatten 6 saate indirdi. Sonuç kümesi üzerinden döngü için nesile WHILE kullanmak zorunda kaldı.
Ravi Ramaswamy

Varsayımlarınızı hiç test etmeyi denediniz mi? Şaşırmış olabilirsiniz. Köşe kasaları hariç, düz SQL en hızlısıdır.
Erwin Brandstetter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.