説明や詳細を気にしない方は、下記の「黒魔術編」をご利用ください。
これまでに他の回答で提示されたすべてのクエリは、サージ可能ではない条件で動作します-インデックスを使用できず、一致する行を見つけるためにベーステーブルのすべての行の式を計算する必要があります. 小さなテーブルではあまり問題ありません。大きなテーブルの問題(たくさん)。
次の簡単な表があるとします。
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
クエリ
以下のバージョン 1. および 2. では、次の形式の単純なインデックスを使用できます。
CREATE INDEX event_event_date_idx ON event(event_date);
ただし、次のソリューションはすべてindex を使用しない方が高速です。
1.簡易版
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);
Subqueryは、2 つの呼び出しのうちの 1 つx
から、指定された年の範囲で考えられるすべての日付を計算します。選択は最後の単純な結合で行われます。CROSS JOIN
generate_series()
2.アドバンス版
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);
年の範囲はテーブルから自動的に推定されるため、生成される年が最小限に抑えられます。ギャップがある場合は、さらに一歩進んで、既存の年のリストを抽出する
ことができます。
有効性は、日付の分布に依存します。数年ごとに多くの行があるため、このソリューションはより便利になります。何年にもわたって行数が少ないと、役に立たなくなります。
遊ぶための単純な SQL Fiddle 。
3.黒魔術バージョン
2016 を更新して、HOT 更新をブロックする「生成された列」を削除しました。よりシンプルで高速な機能。関数のインライン化を可能にする式で
MMDD を計算するように 2018 を更新しました。IMMUTABLE
integer
パターンからを計算する単純な SQL 関数を作成します'MMDD'
。
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
最初は持っていto_char(time, 'MMDD')
ましたが、Postgres 9.6 および 10 での新しいテストで最速であることが証明された上記の式に切り替えました。
デシベル<>ここでフィドル
内部で関数を使用して実装されているため、関数のインライン化が可能です。また、次の必須の複数列式インデックスで使用できるようにする必要があります。EXTRACT (xyz FROM date)
IMMUTABLE
date_part(text, date)
IMMUTABLE
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
いくつかの理由で複数列:特定の年からの選択に
役立ちます。ここORDER BY
を読んでください。インデックスの追加費用はほとんどかかりません。Aは、そうでなければデータの位置合わせのためにパディングで失われる 4 バイトに収まります。ここを読んでください。
また、両方のインデックス列が同じテーブル列を参照するため、HOT更新に関する欠点はありません。ここを読んでください。date
それらすべてを支配する 1 つの PL/pgSQL テーブル関数
2 つのクエリのいずれかにフォークして、年の変わり目をカバーします。
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;
デフォルトを使用した呼び出し: 「今日」から始まる 14 日間:
SELECT * FROM f_anniversary();
'2014-08-23' から 7 日間の通話:
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddleの比較EXPLAIN ANALYZE
。
2月29日
記念日や「誕生日」を扱う場合、うるう年の特殊なケース「2 月 29 日」をどのように扱うかを定義する必要があります。
日付の範囲をテストするFeb 29
場合、現在の年がうるう年でなくても、通常は自動的に含まれます。この日をカバーする場合は、遡って 1 日の範囲が拡張されます。
一方、現在の年がうるう年で、15 日間を検索したい場合、データがうるう年ではない場合、うるう年の 14 日間の結果が得られる可能性があります。
たとえば、ボブが 2 月 29 日に生まれたとし
ます。クエリ 1. と 2. には、うるう年の 2 月 29 日のみが含まれます。ボブの誕生日は 4 年に 1 回しかありません。
私のクエリ 3. には、範囲に 2 月 29 日が含まれています。ボブは毎年誕生日を迎えます。
魔法のような解決策はありません。すべてのケースで必要なものを定義する必要があります。
テスト
私の主張を立証するために、提示されたすべてのソリューションで広範なテストを実行しました。各クエリを指定されたテーブルに適合させ、なしで同じ結果が得られるようにしましたORDER BY
。
良いニュース:構文エラーのある Gordon のクエリと、年が変わると失敗する @wildplasser のクエリを除いて、それらはすべて正しく、同じ結果が得られます (簡単に修正できます)。
20 世紀のランダムな日付を含む 108000 行を挿入します。これは、生存者 (13 歳以上) のテーブルに似ています。
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
~ 8 % を削除して、デッドタプルをいくつか作成し、テーブルをより「リアル」にします。
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
私のテスト ケースには 99289 行、4012 ヒットがありました。
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 - キャットコールのアイデアを書き直した
マイナーな最適化は別として、主な違いは、今年の記念日を取得するために正確な年数のみを追加することです。これにより、CTE が完全に不要になります。 date_trunc('year', age(current_date + 14, event_date))
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;
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 - アーウィン 1
上記「1.簡易版」をご覧ください。
E2 - エルウィン 2
上記「2.アドバンス版」をご覧ください。
E3 - エルヴィン3
上記「3.黒魔術編」参照。
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;
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;
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;
他のすべてと同じものを返すように簡略化されています。
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - 書き直されたワイルドプラッサーのクエリ
上記は、多くの非効率的な詳細に悩まされています(このすでにかなりの記事の範囲を超えています)。書き直されたバージョンははるかに高速です。
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);
試験結果
このテストは、PostgreSQL 9.1.7 の一時テーブルで実行しました。EXPLAIN ANALYZE
結果は、ベスト オブ 5 で収集されました。
結果
インデックスなし
C: 総実行時間: 76714.723 ミリ秒
C1: 総実行時間: 307.987 ミリ秒 -- !
D: 総実行時間: 325.549 ミリ秒
E1: 総実行時間: 253.671 ミリ秒 -- !
E2: 合計実行時間: 484.698 ミリ秒 -- min() と max() はインデックスなしでは高価です
E3: 総実行時間: 213.805 ミリ秒 -- !
G: 総実行時間: 984.788 ミリ秒
H: 総実行時間: 977.297 ミリ秒
W: 総実行時間: 2668.092 ミリ秒
W1: 総実行時間: 596.849 ミリ秒 -- !
インデックス
E1 の場合: 総実行時間: 37.939 ミリ秒 --!!
E2: 総実行時間: 38.097 ミリ秒 --!!
式 E3 のインデックスを使用
: 総実行時間: 11.837 ミリ秒 --!!
他のすべてのクエリは、sargable でない式を使用するため、インデックスの有無にかかわらず同じように実行されます。
結論
これまでのところ、@Daniel のクエリが最速でした。
@wildplassers (書き換えられた) アプローチも許容範囲内で実行されます。
@Catcall のバージョンは、私の逆のアプローチのようなものです。テーブルが大きくなると、パフォーマンスがすぐに手に負えなくなります。
ただし、書き換えられたバージョンはかなりうまく機能します。私が使用する式は、@wildplassser のthis_years_birthday()
関数の単純なバージョンのようなものです。
私の「単純なバージョン」は、必要な計算が少ないため、index がなくても高速です。
インデックスを使用すると、「高度なバージョン」は「簡易バージョン」とほぼ同じくらい高速になります。これは、インデックスを使用すると非常に安価にmin()
なるmax()
ためです。どちらも、インデックスを使用できない残りの部分よりも大幅に高速です。
私の「黒魔術バージョン」は、 index の有無にかかわらず最速です。そして、呼び出すのは非常に簡単です。
更新されたバージョン (ベンチマーク後) は、まだ少し高速です。
実際のテーブルでは、インデックスがさらに大きな違いを生むでしょう。列が増えるとテーブルが大きくなり、シーケンシャル スキャンのコストが高くなりますが、インデックスのサイズは変わりません。