Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

planner:dont push down right condition for anti semi join #12075

Closed
wants to merge 34 commits into from

Conversation

jingyugao
Copy link
Contributor

What problem does this PR solve?

TiDB will push down right condition for anti semi join.
But if we push down the right condition, we can't know why there is no matched rows, null or false.
Fix #12074.

What is changed and how it works?

For anti semi join don't push down the righ condition.

Check List

Tests

  • Unit test

Code changes

Simple change.

Side effects

After this pr,we can't push down the right condition for anti semi join.
So this might decrease the performance.

Related changes

Maybe not

Release note

  • Write release note for bug-fix or new feature.

@sre-bot sre-bot added the contribution This PR is from a community contributor. label Sep 8, 2019
@codecov
Copy link

codecov bot commented Sep 8, 2019

Codecov Report

Merging #12075 into master will decrease coverage by 0.1426%.
The diff coverage is 79.3103%.

@@               Coverage Diff                @@
##             master     #12075        +/-   ##
================================================
- Coverage   80.0859%   79.9433%   -0.1426%     
================================================
  Files           473        473                
  Lines        116787     116186       -601     
================================================
- Hits          93530      92883       -647     
- Misses        15943      15978        +35     
- Partials       7314       7325        +11

@@ -173,6 +173,12 @@ func (p *LogicalJoin) PredicatePushDown(predicates []expression.Expression) (ret
p.OtherConditions = otherCond
leftCond = leftPushCond
rightCond = rightPushCond
if p.JoinType == AntiSemiJoin {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be too strict. Look at the explain-test(tpc-h q21)this PR has affected.

select
	s_name,
	count(*) as numwait
from
	supplier,
	lineitem l1,
	orders,
	nation
where
	s_suppkey = l1.l_suppkey
	and o_orderkey = l1.l_orderkey
	and o_orderstatus = 'F'
	and l1.l_receiptdate > l1.l_commitdate
	and exists (
		select
			*
		from
			lineitem l2
		where
			l2.l_orderkey = l1.l_orderkey
			and l2.l_suppkey <> l1.l_suppkey
	)
	and not exists (
		select
			*
		from
			lineitem l3
		where
			l3.l_orderkey = l1.l_orderkey
			and l3.l_suppkey <> l1.l_suppkey
			and l3.l_receiptdate > l3.l_commitdate
	)
	and s_nationkey = n_nationkey
	and n_name = 'EGYPT'
group by
	s_name

After this change, the subquery condition l3.l_receiptdate > l3.l_commitdate cannot be pushed down. This can casue a performance regression.

Actually, although not exists and not in both use anti-semi-join, but they are not totally equal. I think we'd better fix this issue for not in, and not affect the behavior of not exists.

Here are some reference you can take a look:
This is how mysql deals with exists strategy.
https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization-with-exists.html

And this is how SparkSQL uses a null aware anti join for not in(I prefer this method to rewrite the not in condition). https://databricks-prod-cloudfront.cloud.databricks.com/public/4027ec902e239c93eaaa8714f173bcfc/2728434780191932/1483312212640900/6987336228780374/latest.html

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is how null aware anti join works (quoted from Apache Spark):

Expand the NOT IN expression with the NULL-aware semantic
to its full form. That is from:
  (a1,a2,...) = (b1,b2,...)
to
  (a1=b1 OR isnull(a1=b1)) AND (a2=b2 OR isnull(a2=b2)) AND ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks,maybe I need to rehink about it. Please remove the review request.This commit is too hasty.

Copy link
Contributor

@eurekaka eurekaka Sep 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check the InOperand field of RightConditions's arguments to decide if we should disable pushdown for it. InOperand is introduced to differentiate the AntiSemiJoin / LeftAntiSemiJoin / AntiLeftOuterSemiJoin generated from NOT IN and NOT EXISTS.

Signed-off-by: jingyugao <1121087373@qq.com>
@jingyugao
Copy link
Contributor Author

/rebuild

Signed-off-by: jingyugao <1121087373@qq.com>
Signed-off-by: jingyugao <1121087373@qq.com>
Signed-off-by: jingyugao <1121087373@qq.com>
Signed-off-by: jingyugao <1121087373@qq.com>
└─Projection_22 10000.00 root cast(5_aux_0)
└─HashLeftJoin_21 10000.00 root CARTESIAN left outer semi join, inner:TableReader_20
└─Projection_21 10000.00 root cast(5_aux_0)
└─HashLeftJoin_20 10000.00 root CARTESIAN left outer semi join, inner:TableReader_19, other cond:eq(6, test.t2.c2)
├─IndexReader_17 10000.00 root index:IndexScan_16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here the eq(6, test.t2.c2) is moved into other condition because it is from 6 in (subq)

node [style=filled, color=lightgrey]
color=black
label = "cop"
"Selection_13" -> "TableScan_12"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selection_13 is eq(1,t2.c2) which is from 1 in (subq)

@@ -123,12 +123,11 @@ set @@session.tidb_opt_insubq_to_join_and_agg=0;
explain select 1 in (select c2 from t2) from t1;
id count task operator info
Projection_6 1999.00 root 5_aux_0
└─HashLeftJoin_7 1999.00 root CARTESIAN left outer semi join, inner:TableReader_14
└─HashLeftJoin_7 1999.00 root CARTESIAN left outer semi join, inner:TableReader_13, other cond:eq(1, test.t2.c2)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eq(1, test.t2.c2) is from 1 in (subq)

node [style=filled, color=lightgrey]
color=black
label = "cop"
"Selection_13" -> "TableScan_12"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selection_13 is from 1 in (subq)

tk.MustQuery("select 1 in (select b from t2) from t1").Check(testkit.Rows("<nil>"))
tk.MustQuery("select 1 not in (select b from t2) from t1").Check(testkit.Rows("<nil>"))
// TODO: this query will cause an index out of range panic
// tk.MustQuery("select 1 not in (select null from t1) from t2").Check(testkit.Rows())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is another bug when executing physical_plan,so I can't add this test case.

Signed-off-by: jingyugao <1121087373@qq.com>
Signed-off-by: jingyugao <1121087373@qq.com>
@@ -299,7 +303,7 @@ func (p *LogicalJoin) extractOnCondition(conditions []expression.Expression, der
// false even if t.a is null or s.a is null. To make this join "empty aware",
// we should differentiate `t.a = s.a` from other column equal conditions, so
// we put it into OtherConditions instead of EqualConditions of join.
if binop.FuncName.L == ast.EQ && !arg0.InOperand && !arg1.InOperand {
if binop.FuncName.L == ast.EQ {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the comments here to the front as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

cols := make([]*Column, 0, 1)
cols = ExtractColumnsFromExpressions(cols, sf.GetArgs(), isColumnInOperand)
return len(cols) > 0
return exprsContainInOperand(sf.GetArgs())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unnecessary now?

@@ -114,6 +114,22 @@ func ExtractColumns(expr Expression) []*Column {
return extractColumns(result, expr, nil)
}

func exprsContainInOperand(exprs []Expression) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@winoros
Copy link
Member

winoros commented Nov 15, 2019

Some comments not about the main changes.
Since the special condition cannot be pushed down to the join's children.
We can let the join have one more field like NullSensitiveConditions and put them into this field.
Then the InOperand field can be removed from expression.Column?
@eurekaka @lamxTyler @francis0407

if ok && mysql.HasNotNullFlag(lCol.GetType().Flag) && mysql.HasNotNullFlag(rCol.GetType().Flag) {
// If both input columns of `!= all / = any` expression are not null, we can treat the expression
// as normal column equal condition.
if mysql.HasNotNullFlag(rCol.GetType().Flag) && expression.NotNull(larg) {
Copy link
Contributor

@fzhedu fzhedu Nov 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditions to get rid of in are limited;

mysql> explain select  (t.a in (select s.a from s)) is true from t;
+-------------------------+----------+-----------+-----------------------------------------------------------------------------------------+
| id                      | count    | task      | operator info                                                                           |
+-------------------------+----------+-----------+-----------------------------------------------------------------------------------------+
| Projection_6            | 10000.00 | root      | istrue(Column#8)                                                                        |
| └─HashLeftJoin_7        | 10000.00 | root      | CARTESIAN left outer semi join, inner:TableReader_11, other cond:eq(Column#1, Column#4) |
|   ├─TableReader_9       | 10000.00 | root      | data:TableScan_8                                                                        |
|   │ └─TableScan_8       | 10000.00 | cop[tikv] | table:t, range:[-inf,+inf], keep order:false, stats:pseudo                              |
|   └─TableReader_11      | 3.00     | root      | data:TableScan_10                                                                       |
|     └─TableScan_10      | 3.00     | cop[tikv] | table:s, range:[-inf,+inf], keep order:false, stats:pseudo                              |
+-------------------------+----------+-----------+-----------------------------------------------------------------------------------------+
6 rows in set (0.00 sec)

() is true above the subquery can also get rid of in, so that it can be converted to a inner join.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After considering scalar function, eq(Column#1, Column#4) is treated as euqal condition now.But how can it be converted to inner join?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that () is true is the only special case where the in subquery can be converted to exists subquery . In other most cases, the in subquery should return null, false or true in the projection clause.
So we may ignore the optimization for () is true here.

@eurekaka
Copy link
Contributor

Some comments not about the main changes.
Since the special condition cannot be pushed down to the join's children.
We can let the join have one more field like NullSensitiveConditions and put them into this field.
Then the InOperand field can be removed from expression.Column?
@eurekaka @lamxTyler @francis0407

Yes, code would be cleaner then. We can do it after this PR get merged.

@fzhedu
Copy link
Contributor

fzhedu commented Nov 18, 2019

I find the converted conditions from in subquery in semi join are pushed down. I wonder whether this would induce errors? (eq(2, Column#4))

mysql> explain select * from s where (2 in (select a from t where (s.b=t.b) is null));
+------------------------+-------+-----------+--------------------------------------------------------------------------------------+
| id                     | count | task      | operator info                                                                        |
+------------------------+-------+-----------+--------------------------------------------------------------------------------------+
| HashLeftJoin_9         | 4.00  | root      | CARTESIAN semi join, inner:TableReader_14, other cond:isnull(eq(Column#2, Column#5)) |
| ├─TableReader_11       | 5.00  | root      | data:TableScan_10                                                                    |
| │ └─TableScan_10       | 5.00  | cop[tikv] | table:s, range:[-inf,+inf], keep order:false, stats:pseudo                           |
| └─TableReader_14       | 0.01  | root      | data:Selection_13                                                                    |
|   └─Selection_13       | 0.01  | cop[tikv] | eq(2, Column#4)                                                                      |
|     └─TableScan_12     | 5.00  | cop[tikv] | table:t, range:[-inf,+inf], keep order:false, stats:pseudo                           |
+------------------------+-------+-----------+--------------------------------------------------------------------------------------+
6 rows in set (0.00 sec)

@fzhedu
Copy link
Contributor

fzhedu commented Nov 18, 2019

Some comments not about the main changes.
Since the special condition cannot be pushed down to the join's children.
We can let the join have one more field like NullSensitiveConditions and put them into this field.
Then the InOperand field can be removed from expression.Column?
@eurekaka @lamxTyler @francis0407

Yes, code would be cleaner then. We can do it after this PR get merged.

I also got the same way to store the null-aware conditions, and they cannot be pushed down. Adding a nullAwareConditions can also solve the bugs solved by this PR. But some conditions in nullAwareConditions can also be pushed down, like the referred columns are not null. So it also needs to add the func NotNull(l Expression) bool {} functions in this PR for nullAwareConditions to push down the non-null conditions.

@winoros
Copy link
Member

winoros commented Nov 18, 2019

@fzhedu
When it's not null, inOperand is also false currently. This won't generate new cases which need to be special handled.

case *CorrelatedColumn:
return mysql.HasNotNullFlag(l.GetType().Flag)
case *ScalarFunction:
for _, arg := range l.GetArgs() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though the current implementation may satisfy the InSubquery's needs. But for the real NotNull property of scalar function, we need to take the IsNull sig in to special consideration at least.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for example arg is true is not null when arg is null.

@fzhedu
Copy link
Contributor

fzhedu commented Nov 18, 2019

@fzhedu
When it's not null, inOperand is also false currently. This won't generate new cases which need to be special handled.

Yes. The way in this PR and adding a nullAwareConditions are two ways to solve the current bugs about null in subquery. Such two ways both needs to push down the non-null conditions converted from in subqueries. This is done by setting inOperand in this PR.
It is better to take the second way? @winoros

@jingyugao
Copy link
Contributor Author

explain select * from s where (2 in (select a from t where (s.b=t.b) is null));

This pr seems not change this behavior. Execute plan is same between master and this pr.

@fzhedu
Copy link
Contributor

fzhedu commented Nov 26, 2019

I found the PR also has errors, please see #13743

@XuHuaiyu XuHuaiyu removed their request for review January 2, 2020 03:13
@sre-bot
Copy link
Contributor

sre-bot commented Jan 4, 2020

@jingyugao, please update your pull request.

1 similar comment
@sre-bot
Copy link
Contributor

sre-bot commented Jan 11, 2020

@jingyugao, please update your pull request.

@sre-bot
Copy link
Contributor

sre-bot commented Jan 27, 2020

@jingyugao PR closed due to no update for a long time. Feel free to reopen it anytime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution This PR is from a community contributor. sig/planner SIG: Planner type/bugfix This PR fixes a bug.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

error optimization of anti smei join on daul table
7 participants