sql >> データベース >  >> RDS >> PostgreSQL

年を無視する日付計算をどのように行いますか?

    説明や詳細を気にしない場合は、「黒魔術バージョン」を使用してください 以下。

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

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

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

    クエリ

    以下のバージョン1および2は、次の形式の単純なインデックスを使用できます。

    CREATE INDEX event_event_date_idx ON event(event_date);
    

    ただし、次のすべてのソリューションは、インデックスなしでさらに高速になります

    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);
    

    サブクエリx CROSS JOIN から、指定された年の範囲で可能なすべての日付を計算します 2つの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フィドル 遊ぶ。

    3。黒魔術バージョン

    2016年を更新して、H.O.Tをブロックする「生成された列」を削除しました。更新;よりシンプルで高速な関数。
    2018年に更新され、 IMMUTABLEを使用してMMDDを計算できるようになりました。 関数のインライン化を可能にする式。

    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')がありました 最初は、上記の式に切り替えました。これは、Postgres9.6および10の新しいテストで最速であることが証明されました。

    db<>ここでフィドル

    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を支援できます または指定された年から選択して。ここを読んでください。インデックスの追加コストはほとんどありません。 日付 データアライメントのためにパディングで失われる4バイトに収まります。こちらをお読みください。
    また、両方のインデックス列が同じテーブル列を参照しているため、H.O.T。に関して問題はありません。 更新。ここを読んでください。

    それらすべてを支配する1つのPL/pgSQLテーブル関数

    今年の変わり目をカバーするために、2つのクエリのうちの1つにフォークします。

    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フィドル EXPLAIN ANALYZEの比較 。

    2月29日

    記念日や「誕生日」を扱うときは、うるう年の特別なケース「2月29日」の扱い方を定義する必要があります。

    日付の範囲をテストする場合は、2月29日 現在の年がうるう年ではない場合でも、通常は自動的に含まれます 。この日をカバーすると、日数の範囲が1つ遡って延長されます。
    一方、今年がうるう年で、15日を探したい場合は、14日の結果が得られる可能性があります。データがうるう年以外のものである場合は、うるう年の日数。

    たとえば、ボブは2月29日に生まれます。
    私のクエリ1.および2.には、うるう年の2月29日のみが含まれます。ボブの誕生日は約4年ごとです。
    私のクエリ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-Catcallのアイデアが書き直されました

    マイナーな最適化を除いて、主な違いは、正確な年数のみを追加することです。 date_trunc('year'、age(current_date + 14、event_date)) 今年の記念日を取得するために、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-ダニエル

    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-Erwin 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-wildplasserのクエリが書き直されました

    上記は、(このすでにかなりの規模の投稿の範囲を超えて)多くの非効率的な詳細に悩まされています。書き直されたバージョンは多く より速く:

    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。

    結果

    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  --!!
    

    他のすべてのクエリは、非引数を使用するため、インデックスの有無にかかわらず同じように実行されます 式。

    結論

    • これまでのところ、@Danielのクエリが最速でした。

    • @wildplassers(書き直された)アプローチも許容範囲内で実行されます。

    • @Catcallのバージョンは、私の逆のアプローチのようなものです。テーブルが大きくなると、パフォーマンスはすぐに手に負えなくなります。
      ただし、書き直されたバージョンはかなりうまく機能します。私が使用する式は、@wildplassserのthis_years_birthday()のより単純なバージョンのようなものです。 機能。

    • 私の「シンプルバージョン」はインデックスがなくても高速です 、必要な計算が少ないためです。

    • min()であるため、インデックスを使用すると、「詳細バージョン」は「単純バージョン」とほぼ同じ速度になります。 およびmax() 非常にになる インデックスで安い。どちらも、インデックスを使用できない残りの部分よりも大幅に高速です。

    • 私の「黒魔術バージョン」は、インデックスの有無にかかわらず最速です 。そして、それは非常に 電話するのは簡単です。

    • 生命表ではインデックス さらに大きく 違い。列が多いほどテーブルが大きくなり、順次スキャンのコストが高くなりますが、インデックスサイズは同じままです。



    1. アクティブなSQLServer接続を確認するにはどうすればよいですか?

    2. Hibernateでの同時更新処理

    3. 主キーと一意キーの違い

    4. MicrosoftSQLServerデータベースで保存データを検索する