2014-03-03 19 views
7

Ich fand viele Stackoverflow QnAs über aufeinanderfolgende Tage.
Noch Antworten sind zu kurz für mich zu verstehen, was vor sich geht.aufeinanderfolgenden Tagen in Sql

Für Konkretion, ich werde ein Modell bilden (oder eine Tabelle)
(I postgresql bin mit, wenn es einen Unterschied macht.)

CREATE TABLE work (
    id integer NOT NULL, 
    user_id integer NOT NULL, 
    arrived_at timestamp with time zone NOT NULL 
); 


insert into work(user_id, arrived_at) values(1, '01/03/2011'); 
insert into work(user_id, arrived_at) values(1, '01/04/2011'); 
  1. (In einfachsten Form) Für Bei einem bestimmten Benutzer möchte ich den letzten aufeinander folgenden Datumsbereich finden.

  2. (Mein ultimatives Ziel) Für einen bestimmten Benutzer möchte ich seine aufeinanderfolgenden Arbeitstage finden.
    Wenn er gestern zur Arbeit kam, hat er noch (heute) die Chance, an aufeinanderfolgenden Tagen zu arbeiten. Also zeige ich ihm aufeinanderfolgende Tage bis gestern.
    Aber wenn er gestern verpasst hat, sind seine aufeinanderfolgenden Tage entweder 0 oder 1, abhängig davon, ob er heute kam oder nicht.

Sagen Sie heute ist der 8. Tag.

3 * 5 6 7 * = 3 days (5 to 7) 
3 * 5 6 7 8 = 4 days (5 to 8) 
3 4 5 * 7 * = 1 day (7 to 7) 
3 * * * * * = 0 day 
3 * * * * 8 = 1 day (8 to 8) 
+1

Interessante Frage ... können Sie bitte Schema der Tabelle hinzufügen? –

+2

Schema und Beispieldaten (wie 'CREATE TABLE' und' INSERT') und erwartete Ergebnisse bitte. –

+0

Bitte fügen Sie echte DDL + Beispieldaten hinzu. Keine Kurzschrift, bitte. – joop

Antwort

2

Hier ist meine Lösung für dieses Problem CTE

WITH RECURSIVE CTE(attendanceDate) 
AS 
(
    SELECT * FROM 
    (
     SELECT attendanceDate FROM attendance WHERE attendanceDate = current_date 
     OR attendanceDate = current_date - INTERVAL '1 day' 
     ORDER BY attendanceDate DESC 
     LIMIT 1 
    ) tab 
    UNION ALL 

    SELECT a.attendanceDate FROM attendance a 
    INNER JOIN CTE c 
    ON a.attendanceDate = c.attendanceDate - INTERVAL '1 day' 
) 
SELECT COUNT(*) FROM CTE; 

Überprüfen Sie den Code in SQL Fiddle

Hier verwendet wird, wie die Abfrage funktioniert:

  1. Er wählt Rekord heute von attendance Tabelle.Wenn die heutige Datensatz nicht verfügbar ist, dann wählt er gestern Rekord
  2. Es hält dann das Hinzufügen rekursiv einen Tag vor dem geringstenen Datum notiert

Wenn Sie letzten Mal in Folge Datumsbereich auswählen möge, unabhängig davon, wann war Benutzer neuester Besuch (heute , gestern oder x Tage vor), dann die Initialisierung Teil CTE muss von unten ersetzt werden Schnipsel:

SELECT MAX(attendanceDate) FROM attendance 

[EDIT] Hier Abfrage in SQL Fiddle ist die Frage # 1 löst: SQL Fiddle

+0

Kannst du mir die originale Geige geben, die meine Frage # 1 zu lösen schien? (ohne die heutige/gestrige Überlegung) damit ich zuerst die Grundlagen Ihrer Anfrage verstehen kann? – eugene

+0

http://www.sqlfiddle.com/#!15/7016f/1 –

+0

Wenn ein Benutzer mehr als einmal am Tag teilnehmen kann, müssen Sie die Bearbeitung unter –

0
-- some data 
CREATE table dayworked (
     id SERIAL NOT NULL PRIMARY KEY 
     , user_id INTEGER NOT NULL 
     , arrived_at DATE NOT NULL 
     , UNIQUE (user_id, arrived_at) 
     ); 

INSERT INTO dayworked(user_id, arrived_at) VALUES 
(1, '2014-02-03') 
,(1, '2014-02-05') 
,(1, '2014-02-06') 
,(1, '2014-02-07') 
     -- 
,(2, '2014-02-03') 
,(2, '2014-02-05') 
,(2, '2014-02-06') 
,(2, '2014-02-07') 
,(2, '2014-02-08') 
     -- 
,(3, '2014-02-03') 
,(3, '2014-02-04') 
,(3, '2014-02-05') 
,(3, '2014-02-07') 
     -- 
,(5, '2014-02-08') 
     ; 

-- The query 
WITH RECURSIVE stretch AS (
     SELECT dw.user_id AS user_id 
       , dw.arrived_at AS first_day 
       , dw.arrived_at AS last_day 
       , 1::INTEGER AS nday 
     FROM dayworked dw 
     WHERE NOT EXISTS (-- Find start of chain: no previous day 
       SELECT * FROM dayworked nx 
       WHERE nx.user_id = dw.user_id 
       AND nx. arrived_at = dw.arrived_at -1 
       ) 
     UNION ALL 
     SELECT dw.user_id AS user_id 
       , st.first_day AS first_day 
       , dw.arrived_at AS last_day 
       , 1+st.nday AS nday 
     FROM dayworked dw -- connect to chain: previous day := day before this day 
     JOIN stretch st ON st.user_id = dw.user_id AND st.last_day = dw.arrived_at -1 
     ) 
SELECT * FROM stretch st 
WHERE (st.nday > 1 OR st.first_day = NOW()::date) -- either more than one consecutive dat or starting today 
AND NOT EXISTS (-- Only the most recent stretch 
     SELECT * FROM stretch nx 
     WHERE nx.user_id = st .user_id 
     AND nx.first_day > st.first_day 
     ) 
AND NOT EXISTS (-- omit partial chains 
     SELECT * FROM stretch nx 
     WHERE nx.user_id = st .user_id 
     AND nx.first_day = st.first_day 
     AND nx.last_day > st.last_day 
     ) 
     ; 

Ergebnis:

CREATE TABLE 
INSERT 0 14 
user_id | first_day | last_day | nday 
---------+------------+------------+------ 
     1 | 2014-02-05 | 2014-02-07 | 3 
     2 | 2014-02-05 | 2014-02-08 | 4 
(2 rows) 
0

Sie ein Aggregat mit den Bereichstypen erstellen:

Create function sfunc (tstzrange, timestamptz) 
    returns tstzrange 
    language sql strict as $$ 
     select case when $2 - upper($1) <= '1 day'::interval 
       then tstzrange(lower($1), $2, '[]') 
       else tstzrange($2, $2, '[]') end 
    $$; 

Create aggregate consecutive (timestamptz) (
     sfunc = sfunc, 
     stype = tstzrange, 
     initcond = '[,]' 
); 

Verwenden Sie das Aggregat mit der richtigen Reihenfolge die den Tag in Folge Bereich für die letzten arrived_at erhalten:

Select user_id, consecutive(arrived_at order by arrived_at) 
    from work 
    group by user_id; 

    ┌─────────┬─────────────────────────────────────────────────────┐ 
    │ user_id │      consecutive      │ 
    ├─────────┼─────────────────────────────────────────────────────┤ 
    │  1 │ ["2011-01-03 00:00:00+02","2011-01-05 00:00:00+02"] │ 
    │  2 │ ["2011-01-06 00:00:00+02","2011-01-06 00:00:00+02"] │ 
    └─────────┴─────────────────────────────────────────────────────┘ 

Verwenden Sie das Aggregat in einer Fensterfunktion :

Select *, 
     consecutive(arrived_at) 
       over (partition by user_id order by arrived_at) 
    from work; 

    ┌────┬─────────┬────────────────────────┬─────────────────────────────────────────────────────┐ 
    │ id │ user_id │  arrived_at  │      consecutive      │ 
    ├────┼─────────┼────────────────────────┼─────────────────────────────────────────────────────┤ 
    │ 1 │  1 │ 2011-01-03 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-03 00:00:00+02"] │ 
    │ 2 │  1 │ 2011-01-04 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-04 00:00:00+02"] │ 
    │ 3 │  1 │ 2011-01-05 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-05 00:00:00+02"] │ 
    │ 4 │  2 │ 2011-01-06 00:00:00+02 │ ["2011-01-06 00:00:00+02","2011-01-06 00:00:00+02"] │ 
    └────┴─────────┴────────────────────────┴─────────────────────────────────────────────────────┘ 

Abfrage der Ergebnisse zu finden, was Sie brauchen:

With work_detail as (select *, 
      consecutive(arrived_at) 
        over (partition by user_id order by arrived_at) 
     from work) 
    select arrived_at, upper(consecutive) - lower(consecutive) as days 
     from work_detail 
      where user_id = 1 and upper(consecutive) != lower(consecutive) 
      order by arrived_at desc 
       limit 1; 

    ┌────────────────────────┬────────┐ 
    │  arrived_at  │ days │ 
    ├────────────────────────┼────────┤ 
    │ 2011-01-05 00:00:00+02 │ 2 days │ 
    └────────────────────────┴────────┘ 
0

Sie können auch dies tun, ohne ein rekursiven CTE:
mit generate_series(), LEFT JOIN, row_count() und einer abschließenden LIMIT 1:

1 für "heute" plus aufeinanderfolgende Tage bis "gestern":

SELECT count(*) -- 1/0 for "today" 
    + COALESCE((-- + optional count of consecutive days up until "yesterday" 
     SELECT ct 
     FROM (
      SELECT d.ct, count(w.arrived_at) OVER (ORDER BY d.ct) AS day_ct 
      FROM generate_series(1, 8) AS d(ct) -- maximum = 8 
      LEFT JOIN work w ON w.arrived_at >= current_date - d.ct 
          AND w.arrived_at < current_date - (d.ct - 1) 
          AND w.user_id = 1 -- given user 
     ) sub 
     WHERE ct = day_ct 
     ORDER BY ct DESC 
     LIMIT 1 
     ), 0) AS total 
FROM work 
WHERE arrived_at >= current_date -- no future timestamps 
AND user_id = 1     -- given user 

Angenommen, 0 oder 1 Eintrag pro Tag. Sollte schnell sein.

Für die beste Leistung (für diese oder die CTE-Lösung gleichermaßen) Sie möchten einen mehrspaltigen Index haben:

CREATE INDEX foo_idx ON work (user_id,arrived_at); 
+0

wäre dies schneller als CTE-Lösung? – eugene

+0

@eugene: Wahrscheinlich ja. Betrachten Sie das vereinfachte Update. Können Sie 'EXPLAIN ANALYSE' mit jeder Variante Ihrer Daten ausführen? –

+0

Ich habe noch nicht groß genug Datensatz. und es dauert ziemlich lange, bis ich die Antwort in mein eigentliches Schema umwandeln kann. :( – eugene

Verwandte Themen