16

次の 14 日間に記念日がある日付を選択しようとしています。年を除く日付に基づいて選択するにはどうすればよいですか? 私は次のようなことを試しました。

SELECT * FROM events
WHERE EXTRACT(month FROM "date") = 3
AND EXTRACT(day FROM "date") < EXTRACT(day FROM "date") + 14

これに関する問題は、月がラップすることです。
このようなことをしたいのですが、年を無視する方法がわかりません。

SELECT * FROM events
WHERE (date > '2013-03-01' AND date < '2013-04-01')

Postgres でこの種の日付計算を行うにはどうすればよいですか?

4

8 に答える 8

44

説明や詳細を気にしない方は、下記の「黒魔術編」をご利用ください。

これまでに他の回答で提示されたすべてのクエリは、サージ可能ではない条件で動作します-インデックスを使用できず、一致する行を見つけるためにベーステーブルのすべての行の式を計算する必要があります. 小さなテーブルではあまり問題ありません。大きなテーブルの問題(たくさん)。

次の簡単な表があるとします。

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 JOINgenerate_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)IMMUTABLEdate_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 ヒットがありました。

C -キャットコール

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;

D -ダニエル

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.黒魔術編」参照。

G -ゴードン

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 -ワイルドプラッサー

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 の有無にかかわらず最速です。そして、呼び出すのは非常に簡単です。
    更新されたバージョン (ベンチマーク後) は、まだ少し高速です。

  • 実際のテーブルでは、インデックスさらに大きな違いを生むでしょう。列が増えるとテーブルが大きくなり、シーケンシャル スキャンのコストが高くなりますが、インデックスのサイズは変わりません。

于 2013-03-02T21:40:21.690 に答える
7

という名前の列を想定すると、次のテストはすべての場合に機能すると思いますanniv_date

select * from events
where extract(month from age(current_date+interval '14 days', anniv_date))=0
  and extract(day from age(current_date+interval '14 days', anniv_date)) <= 14

年 (および月) を超える場合の動作の例として、記念日2009-01-04の日付が で、テストが実行される日付が であるとし2012-12-29ます。

2012-12-292013-01-12(14 日)の間の任意の日付を考慮したい

age('2013-01-12'::date, '2009-01-04'::date)です4 years 8 days

extract(month...)これより is0extract(days...)is8で、どちらが低い14ので一致します。

于 2013-03-02T12:53:41.437 に答える
3

これはどう?

select *
from events e
where to_char(e."date", 'MM-DD') between to_char(now(), 'MM-DD') and 
                                         to_char(date(now())+14, 'MM-DD')

比較は文字列として行うことができます。

年末を考慮に入れるために、日付に戻します。

select *
from events e
where to_date(to_char(now(), 'YYYY')||'-'||to_char(e."date", 'MM-DD'), 'YYYY-MM-DD')
           between date(now()) and date(now())+14

2 月 29 日に少し調整する必要があります。

select *
from (select e.*,
             to_char(e."date", 'MM-DD') as MMDD
      from events
     ) e
where to_date(to_char(now(), 'YYYY')||'-'||(case when MMDD = '02-29' then '02-28' else MMDD), 'YYYY-MM-DD')
           between date(now()) and date(now())+14
于 2013-03-02T01:15:56.953 に答える
2

便宜上、今年の (予想される、または過去の) 誕生日と、次の誕生日を生成する 2 つの関数を作成しました。

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;

CREATE OR REPLACE FUNCTION next_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)
          )
        ;
        IF (ret < date_trunc( 'day' , current_timestamp))
           THEN ret = ret + '1year'::interval; END IF;
        RETURN ret;
END;
$func$ LANGUAGE plpgsql;

      --
      -- call the function
      --
SELECT date_trunc( 'day' , t.topic_date) AS the_date
        , this_years_birthday( t.topic_date::date ) AS the_day
        , next_birthday( t.topic_date::date ) AS next_day
FROM topic t
WHERE this_years_birthday( t.topic_date::date )
        BETWEEN  current_date
        AND  current_date + '2weeks':: interval
        ;

注: タイムスタンプしか利用できないため、キャストが必要です。

于 2013-03-02T13:21:43.757 に答える
1

記念日の仮想テーブルを生成し、そこから選択することができます。

with anniversaries as (
  select event_date, 
         (event_date + (n || ' years')::interval)::date anniversary
  from events, generate_series(1,10) n
)
select event_date, anniversary
from anniversaries
where anniversary between current_date and current_date + interval '14' day
order by event_date, anniversary

の呼び出しにgenerate_series(1,10)は、event_dateごとに10年の記念日を生成する効果があります。本番環境ではリテラル値10を使用しません。代わりに、サブクエリで使用する適切な年数を計算するか、100のような大きなリテラルを使用します。

アプリケーションに合わせてWHERE句を調整する必要があります。

仮想テーブルでパフォーマンスの問題が発生した場合(「イベント」に多数の行がある場合)、共通テーブル式を同じ構造のベーステーブルに置き換えます。記念日をベーステーブルに保存すると、その値が明確になり(特に、2月29日の記念日など)、そのようなテーブルに対するクエリでインデックスを使用できます。上記のSELECTステートメントだけを使用して50万行の記念日テーブルをクエリすると、デスクトップで25ミリ秒かかります。

于 2013-03-02T01:32:09.427 に答える
1

これにより、年末のラップアラウンドも処理されるはずです。

with upcoming as (
  select name, 
         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 events
)
select name, 
       next_event, 
       next_event - current_date as days_until_next
from upcoming
order by next_event - current_date 

next_event - current_date「次の 14 日間」を適用する式よりもフィルタリングできます。

case ...、「今日」のイベントを「今後」と見なす場合にのみ必要です。elseそれ以外の場合は、case ステートメントの部分に減らすことができます。

"date"列をに「名前変更」したことに注意してくださいevent_date。主な理由は、予約語を識別子として使用するべきではないためdateですが、列名がひどいためでもあります。保存されているものについては何も教えてくれません。

于 2013-03-02T13:49:10.907 に答える
0

私はそれを行う方法を見つけました。

SELECT EXTRACT(DAYS FROM age('1999-04-10', '2003-05-12')), 
       EXTRACT(MONTHS FROM age('1999-04-10', '2003-05-12'));
 date_part | date_part 
-----------+-----------
        -2 |        -1

次に、月が 0 で、日が 14 未満であることを確認します。

よりエレガントなソリューションがある場合は、投稿してください。質問を少し開いたままにします。

于 2013-03-02T01:02:47.603 に答える
0

私はpostgresqlを扱っていないので、日付関数をグーグルで調べたところ、これが見つかりました: http://www.postgresql.org/docs/current/static/functions-datetime.html

正しく読めば、次の 14 日間のイベントを探すのは次のように簡単です。

 where mydatefield >= current_date
 and mydatefield < current_date + integer '14'

もちろん、正しく読んでいない可能性もあります。

于 2013-03-02T01:30:49.027 に答える