Konstantin Knizhnik <k.knizh...@postgrespro.ru> wrote:

> On 14.08.2017 19:33, Konstantin Knizhnik wrote:
> 
>  On 14.08.2017 12:37, Konstantin Knizhnik wrote: 
> 
>  Hi hackers, 
> 
>  I am trying to compare different ways of optimizing work with huge 
> append-only tables in PostgreSQL where primary key is something like 
> timestamp and queries are usually accessing most recent data using some 
> secondary keys. Size of secondary index is one of the most critical
>  factors limiting insert/search performance. As far as data is inserted in 
> timestamp ascending order, access to primary key is well localized and 
> accessed tables are present in memory. But if we create secondary key for the 
> whole table, then access to it will require random reads from
>  the disk and significantly decrease performance. 
> 
>  There are two well known solutions of the problem: 
>  1. Table partitioning 
>  2. Partial indexes 
> 
>  This approaches I want to compare. First of all I want to check if optimizer 
> is able to generate efficient query execution plan covering different time 
> intervals. 
>  Unfortunately in both cases generated plan is not optimal. 
> 
>  1. Table partitioning: 
> 
>  create table base (k integer primary key, v integer); 
>  create table part1 (check (k between 1 and 10000)) inherits (base); 
>  create table part2 (check (k between 10001 and 20000)) inherits (base); 
>  create index pi1 on part1(v); 
>  create index pi2 on part2(v); 
>  insert int part1 values (generate series(1,10000), random()); 
>  insert into part2 values (generate_series(10001,20000), random()); 
>  explain select * from base where k between 1 and 20000 and v = 100; 
>  QUERY PLAN 
>  ----------------------------------------------------------------------- 
>  Append (cost=0.00..15.65 rows=3 width=8) 
>  -> Seq Scan on base (cost=0.00..0.00 rows=1 width=8) 
>  Filter: ((k >= 1) AND (k <= 20000) AND (v = 100)) 
>  -> Index Scan using pi1 on part1 (cost=0.29..8.31 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  Filter: ((k >= 1) AND (k <= 20000)) 
>  -> Index Scan using pi2 on part2 (cost=0.29..7.34 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  Filter: ((k >= 1) AND (k <= 20000)) 
> 
>  Questions: 
>  - Is there some way to avoid sequential scan of parent table? Yes, it is 
> empty and so sequential scan will not take much time, but ... it still 
> requires some additional actions and so increasing query execution time. 
>  - Why index scan of partition indexes includes filter condition if it is 
> guaranteed by check constraint that all records of this partition match 
> search predicate? 
> 
>  2. Partial indexes: 
> 
>  create table t (k integer primary key, v integer); 
>  insert into t values (generate_series(1,20000),random()); 
>  create index i1 on t(v) where k between 1 and 10000; 
>  create index i2 on t(v) where k between 10001 and 20000; 
>  postgres=# explain select * from t where k between 1 and 10000 and v = 100; 
>  QUERY PLAN 
>  ------------------------------------------------------------ 
>  Index Scan using i1 on t (cost=0.29..7.28 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  (2 rows) 
> 
>  Here we get perfect plan. Let's try to extend search interval: 
> 
>  postgres=# explain select * from t where k between 1 and 20000 and v = 100; 
>  QUERY PLAN 
>  ------------------------------------------------------------------ 
>  Index Scan using t_pkey on t (cost=0.29..760.43 rows=1 width=8) 
>  Index Cond: ((k >= 1) AND (k <= 20000)) 
>  Filter: (v = 100) 
>  (3 rows) 
> 
>  Unfortunately in this case Postgres is not able to apply partial indexes. 
>  And this is what I expected to get: 
> 
>  postgres=# explain select * from t where k between 1 and 10000 and v = 100 
> union all select * from t where k between 10001 and 20000 and v = 100; 
>  QUERY PLAN 
>  ---------------------------------------------------------------------- 
>  Append (cost=0.29..14.58 rows=2 width=8) 
>  -> Index Scan using i1 on t (cost=0.29..7.28 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  -> Index Scan using i2 on t t_1 (cost=0.29..7.28 rows=1 width=8) 
>  Index Cond: (v = 100) 
> 
>  I wonder if there are some principle problems in supporting this two things 
> in optimizer: 
>  1. Remove search condition for primary key if it is fully satisfied by 
> derived table check constraint. 
>  2. Append index scans of several partial indexes if specified interval is 
> covered by their conditions. 
> 
>  I wonder if someone is familiar with this part of optimizer ad can easily 
> fix it. 
>  Otherwise I am going to spend some time on solving this problems (if 
> community think that such optimizations will be useful). 
> 
>  Replying to myself: the following small patch removes redundant checks from 
> index scans for derived tables: 
> 
>  diff --git a/src/backend/optimizer/util/plancat.c 
> b/src/backend/optimizer/util/plancat.c 
>  index 939045d..1f7c9cf 100644 
>  --- a/src/backend/optimizer/util/plancat.c 
>  +++ b/src/backend/optimizer/util/plancat.c 
>  @@ -1441,6 +1441,20 @@ relation_excluded_by_constraints(PlannerInfo *root, 
>  if (predicate_refuted_by(safe_constraints, rel->baserestrictinfo, false)) 
>  return true; 
> 
>  + /* 
>  + * Remove from restriction list items implied by table constraints 
>  + */ 
>  + safe_restrictions = NULL; 
>  + foreach(lc, rel->baserestrictinfo) 
>  + { 
>  + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); 
>  + if (!predicate_implied_by(list_make1(rinfo->clause), safe_constraints, 
> false)) { 
>  + safe_restrictions = lappend(safe_restrictions, rinfo); 
>  + } 
>  + } 
>  + rel->baserestrictinfo = safe_restrictions; 
>  + 
>  + 
>  return false; 
>  } 
> 
>  ================================================= 
> 
>  I am not sure if this is the right place of adjusting restriction list and 
> is such transformation always correct. 
>  But for the queries I have tested produced plans are correct: 
> 
>  postgres=# explain select * from base where k between 1 and 20000 and v = 
> 100; 
>  QUERY PLAN 
>  ----------------------------------------------------------------------- 
>  Append (cost=0.00..15.64 rows=3 width=8) 
>  -> Seq Scan on base (cost=0.00..0.00 rows=1 width=8) 
>  Filter: ((k >= 1) AND (k <= 20000) AND (v = 100)) 
>  -> Index Scan using pi1 on part1 (cost=0.29..8.30 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  -> Index Scan using pi2 on part2 (cost=0.29..7.34 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  (7 rows) 
> 
>  postgres=# explain select * from base where k between 1 and 15000 and v = 
> 100; 
>  QUERY PLAN 
>  ----------------------------------------------------------------------- 
>  Append (cost=0.00..15.64 rows=3 width=8) 
>  -> Seq Scan on base (cost=0.00..0.00 rows=1 width=8) 
>  Filter: ((k >= 1) AND (k <= 15000) AND (v = 100)) 
>  -> Index Scan using pi1 on part1 (cost=0.29..8.30 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  -> Index Scan using pi2 on part2 (cost=0.29..7.34 rows=1 width=8) 
>  Index Cond: (v = 100) 
>  Filter: (k <= 15000) 
>  (8 rows) 
> 
> There is one more problem with predicate_implied_by function and standard 
> Postgres partitions.
> Right now it specifies constrains for partitions using intervals with open 
> high boundary:
> 
> postgres=# create table bt (k integer, v integer) partition by range (k);
> postgres=# create table dt1 partition of bt for values from (1) to (10001);
> postgres=# create table dt2 partition of bt for values from (10001) to 
> (20001);
> postgres=# create index dti1 on dt1(v);
> postgres=# create index dti2 on dt2(v);
> postgres=# insert into bt values (generate_series(1,20000), random());
> 
> postgres=# \d+ dt2
> Table "public.dt2"
> Column | Type | Collation | Nullable | Default | Storage | Stats target | De
> scription 
> --------+---------+-----------+----------+---------+---------+--------------+---
> ----------
> k | integer | | | | plain | | 
> v | integer | | | | plain | | 
> Partition of: bt FOR VALUES FROM (10001) TO (20001)
> Partition constraint: ((k IS NOT NULL) AND (k >= 10001) AND (k < 20001))
> Indexes:
> "dti2" btree (v)
> 
> If now I run the query with predicate "between 1 and 20000", I still see 
> extra check in the plan:
> 
> postgres=# explain select * from bt where k between 1 and 20000 and v = 100;
> QUERY PLAN 
> ----------------------------------------------------------------------
> Append (cost=0.29..15.63 rows=2 width=8)
> -> Index Scan using dti1 on dt1 (cost=0.29..8.30 rows=1 width=8)
> Index Cond: (v = 100)
> -> Index Scan using dti2 on dt2 (cost=0.29..7.33 rows=1 width=8)
> Index Cond: (v = 100)
> Filter: (k <= 20000)
> (6 rows)
> 
> It is because operator_predicate_proof is not able to understand that k < 
> 20001 and k <= 20000 are equivalent for integer type.

> If I rewrite query as (k >= 1 and k < 20001) then plan is optimal:
> 
> postgres=# explain select * from bt where k >= 1 and k < 20001 and v = 100;
> QUERY PLAN 
> ----------------------------------------------------------------------
> Append (cost=0.29..15.63 rows=2 width=8)
> -> Index Scan using dti1 on dt1 (cost=0.29..8.30 rows=1 width=8)
> Index Cond: (v = 100)
> -> Index Scan using dti2 on dt2 (cost=0.29..7.33 rows=1 width=8)
> Index Cond: (v = 100)
> (5 rows)
> 
> It is fixed by one more patch of predtest.c:

Have you considered using the range types (internally in
operator_predicate_proof()) instead of hard-coding operator OIDs? The range
types do have the knowledge that k < 20001 and k <= 20000 are equivalent for
integer type:

postgres=# SELECT int4range '(, 20001)' = int4range '(, 20000]';
 ?column? 
----------
 t
(1 row)


-- 
Antonin Houska
Cybertec Schönig & Schönig GmbH
Gröhrmühlgasse 26
A-2700 Wiener Neustadt
Web: http://www.postgresql-support.de, http://www.cybertec.at

Reply via email to