Re: [HACKERS] path toward faster partition pruning - Mailing list pgsql-hackers

From David Rowley
Subject Re: [HACKERS] path toward faster partition pruning
Date
Msg-id CAKJS1f_Vs6tmDjx=TiL9MKdDy0xrh-Qotd6_ZHYA8fa92qahpQ@mail.gmail.com
Whole thread Raw
In response to Re: [HACKERS] path toward faster partition pruning  (Amit Langote <Langote_Amit_f8@lab.ntt.co.jp>)
Responses Re: [HACKERS] path toward faster partition pruning
Re: [HACKERS] path toward faster partition pruning
List pgsql-hackers
On 14 March 2018 at 00:37, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:
> Attached is a significantly revised version of the patch, although I admit
> it could still use some work with regard to comments and other cleanup.

Thanks for making all those changes. There's been quite a bit of churn!

I've looked over the patch and agree that there need to be more
comments to explain things. There were certainly times during my
review that I just skipped ahead having not understood what I'd been
looking at.

Here are the notes from my review:

1. I think get_unpruned_partitions is not a great name for this
function. get_matching_partitions would be better.

2. HASH partitioning can no longer prune non-matching partitions in
cases like: SELECT * FROM hashp WHERE partkey1 IS NULL and partkey2 IS
NULL; This might mean you need to process IS NULL and the key clauses
in the same step.

I see you have a test which checks the plan for

explain (costs off) select * from hp where a = 1 and b is null;

which ensures all partitions are included. Why?

3. Header comment for get_partitions_for_keys references 'keys' which
is not a parameter to that function.

4. If you wrote a function to process an individual step and called
that in a foreach loop inside get_unpruned_partitions, then you
wouldn't need to list_make1() here. Instead just call the function to
perform one step.

argparts =  get_unpruned_partitions(context,
list_make1(step));

5. I see you're performing AND step combination in two different ways.
In perform_pruning_combine_step you do;

andparts = andparts == NULL
? argparts
: bms_int_members(andparts, argparts);

but in get_unpruned_partitions, you add the entire range to the set
using bms_add_range, then intersect on that.

The code seems a bit fragile and relies on get_unpruned_partitions
returning an allocated Bitmapset all the time, even if bms_is_empty()
is true. There should be no distinction between NULL and a Bitmapset
that returns true on bms_is_empty().

What you've got here probably works for now, but only due to the fact
that get_unpruned_partitions allocate the entire range with
bms_add_range and that bms_int_members does not return NULL with no
matching members if both input sets are non-NULL. If that were to
change then your code would misbehave in cases like:

WHERE <matches no partition> AND <matches a partition>;

When processing the <matches no partition> clause, no partitions would
match, then if that resulted in an empty set you'd then surprisingly
match partitions again despite the AND clause not actually making it
possible for any partitions to have matched.

Probably you just need to bms_add_range in
perform_pruning_combine_step too and perform bms_int_members
unconditionally, just like you're doing in get_unpruned_partitions

6. The switch (last->op_strategy) in
generate_partition_pruning_steps_internal is missing a default ERROR
for unknown strategy

7. The switch: switch (context->strategy) in
generate_partition_pruning_steps_internal should ERROR rather than
break when it sees an unknown partition strategy.

8. Instead of copying the opclause here, wouldn't you be better just
this code come after you palloc the PartClause then just setup the
PartClause with the negator directly?

if (*is_neop_listp)
{
Assert(OidIsValid(negator));
opclause = copyObject(opclause);
opclause->opno = negator;
}

8. PartitionedChildRelInfo is still mentioned in typedefs.list

9. I don't quite understand PartitionPruneStepNoop. Why can't you just
skip adding anything to the list in
generate_partition_pruning_steps_internal?

10. The following test does not make sense:

explain (costs off) select * from hp where (a = 10 and b = 'yyy') or
(a = 10 and b = 'xxx') or (a is null and b is null);
                                                       QUERY PLAN

-------------------------------------------------------------------------------------------------------------------------
 Append
   ->  Seq Scan on hp0
         Filter: (((a = 10) AND (b = 'yyy'::text)) OR ((a = 10) AND (b
= 'xxx'::text)) OR ((a IS NULL) AND (b IS NULL)))
   ->  Seq Scan on hp1
         Filter: (((a = 10) AND (b = 'yyy'::text)) OR ((a = 10) AND (b
= 'xxx'::text)) OR ((a IS NULL) AND (b IS NULL)))
   ->  Seq Scan on hp2
         Filter: (((a = 10) AND (b = 'yyy'::text)) OR ((a = 10) AND (b
= 'xxx'::text)) OR ((a IS NULL) AND (b IS NULL)))
   ->  Seq Scan on hp3
         Filter: (((a = 10) AND (b = 'yyy'::text)) OR ((a = 10) AND (b
= 'xxx'::text)) OR ((a IS NULL) AND (b IS NULL)))
(9 rows)

Why do 4 partitions match when there are only 3 sets of clauses when
each one can only match a single partition?

11. What does "root" mean here?

-- case for list partitioned table that's not root
explain (costs off) select * from rlp where a = 15 and b <> 'ab' and b
<> 'cd' and b <> 'xy' and b is not null;

-- 
 David Rowley                   http://www.2ndQuadrant.com/
 PostgreSQL Development, 24x7 Support, Training & Services


pgsql-hackers by date:

Previous
From: Simon Riggs
Date:
Subject: Re: Faster inserts with mostly-monotonically increasing values
Next
From: Ildus Kurbangaliev
Date:
Subject: Re: committing inside cursor loop