Skip to main content
Question

Vertica chosing non optimal join order

  • March 4, 2019
  • 7 replies
  • 21 views

chaima
Forum|alt.badge.img+2
  • Participating Frequently

Hi experts,

One of our customers has complained of the performance of a query that should be fast because one of the tables was empty:

        select count(*) from empty_table c,  big_table b
        WHERE c.id1 = b.id1  
        AND c.id2       = b.id2  
        AND c.id3   = b.id3
        AND c.col = 354828;

the query takes 27 seconds (join without where clause takes 50ms)

Looking at the query plan, Vertica was choosing the empty table in the outer part of the join and the big one in the Inner part, I noticed that this only happens when 1- there is a predicate on the empty table and 2- there is a primary key defined on the big table (on which we do the join).
I managed to reproduce this behavior with a simple example (below ), does anyone know why Vertica doesn't make an optimal choice in this case? or should I ask the customer to open a support case?

My example:

    CREATE TABLE public.should_be_outer
    (
        id  IDENTITY ,
        col1 char(20),
        col2 int,
        col3 int,
        col4 char(12)
    );

ALTER TABLE public.should_be_outer ADD CONSTRAINT C_PRIMARY PRIMARY KEY (id) ENABLED;

    CREATE TABLE public.should_be_inner
    (
        id  IDENTITY ,
        val int
    );

Query: takes 1474.912ms

    dbadmin=>  select  count(*) from should_be_inner a JOIN should_be_outer b
    dbadmin-> ON a.id=b.id
    dbadmin-> WHERE a.val=1;
     count
    -------
         0
    (1 row)

    Time: First fetch (1 row): 1474.885 ms. All rows formatted: 1474.912 ms

Explain plan:

 explain  select  count(*) from should_be_inner a JOIN should_be_outer b
 ON a.id=b.id
 WHERE a.val=1;

 Access Path:
 +-GROUPBY NOTHING [Cost: 47K, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 1)
 |  Aggregates: count(*)
 |  Execute on: All Nodes
 | +---> JOIN HASH [Cost: 47K, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 2) Outer (BROADCAST)(LOCAL ROUND ROBIN)
 | |      Join Cond: (a.id = b.id)
 | |      Execute on: All Nodes
 | | +-- Outer -> STORAGE ACCESS for a [Cost: 22, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 3)
 | | |      Projection: public.should_be_inner_b0
 | | |      Materialize: a.id
 | | |      Filter: (a.val = 1)
 | | |      Execute on: All Nodes
 | | +-- Inner -> STORAGE ACCESS for b [Cost: 28K, Rows: 48M] (PATH ID: 4)
 | | |      Projection: public.should_be_outer_b0
 | | |      Materialize: b.id
 | | |      Execute on: All Nodes

When doing just a join without a where clause:

 explain  select  count(*) from should_be_inner a JOIN should_be_outer b
  ON a.id=b.id
 ;
 Access Path:
 +-GROUPBY NOTHING [Cost: 33K, Rows: 1] (PATH ID: 1)
 |  Aggregates: count(*)
 |  Execute on: All Nodes
 | +---> JOIN HASH [Cost: 33K, Rows: 10] (PATH ID: 2) Inner (BROADCAST)
 | |      Join Cond: (a.id = b.id)
 | |      Execute on: All Nodes
 | | +-- Outer -> STORAGE ACCESS for b [Cost: 28K, Rows: 48M] (PATH ID: 3)
 | | |      Projection: public.should_be_outer_b0
 | | |      Materialize: b.id
 | | |      Execute on: All Nodes
 | | |      Runtime Filter: (SIP1(HashJoin): b.id)
 | | +-- Inner -> STORAGE ACCESS for a [Cost: 17, Rows: 10] (PUSHED GROUPING) (PATH ID: 4)
 | | |      Projection: public.should_be_inner_b0
 | | |      Materialize: a.id
 | | |      Execute on: All Nodes

When forcing the join order on the query: 276.077 ms

dbadmin=>  select /*+syntactic_join*/ count(*) from should_be_outer b JOIN should_be_inner a
dbadmin-> ON a.id=b.id
dbadmin-> WHERE a.val=1;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 276.034 ms. All rows formatted: 276.077 ms

             Access Path:
             +-GROUPBY NOTHING [Cost: 33K, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 1)
             |  Aggregates: count(*)
             |  Execute on: All Nodes
             | +---> JOIN HASH [Cost: 33K, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 2) Inner (BROADCAST)
             | |      Join Cond: (a.id = b.id)
             | |      Execute on: All Nodes
             | | +-- Outer -> STORAGE ACCESS for b [Cost: 28K, Rows: 48M] (PATH ID: 3)
             | | |      Projection: public.should_be_outer_b0
             | | |      Materialize: b.id
             | | |      Execute on: All Nodes
             | | |      Runtime Filter: (SIP1(HashJoin): b.id)
             | | +-- Inner -> STORAGE ACCESS for a [Cost: 22, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PUSHED GROUPING) (PATH ID: 4)
             | | |      Projection: public.should_be_inner_b0
             | | |      Materialize: a.id
             | | |      Filter: (a.val = 1)
             | | |      Execute on: All Nodes

When I drop the primary key:

dbadmin=>  select  count(*) from should_be_inner a JOIN should_be_outer b
dbadmin-> ON a.id=b.id
dbadmin-> WHERE a.val=1;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 265.322 ms. All rows formatted: 265.349 ms
 explain  select  count(*) from should_be_inner a JOIN should_be_outer b
 ON a.id=b.id
 WHERE a.val=1;

 Access Path:
 +-GROUPBY NOTHING [Cost: 33K, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 1)
 |  Aggregates: count(*)
 |  Execute on: All Nodes
 | +---> JOIN HASH [Cost: 33K, Rows: 2 (PREDICATE VALUE OUT-OF-RANGE)] (PATH ID: 2) Inner (BROADCAST)
 | |      Join Cond: (a.id = b.id)
 | |      Execute on: All Nodes
 | | +-- Outer -> STORAGE ACCESS for b [Cost: 28K, Rows: 48M] (PATH ID: 3)
 | | |      Projection: public.should_be_outer_b0
 | | |      Materialize: b.id
 | | |      Execute on: All Nodes
 | | |      Runtime Filter: (SIP1(HashJoin): b.id)
 | | +-- Inner -> STORAGE ACCESS for a [Cost: 22, Rows: 1 (PREDICATE VALUE OUT-OF-RANGE)] (PUSHED GROUPING) (PATH ID: 4)
 | | |      Projection: public.should_be_inner_b0
 | | |      Materialize: a.id
 | | |      Filter: (a.val = 1)
 | | |      Execute on: All Nodes

Many thanks,

Chaima

7 replies

Jim_Knicely
Forum|alt.badge.img+2
  • Participating Frequently
  • March 4, 2019

Hi,

I'm not seeing a performance issue.

Example:

dbadmin->vmart@sandbox1=> SELECT COUNT(*) FROM empty_table;
 COUNT
-------
     0
(1 row)

Time: First fetch (1 row): 78.323 ms. All rows formatted: 78.385 ms

dbadmin->vmart@sandbox1=>* SELECT COUNT(*) FROM big_table;
  COUNT
----------
 67108864
(1 row)

Time: First fetch (1 row): 472.884 ms. All rows formatted: 472.917 ms

dbadmin->vmart@sandbox1=>* select count(*) from empty_table c,  big_table b
dbadmin->vmart@sandbox1-->*         WHERE c.id1 = b.id1
dbadmin->vmart@sandbox1-->*         AND c.id2       = b.id2
dbadmin->vmart@sandbox1-->*         AND c.id3   = b.id3
dbadmin->vmart@sandbox1-->*         AND c.col = 354828;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 41.650 ms. All rows formatted: 41.683 ms

chaima
Forum|alt.badge.img+2
  • Author
  • Participating Frequently
  • March 4, 2019

Hi Jim,

Thank you for your testing.
Does your table big_table has an enabled primary key on id1,id2,id3?
Could you please share your Vertica version as well as the explain plans to see if we have the same join orders?


Jim_Knicely
Forum|alt.badge.img+2
  • Participating Frequently
  • March 4, 2019

Interesting. If I add the PK on the big table, the query performs terribly!

dbadmin->vmart@sandbox1=> ALTER TABLE big_table ADD CONSTRAINT empty_table_pk PRIMARY KEY(id1, id2, id3) ENABLED;
WARNING 2623:  Column "id1" definition changed to NOT NULL
WARNING 2623:  Column "id2" definition changed to NOT NULL
WARNING 2623:  Column "id3" definition changed to NOT NULL
ALTER TABLE
Time: First fetch (0 rows): 8313.119 ms. All rows formatted: 8313.129 ms

dbadmin->vmart@sandbox1=> select count(*) from empty_table c,  big_table b
dbadmin->vmart@sandbox1-->         WHERE c.id1 = b.id1
dbadmin->vmart@sandbox1-->         AND c.id2       = b.id2
dbadmin->vmart@sandbox1-->         AND c.id3   = b.id3
dbadmin->vmart@sandbox1-->         AND c.col = 354828;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 47271.907 ms. All rows formatted: 47271.939 ms

But if I add a PK to the empty table, the performance is what is expected:

dbadmin->vmart@sandbox1=>* ALTER TABLE empty_table ADD CONSTRAINT empty_table_pk PRIMARY KEY(id1, id2, id3) ENABLED;
ALTER TABLE
Time: First fetch (0 rows): 35.093 ms. All rows formatted: 35.103 ms

dbadmin->vmart@sandbox1=>* select count(*) from empty_table c,  big_table b
dbadmin->vmart@sandbox1-->*         WHERE c.id1 = b.id1
dbadmin->vmart@sandbox1-->*         AND c.id2       = b.id2
dbadmin->vmart@sandbox1-->*         AND c.id3   = b.id3
dbadmin->vmart@sandbox1-->*         AND c.col = 354828;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 35.167 ms. All rows formatted: 35.199 ms

Jim_Knicely
Forum|alt.badge.img+2
  • Participating Frequently
  • March 4, 2019

Actually, running stats should all that is needed:

dbadmin->vmart@sandbox1=>* ALTER TABLE empty_table DROP CONSTRAINT empty_table_pk;
ALTER TABLE
Time: First fetch (0 rows): 35.093 ms. All rows formatted: 35.103 ms

dbadmin->vmart@sandbox1=>* SELECT analyze_statistics('empty_table');
 analyze_statistics
--------------------
                  0
(1 row)

Time: First fetch (1 row): 73.393 ms. All rows formatted: 73.427 ms

dbadmin->vmart@sandbox1=>* SELECT analyze_statistics('big_table');
 analyze_statistics
--------------------
                  0
(1 row)

Time: First fetch (1 row): 3224.203 ms. All rows formatted: 3224.235 ms

dbadmin->vmart@sandbox1=>* select count(*) from empty_table c,  big_table b
dbadmin->vmart@sandbox1-->*         WHERE c.id1 = b.id1
dbadmin->vmart@sandbox1-->*         AND c.id2       = b.id2
dbadmin->vmart@sandbox1-->*         AND c.id3   = b.id3
dbadmin->vmart@sandbox1-->*         AND c.col = 354828;
 count
-------
     0
(1 row)

Time: First fetch (1 row): 29.584 ms. All rows formatted: 29.616 ms

chaima
Forum|alt.badge.img+2
  • Author
  • Participating Frequently
  • March 4, 2019

weird .. In my example statistics are up to date, If big_table has a primary key and I use a where clause on the small or empty table, the big_table is in the Inner part of the join, if I don't put a where clause, big_table is in the Outer join as I would expect. this is what the customer is having also on their real data and query...


Car1os
Forum|alt.badge.img
  • Participating Frequently
  • March 4, 2019

You can set the join priority (FORCE OUTER integer) on the big table to force it to always be on the outside. Check the alter table docs.


Vertica_Curtis
Forum|alt.badge.img+1
  • Participating Frequently
  • March 4, 2019

I've seen situations where the number of columns in the table will flip the inner/outer of a query because Vertica will assign a cost to the number of columns it has to read from. Since this query is a doing a count() that could be affecting the behavior here, since it has to weigh the cost of one tables count() vs. the other one. Just a thought. I didn't test this on this particular case.