PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Hitung jam kerja antara 2 tanggal di PostgreSQL

Menurut pertanyaan Anda jam kerja adalah:Mo–Fr, 08:00–15:00 .

Hasil bulat

Hanya untuk dua stempel waktu yang diberikan

Beroperasi pada unit 1 jam . Pecahan diabaikan, oleh karena itu tidak tepat tapi sederhana:

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';
  • Fungsi generate_series() menghasilkan satu baris jika akhir lebih besar dari awal dan baris lain untuk setiap penuh diberikan interval (1 jam). Ini menghitung setiap jam masuk . Untuk mengabaikan jam pecahan, kurangi 1 jam dari akhir. Dan jangan hitung jam mulai sebelum pukul 14:00.

  • Gunakan pola bidang ISODOW bukannya DOW untuk EXTRACT() untuk menyederhanakan ekspresi. Mengembalikan 7 bukannya 0 untuk hari Minggu.

  • Pemeran sederhana (dan sangat murah) untuk time memudahkan untuk mengidentifikasi jam kualifikasi.

  • Pecahan satu jam diabaikan, bahkan jika pecahan di awal dan akhir interval akan bertambah hingga satu jam atau lebih.

Untuk seluruh tabel

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

Pertanyaan:

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.

Lebih presisi

Untuk mendapatkan lebih presisi Anda dapat menggunakan unit waktu yang lebih kecil. Irisan 5 menit misalnya:

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;

Semakin kecil unitnya semakin tinggi biayanya .

Pembersih dengan LATERAL di Postgres 9.3+

Dikombinasikan dengan LATERAL baru fitur di Postgres 9.3, kueri di atas kemudian dapat ditulis sebagai:

Presisi 1 jam:

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;

Presisi 5 menit:

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;

Ini memiliki keuntungan tambahan bahwa interval yang mengandung nol jam kerja tidak dikecualikan dari hasil seperti pada versi di atas.

Selengkapnya tentang LATERAL :

  • Temukan elemen paling umum dalam larik dengan grup menurut
  • Menyisipkan beberapa baris dalam satu tabel berdasarkan nomor di tabel lain

Hasil yang tepat

Postgres 8.4+

Atau Anda berurusan dengan awal dan akhir kerangka waktu secara terpisah untuk mendapatkan tepat hasilnya ke mikrodetik. Membuat kueri lebih kompleks, tetapi lebih murah dan tepat:

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+ dengan tsrange

Jenis rentang baru menawarkan solusi yang lebih elegan untuk hasil yang tepat dalam kombinasi dengan operator persimpangan * :

Fungsi sederhana untuk rentang waktu yang hanya mencakup satu hari:

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;

Jika rentang Anda tidak pernah menjangkau beberapa hari, itu yang Anda butuhkan .
Selain itu, gunakan fungsi pembungkus ini untuk menangani apa saja 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;

Telepon:

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

SQL Fiddle.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Upser dengan transaksi

  2. Orbeon Membentuk koneksi Postgres DB

  3. Apakah ada jalan pintas untuk SELECT * FROM?

  4. rake db:create throws database tidak ada kesalahan dengan postgresql

  5. Cara memasukkan dan menghapus data di PostgreSQL