Обсуждение: Optimizer cleanup to avoid redundant work on joins

Поиск
Список
Период
Сортировка

Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
(This is mostly directed at Bruce, but anyone else who's looked at the
planner/optimizer is welcome to chime in.)

On the way to implementing estimates of WHERE-clause costs, I was
forced to notice that the estimated sizes of join relations are set
*after* all the planning work is done for a rel, instead of before,
which makes it hard to use the info for cost estimation :-(.
In looking at whether the sizes couldn't be set earlier, I saw that
trying to set them early enough to be used in planning would result in
duplicate effort; in fact there is a lot of duplicated effort already.
This is a proposal to rearrange the optimizer to clean that up.

Right now, the sequence of events in constructing a join tree is that
at each join level, make_one_rel_by_joins invokes make_rels_by_joins
to prepare a list of the joins to be considered at this level (where
a "join" means a particular outer rel and inner rel, and each "rel"
might consist of several already-joined base relations).  Each join
is represented by a RelOptInfo node constructed by make_join_rel (see
joinrels.c for these routines).  Then update_rels_pathlist_for_joins is
called to determine, for each of these joins, the best implementation or
"path".  Finally, since different "joins" in this sense may represent
the same set of joined base relations, merge_rels_with_same_relids is
called to match equivalent joinrels together and keep just the best path
for each equivalence class of joinrels.

My beef with this is that we should never be generating distinct
RelOptInfos in the first place for different ways of producing the
same join relation.  make_join_rel spends a fair amount of time and
memory space to produce the RelOptInfo and its substructure, and
for an N-component joinrel this price will be paid (at least) N times
over, after which we throw away all but one of the copies.   Even more
critically, once one joinrel has been completed for a particular set
of base rels, the implementation paths for other equivalent joinrels
will be considered in a vacuum --- we may have already discovered a
path that will dominate many of the paths for other ways of building
the same join relation, but because that path isn't available to
add_pathlist when we are looking at another joinrel, we will have to
keep paths that could have been discarded instantly.

It seems to me that join RelOptInfos should be managed in the same way
as base-relation RelOptInfos: there ought never be more than one of them
for a given set of Relids.  When we are considering a new pair of outer
and inner rels that can produce an already-considered join relation,
we should find the existing RelOptInfo for that join relation.  Then
add_pathlist will keep proposed paths only if they survive comparison
against paths already found from the earlier ways of generating the same
join relation.

This looks like it should be a fairly straightforward change: we should
meld make_join_rel and get_join_rel so that a requested join rel is
first searched for in root->join_rel_list, and only built if not present
(like get_base_rel).  The join rel would have a flat relids list from the
beginning, since it would be agnostic about which inner and outer subset
rels should be used to produce it.  Then update_rels_pathlist_for_joins
would be called *immediately* to process just that one join rel, passing
the outer and inner subset relid lists as separate parameters.  It would
generate paths using this pair of outer and inner rels, and would add
them to the join rel's path list only if they survive comparison against
all the already-found paths for that join rel.  On return from
make_rels_by_joins, all the work is done, so make_one_rel_by_joins
doesn't need to invoke either update_rels_pathlist_for_joins or
merge_rels_with_same_relids (the latter routine disappears entirely).
We have but to invoke rels_set_cheapest and then advance to the next
level of joining.

With this change, I could add more processing to make_join_rel to set
the estimated output rel size, without fear that it would be repeated
uselessly.

Anyone see a problem with this, or have a better idea about how to do
it?
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> This looks like it should be a fairly straightforward change: we should
> meld make_join_rel and get_join_rel so that a requested join rel is
> first searched for in root->join_rel_list, and only built if not present
> (like get_base_rel).  The join rel would have a flat relids list from the
> beginning, since it would be agnostic about which inner and outer subset
> rels should be used to produce it.  Then update_rels_pathlist_for_joins
> would be called *immediately* to process just that one join rel, passing
> the outer and inner subset relid lists as separate parameters.  It would
> generate paths using this pair of outer and inner rels, and would add
> them to the join rel's path list only if they survive comparison against
> all the already-found paths for that join rel.  On return from
> make_rels_by_joins, all the work is done, so make_one_rel_by_joins
> doesn't need to invoke either update_rels_pathlist_for_joins or
> merge_rels_with_same_relids (the latter routine disappears entirely).
> We have but to invoke rels_set_cheapest and then advance to the next
> level of joining.
> 
> With this change, I could add more processing to make_join_rel to set
> the estimated output rel size, without fear that it would be repeated
> uselessly.
> 
> Anyone see a problem with this, or have a better idea about how to do
> it?

Sounds good.  The only question is how easy it will be to see if there
already is a RelOptInfo for that combination.  My guess is that the
current code is brain-dead like the many fixes we made long ago.  It was
carrying around too many versions, instead of keeping the cheapest. 
Seems you have found another place that should be doing this.

--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026
 


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
>> This looks like it should be a fairly straightforward change: we should
>> meld make_join_rel and get_join_rel so that a requested join rel is
>> first searched for in root->join_rel_list, and only built if not present
>> (like get_base_rel).  The join rel would have a flat relids list from the
>> beginning, since it would be agnostic about which inner and outer subset
>> rels should be used to produce it.

Well, drat.  This idea looked good, and I still think it's good, but
implementation turns out to be trickier than I thought.  I was thinking
that RelOptInfos for join rels were essentially independent of which
pair of sub-relations were used to produce them --- eg, {1 2 3} doesn't
care if you made it from {1 2} joined to 3 or {1 3} joined to 2, etc.
That's almost true ... but it falls down on the restrictinfo list,
because which qual clauses are restrictions at a particular join level
*does* depend on the path you took to build it.  For example, if you
have a qual clause "t1.v1 = t2.v2", this clause will be a restrict
clause for {1 2 3} if you make it from {1 3} joined to 2, but if you
make it from {1 2} joined to 3 then the clause was already handled when
{1 2} was produced.

We could still unify the RelOptInfos for different ways of making the
same joinrel if we stored restrictinfo lists for joins in individual
join paths, rather than in the RelOptInfo.  I think that might be worth
doing, but the change is looking larger and subtler than I thought.
Probably best not to try to squeeze it in right before beta.

I will set aside the code I already rewrote for this purpose, and come
back to it after we start 7.1.
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
> I will set aside the code I already rewrote for this purpose, and come
> back to it after we start 7.1.

Wait a minute ... stop the presses ...

I just realized that the bug I was complaining of is *already there
in current sources*, and has been for a while (certainly since 6.5).
Look at prune.c's code that merges together RelOptInfos after-the-
fact:
       if (same(rel->relids, unmerged_rel->relids))       {           /*            * These rels are for the same set
ofbase relations,            * so get the best of their pathlists.  We assume it's            * ok to reassign a path
tothe other RelOptInfo without            * doing more than changing its parent pointer (cf. pathnode.c).            */
         rel->pathlist = add_pathlist(rel,                                        rel->pathlist,
               unmerged_rel->pathlist);       }
 

This is wrong, wrong, wrong, because the paths coming from different
RelOptInfos (different pairs of sub-relations) may need different sets
of qual clauses applied as restriction clauses.  There's no way to
represent that in the single RelOptInfo that will be left over.  The
worst case is that the generated plan is missing a needed restriction
clause (the other possibility is that the clause is evaluated
redundantly, which is much less painful).

I am not sure why we haven't heard bug reports about this.  It seems
like it wouldn't be hard at all to provoke the failure (I'm going to
try to make a test case right now).  Assuming I can do that, I think
we have no choice but to move join restrictlists into JoinPath nodes.
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> We could still unify the RelOptInfos for different ways of making the
> same joinrel if we stored restrictinfo lists for joins in individual
> join paths, rather than in the RelOptInfo.  I think that might be worth
> doing, but the change is looking larger and subtler than I thought.
> Probably best not to try to squeeze it in right before beta.
> 
> I will set aside the code I already rewrote for this purpose, and come
> back to it after we start 7.1.

What happened to the time-honored tradition of jamming partially-tested
features in before the beta feature freeze.  Are we getting too
conservative in our old age?  :-)

--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026
 


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> Well, drat.  This idea looked good, and I still think it's good, but
> implementation turns out to be trickier than I thought.  I was thinking
> that RelOptInfos for join rels were essentially independent of which
> pair of sub-relations were used to produce them --- eg, {1 2 3} doesn't
> care if you made it from {1 2} joined to 3 or {1 3} joined to 2, etc.
> That's almost true ... but it falls down on the restrictinfo list,
> because which qual clauses are restrictions at a particular join level
> *does* depend on the path you took to build it.  For example, if you
> have a qual clause "t1.v1 = t2.v2", this clause will be a restrict
> clause for {1 2 3} if you make it from {1 3} joined to 2, but if you
> make it from {1 2} joined to 3 then the clause was already handled when
> {1 2} was produced.

I thought "t1.v1 = t2.v2" would be in t1 RelOptInfo and t2 RelOptInfo. 
Of course, this is a join info restriction, not a restrict info
restriction.


> We could still unify the RelOptInfos for different ways of making the
> same joinrel if we stored restrictinfo lists for joins in individual
> join paths, rather than in the RelOptInfo.  I think that might be worth
> doing, but the change is looking larger and subtler than I thought.
> Probably best not to try to squeeze it in right before beta.

Aren't the restrict-info/join-info of the Final RelOptInfo set only when
the cheapest is found, and before that, the individual Reloptinfo's
restrict-info/join-info that are part of the Path are used?  Maybe these
are stupid questions.

--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026
 


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
Bruce Momjian <pgman@candle.pha.pa.us> writes:
> I thought "t1.v1 = t2.v2" would be in t1 RelOptInfo and t2 RelOptInfo. 
> Of course, this is a join info restriction, not a restrict info
> restriction.

Right, it would appear in t1's joininfo list (showing t2 as unjoined_relids)
and in t2's joininfo list (showing t1 as unjoined_relids).  Then when
we make a join rel from t1 + t2, the clause would be put into that rel's
restrictinfo list, since it's no longer a joining clause for the
joinrel; but it does need to be implemented at the time of the join.
(The bug is probably only visible for auxiliary quals that are not
being used as the driving clause of the join method; they need to show
up in the qpquals of the final plan, or they won't get enforced.)

The trouble comes when there are more rels in the picture.  If we make
a joinrel from t1 + t3, this clause will still appear in that joinrel's
joininfo list, since it's still a joinclause for that rel.  Then when
we make t1+t2+t3 from {t1 t3} and {t2}, the clause propagates up to
become a restrict clause of that rel, and that's where the buck stops
(and where the clause gets enforced at runtime).

*BUT*, if we make {t1 t2 t3} from {t1 t2} and {t3}, it will *not* show
this clause as a restrictclause, because in that path it gets handled at
the {t1 t2} join.  So a joinpath for {t1 t3} against {t2}, which needs
this clause to appear as a restrictclause, loses if it is copied into a
{t1 t2 t3} RelOptInfo that was made from the other pair of
sub-relations.

I find that I can exhibit the bug very easily in current sources:

create table t1 (k1 int, d1 int);
create table t2 (k2 int, d2 int);
create table t3 (k3 int, d3 int);
create table t4 (k4 int, d4 int);

insert into t1 values (1, 1);
insert into t1 values (2, 2);
insert into t1 values (3, 3);

insert into t2 values (1, 11);
insert into t2 values (2, 22);
insert into t2 values (3, 33);

insert into t3 values (1, 111);
insert into t3 values (2, 222);
insert into t3 values (3, 333);

insert into t4 values (1, 1111);
insert into t4 values (2, 2222);
insert into t4 values (3, 3333);

select * from t1,t2,t3,t4 where k1 = k2 and k1 = k3 and k2=k4
and d1<d2 and d1<d3 and d1<d4 and d2<d3 and d2<d4 and d3>d4;
k1 | d1 | k2 | d2 | k3 | d3  | k4 |  d4
----+----+----+----+----+-----+----+------ 1 |  1 |  1 | 11 |  1 | 111 |  1 | 1111 2 |  2 |  2 | 22 |  2 | 222 |  2 |
22223 |  3 |  3 | 33 |  3 | 333 |  3 | 3333
 
(3 rows)

which is obviously not meeting the restriction d3>d4.  So we have
a problem.

I haven't been able to make 6.5.3 fail similarly, but I do not
understand why not --- it certainly looks like it *ought* to fail given
the right combination of circumstances.  I think it may be escaping a
failure by sheer luck having to do with the order that RelOptInfos get
inserted into the query's join_rel_list.  Our changes since 6.5 may have
exposed a problem that was only latent before.  (Or maybe I just haven't
hit the right combination to trip up 6.5.3 ... but it does seem that
current sources fail more easily.)

Anyway, in the current sources things are certainly broken, and I don't
see any real alternative except to press forward with moving join
restrictinfos into JoinPaths.  Even if we figure out exactly why 6.5.*
is somehow failing to fail, I am pretty certain that it must be a
non-robust coincidence rather than a solution that we want to keep using.
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> Bruce Momjian <pgman@candle.pha.pa.us> writes:
> > I thought "t1.v1 = t2.v2" would be in t1 RelOptInfo and t2 RelOptInfo. 
> > Of course, this is a join info restriction, not a restrict info
> > restriction.
> 
> Right, it would appear in t1's joininfo list (showing t2 as unjoined_relids)
> and in t2's joininfo list (showing t1 as unjoined_relids).  Then when
> we make a join rel from t1 + t2, the clause would be put into that rel's
> restrictinfo list, since it's no longer a joining clause for the
> joinrel; but it does need to be implemented at the time of the join.
> (The bug is probably only visible for auxiliary quals that are not
> being used as the driving clause of the join method; they need to show
> up in the qpquals of the final plan, or they won't get enforced.)

I understand.  Is it only non-equi-joins that show this, where the join
is actually also a restriction in a sense.


--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026
 


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
> Anyway, in the current sources things are certainly broken, and I don't
> see any real alternative except to press forward with moving join
> restrictinfos into JoinPaths.  Even if we figure out exactly why 6.5.*
> is somehow failing to fail,

Er ... um ... ahem ... DUH!  The reason 6.5.3 works is that it does in
fact keep join restrictinfo pointers in JoinPaths.  I had eliminated
those pointers (the thoroughly undocumented "pathinfo" field) because
I thought that the lists were always the same as the parent relations'
restrictinfo lists.  Which they were --- at the time of creation of a
JoinPath.  What I missed was that prune.c moved a joinpath to belong
to a different RelOptInfo with (potentially) a different restrictinfo
list, but the joinpath needs to keep its original restrictinfo list.

In other words, I broke it.

Since surgery needs to be done anyway, I'm inclined to press ahead
with the changes I was going to put off.  On the other hand, if the
patient had a vote, it might ask for a second opinion ;-)
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> Er ... um ... ahem ... DUH!  The reason 6.5.3 works is that it does in
> fact keep join restrictinfo pointers in JoinPaths.  I had eliminated
> those pointers (the thoroughly undocumented "pathinfo" field) because
> I thought that the lists were always the same as the parent relations'
> restrictinfo lists.  Which they were --- at the time of creation of a
> JoinPath.  What I missed was that prune.c moved a joinpath to belong
> to a different RelOptInfo with (potentially) a different restrictinfo
> list, but the joinpath needs to keep its original restrictinfo list.
> 
> In other words, I broke it.
> 
> Since surgery needs to be done anyway, I'm inclined to press ahead
> with the changes I was going to put off.  On the other hand, if the
> patient had a vote, it might ask for a second opinion ;-)

Go for it.  Beta is for testing.  No better time to break things than
the present.

This is the first time I remember hearing about pre-beta release jitters
from many people.

--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026
 


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Tom Lane
Дата:
Bruce Momjian <pgman@candle.pha.pa.us> writes:
> This is the first time I remember hearing about pre-beta release jitters
> from many people.

Maybe our standards have gotten higher than they used to be.  I know
I really want 7.0 to be rock-solid, because I expect a lot of people
will be taking a new look at us when it comes out.
        regards, tom lane


Re: [HACKERS] Optimizer cleanup to avoid redundant work on joins

От
Bruce Momjian
Дата:
> Bruce Momjian <pgman@candle.pha.pa.us> writes:
> > This is the first time I remember hearing about pre-beta release jitters
> > from many people.
> 
> Maybe our standards have gotten higher than they used to be.  I know
> I really want 7.0 to be rock-solid, because I expect a lot of people
> will be taking a new look at us when it comes out.

I had a terrible habit of trowing things in.  I guess we are getting
more professional.

--  Bruce Momjian                        |  http://www.op.net/~candle pgman@candle.pha.pa.us               |  (610)
853-3000+  If your life is a hard drive,     |  830 Blythe Avenue +  Christ can be your backup.        |  Drexel Hill,
Pennsylvania19026