sql >> Database >  >> RDS >> PostgreSQL

Bereken werkuren tussen 2 datums in PostgreSQL

Volgens uw vraag werkuren zijn:Ma–Vr, 08:00–15:00 .

Afgeronde resultaten

Voor slechts twee gegeven tijdstempels

Werkend op eenheden van 1 uur . Breuken worden genegeerd, daarom niet precies maar eenvoudig:

SELECT count(*) AS work_hours
FROM   generate_series (timestamp '2013-06-24 13:30'
                      , timestamp '2013-06-24 15:29' - interval '1h'
                      , interval '1h') h
WHERE  EXTRACT(ISODOW FROM h) < 6
AND    h::time >= '08:00'
AND    h::time <= '14:00';
  • De functie generate_series() genereert één rij als het einde groter is dan het begin en een andere rij voor elke volledige gegeven interval (1 uur). Deze wold telt elk uur dat is ingevoerd . Om gebroken uren te negeren, trekt u 1 uur van het einde af. En tel geen uren vanaf 14:00 uur mee.

  • Gebruik het veldpatroon ISODOW in plaats van DOW voor EXTRACT() uitdrukkingen te vereenvoudigen. Retourneert 7 in plaats van 0 voor zondag.

  • Een eenvoudige (en zeer goedkope) cast naar time maakt het gemakkelijk om kwalificatie-uren te identificeren.

  • Breuken van een uur worden genegeerd, zelfs als breuken aan het begin en einde van het interval zouden oplopen tot een uur of meer.

Voor een hele tafel

CREATE TEMP TABLE t (t_id int PRIMARY KEY, t_start timestamp, t_end timestamp);
INSERT INTO t VALUES 
  (1, '2009-12-03 14:00', '2009-12-04 09:00')
 ,(2, '2009-12-03 15:00', '2009-12-07 08:00')  -- examples in question
 ,(3, '2013-06-24 07:00', '2013-06-24 12:00')
 ,(4, '2013-06-24 12:00', '2013-06-24 23:00')
 ,(5, '2013-06-23 13:00', '2013-06-25 11:00')
 ,(6, '2013-06-23 14:01', '2013-06-24 08:59');  -- max. fractions at begin and end

Vraag:

SELECT t_id, count(*) AS work_hours
FROM  (
   SELECT t_id, generate_series (t_start, t_end - interval '1h', interval '1h') AS h
   FROM   t
   ) sub
WHERE  EXTRACT(ISODOW FROM h) < 6
AND    h::time >= '08:00'
AND    h::time <= '14:00'
GROUP  BY 1
ORDER  BY 1;

SQL Fiddle.

Meer precisie

Om meer precisie te krijgen, kunt u kleinere tijdseenheden gebruiken. Plakjes van 5 minuten bijvoorbeeld:

SELECT t_id, count(*) * interval '5 min' AS work_interval
FROM  (
   SELECT t_id, generate_series (t_start, t_end - interval '5 min', interval '5 min') AS h
   FROM   t
   ) sub
WHERE  EXTRACT(ISODOW FROM h) < 6
AND    h::time >= '08:00'
AND    h::time <= '14:55'  -- 15.00 - interval '5 min'
GROUP  BY 1
ORDER  BY 1;

Hoe kleiner de eenheid, hoe hoger de kosten .

Schoonmaken met LATERAL in Postgres 9.3+

In combinatie met de nieuwe LATERAL functie in Postgres 9.3, kan de bovenstaande vraag worden geschreven als:

1-uur precisie:

SELECT t.t_id, h.work_hours
FROM   t
LEFT   JOIN LATERAL (
   SELECT count(*) AS work_hours
   FROM   generate_series (t.t_start, t.t_end - interval '1h', interval '1h') h
   WHERE  EXTRACT(ISODOW FROM h) < 6
   AND    h::time >= '08:00'
   AND    h::time <= '14:00'
   ) h ON TRUE
ORDER  BY 1;

5-minuten precisie:

SELECT t.t_id, h.work_interval
FROM   t
LEFT   JOIN LATERAL (
   SELECT count(*) * interval '5 min' AS work_interval
   FROM   generate_series (t.t_start, t.t_end - interval '5 min', interval '5 min') h
   WHERE  EXTRACT(ISODOW FROM h) < 6
   AND    h::time >= '08:00'
   AND    h::time <= '14:55'
   ) h ON TRUE
ORDER  BY 1;

Dit heeft het extra voordeel dat intervallen met nul werkuren niet worden uitgesloten van het resultaat zoals in de bovenstaande versies.

Meer over LATERAL :

  • Vind de meest voorkomende elementen in een array met een groep op
  • Voeg meerdere rijen in een tabel in op basis van het aantal in een andere tabel

Exacte resultaten

Postgres 8.4+

Of u behandelt het begin en het einde van het tijdsbestek afzonderlijk om exact te krijgen resultaten tot op de microseconde. Maakt de zoekopdracht complexer, maar goedkoper en nauwkeuriger:

WITH var AS (SELECT '08:00'::time  AS v_start
                  , '15:00'::time  AS v_end)
SELECT t_id
     , COALESCE(h.h, '0')  -- add / subtract fractions
       - CASE WHEN EXTRACT(ISODOW FROM t_start) < 6
               AND t_start::time > v_start
               AND t_start::time < v_end
         THEN t_start - date_trunc('hour', t_start)
         ELSE '0'::interval END
       + CASE WHEN EXTRACT(ISODOW FROM t_end) < 6
               AND t_end::time > v_start
               AND t_end::time < v_end
         THEN t_end - date_trunc('hour', t_end)
         ELSE '0'::interval END                 AS work_interval
FROM   t CROSS JOIN var
LEFT   JOIN (  -- count full hours, similar to above solutions
   SELECT t_id, count(*)::int * interval '1h' AS h
   FROM  (
      SELECT t_id, v_start, v_end
           , generate_series (date_trunc('hour', t_start)
                            , date_trunc('hour', t_end) - interval '1h'
                            , interval '1h') AS h
      FROM   t, var
      ) sub
   WHERE  EXTRACT(ISODOW FROM h) < 6
   AND    h::time >= v_start
   AND    h::time <= v_end - interval '1h'
   GROUP  BY 1
   ) h USING (t_id)
ORDER  BY 1;

SQL Fiddle.

Postgres 9.2+ met tsrange

De nieuwe reekstypes bieden een elegantere oplossing voor exacte resultaten in combinatie met de kruispuntoperator * :

Eenvoudige functie voor tijdbereiken die slechts één dag beslaan:

CREATE OR REPLACE FUNCTION f_worktime_1day(_start timestamp, _end timestamp)
  RETURNS interval AS
$func$  -- _start & _end within one calendar day! - you may want to check ...
SELECT CASE WHEN extract(ISODOW from _start) < 6 THEN (
   SELECT COALESCE(upper(h) - lower(h), '0')
   FROM  (
      SELECT tsrange '[2000-1-1 08:00, 2000-1-1 15:00)' -- hours hard coded
           * tsrange( '2000-1-1'::date + _start::time
                    , '2000-1-1'::date + _end::time ) AS h
      ) sub
   ) ELSE '0' END
$func$  LANGUAGE sql IMMUTABLE;

Als uw reeksen nooit meerdere dagen beslaan, is dat alles wat u nodig heeft .
Anders, gebruik deze wrapper-functie om elke . af te handelen interval:

CREATE OR REPLACE FUNCTION f_worktime(_start timestamp
                                    , _end timestamp
                                    , OUT work_time interval) AS
$func$
BEGIN
   CASE _end::date - _start::date  -- spanning how many days?
   WHEN 0 THEN                     -- all in one calendar day
      work_time := f_worktime_1day(_start, _end);
   WHEN 1 THEN                     -- wrap around midnight once
      work_time := f_worktime_1day(_start, NULL)
                +  f_worktime_1day(_end::date, _end);
   ELSE                            -- multiple days
      work_time := f_worktime_1day(_start, NULL)
                +  f_worktime_1day(_end::date, _end)
                + (SELECT count(*) * interval '7:00'  -- workday hard coded!
                   FROM   generate_series(_start::date + 1
                                        , _end::date   - 1, '1 day') AS t
                   WHERE  extract(ISODOW from t) < 6);
   END CASE;
END
$func$  LANGUAGE plpgsql IMMUTABLE;

Bel:

SELECT t_id, f_worktime(t_start, t_end) AS worktime
FROM   t
ORDER  BY 1;

SQL Fiddle.



  1. Maak een database-e-mailprofiel (SSMS)

  2. Wordt het uitgesproken als "S-Q-L" of "Vervolg"?

  3. Is er een manier om door een tabelvariabele in TSQL te lopen zonder een cursor te gebruiken?

  4. YEAR() Voorbeelden in SQL Server (T-SQL)