web-dev-qa-db-ja.com

時間を任意の時間間隔の上位の倍数に丸めるにはどうすればよいですか?

例:

  • 現在の時刻が2018-05-17 22:45:30で、望ましい間隔がINTERVAL '5 minute'の場合、必要な出力は2018-05-17 22:50:00です。
  • 現在の時刻が2018-05-17 22:45:30で、望ましい間隔がINTERVAL '10 minute'の場合、必要な出力は2018-05-17 22:50:00です。
  • 現在の時刻が2018-05-17 22:45:30で、望ましい間隔がINTERVAL '1 hour'の場合、必要な出力は2018-05-17 23:00:00です。
  • 現在の時刻が2018-05-17 22:45:30で、望ましい間隔がINTERVAL '1 day'の場合、必要な出力は2018-05-18 00:00:00です。
5
Gajus

データ型timestampを想定しています。 dateまたはtimestamptzの詳細は一部異なります。

any時間間隔の一般的な解決策は、Epoch値と整数除算に基づいて切り捨てることができます。すべての例をカバーします。

あなたのタスクの特別な困難: floor (より一般的です)ではなく、 ceiling が必要です。コーナーケースのバグを回避するために、下限と上限に注意してください。床の値をexact増やしたくない場合。 (または私はそう思います。)

組み込みの一般的な時間間隔 date_trunc() (例では_1 hour_および_1 day_のように)ショートカットを使用します。 daysの定義は、timestamptzを使用する(timestampを使用しない)セッションのタイムゾーン設定に依存します。

自然な代替手段は ceil() です。テストでは少し遅いですが、よりクリーンです。

短いデモ

_-- short demo
WITH t(ts) AS (SELECT timestamp '2018-05-17 22:45:30')  -- your input timestamp
SELECT t2.*
FROM  (SELECT *, ts - interval '1 microsecond' AS ts1 FROM t) t1 -- subtract min time interval 1 µs
     , LATERAL (
   VALUES
      ('input timestamp' , ts)
    , ('5 min' , to_timestamp(trunc(extract(Epoch FROM ts1))::int / 300 * 300 + 300) AT TIME ZONE 'UTC')
    , ('10 min', to_timestamp(ceil (extract(Epoch FROM ts)/ 600) * 600) AT TIME ZONE 'UTC') -- based on unaltered ts!
    , ('hour'  , date_trunc('hour', ts1) + interval '1 hour')
    , ('day'   , date_trunc('day' , ts1) + interval '1 day')
   ) t2(interval, ceil_ts);
_
間隔| ceil_ts 
:-------------- | :------------------ 
入力タイムスタンプ| 2018-05-17 22:45:30 
 5分| 2018-05-17 22:50:00 
 10分| 2018-05-17 22:50:00 
時間| 2018-05-17 23:00:00 
日| 2018-05-18 00:00:00 

'5 min'計算の「トリック」は、 1 µs の最小時間間隔を差し引いてから切り捨ててから、 ceilingを効果的に取得するためのそれぞれの時間間隔 EXTRACT() は、タイムスタンプの秒数、小数桁がマイクロ秒までの_double precision_数値を返します。 trunc() が必要です。これは、integerへの単純なキャストがroundtruncateする必要があります。

このようにして、上限に該当するタイムスタンプの増分を回避します。ただし、最小の時間間隔は現在のPostgresバージョンの実装の詳細であるため、少し汚れています。 非常にしかし、変更される可能性はほとんどありません。関連:

'10 min'の計算はceil()の方が簡単です。1µsを減算して境界をシフトする必要はありません。クリーナー。しかし、ceil()は、私のテストでは少し高価です。

拡張テストケース

_WITH t(id, ts) AS (
   VALUES
     (1, timestamp '2018-05-17 22:45:30')  -- your input timestamps here
   , (2, timestamp '2018-05-20 00:00:00')
   , (3, timestamp '2018-05-20 00:00:00.000001')
   )
SELECT *
FROM  (SELECT *, ts - interval '1 microsecond' AS ts1 FROM t) t1  -- subtract min time interval 1 µs
     , LATERAL (
   VALUES
      ('input timestamp' , ts)
    , ('5 min'  , to_timestamp(trunc(extract(Epoch FROM ts1))::int / 300 * 300 + 300) AT TIME ZONE 'UTC')
    , ('10 min' , to_timestamp(ceil (extract(Epoch FROM ts)/ 600) * 600) AT TIME ZONE 'UTC') -- based on unaltered ts!
    , ('hour'   , date_trunc('hour', ts1) + interval '1 hour')
    , ('day'    , date_trunc('day' , ts1) + interval '1 day')
    , ('alt_day', ts1::date + 1)
   ) t2(interval, ceil_ts)
ORDER  BY id;
_
 id | ts | ts1 |インターバル| ceil_ts 
-:| :------------------------- | :------------------------- | :-------------- | :------------------------- 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 |入力タイムスタンプ| 2018-05-17 22:45:30 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 | 5分| 2018-05-17 22:50:00 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 | 10分| 2018-05-17 22:50:00 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 |時間| 2018-05-17 23:00:00 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 |日| 2018-05-18 00:00:00 
 1 | 2018-05-17 22:45:30 | 2018-05-17 22:45:29.999999 | alt_day | 2018-05-18 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 |入力タイムスタンプ| 2018-05-20 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 | 5分| 2018-05-20 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 | 10分| 2018-05-20 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 |時間| 2018-05-20 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 |日| 2018-05-20 00:00:00 
 2 | 2018-05-20 00:00:00 | 2018-05-19 23:59:59.999999 | alt_day | 2018-05-20 00:00:00 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 |入力タイムスタンプ| 2018-05-20 00:00:00.000001 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 | 5分| 2018-05-20 00:05:00 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 | 10分| 2018-05-20 00:10:00 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 |時間| 2018-05-20 01:00:00 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 |日| 2018-05-21 00:00:00 
 3 | 2018-05-20 00:00:00.000001 | 2018-05-20 00:00:00 | alt_day | 2018-05-21 00:00:00 

db <> fiddle ここ

丸一日の代替ショートカットを追加しました:_ts1::date + 1_。 dateへのキャストは丸一日に切り捨てられ、integer 1を追加して1日を追加できます。

関数ラッパー

後でtimestamptzを使用することを開示したので、式から_AT TIME ZONE_を削除できます。

私のテストでは、関数STABLEを宣言すると、関数のインライン化が可能になったため、最高のパフォーマンスが得られました。私はIMMUTABLEが最良であると期待していましたが、その宣言は、内部にインライン化できるものについてよりうるさいです。関連:

私のテストでは少し速くなりました:

_CREATE OR REPLACE FUNCTION f_tstz_interval_ceiling2(_tstz timestamptz, _int_seconds int)
  RETURNS timestamptz AS
$func$   
SELECT to_timestamp(trunc(extract(Epoch FROM ($1 - interval '1 microsecond')))::int / $2 * $2 + $2)
$func$  LANGUAGE sql STABLE;
_

クリーナーIMO:

_CREATE OR REPLACE FUNCTION f_tstz_interval_ceiling1(_tstz timestamptz, _int_seconds int)
  RETURNS timestamptz AS
$func$   
SELECT to_timestamp(ceil(extract(Epoch FROM $1) / $2) * $2)
$func$  LANGUAGE sql STABLE;
_

コール:

_SELECT f_tstz_interval_ceiling1(my_tstz, 600);  -- 600 = seconds in 10 min
_

便宜上、intervalを_$2_として取る別の方法で各関数をオーバーロードできます。

_CREATE OR REPLACE FUNCTION f_tstz_interval_ceiling1(_tstz timestamptz, _interval interval)
  RETURNS timestamptz LANGUAGE sql STABLE AS
'SELECT f_tstz_interval_ceiling1($1, extract(Epoch FROM $2)::int)';
_

秒を抽出して最初のバージョンを呼び出すだけです。次に、以下を呼び出すこともできます。

_SELECT f_tstz_interval_ceiling1(my_tstz, interval '10 min');
_
9

3つのケースそれぞれについて、例を挙げます。

_select now(), date_trunc('hour', now()), date_trunc('hour', now()) + (10 * round(extract(minute from now())/10) || ' minute')::interval;
              now              |       date_trunc       |        ?column?
-------------------------------+------------------------+------------------------
 2018-05-17 19:21:44.797717-05 | 2018-05-17 19:00:00-05 | 2018-05-17 19:20:00-05
(1 row)


select now(), date_trunc('day', now()), date_trunc('day', now()) + (1 * round(extract(hour from now())/1) || ' hour')::interval;
              now              |       date_trunc       |        ?column?
-------------------------------+------------------------+------------------------
 2018-05-17 19:22:34.508226-05 | 2018-05-17 00:00:00-05 | 2018-05-17 19:00:00-05
(1 row)


select now(), date_trunc('month', now()), date_trunc('month', now()) + (1 * round(extract(day from now())/1) || ' day')::interval;
              now              |       date_trunc       |        ?column?
-------------------------------+------------------------+------------------------
 2018-05-17 19:23:56.562104-05 | 2018-05-01 00:00:00-05 | 2018-05-18 00:00:00-05
(1 row)
_

だから基本的に

_date_trunc('X', now()) + (Y * round(extract(Z from now())/Y) || ' Z')::interval
_

あなたは交換する必要があります:

  • Xは、間隔で使用されているベースの次の上位項目(例:間隔が日を使用する場合、Xは月でなければなりません)
  • Yは、間隔で使用される値です
  • Zは時間のように間隔で使用されるアイテムです

そしてもちろんnow()をあなたが扱っているタイムスタンプに置き換えてください。

これは、特定のタイムスタンプに対して、指定した間隔値とステップに基づいてこれを作成する関数で抽象化できます。

必要な切り捨てを正確に理解しているとは限らないため、roundceilに置き換える必要がある場合があります。

2
Patrick Mevzek

現在、サーバー年のタイムスタンプストレージのデフォルトの方法であるINTEGER_DATETIMES(ビルドオプション)を想定しています。

timestamp(またはtimestamp with timezone)からbigintへのキャストを作成し、それらを2000-01-01 00:00からのマイクロ秒の整数として操作して、timestampwith timezone

CREATE CAST (bigint AS timestamp) WITHOUT FUNCTION;
CREATE CAST (timestamp AS bigint) WITHOUT FUNCTION;

/のような整数演算を使用して、タイムスタンプの 内部表現 を操作できるようになりました。

次の2週間に転送します。

-- 1209600000000 microseconds is two weeks.

select (((now()::timestamp::bigint-1)/1209600000000+1) * (1209600000000))::timestamp;

そう

 create or replace function timestamp_ceil(timestamp,interval)
       returns timestamp 
       language plpgsql
   as $$ 
     declare 
       i  bigint = 1000000*extract( Epoch from $2);
       ts bigint = $1::bigint;
     begin
       return (((ts-1)/i + (ts>0)::int )*i)::timestamp;
     end $$;

免責事項、これは一定サイズの間隔でのみ機能し、「月」や「年」のような不規則なサイズの間隔は「30日」と「365.25日」に折りたたまれます。

負の数値はゼロに丸められるため、(ts>0)::intの代わりに1が必要であるため、増分は必要ありません。 (booleanからintへのキャストは、結果として0または1になります)

1
Jasen