Suchen Sie in der Tabelle nach "n" aufeinanderfolgenden freien Nummern

16

Ich habe eine Tabelle mit Zahlen wie dieser (Status ist entweder FREI oder ZUGEWIESEN)

ID_SET-Nummernstatus         
-----------------------
1 000001 ZUGEWIESEN
1 000002 KOSTENLOS
1 000003 ZUGEWIESEN
1 000004 KOSTENLOS
1 000005 KOSTENLOS
1 000006 ZUGEWIESEN
1 000007 ZUGEWIESEN
1 000008 KOSTENLOS
1 000009 KOSTENLOS
1 000010 KOSTENLOS
1 000011 ZUGEWIESEN
1 000012 ZUGEWIESEN
1 000013 ZUGEWIESEN
1 000014 KOSTENLOS
1 000015 ZUGEWIESEN

und ich muss "n" aufeinanderfolgende Zahlen finden, so dass für n = 3 die Abfrage zurückkehren würde

1 000008 KOSTENLOS
1 000009 KOSTENLOS
1 000010 KOSTENLOS

Es sollte nur die erste mögliche Gruppe jedes id_set zurückgeben (tatsächlich würde es nur für id_set pro Abfrage ausgeführt).

Ich habe WINDOW-Funktionen überprüft, einige Abfragen ausprobiert COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), aber das ist alles, was ich habe :) Mir fiel keine Logik ein, wie man das in Postgres macht.

Ich habe darüber nachgedacht, eine virtuelle Spalte mit WINDOW-Funktionen zu erstellen, indem ich die vorhergehenden Zeilen für jede Nummer gezählt habe, bei der status = 'FREE' ist, und dann die erste Nummer ausgewählt habe, bei der count gleich meiner "n" -Nummer ist.

Oder gruppieren Sie Nummern nach Status, aber nur von einem ZUGEWIESENEN zu einem anderen ZUGEWIESENEN, und wählen Sie nur Gruppen aus, die mindestens "n" Nummern enthalten

BEARBEITEN

Ich habe diese Abfrage gefunden (und ein wenig geändert)

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

das produziert Gruppen von FREE / ASSIGNED-Nummern, aber ich möchte alle Nummern nur von der ersten Gruppe haben, die die Bedingung erfüllt

SQL-Geige

boobiq
quelle

Antworten:

16

Dies ist ein Problem. Vorausgesetzt, es gibt keine Lücken oder Duplikate in derselben id_setMenge:

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
;

Hier ist ein SQL Fiddle-Demo * -Link für diese Abfrage: http://sqlfiddle.com/#!1/a2633/1 .

AKTUALISIEREN

Um nur einen Satz zurückzugeben, können Sie in einer weiteren Runde der Rangfolge Folgendes hinzufügen:

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
;

Hier ist auch für diesen eine Demo: http://sqlfiddle.com/#!1/a2633/2 .

Wenn Sie jemals einen Satz proid_set Satz erstellen müssen , ändern Sie den RANK()Aufruf wie folgt:

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

Außerdem können Sie die Abfrage so einstellen, dass sie die kleinste übereinstimmende Menge zurückgibt (dh versuchen Sie zunächst, die erste Menge von genau drei aufeinanderfolgenden Zahlen zurückzugeben, falls vorhanden, andernfalls vier, fünf usw.):

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

oder wie folgt (eine pro id_set):

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

* Die in dieser Antwort verlinkten SQL Fiddle-Demos verwenden die 9.1.8-Instanz, da die 9.2.1-Instanz momentan nicht zu funktionieren scheint.

Andriy M
quelle
Vielen Dank, das sieht gut aus, kann aber geändert werden, sodass nur die erste Zahlengruppe zurückgegeben wird. Wenn ich es auf cnt> = 2 ändere, erhalte ich 5 Zahlen (2 Gruppen = 2 + 3 Zahlen)
Boobiq
@boobiq: Möchtest du eine pro id_setoder nur eine? Bitte aktualisieren Sie Ihre Frage, wenn dies von Anfang an beabsichtigt war. (Damit andere die vollständigen Anforderungen einsehen und ihre Vorschläge unterbreiten oder ihre Antworten aktualisieren können.)
Andriy M
Ich habe meine Frage bearbeitet (nach der gewünschten Rückgabe), sie wird nur für eine id_set ausgeführt, daher wurde nur die erste mögliche Gruppe gefunden
boobiq
10

Eine einfache und schnelle Variante:

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;
  • Benötigt eine lückenlose Folge von Zahlen in number(wie in der Frage angegeben).

  • Arbeitet für eine beliebige Anzahl von möglichen Werten in statusaußer 'FREE', auch mit NULL.

  • Das Hauptmerkmal ist das Subtrahieren row_number()von, numbernachdem nicht qualifizierende Zeilen entfernt wurden. Aufeinanderfolgende Nummern enden in derselben Reihenfolgegrp - und sie grpsind garantiert auch in aufsteigender Reihenfolge .

  • Dann können Sie GROUP BY grpdie Mitglieder zählen. Da du scheinbar das erste Vorkommen haben ORDER BY grp LIMIT 1willst, erhältst du Startposition und Länge der Sequenz (kann> = n sein ).

Reihe von Zeilen

Schauen Sie nicht ein weiteres Mal in der Tabelle nach, um eine tatsächliche Anzahl von Zahlen zu erhalten. Viel billiger mit 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;

Wenn Sie tatsächlich eine Zeichenfolge mit führenden Nullen wünschen, wie sie in Ihren Beispielwerten angezeigt wird, verwenden Sie to_char()den FMModifikator (Füllmodus):

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

SQL Fiddle mit erweitertem Testfall und beiden Abfragen.

Eng verwandte Antwort:

Erwin Brandstetter
quelle
8

Dies ist ein ziemlich allgemeiner Weg, um dies zu tun.

Denken Sie daran, dass dies davon abhängt, ob Ihre numberKolumne fortlaufend ist. Wenn es sich nicht um eine Windows-Funktion und / oder eine CTE-Typ-Lösung handelt, wird wahrscheinlich Folgendes benötigt:

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
quelle
Das wird in Postgres nicht so funktionieren.
a_horse_with_no_name
@a_horse_with_no_name Bitte zögern Sie nicht, das dann zu beheben :)
JNK
Keine Fensterfunktionen, sehr schön! Obwohl ich denke, es sollte sein M.number-consec+1(zB für 10 müsste es sein 10-3+1=8).
Andriy M
@AndriyM Nun, es ist nicht "schön", es ist zerbrechlich, da es auf sequentiellen Werten dieses numberFeldes beruht . Ich werde es korrigieren.
JNK
2
Ich habe mir die Freiheit genommen, die Syntax für Postgres zu korrigieren. das erste EXISTSkönnte vereinfacht werden. Da wir nur sicherstellen müssen , jede n früheren Reihen vorhanden sind , können wir die fallen AND status = 'FREE'. Und ich würde den Zustand in der 2. ändern , EXISTSum status <> 'FREE'es gegen zusätzlichen Optionen in der Zukunft zu härten.
Erwin Brandstetter
5

Dies gibt nur die erste der 3 Zahlen zurück. Es ist nicht erforderlich, dass die Werte von numberfortlaufend sind. Getestet bei 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 ;

Und dies zeigt alle Zahlen (wo es 3 oder mehr aufeinanderfolgende 'FREE'Positionen gibt):

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ᵀᴹ
quelle
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 ;

In diesem Fall 5 aufeinanderfolgende Zahlen - daher muss die Differenz 4 oder mit anderen Worten count(r3.number) = nund sein r2.number = r1.number + n - 1.

Mit Joins:

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
quelle
Sie denken, ein kartesisches 4-Wege-Produkt ist ein effizienter Weg, dies zu tun?
JNK
Alternativ können Sie es mit moderner JOINSyntax schreiben ?
JNK
Nun, ich wollte mich nicht auf Fensterfunktionen verlassen und gab eine Lösung, die auf jeder SQL-DB funktionieren würde.
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
quelle
Ich benutze Cursor für eine bessere Leistung - sollte das SELECT eine große Anzahl von Zeilen zurückgeben
Ravi Ramaswamy
Ich habe Ihre Antwort neu formatiert, indem ich den Code hervorgehoben und den { }Knopf im Editor gedrückt habe. Genießen!
Jcolebrand
Möglicherweise möchten Sie auch Ihre Antwort bearbeiten und erklären, warum der Cursor Ihrer Meinung nach eine bessere Leistung bietet.
Jcolebrand
Cursor ist ein sequentieller Prozess. Es ist fast so, als würde man eine Flat-Datei nacheinander lesen. In einer der Situationen habe ich die MEM TEMP-Tabelle durch einen einzelnen Cursor ersetzt. Dies verringerte die Verarbeitungszeit von 26 Stunden auf 6 Stunden. Ich musste neseted WHILE verwenden, um die Ergebnismenge zu durchlaufen.
Ravi Ramaswamy
Haben Sie jemals versucht, Ihre Annahmen zu testen? Sie könnten überrascht sein. Mit Ausnahme von Eckfällen ist einfaches SQL am schnellsten.
Erwin Brandstetter