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

PostgreSQLでこの時間の操作クエリを実行します

    テーブルレイアウト

    テーブルを再設計して、営業時間(営業時間)を tsrangeのセットとして保存します。 (timestamp without time zoneの範囲 ) 値。 Postgres9.2以降が必要です 。

    ランダムな週を選んで、営業時間をステージングします。私はその週が好きです:
    1996-01-01(月曜日) 1996-01-07(日曜日)
    これは、1月1日が月曜日であることが都合のよい最近のうるう年です。ただし、この場合は任意の週になる可能性があります。一貫性を保つだけです。

    追加モジュールbtree_gistをインストールします 最初:

    CREATE EXTENSION btree_gist;
    

    参照:

    • 整数と範囲で構成される除外制約に相当します

    次に、次のようなテーブルを作成します。

    CREATE TABLE hoo (
       hoo_id  serial PRIMARY KEY
     , shop_id int NOT NULL -- REFERENCES shop(shop_id)     -- reference to shop
     , hours   tsrange NOT NULL
     , CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
     , CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
     , CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
    );
    

    1つhours すべての列を置き換えます:

    opens_on, closes_on, opens_at, closes_at

    たとえば、水曜日の18:30からの営業時間 木曜日05:00 UTCは次のように入力されます:

    '[1996-01-03 18:30, 1996-01-04 05:00]'
    

    除外制約hoo_no_overlap ショップごとのエントリの重複を防ぎます。 GiSTインデックスで実装されています 、これもクエリをサポートします。 「インデックスとパフォーマンス」の章を検討してください。 以下でインデックス作成戦略について説明します。

    チェック制約hoo_bounds_inclusive 範囲に包括的境界を適用し、2つの注目すべき結果をもたらします:

    • 下限または上限に正確に該当する時点が常に含まれます。
    • 同じショップへの隣接するエントリは事実上禁止されています。包括的境界を使用すると、それらは「重複」し、除外制約によって例外が発生します。代わりに、隣接するエントリを1つの行にマージする必要があります。 日曜日の深夜にラップアラウンドする場合を除く 、この場合、2つの行に分割する必要があります。関数f_hoo_hours() 以下がこれを処理します。

    チェック制約hoo_standard_week 「範囲は含まれています」演算子<@を使用して、ステージング週の外側の境界を強制します 。

    包括的 境界、コーナーケースを観察する必要があります 日曜日の深夜に時間がラップアラウンドする場所:

    '1996-01-01 00:00+0' = '1996-01-08 00:00+0'
     Mon 00:00 = Sun 24:00 (= next Mon 00:00)
    

    両方のタイムスタンプを一度に検索する必要があります。これは、排他的に関連するケースです。 この欠点を示さない上限:

    • PostgreSQLでEXCLUDEを使用して隣接/重複するエントリを防止する

    関数f_hoo_time(timestamptz)

    任意のtimestamp with time zone

    CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
      RETURNS timestamp
      LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
    $func$
    SELECT timestamp '1996-01-01' + ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
    $func$;
    

    PARALLEL SAFE Postgres9.6以降のみ。

    この関数はtimestamptzを取ります timestampを返します 。それぞれの週の経過間隔を追加します($1 - date_trunc('week', $1) UTC時間で、ステージング週の開始点まで。 (date +interval timestampを生成します 。)

    関数f_hoo_hours(timestamptz, timestamptz)

    範囲を正規化し、月曜日00:00を通過する範囲を分割します。この関数は任意の間隔を取ります(2つのtimestamptzとして) )そして、1つまたは2つの正規化されたtsrangeを生成します 値。 すべてをカバーします 法的な入力と残りの禁止:

    CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
      RETURNS TABLE (hoo_hours tsrange)
      LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE COST 500 ROWS 1 AS
    $func$
    DECLARE
       ts_from timestamp := f_hoo_time(_from);
       ts_to   timestamp := f_hoo_time(_to);
    BEGIN
       -- sanity checks (optional)
       IF _to <= _from THEN
          RAISE EXCEPTION '%', '_to must be later than _from!';
       ELSIF _to > _from + interval '1 week' THEN
          RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
       END IF;
    
       IF ts_from > ts_to THEN  -- split range at Mon 00:00
          RETURN QUERY
          VALUES (tsrange('1996-01-01', ts_to  , '[]'))
               , (tsrange(ts_from, '1996-01-08', '[]'));
       ELSE                     -- simple case: range in standard week
          hoo_hours := tsrange(ts_from, ts_to, '[]');
          RETURN NEXT;
       END IF;
    
       RETURN;
    END
    $func$;
    

    INSERTシングル 入力行:

    INSERT INTO hoo(shop_id, hours)
    SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
    

    任意の場合 入力行数:

    INSERT INTO hoo(shop_id, hours)
    SELECT id, f_hoo_hours(f, t)
    FROM  (
       VALUES (7, timestamptz '2016-01-11 00:00+0', timestamptz '2016-01-11 08:00+0')
            , (8, '2016-01-11 00:00+1', '2016-01-11 08:00+1')
       ) t(id, f, t);
    

    月曜日00:00UTCに範囲を分割する必要がある場合は、それぞれに2つの行を挿入できます。

    クエリ

    調整されたデザインで、大きくて複雑で高価なクエリ全体 ... this:

    に置き換えることができます

    SELECT *
    FROM hoo
    WHERE hours @> f_hoo_time(now());

    少しサスペンドするために、溶液の上にスポイラープレートを置きます。 マウスを上に移動 それ。

    クエリは、GiSTインデックスに裏打ちされており、大きなテーブルの場合でも高速です。

    db<>ここでフィドル (その他の例を含む)
    古いsqlfiddle

    総営業時間(ショップあたり)を計算する場合は、レシピを次に示します。

    • PostgreSQLで2つの日付の間の労働時間を計算します

    インデックスとパフォーマンス

    範囲タイプの包含演算子は、GiSTまたは SP-GiSTでサポートできます。 索引。どちらも除外制約を実装するために使用できますが、GiSTのみが複数列のインデックスをサポートします:

    現在、マルチカラムインデックスをサポートしているのは、Bツリー、GiST、GIN、およびBRINインデックスタイプのみです。

    そして、インデックス列の順序は重要です:

    複数列のGiSTインデックスは、インデックスの列のサブセットを含むクエリ条件で使用できます。追加の列の条件は、インデックスによって返されるエントリを制限しますが、最初の列の条件は、スキャンする必要のあるインデックスの量を決定するための最も重要な条件です。追加の列に多数の個別の値がある場合でも、最初の列に少数の個別の値しかない場合、GiSTインデックスは比較的効果がありません。

    したがって、相反する利益があります ここ。大きなテーブルの場合、shop_idにはさらに多くの異なる値があります hoursより 。

    • 先頭にshop_idが付いたGiSTインデックス 書き込みと除外制約の適用が高速です。
    • しかし、私たちはhoursを検索しています 私たちのクエリで。その列を最初に持つ方が良いでしょう。
    • shop_idを検索する必要がある場合 他のクエリでは、プレーンなbtreeインデックスの方がはるかに高速です。
    • 最後に、 SP-GiSTを見つけました。 ちょうどhoursのインデックス 最速になる クエリ用。

    ベンチマーク

    古いラップトップでのPostgres12を使用した新しいテスト。ダミーデータを生成するためのスクリプト:

    INSERT INTO hoo(shop_id, hours)
    SELECT id
         , f_hoo_hours(((date '1996-01-01' + d) + interval  '4h' + interval '15 min' * trunc(32 * random()))            AT TIME ZONE 'UTC'
                     , ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC')
    FROM   generate_series(1, 30000) id
    JOIN   generate_series(0, 6) d ON random() > .33;
    

    結果として、ランダムに生成された行が約141k、個別のshop_idが約30kになります。 、〜12k個の異なるhours 。テーブルサイズ8MB。

    除外制約を削除して再作成しました:

    ALTER TABLE hoo
      DROP CONSTRAINT hoo_no_overlap
    , ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (shop_id WITH =, hours WITH &&);  -- 3.5 sec; index 8 MB
        
    ALTER TABLE hoo
      DROP CONSTRAINT hoo_no_overlap
    , ADD CONSTRAINT hoo_no_overlap  EXCLUDE USING gist (hours WITH &&, shop_id WITH =);  -- 13.6 sec; index 12 MB
    

    shop_id 1つ目は、このディストリビューションでは約4倍高速です。

    さらに、読み取りパフォーマンスについてさらに2つテストしました。

    CREATE INDEX hoo_hours_gist_idx   on hoo USING gist (hours);
    CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours);  -- !!
    

    VACUUM FULL ANALYZE hoo;の後 、2つのクエリを実行しました:

    • Q1 :深夜、35行のみを検索
    • Q2 :午後、4547行を見つける 。

    結果

    インデックスのみのスキャンを取得しました それぞれについて(もちろん「インデックスなし」を除く):

    index                 idx size  Q1        Q2
    ------------------------------------------------
    no index                        38.5 ms   38.5 ms 
    gist (shop_id, hours)    8MB    17.5 ms   18.4 ms
    gist (hours, shop_id)   12MB     0.6 ms    3.4 ms
    gist (hours)            11MB     0.3 ms    3.1 ms
    spgist (hours)           9MB     0.7 ms    1.8 ms  -- !
    
    • SP-GiSTとGiSTは、結果がほとんどないクエリと同等です(GiSTは非常に 少数)。
    • SP-GiSTは、結果の数が増えるにつれてスケーリングが向上し、サイズも小さくなります。

    書くよりも多く読む場合(通常のユースケース)、最初に提案されたように除外制約を維持し、読み取りパフォーマンスを最適化するために追加のSP-GiSTインデックスを作成します。




    1. MySQLユーザーDBにパスワード列がありません-OSXへのMySQLのインストール

    2. フィルタリングされたインデックスがより強力な機能になる方法

    3. OpenCart1.5でのデータベースバックアップの復元

    4. ブールパラメータをOracleプロシージャC#に渡す方法