-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[SPARK-16849][SQL] Improve subquery execution by deduplicating the subqueries with the same results #14452
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
Conversation
00b29ed to
55a44c8
Compare
|
Test build #63107 has finished for PR 14452 at commit
|
|
Test build #63108 has finished for PR 14452 at commit
|
55a44c8 to
ebaa1c6
Compare
|
Test build #63114 has finished for PR 14452 at commit
|
|
cc @cloud-fan @hvanhovell Can you take a look? Thanks! |
ebaa1c6 to
6bfdb32
Compare
|
Test build #63120 has finished for PR 14452 at commit
|
|
Test build #63202 has finished for PR 14452 at commit
|
7fe57a0 to
6bfdb32
Compare
|
Test build #63216 has finished for PR 14452 at commit
|
|
Test build #63270 has finished for PR 14452 at commit
|
|
ping @cloud-fan @hvanhovell Can you look at this if it is making sense for you? Thanks. |
|
ping @cloud-fan @hvanhovell @liancheng Could you review this change? Thanks. |
|
Will the deduplication logics on conflicting attributes in Analyzer affect your solution? spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala Lines 478 to 482 in 06f5dc8
|
Conflicts: sql/core/src/test/scala/org/apache/spark/sql/SubquerySuite.scala
|
@gatorsmile I think it doesn't affect this. This change is happened after analysis. |
|
Test build #63491 has finished for PR 14452 at commit
|
|
Will it affect |
|
I think the deduplicating the conflicting attributes in Analyzer is generating new expression ids. |
|
If |
|
Test build #63494 has finished for PR 14452 at commit
|
|
Yea, but other attribute references will be bound before comparing in |
|
Test build #63496 has finished for PR 14452 at commit
|
|
It is just bound to its own |
|
Adding an explicit cache call btw -- can actually slow things down due to bad memory management. |
|
According to my test in local cluster, without the cache call, I can't see actual speed-up. Only with cache call, I can see improvement. But it is true that the effective caching for SQL needs additional |
|
Test build #70541 has started for PR 14452 at commit |
|
Revisit this by rebasing with master. BTW, in 500+ LOC changes, actually there are 200+ LOC changes are test cases. |
|
retest this please. |
|
Test build #70542 has finished for PR 14452 at commit
|
|
After reading the PR description, I think this improvement is quite complex and worth a design doc. We should explain how the subquery execution works now and how you are going to change it. |
|
@cloud-fan Thanks for comment. I agreed that. I will prepare a design doc soon. |
|
@viirya For duplicated CTE, without some optimization (pushing down different predicates in different positions), the physical plan should be identical. So I'm wondering some aggressive pushing down cause the problem for some queries (IsNotNull(xxx)). This is the reason I asked that. |
|
@davies It is true that pushing down different predicates results in different CTE logical/physical plans. I spend some LOC changes in this to address that cases, i.e., preparing a disjunctive predicate for duplicated CTE with different predicates. For Q64, a disjunctive predicate will be pushed down too. I am not sure what the problem is you mentioned. Let me try to get and show the pushed down predicate. |
|
I would like to share some numbers I ran this on my local cluster today (5 nodes, on yarn, 8GB each node). After this (we cached the output of duplicated CTEs): q2: 10037 ms (-7%) (no disjunctive predicate pushed down, 2 duplicated and simple CTEs) ** Because caching will introduce extra cost, if the duplicated CTEs are not complicated enough, it might be regression. Before this: q2: 9414 ms Q64 causes out-of-memory on the cluster. These queries are CTE queries. The data is generated with spark-sql-perf using scaleFactor = 1 to make these queries working on my cluster. |
|
Test build #70587 has finished for PR 14452 at commit
|
|
can you provide a typical query that can benefit from this PR? And also show why it's slow without your PR. Thanks! |
14e1519 to
e2bd146
Compare
|
@cloud-fan ok. I will do it. |
|
@rxin has pointed out:
In my experiment, I found: According to my test in local cluster, without the cache call, I can't see actual speed-up. Only with cache call, I can see improvement. However the effective caching for rows needs additional |
|
Test build #70604 has finished for PR 14452 at commit
|
|
Q47 is a typical query that can benefit from this PR: It has 2 CTEs, Obviously without this PR we will run three physical plans of |
e2bd146 to
f153c12
Compare
|
Test build #70655 has started for PR 14452 at commit |
| subqueries.tail.foreach { | ||
| case Filter(otherCond, child) => | ||
| val rewrites = buildRewrites(child, subqueries(0).asInstanceOf[Filter].child) | ||
| // We can't simply push down all conditions from other Filter by concatenating them with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part has been extracted out as #15558 and can be removed if that PR is merged.
|
Test build #70659 has started for PR 14452 at commit |
|
retest this please. |
|
Test build #70661 has finished for PR 14452 at commit
|
|
Because the actual improvement of this de-duplication depends on the complexity of disjunctive predicate pushdown and CTE subquery, if we can't have general rule to decide whether de-duplicate or not, I think can we have a config (default off) to enable/disable this subquery de-duplication? So users can decide if they want to cache and de-duplicate subqueries in their query. |
What changes were proposed in this pull request?
Design doc: https://issues.apache.org/jira/secure/attachment/12845016/de-duplicating%20subqueries.pdf
The subqueries in SparkSQL will be run even they have the same physical plan and output same results. We should be able to deduplicate these subqueries which are referred in a query for many times.
This change is motivated by the attempt to improve the performance of CTE subqueries. However, it is not limited to CTE but can be applied to other subqueries in general. Note that because CTE provides the scope information, we can better utilize it to find common subqueries among the whole query plan.
For the other subqueries, we rely on
sameResultto decide if two subqueries are the same common subquery. As we know,sameResultdoesn't guarantee to determine two given plans will produce the same results, so it is possibly two non-CTE subqueries which produce the same results are not considered as common subquery.Compared to existing result reusing rule ReuseExchange
The planning rule
ReuseExchangecan find out duplicated exchanges in the spark plan and reuse the same exchange for all the references. So we already have it to reuse Broadcast and Shuffle. Why do we need this PR to reuse duplicated subqueries?The mean we rely on to find the duplicated exchanges is
sameResult. Quoted from its comments:That's said, for some cases (if not most cases), the query plan can't meet the requirement of
sameResultto reuse Shuffle or Broadcast, even the two plans are actually producing the same results. That is what we observed when benchmarking on TPC-DS queries.However, CTE subqueries provide the good scope information (i.e., the subquery alias) we can utilize to find duplicated query plans. E.g., for a query like:
WITH cte AS (SELECT * FROM src) SELECT * FROM cte a JOIN cte b
It is guaranteed that
aandbare the same, and we can reuse the same result.Besides, there are operators and situations which don't involve Exchange and we still need to de-duplicate the same subqueries.
Eliminating SubqueryAlias
Previously, we eliminate all
SubqueryAliasat the beginning ofOptimizer. Now it is changed to only eliminate theSubqueryAliaswhich is only referred once in the whole query plan. This is performed by the newEliminateOneTimeSubqueryAliases.De-duplicate common SubqueryAlias
Another new rule
DedupCommonSubqueriesis introduced to de-duplicate common subqueries before optimization.Each
SubqueryAliashas a child logical plan representing the subquery. We generate the executed plan for the logical plan and attach it to aCommonSubqueryoperator which is just a wrapper for the executed plan.Note that we will search if any same logical plan exists. So for any distinct subquery logical plan, we will have only one executed plan shared between many
CommonSubquery. Once the executed plan is executed, its result will be kept and shared between theseCommonSubquerytoo.Note that we will only keep the common subqueries at the highest level in the query. That is said we will strip the common subqueries wrapped in other common subquery. That will simplify the logic to optimize filter/projection pushdown between such nested common subqueries. And the common subquery at the highest level should be the most expensive one to de-duplicate.
Modifying sameResult of SubqueryAlias
This PR override the default
sameResultmethod inSubqueryAlias. If twoSubqueryAliashave the child logical plans generating the same results, thesameResulton them should returntrue.Optimization regarding common subqueries and operators on them
It is possibly that we have a logical plan including a common subquery like this:
Optimization rules that push down
Filterwould work for it because the additionalSubqueryAlias. We knowFilterandProjectpushdown can improve the query performance a lot. So we introduce a new optimization ruleOptimizeCommonSubqueries.Simply said, the rule finds the common subqueries. For each common subquery, we optimize all subqueries producing the same results with the operator on them. We consider two cases. First, all subqueries have a
Projecton them. Second, all subqueries have aFilteron them.For Project pushdown, we concatenate all project lists and push down it to all subqueries. For Filter pushdown, we concatenate all filtering conditions with a
Orexpression and push down it to all subqueries. For example, the above query plan would look like:Note that the two subqueries have the same filter operator pushed down. So they are still common subqueries and the results can be reused.
How was this patch tested?
Jenkins tests.