web-dev-qa-db-ja.com

Postgresql分割テーブルtimestamptz制約の問題

テーブルreportsは、_reports_20170414_、_reports_20170415_のように日ごとに分割されたテーブルです。

制約SQLは次のように定義されています

_CHECK (
    rpt_datetime >= '2017-04-14 00:00:00+00'::timestamp with time zone 
    AND 
    rpt_datetime < '2017-04-15 00:00:00+00'::timestamp with time zone
)
_

2つのタイプのクエリを考えてみましょう

_SELECT SUM(rpt_unique_clicks) 
    FROM reports WHERE rpt_datetime >= '2017-04-14 00:00:00';
_

上記のクエリは1秒以内に実行され、すべて正常です。

_SELECT SUM(rpt_unique_clicks) 
    FROM reports WHERE rpt_datetime >= 
 date_trunc('day', current_timestamp);
_

逆に、上記のクエリは少なくとも15秒実行されます。

_SELECT date_trunc('day', CURRENT_TIMESTAMP), '2017-04-14 00:00:00';
_

戻り値

_2017-04-14 00:00:00 +00:00 | 2017-04-14 00:00:00
_

後者が(explain analyzeを使用して)長く実行される理由を調べると、すべてのテーブルを訪問してスキャンする結果(〜500)を終了しましたが、前者は_reports_20170414_のみを訪問するため、制約チェックに問題があります。

後者のクエリのように準備されたステートメントを使用せずに、今日のクエリを実行したいと思います。なぜdate_trunc('day', CURRENT_TIMESTAMP)は_2017-04-14 00:00:00_と同等ではないのですか?

5
onesvat

_CURRENT_TIMESTAMP_と_date_trunc_の両方がIMMUTABLEとして定義されている場合でも、date_trunc('day', CURRENT_TIMESTAMP)が定数と等しくない理由について完全に応答することはできません。

しかし、私たちは実験的な推測を行うことができると思います:どうやら、PostgreSQLプランナー関数を評価しません。そのため、どのパーティションをチェックするかを知る良い方法がなく、すべてをチェックする計画を立てます。


実験的チェック

ベース(親)テーブルを作成します。

_ -- Base table
 CREATE TABLE reports
 (
     rpt_datetime timestamp without time zone DEFAULT now() PRIMARY KEY,
     rpt_unique_clicks integer NOT NULL DEFAULT 1,
     something_else text
 ) ;
_

自動パーティション挿入トリガーを作成します。

_ -- Auto-partition using trigger
 -- Adapted from http://blog.l1x.me/post/2016/02/16/creating-partitions-automatically-in-postgresql.html
 CREATE OR REPLACE FUNCTION create_partition_and_insert ()
 RETURNS TRIGGER AS 
 $$
 DECLARE
     _partition_date text ;
     _partition_date_p1 text ;
     _partition text ;
 BEGIN
     _partition_date := to_char(new.rpt_datetime, 'YYYYMMDD');
     _partition := 'reports_' || _partition_date ;

     -- Check if table exists... 
     -- (oversimplistic: doesn't take schemas into account... doesn't check for possible race conditions)
     if not exists (SELECT relname FROM pg_class WHERE relname=_partition) THEN
        _partition_date_p1 := to_char(new.rpt_datetime + interval '1 day', 'YYYYMMDD');

        RAISE NOTICE 'Creating %', _partition ;

        EXECUTE 'CREATE TABLE ' || _partition || 
              ' (CHECK (rpt_datetime >= timestamp ''' || _partition_date || ''' AND rpt_datetime < timestamp ''' || _partition_date_p1 || '''))' ||
              ' INHERITS (reports)' ;
     end if ;

     EXECUTE 'INSERT INTO ' || _partition || ' SELECT(reports ' || quote_literal(NEW) || ').* ;' ;

     -- We won't insert anything on parent table
     RETURN NULL ;
 END 
 $$
 LANGUAGE plpgsql VOLATILE
 COST 1000;

 -- Attach trigger to parent table
 CREATE TRIGGER reports_insert_trigger
 BEFORE INSERT ON reports
 FOR EACH ROW EXECUTE PROCEDURE create_partition_and_insert();
_

(パーティション化された)テーブルにデータを入力します。トリガーが作成したパーティションを確認します。

_ INSERT INTO 
     reports (rpt_datetime, rpt_unique_clicks, something_else)
 SELECT
     d, 1, 'Hello'
 FROM
     generate_series(timestamp '20170416' - interval '7 days', timestamp '20170416', interval '10 minutes') x(d) ;

 -- Check how many partitions we made
 SELECT 
     table_name 
 FROM 
      information_schema.tables 
 WHERE 
     table_name like 'reports_%' 
 ORDER BY 
     table_name;
_
 | table_name | 
 | :------------ | reports_20170409 | 
 | reports_20170410 | 
 | reports_20170411 | 
 | reports_20170412 | 
 | reports_20170413 | 
 | reports_20170414 | 
 | reports_20170415 | 
 | reports_20170416 | 
 

この時点で、2つの異なるクエリを確認します。最初のものは、_rpt_datetime_と比較してconstantを使用します。

_ EXPLAIN (ANALYZE) 
 SELECT 
     SUM(rpt_unique_clicks) 
 FROM 
     reports 
 WHERE 
     rpt_datetime >= timestamp '20170416' ; 
_

一定のタイムスタンプを使用して、「レポート」と適切なパーティションのみがチェックされます。

 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ------------------- | 
 |集計(コスト= 25.07..25.08行= 1幅= 8)(実際の時間= 0.015..0.015行= 1ループ= 1)| 
 | ->追加(コスト= 0.00..24.12行= 378幅= 4)(実際の時間= 0.009..0.010行= 1ループ= 1)| 
 | ->レポートのシーケンススキャン(コスト= 0.00..0.00行= 1幅= 4)(実際の時間= 0.003..0.003行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = '2017-04-16 00:00:00' :: timestamp without time zone)| 
 | ->レポートのシーケンススキャン_20170416(コスト= 0.00..24.12行= 377幅= 4)(実際の時間= 0.006..0.007行= 1ループ= 1)| 
 |フィルター:(rpt_datetime> = '2017-04-16 00:00:00' :: timestamp without time zone)| 
 |計画時間:0.713 ms | 
 |実行時間:0.040 ms | 
 

関数呼び出しを使用して同等のSELECTを使用する場合(この関数呼び出しの結果が定数であっても)、計画は完全に異なります。

_ EXPLAIN (ANALYZE) 
 SELECT 
     SUM(rpt_unique_clicks) 
 FROM 
     reports 
 WHERE 
     rpt_datetime >= date_trunc('day', now()) ;  
_
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ------------------- | 
 |集計(コスト= 245.74..245.75行= 1幅= 8)(実際の時間= 0.842..0.843行= 1ループ= 1)| 
 | ->追加(コスト= 0.00..238.20行= 3017幅= 4)(実際の時間= 0.837..0.838行= 1ループ= 1)| 
 | ->レポートのシーケンススキャン(コスト= 0.00..0.00行= 1幅= 4)(実際の時間= 0.003..0.003行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 | ->レポートのシーケンススキャン_20170409(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.214..0.214行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170410(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.097..0.097行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170411(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.095..0.095行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170412(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.096..0.096行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170413(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.131..0.131行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170414(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.098..0.098行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170415(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.095..0.095行= 0ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |フィルターによって削除された行:144 | 
 | ->レポートのシーケンススキャン_20170416(コスト= 0.00..29.78行= 377幅= 4)(実際の時間= 0.004..0.005行= 1ループ= 1)| 
 |フィルター:(rpt_datetime> = date_trunc( 'day' :: text、now()))| 
 |計画時間:0.298 ms | 
 |実行時間:0.892 ms | 
 

dbfiddle ---(ここ

4
joanolo