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 darithis_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()
danmax()
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.