Thread: 8.2 bug with outer join reordering

8.2 bug with outer join reordering

From
Jeff Davis
Date:
Thanks to rcohen on IRC yesterday for pointing this out and providing
his query + EXPLAIN output.

It looks like he still hasn't posted it to -bugs, and I was finally able
to reproduce it in a narrower, self contained test case, so I'm posting
this right now.

On 8.1 this returns 1 record. On 8.2 this returns 100000. It appears to
be applying the filter too soon, and then it does an outer join which
violates the WHERE.

Regards,
    Jeff Davis

test=> SELECT version();
                                              version
-------------------------------------------------------------------------
 PostgreSQL 8.2.0 on i686-pc-linux-gnu, compiled by GCC gcc (GCC) 3.4.5
20051201 (Red Hat 3.4.5-2)
(1 row)

test=> CREATE TABLE t1 (a int, b int);
CREATE TABLE
test=> CREATE TABLE t2 (c int, d int);
CREATE TABLE
test=> CREATE TABLE t3 (e int, f int);
CREATE TABLE
test=> CREATE TABLE t4 (g int, h int);
CREATE TABLE
test=>
test=> INSERT INTO t1 SELECT generate_series, 1 from generate_series
(1,100000);
INSERT 0 100000
test=> COPY t2 FROM stdin DELIMITER ',';
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> 1,19
2,22
3,23
4,24
5,25
6,26
7,27
8,28
9,29
10,30
11,31
\.>> >> >> >> >> >> >> >> >> >> >>
test=> INSERT INTO t3 SELECT generate_series, 10 from generate_series
(1,10000);
INSERT 0 10000
test=> INSERT INTO t4 VALUES
test->   (19,4);
INSERT 0 1
test=>
test=> CREATE INDEX t3_e_idx ON t3 (e);
CREATE INDEX
test=> CREATE INDEX t4_g_idx ON t4 (g);
CREATE INDEX
test=> SELECT COUNT(*) FROM
test-> t1
test-> LEFT JOIN
test-> t2 ON ( t1.a = t2.c )
test-> LEFT JOIN
test-> t3 ON ( t2.d = t3.e )
test-> LEFT JOIN
test-> t4 ON ( t2.d = t4.g )
test-> WHERE ( t3.e = 19 OR t4.g = 19);
 count
--------
 100000
(1 row)

test=> EXPLAIN SELECT COUNT(*) FROM
test-> t1
test-> LEFT JOIN
test-> t2 ON ( t1.a = t2.c )
test-> LEFT JOIN
test-> t3 ON ( t2.d = t3.e )
test-> LEFT JOIN
test-> t4 ON ( t2.d = t4.g )
test-> WHERE ( t3.e = 19 OR t4.g = 19);
                                              QUERY PLAN
-------------------------------------------------------------------------
 Aggregate  (cost=831486.42..831486.43 rows=1 width=0)
   ->  Merge Left Join  (cost=22541.80..715990.92 rows=46198200 width=0)
         Merge Cond: (t1.a = t2.c)
         ->  Sort  (cost=10626.30..10864.44 rows=95254 width=4)
               Sort Key: t1.a
               ->  Seq Scan on t1  (cost=0.00..1443.54 rows=95254
width=4)
         ->  Sort  (cost=11915.49..12157.99 rows=97000 width=4)
               Sort Key: t2.c
               ->  Hash Left Join  (cost=136.35..2554.63 rows=97000
width=4)
                     Hash Cond: (t2.d = t4.g)
                     Filter: ((t3.e = 19) OR (t4.g = 19))
                     ->  Merge Right Join  (cost=135.34..2061.34
rows=97000 width=12)
                           Merge Cond: (t3.e = t2.d)
                           ->  Index Scan using t3_e_idx on t3
(cost=0.00..446.00 rows=10000 width=4)
                           ->  Sort  (cost=135.34..140.19 rows=1940
width=8)
                                 Sort Key: t2.d
                                 ->  Seq Scan on t2  (cost=0.00..29.40
rows=1940 width=8)
                     ->  Hash  (cost=1.01..1.01 rows=1 width=4)
                           ->  Seq Scan on t4  (cost=0.00..1.01 rows=1
width=4)
(19 rows)

test=>

Re: 8.2 bug with outer join reordering

From
Tom Lane
Date:
Jeff Davis <pgsql@j-davis.com> writes:
> On 8.1 this returns 1 record. On 8.2 this returns 100000. It appears to
> be applying the filter too soon, and then it does an outer join which
> violates the WHERE.

AFAICS the outer join reordering is perfectly legal --- the problem is
that the WHERE condition is being allowed to bubble down too far.
I can't reproduce it with less than four tables, so it's a pretty
weird corner case.  Apparently there's something wrong with
distribute_qual_to_rels' logic for determining qual placement, but
I'm not sure what yet ...

            regards, tom lane

Re: 8.2 bug with outer join reordering

From
Tom Lane
Date:
I wrote:
> I can't reproduce it with less than four tables, so it's a pretty
> weird corner case.  Apparently there's something wrong with
> distribute_qual_to_rels' logic for determining qual placement, but
> I'm not sure what yet ...

OK, I've localized the problem and been able to reduce it to a
three-table example.  Using the regression database:

select count(*) from tenk1 a left join tenk1 b on (a.unique1=b.unique1) left join onek c on (b.unique1=c.unique1)
where case c.ten when 1 then true else false end;

This should report 100, but in 8.2 it's reporting 10000, because the
WHERE condition is being pushed down below the outer join with "a".
(Note: you might think the WHERE should be just "c.ten = 1", but the
planner understands that that's strict and reduces all the left joins
to plain joins, hiding the bug.  It's not bright enough to see the
CASE as strict, however.)

The problem comes in distribute_qual_to_rels() where it's trying to
decide the minimum set of relations that have to be joined before the
WHERE qual can be applied:
        * For a non-outer-join qual, we can evaluate the qual as soon as (1)        * we have all the rels it mentions,
and(2) we are at or above any        * outer joins that can null any of these rels and are below the        * syntactic
locationof the given qual. To enforce the latter, scan        * the oj_info_list and merge the required-relid sets of
anysuch OJs        * into the clause's own reference list.  At the time we are called,        * the oj_info_list
containsonly outer joins below this qual.
 

The oj_info_list contains two outer joins, respectively having
min_lefthand = a    min_righthand = b
min_lefthand = b    min_righthand = c

which (correctly) asserts than we can do either the a/b or b/c join
first ... but not a/c first.  However, since the WHERE qual mentions
only c, distribute_qual_to_rels concludes that it can be applied as
soon as the b/c join is formed.  And because of the relative sizes
of the tables, the planner prefers to do that join before the a/c one.

The reason the bug doesn't exist in 8.1 and older is that those releases
applied essentially this logic using the *syntactic* sets of relations
contained in different levels of outer join, and so a qual using only
(c) would have a and b added to it based on the syntax tree alone.

I think that this can be fixed by having the code iterate over the
oj_info_list repeatedly, until unioning adds no more rels.  That is, in
the first pass we'd have (c) and we'd add b because c appears in the RHS
of the second OJ; then in the second pass we'd start with (b,c) and add
a because b appears in the RHS of the first OJ; then the third pass
would start with (a,b,c), find nothing to add, and be done.  This is
appropriate because an OJ having b on its RHS must in fact have been
syntactically above the b/c join --- the only reason it doesn't have c
in its RHS now is that make_outerjoininfo() decided it was safe to
interchange the ordering of the two OJs.  But a qual coming from above
both OJs can't be allowed to bubble down below either.

Comments?  Anyone see any remaining holes in this logic?
        regards, tom lane