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

Bagaimana Anda melakukan matematika tanggal yang mengabaikan tahun?

Jika Anda tidak peduli dengan penjelasan dan detail, gunakan "Versi ilmu hitam" di bawah.

Semua pertanyaan yang disajikan dalam jawaban lain sejauh ini beroperasi dengan kondisi yang tidak dapat dikritik - mereka tidak dapat menggunakan indeks dan harus menghitung ekspresi untuk setiap baris dalam tabel dasar untuk menemukan baris yang cocok. Tidak masalah dengan meja kecil. Penting (banyak ) dengan meja besar.

Diberikan tabel sederhana berikut:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

Kueri

Versi 1. dan 2. di bawah ini dapat menggunakan indeks sederhana dalam bentuk:

CREATE INDEX event_event_date_idx ON event(event_date);

Tetapi semua solusi berikut bahkan lebih cepat tanpa indeks .

1. Versi sederhana

SELECT *
FROM  (
   SELECT ((current_date + d) - interval '1 year' * y)::date AS event_date
   FROM       generate_series( 0,  14) d
   CROSS JOIN generate_series(13, 113) y
   ) x
JOIN  event USING (event_date);

Subkueri x menghitung semua kemungkinan tanggal selama rentang tahun tertentu dari CROSS JOIN dari dua generate_series() panggilan. Pemilihan dilakukan dengan simple join terakhir.

2. Versi lanjutan

WITH val AS (
   SELECT extract(year FROM age(current_date + 14, min(event_date)))::int AS max_y
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_y
   FROM   event
   )
SELECT e.*
FROM  (
   SELECT ((current_date + d.d) - interval '1 year' * y.y)::date AS event_date
   FROM   generate_series(0, 14) d
        ,(SELECT generate_series(min_y, max_y) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

Rentang tahun disimpulkan dari tabel secara otomatis - sehingga meminimalkan tahun yang dihasilkan.
Anda bisa melangkah lebih jauh dan menyaring daftar tahun yang ada jika ada kesenjangan.

Efektivitas tergantung pada distribusi tanggal. Beberapa tahun dengan banyak baris masing-masing membuat solusi ini lebih berguna. Bertahun-tahun dengan beberapa baris masing-masing membuatnya kurang berguna.

Fiddle SQL Sederhana untuk bermain bersama.

3. Versi ilmu hitam

Diperbarui 2016 untuk menghapus "kolom yang dibuat", yang akan memblokir H.O.T. pembaruan; fungsi yang lebih sederhana dan lebih cepat.
Memperbarui 2018 untuk menghitung MMDD dengan IMMUTABLE ekspresi untuk memungkinkan fungsi inlining.

Buat fungsi SQL sederhana untuk menghitung integer dari pola 'MMDD' :

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Saya memiliki to_char(time, 'MMDD') pada awalnya, tetapi beralih ke ekspresi di atas yang terbukti tercepat dalam pengujian baru pada Postgres 9.6 dan 10:

db<>main biola di sini

Ini memungkinkan fungsi inlining karena EXTRACT (xyz FROM date) diimplementasikan dengan IMMUTABLE fungsi date_part(text, date) secara internal. Dan itu harus IMMUTABLE untuk mengizinkan penggunaannya dalam indeks ekspresi multikolom penting berikut:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Multikolom karena beberapa alasan:
Dapat membantu dengan ORDER BY atau dengan memilih dari tahun-tahun tertentu. Baca di sini. Hampir tanpa biaya tambahan untuk indeks. date cocok dengan 4 byte yang jika tidak akan hilang ke padding karena penyelarasan data. Baca di sini.
Selain itu, karena kedua kolom indeks merujuk pada kolom tabel yang sama, tidak ada kekurangan terkait H.O.T. pembaruan. Baca di sini.

Satu tabel PL/pgSQL berfungsi untuk mengatur semuanya

Fork ke salah satu dari dua pertanyaan untuk menutupi pergantian tahun:

CREATE OR REPLACE FUNCTION f_anniversary(date = current_date, int = 14)
  RETURNS SETOF event AS
$func$
DECLARE
   d  int := f_mmdd($1);
   d1 int := f_mmdd($1 + $2 - 1);  -- fix off-by-1 from upper bound
BEGIN
   IF d1 > d THEN
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) BETWEEN d AND d1
      ORDER  BY f_mmdd(e.event_date), e.event_date;

   ELSE  -- wrap around end of year
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) >= d OR
             f_mmdd(e.event_date) <= d1
      ORDER  BY (f_mmdd(e.event_date) >= d) DESC, f_mmdd(e.event_date), event_date;
      -- chronological across turn of the year
   END IF;
END
$func$  LANGUAGE plpgsql;

Telepon menggunakan default:14 hari dimulai "hari ini":

SELECT * FROM f_anniversary();

Telepon selama 7 hari mulai '23-08-2014':

SELECT * FROM f_anniversary(date '2014-08-23', 7);

SQL Fiddle membandingkan EXPLAIN ANALYZE .

29 Februari

Saat berurusan dengan peringatan atau "ulang tahun", Anda perlu menentukan cara menangani kasus khusus "29 Februari" di tahun kabisat.

Saat menguji rentang tanggal, Feb 29 biasanya disertakan secara otomatis, meskipun tahun saat ini bukan tahun kabisat . Rentang hari diperpanjang 1 secara surut jika mencakup hari ini.
Di sisi lain, jika tahun ini adalah tahun kabisat, dan Anda ingin mencari 15 hari, Anda mungkin akan mendapatkan hasil untuk 14 hari. hari dalam tahun kabisat jika data Anda berasal dari tahun bukan kabisat.

Katakanlah, Bob lahir pada tanggal 29 Februari:
Permintaan saya 1. dan 2. hanya menyertakan 29 Februari dalam tahun kabisat. Bob berulang tahun hanya setiap ~ 4 tahun.
Kueri saya 3. menyertakan 29 Februari dalam rentang tersebut. Bob berulang tahun setiap tahun.

Tidak ada solusi ajaib. Anda harus menentukan apa yang Anda inginkan untuk setiap kasus.

Uji

Untuk memperkuat poin saya, saya menjalankan tes ekstensif dengan semua solusi yang disajikan. Saya mengadaptasi setiap kueri ke tabel yang diberikan dan untuk menghasilkan hasil yang identik tanpa ORDER BY .

Kabar baiknya:semuanya benar dan menghasilkan hasil yang sama - kecuali untuk kueri Gordon yang memiliki kesalahan sintaksis, dan kueri @wildplasser yang gagal saat pergantian tahun (mudah diperbaiki).

Masukkan 108000 baris dengan tanggal acak dari abad ke-20, yang mirip dengan tabel orang yang masih hidup (13 atau lebih tua).

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Hapus ~ 8% untuk membuat beberapa tupel mati dan membuat tabel lebih "nyata".

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

Kasus pengujian saya memiliki 99289 baris, 4012 hit.

C - Catcall

WITH anniversaries as (
   SELECT event_id, event_date
         ,(event_date + (n || ' years')::interval)::date anniversary
   FROM   event, generate_series(13, 113) n
   )
SELECT event_id, event_date -- count(*)   --
FROM   anniversaries
WHERE  anniversary BETWEEN current_date AND current_date + interval '14' day;

C1 - Ide Catcall ditulis ulang

Selain pengoptimalan kecil, perbedaan utamanya adalah menambahkan hanya jumlah tahun yang tepat date_trunc('year', age(current_date + 14, event_date)) untuk mendapatkan peringatan tahun ini, yang sama sekali tidak memerlukan CTE:

SELECT event_id, event_date
FROM   event
WHERE (event_date + date_trunc('year', age(current_date + 14, event_date)))::date
       BETWEEN current_date AND current_date + 14;

D - Daniel

SELECT *   -- count(*)   -- 
FROM   event
WHERE  extract(month FROM age(current_date + 14, event_date))  = 0
AND    extract(day   FROM age(current_date + 14, event_date)) <= 14;

E1 - Erwin 1

Lihat "1. Versi sederhana" di atas.

E2 - Erwin 2

Lihat "2. Versi lanjutan" di atas.

E3 - Erwin 3

Lihat "3. Versi ilmu hitam" di atas.

G - Gordon

SELECT * -- count(*)   
FROM  (SELECT *, to_char(event_date, 'MM-DD') AS mmdd FROM event) e
WHERE  to_date(to_char(now(), 'YYYY') || '-'
                 || (CASE WHEN mmdd = '02-29' THEN '02-28' ELSE mmdd END)
              ,'YYYY-MM-DD') BETWEEN date(now()) and date(now()) + 14;

H - a_horse_with_no_name

WITH upcoming as (
   SELECT event_id, event_date
         ,CASE 
            WHEN date_trunc('year', age(event_date)) = age(event_date)
                 THEN current_date
            ELSE cast(event_date + ((extract(year FROM age(event_date)) + 1)
                      * interval '1' year) AS date) 
          END AS next_event
   FROM event
   )
SELECT event_id, event_date
FROM   upcoming
WHERE  next_event - current_date  <= 14;

W - wildplasser

CREATE OR REPLACE FUNCTION this_years_birthday(_dut date) RETURNS date AS
$func$
DECLARE
    ret date;
BEGIN
    ret :=
    date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
         - date_trunc( 'year' , _dut));
    RETURN ret;
END
$func$ LANGUAGE plpgsql;

Sederhana untuk mengembalikan sama seperti yang lainnya:

SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1 - kueri wildplasser ditulis ulang

Di atas menderita sejumlah detail yang tidak efisien (di luar cakupan posting yang sudah cukup besar ini). Versi yang ditulis ulang adalah banyak lebih cepat:

CREATE OR REPLACE FUNCTION this_years_birthday(_dut INOUT date) AS
$func$
SELECT (date_trunc('year', now()) + ($1 - date_trunc('year', $1)))::date
$func$ LANGUAGE sql;

SELECT *
FROM   event e
WHERE  this_years_birthday(e.event_date)
        BETWEEN current_date
        AND    (current_date + 14);

Hasil pengujian

Saya menjalankan tes ini dengan tabel sementara di PostgreSQL 9.1.7.Hasil dikumpulkan dengan EXPLAIN ANALYZE , terbaik dari 5.

Hasil

Without index
C:  Total runtime: 76714.723 ms
C1: Total runtime:   307.987 ms  -- !
D:  Total runtime:   325.549 ms
E1: Total runtime:   253.671 ms  -- !
E2: Total runtime:   484.698 ms  -- min() & max() expensive without index
E3: Total runtime:   213.805 ms  -- !
G:  Total runtime:   984.788 ms
H:  Total runtime:   977.297 ms
W:  Total runtime:  2668.092 ms
W1: Total runtime:   596.849 ms  -- !

With index
E1: Total runtime:    37.939 ms  --!!
E2: Total runtime:    38.097 ms  --!!

With index on expression
E3: Total runtime:    11.837 ms  --!!

Semua kueri lain berkinerja sama dengan atau tanpa indeks karena menggunakan non-sargable ekspresi.

Kesimpulan

  • Sejauh ini, kueri @Daniel adalah yang tercepat.

  • Pendekatan @wildplassers (ditulis ulang) juga dapat diterima.

  • Versi @ Catcall adalah sesuatu seperti pendekatan kebalikan dari saya. Performa menjadi tidak terkendali dengan cepat dengan tabel yang lebih besar.
    Namun, versi yang ditulis ulang memiliki performa yang cukup baik. Ekspresi yang saya gunakan adalah sesuatu seperti versi sederhana dari this_years_birthday() @wildplassser fungsi.

  • "Versi sederhana" saya lebih cepat bahkan tanpa indeks , karena membutuhkan lebih sedikit perhitungan.

  • Dengan indeks, "versi lanjutan" hampir secepat "versi sederhana", karena min() dan max() menjadi sangat murah dengan indeks. Keduanya jauh lebih cepat daripada yang lain yang tidak dapat menggunakan indeks.

  • "Versi ilmu hitam" saya adalah yang tercepat dengan atau tanpa indeks . Dan itu sangat mudah dipanggil.

  • Dengan tabel kehidupan nyata sebuah indeks akan membuat lebih hebat perbedaan. Lebih banyak kolom membuat tabel lebih besar, dan pemindaian berurutan lebih mahal, sementara ukuran indeks tetap sama.



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

  2. Dapatkan jumlah catatan yang terpengaruh oleh INSERT atau UPDATE di PostgreSQL

  3. Menggunakan Slot Replikasi PostgreSQL

  4. Pemantauan PostgreSQL Penting - Bagian 3

  5. Kembalikan baris yang cocok dengan elemen array input dalam fungsi plpgsql