Postgresql分区表timestamptz约束问题

one*_*vat 5 postgresql constraint partitioning timestamp check-constraints

该表reports按天分区表,例如reports_20170414reports_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
)
Run Code Online (Sandbox Code Playgroud)

让我们考虑两种类型的查询

SELECT SUM(rpt_unique_clicks) 
    FROM reports WHERE rpt_datetime >= '2017-04-14 00:00:00';
Run Code Online (Sandbox Code Playgroud)

以上查询在亚秒内运行,一切正常。

SELECT SUM(rpt_unique_clicks) 
    FROM reports WHERE rpt_datetime >= 
 date_trunc('day', current_timestamp);
Run Code Online (Sandbox Code Playgroud)

相反,上面的查询运行至少 15 秒。

SELECT date_trunc('day', CURRENT_TIMESTAMP), '2017-04-14 00:00:00';
Run Code Online (Sandbox Code Playgroud)

返回

2017-04-14 00:00:00 +00:00 | 2017-04-14 00:00:00
Run Code Online (Sandbox Code Playgroud)

当我检查为什么后者运行时间长(使用解释分析)时,我完成了它访问并扫描每个表(~500)的结果,但前者仅访问,reports_20170414因此约束检查存在问题。

我想查询今天,而不是像后一个查询那样使用准备好的语句。为什么date_trunc('day', CURRENT_TIMESTAMP)不等于 2017-04-14 00:00:00

joa*_*olo 5

我无法完全回答为什么date_trunc('day', CURRENT_TIMESTAMP)不等于常量......即使两者CURRENT_TIMESTAMPdate_trunc定义为IMMUTABLE

但我认为我们可以做出一个实验性的、有根据的猜测:显然,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
 ) ;
Run Code Online (Sandbox Code Playgroud)

我们创建一个自动分区插入触发器:

 -- 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();
Run Code Online (Sandbox Code Playgroud)

用一些数据填充(分区)表;并检查触发器所做的分区:

 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;
Run Code Online (Sandbox Code Playgroud)

| 表名 |
 | :-------------- |
 | 报告_20170409 |
 | 报告_20170410 |
 | 报告_20170411 |
 | 报告_20170412 |
 | 报告_20170413 |
 | 报告_20170414 |
 | 报告_20170415 |
 | 报告_20170416 |
 

此时,我们检查两个不同的查询。第一个确实使用了constantrpt_datetime

 EXPLAIN (ANALYZE) 
 SELECT 
     SUM(rpt_unique_clicks) 
 FROM 
     reports 
 WHERE 
     rpt_datetime >= timestamp '20170416' ; 
Run Code Online (Sandbox Code Playgroud)

使用恒定时间戳,仅检查“报告”和适当的分区:

| 查询计划|
 | :---------------------------------------------------------------- -------------------------------------------------- ------------------- |
 | 聚合(成本=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) |
 | -> 对 reports_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()) ;  
Run Code Online (Sandbox Code Playgroud)

| 查询计划|
 | :---------------------------------------------------------------- -------------------------------------------------- ------------------- |
 | 聚合(成本=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())) |
 | -> 对 reports_20170409 进行顺序扫描(成本=0.00..29.78行=377宽度=4)(实际时间=0.214..0.214行=0循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170410 进行顺序扫描(成本=0.00..29.78 行=377 宽度=4)(实际时间=0.097..0.097 行=0 循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170411 进行顺序扫描(成本=0.00..29.78 行=377 宽度=4)(实际时间=0.095..0.095 行=0 循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170412 进行顺序扫描(成本=0.00..29.78 行=377 宽度=4)(实际时间=0.096..0.096 行=0 循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170413 进行顺序扫描(成本=0.00..29.78行=377宽度=4)(实际时间=0.131..0.131行=0循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170414 进行顺序扫描(成本=0.00..29.78 行=377 宽度=4)(实际时间=0.098..0.098 行=0 循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_20170415 进行顺序扫描(成本=0.00..29.78 行=377 宽度=4)(实际时间=0.095..0.095 行=0 循环=1)|
 | 过滤器: (rpt_datetime >= date_trunc('day'::text, now())) |
 | 过滤器删除的行数:144 |
 | -> 对 reports_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在这里